diff --git a/web/src/community/publish-dialog.test.ts b/web/src/community/publish-dialog.test.ts
index 2740250..53de4b8 100644
--- a/web/src/community/publish-dialog.test.ts
+++ b/web/src/community/publish-dialog.test.ts
@@ -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: '
' });
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();
diff --git a/web/src/community/publish-dialog.ts b/web/src/community/publish-dialog.ts
index 881397a..90574b3 100644
--- a/web/src/community/publish-dialog.ts
+++ b/web/src/community/publish-dialog.ts
@@ -151,8 +151,9 @@ export function publishDialog(opts: PublishDialogOptions): Promise`), sonst zeigt sein Label auf die Überschrift.
+ el('div', { class: 'report-card' }, el('h2', { id: 'publish-heading', class: 'report-title' }, 'Karte veröffentlichen'), form),
);
const finish = () => {
@@ -165,6 +166,17 @@ export function publishDialog(opts: PublishDialogOptions): Promise {
if (busy) event.preventDefault();
});
- dialog.addEventListener('close', finish);
+ // Schließt der Browser den Dialog trotzdem mitten im Senden, wird er wieder geöffnet; aufgelöst wird erst mit dem echten Ergebnis.
+ dialog.addEventListener('close', () => {
+ if (busy && !closed) {
+ reopen();
+ return;
+ }
+ finish();
+ });
// Hintergrund = Klick auf das