fix(community): Label-ID im Veröffentlichen-Dialog, Status nie in „lädt“, Nachprüfung robust

Die Überschrift des Dialogs hatte dieselbe ID wie das Titelfeld (publish-title), das Label des
Pflichtfelds zeigte damit auf die Überschrift; jetzt publish-heading. Schließt der Browser den Dialog
mitten im Senden, wird er wieder geöffnet statt als abgebrochen aufgelöst. Die Status-Abfrage der Seite
wird bei einer lokalen Änderung während der Anfrage wiederholt (nie in „lädt“ hängen), die Nachprüfung nach
offenem Ausgang ist eine eigene Abfrage. Der Löschen-Dialog nennt bei unbekanntem Status einen allgemeinen
Hinweis und sagt, dass sich eine Veröffentlichung nur auf der Community-Seite zurückziehen lässt.
publishCard heißt lokal publishWithCheck.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 15:56:21 +00:00
1 parent f6b9ee61f4
commit 87b71ec71f
4 files changed
+216 -33

No files matched your search

+47
View File
@@ -85,6 +85,20 @@ describe('Veröffentlichen-Dialog: Felder', () => {
expect(consent().closest('label')!.textContent).toBe(CONSENT_TEXT);
});
it('jedes Feld hat sein Label (label.control === Eingabe), alle IDs sind eindeutig, der Titel des Dialogs ist die Überschrift', () => {
void open();
const labels = [...form().querySelectorAll('label')];
const inputs = [...form().querySelectorAll('input')];
expect(labels).toHaveLength(5);
expect(labels.map((l) => l.control)).toEqual(inputs);
for (const control of inputs) expect(control.labels).toHaveLength(1);
const ids = [...dialogEl()!.querySelectorAll('[id]')].map((n) => n.id);
expect(new Set(ids).size).toBe(ids.length);
const heading = dialogEl()!.querySelector('h2')!;
expect(dialogEl()!.getAttribute('aria-labelledby')).toBe(heading.id);
expect(inputs.map((i) => i.id)).not.toContain(heading.id);
});
it('nennt den Kartennamen als Text, nie als HTML', () => {
void open({ cardName: '<img src=x onerror=alert(1)>' });
expect(dialogEl()!.querySelector('img')).toBeNull();
@@ -221,6 +235,39 @@ describe('Veröffentlichen-Dialog: Senden', () => {
await outcome;
});
it('schließt der Browser den Dialog mitten im Senden (close-Ereignis), geht er wieder auf und löst erst mit dem echten Ergebnis auf', async () => {
let finish!: (r: { id: string }) => void;
submit.mockReturnValue(new Promise((r) => (finish = r)));
let resolved: PublishOutcome | null = null;
const outcome = open().then((o) => (resolved = o));
fill();
send();
dialogEl()!.close(); // wie ein Browser, der Esc trotz verhindertem `cancel` auswertet
await settle();
expect(dialogEl()?.open).toBe(true);
dialogEl()!.dispatchEvent(new Event('close'));
await settle();
expect(resolved).toBeNull();
expect(dialogEl()?.open).toBe(true);
finish({ id: 'g1' });
await outcome;
expect(resolved).toBe('published');
expect(dialogEl()).toBeNull();
});
it('wurde der Dialog mitten im Senden geschlossen und das Senden scheitert, ist der Fehler sichtbar (Dialog wieder offen)', async () => {
let fail!: (e: Error) => void;
submit.mockReturnValue(new Promise((_, r) => (fail = r)));
void open();
fill();
send();
dialogEl()!.close();
fail(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await settle();
expect(dialogEl()?.open).toBe(true);
expect(errorText()).toBe('Veröffentlichen fehlgeschlagen – Der Dienst ist gerade nicht erreichbar.');
});
it('Fehler des Dienstes: Servertext im Dialog, der Dialog bleibt offen, ein neuer Versuch ist möglich', async () => {
submit.mockRejectedValueOnce(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
const outcome = open();