feat(community): Veröffentlichen aus Meine Karten

Neuer Dialog (publish-dialog.ts) laut Spec § 6: Titel (Pflicht, höchstens 60), Herausgeber/Bank (40),
Tags (bis 5, je 20, Komma-getrennt), Anzeigename (30, leer = Anonym, nie vorbelegt) und die
Pflicht-Checkbox im Wortlaut der Spec. Prüfung vor dem Senden mit deutschen Texten, während des Sendens
lässt sich der Dialog nicht schließen; Fehler stehen im Dialog („Veröffentlichen fehlgeschlagen – …“).

Meine Karten zeigt je Kachel den Status aus myPublications (einmal je Seitenaufbau, danach lokal
nachgeführt): Veröffentlicht + Ansehen + Zurückziehen, Ausgeblendet (gemeldet) + Zurückziehen, sonst
Veröffentlichen. Bei 409 wird der Status neu geholt; bleibt der Ausgang offen (503/5xx/429/408), wird vor
der Fehlermeldung nachgesehen und bei Erfolg der Erfolgszustand gezeigt. Zurückziehen fragt nach, 404 gilt
als erledigt. Nicht angemeldet läuft über withLogin. Das Löschen einer veröffentlichten Karte weist darauf
hin, dass die Veröffentlichung bleibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 15:47:56 +00:00
1 parent be440bc8e9
commit f6b9ee61f4
7 files changed
+1403 -9

No files matched your search

+487
View File
@@ -3,6 +3,7 @@ import type { User } from '../account/auth';
import type { SavedCard } from '../account/cards';
import { DEFAULT_CONFIG } from '../card/config';
import { createStore, initialEditorState } from '../editor/state';
import type { GalleryCard } from '../community/gallery';
const mocks = vi.hoisted(() => ({
list: vi.fn(),
@@ -17,6 +18,9 @@ const mocks = vi.hoisted(() => ({
loadCardAssets: vi.fn(),
exportCard: vi.fn(),
saveBlobAs: vi.fn(),
publishSavedCard: vi.fn(),
withdrawCard: vi.fn(),
myPublications: vi.fn(),
authListeners: new Set<(user: User | null) => void>(),
}));
@@ -52,6 +56,17 @@ vi.mock('../account/cards', async (importOriginal) => ({
}),
}));
vi.mock('../community/publish', async (importOriginal) => ({
...(await importOriginal<typeof import('../community/publish')>()),
publishSavedCard: mocks.publishSavedCard,
}));
vi.mock('../community/api', async (importOriginal) => ({
...(await importOriginal<typeof import('../community/api')>()),
withdrawCard: mocks.withdrawCard,
}));
vi.mock('../community/gallery', () => ({ myPublications: mocks.myPublications }));
import { ApiError } from '../community/api';
import { mountCardsPage } from './cards-page';
const user: User = { $id: 'u1', email: 'a@b.de' };
@@ -86,6 +101,7 @@ beforeEach(() => {
store = createStore(initialEditorState());
mocks.getCurrentUser.mockResolvedValue(user);
mocks.requireLogin.mockResolvedValue(true);
mocks.myPublications.mockResolvedValue(new Map());
mocks.loadPreview.mockImplementation(async () => new Blob(['p'], { type: 'image/webp' }));
unmount = null;
});
@@ -621,3 +637,474 @@ describe('Meine Karten: Seite wird verlassen', () => {
});
});
const published = (id: string, sourceCardId: string, patch: Partial<GalleryCard> = {}): GalleryCard => ({
id,
title: 'Mein Motiv',
issuer: '',
tags: [],
displayName: '',
imageFileId: `gi-${id}`,
previewFileId: `gp-${id}`,
sourceCardId,
ownerId: 'u1',
hidden: false,
reportCount: 0,
createdAt: '2026-10-04T12:00:00.000Z',
...patch,
});
const labels = (scope: ParentNode) => [...scope.querySelectorAll('.card-buttons button, .card-buttons a')].map((n) => n.textContent);
const badge = (id: string) => tileFor(id).querySelector('.card-pub')?.textContent ?? '';
const publishDialogEl = () => document.querySelector<HTMLDialogElement>('dialog.publish-dialog');
const dlgInput = (name: string) => publishDialogEl()!.querySelector<HTMLInputElement>(`input[name="${name}"]`)!;
const sendForm = () =>
publishDialogEl()!.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
const fillDialog = (title = 'Mein Motiv', values: Record<string, string> = {}) => {
dlgInput('title').value = title;
for (const [name, value] of Object.entries(values)) dlgInput(name).value = value;
dlgInput('consent').checked = true;
};
const dlgError = () => publishDialogEl()!.querySelector('[role="alert"]')!.textContent;
const openPublish = async (id: string) => {
await vi.waitFor(() => expect(button(tileFor(id), 'Veröffentlichen').disabled).toBe(false));
button(tileFor(id), 'Veröffentlichen').click();
await vi.waitFor(() => expect(publishDialogEl()?.open).toBe(true));
};
describe('Meine Karten: Veröffentlichung (Status an der Kachel)', () => {
const pubMap = (...cards: GalleryCard[]) => new Map(cards.map((c) => [c.sourceCardId, c]));
const view = (id: string) => tileFor(id).querySelector<HTMLAnchorElement>('a[data-action="view"]');
const mountWith = async (publications: Map<string, GalleryCard> = new Map()) => {
mocks.list.mockResolvedValue([card('a', 'Alpha'), card('b', 'Beta')]);
mocks.myPublications.mockResolvedValue(publications);
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(2));
await vi.waitFor(() => expect(button(tileFor('a'), 'Löschen')).toBeTruthy());
await vi.waitFor(() => expect(tileFor('a').querySelector('[data-action="publish"], [data-action="withdraw"]')).not.toBeNull());
};
it('lädt den Status einmal je Seitenaufbau (für die angemeldete Nutzer-ID), nicht abgemeldet', async () => {
await mountWith();
expect(mocks.myPublications).toHaveBeenCalledTimes(1);
expect(mocks.myPublications).toHaveBeenCalledWith('u1');
unmount?.();
unmount = null;
mocks.myPublications.mockClear();
mocks.getCurrentUser.mockResolvedValue(null);
mount();
await vi.waitFor(() => expect(text()).toContain('Melde dich an'));
expect(mocks.myPublications).not.toHaveBeenCalled();
});
it('nicht veröffentlicht: „Veröffentlichen“, kein Badge', async () => {
await mountWith();
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Veröffentlichen', 'Löschen']);
expect(badge('a')).toBe('');
expect(view('a')).toBeNull();
});
it('veröffentlicht: Badge „Veröffentlicht“, „Ansehen“ (/community/<id>) und „Zurückziehen“ statt „Veröffentlichen“', async () => {
await mountWith(pubMap(published('g1', 'a')));
expect(badge('a')).toBe('Veröffentlicht');
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Ansehen', 'Zurückziehen', 'Löschen']);
expect(view('a')!.getAttribute('href')).toBe('/community/g1');
expect(view('a')!.hasAttribute('data-link')).toBe(true);
// Die andere Kachel bleibt unberührt.
expect(badge('b')).toBe('');
expect(labels(tileFor('b'))).toContain('Veröffentlichen');
});
it('ausgeblendet (gemeldet): Badge „Ausgeblendet (gemeldet)“ und „Zurückziehen“, aber kein „Ansehen“ und kein „Veröffentlichen“', async () => {
await mountWith(pubMap(published('g1', 'a', { hidden: true, reportCount: 3 })));
expect(badge('a')).toBe('Ausgeblendet (gemeldet)');
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Zurückziehen', 'Löschen']);
expect(view('a')).toBeNull();
});
it('solange der Status lädt, gibt es weder „Veröffentlichen“ noch „Zurückziehen“; danach erscheint der Stand', async () => {
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
let finish!: (m: Map<string, GalleryCard>) => void;
mocks.myPublications.mockReturnValue(new Promise((r) => (finish = r)));
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Löschen']);
finish(pubMap(published('g1', 'a')));
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
});
it('Status lässt sich nicht laden: Hinweis mit „Nochmal versuchen“, „Veröffentlichen“ bleibt möglich (409 fängt Doppeltes ab)', async () => {
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
mocks.myPublications.mockRejectedValueOnce(new TypeError('offline'));
mount();
await vi.waitFor(() => expect(text()).toContain('Der Veröffentlichungsstatus konnte nicht geladen werden.'));
expect(labels(tileFor('a'))).toContain('Veröffentlichen');
mocks.myPublications.mockResolvedValue(pubMap(published('g1', 'a')));
button(root, 'Nochmal versuchen').click();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(text()).not.toContain('konnte nicht geladen werden');
});
it('Abmelden räumt den Status weg, nach dem Anmelden wird er neu geholt', async () => {
await mountWith(pubMap(published('g1', 'a')));
for (const fn of mocks.authListeners) fn(null);
expect(tiles()).toHaveLength(0);
mocks.myPublications.mockResolvedValue(new Map());
for (const fn of mocks.authListeners) fn(user);
await vi.waitFor(() => expect(tiles()).toHaveLength(2));
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
expect(badge('a')).toBe('');
expect(mocks.myPublications).toHaveBeenCalledTimes(2);
});
it('der Status überlebt Umbenennen (Liste wird neu aufgebaut) ohne neue Anfrage', async () => {
mocks.rename.mockImplementation(async (id: string, name: string) => card(id, name, { updatedAt: '2026-10-06T08:00:00.000Z' }));
await mountWith(pubMap(published('g1', 'a')));
button(tileFor('a'), 'Umbenennen').click();
const input = tileFor('a').querySelector<HTMLInputElement>('input')!;
input.value = 'Anders';
submit(input.closest('form')!);
await vi.waitFor(() => expect(tileFor('a').querySelector('.card-name')!.textContent).toBe('Anders'));
expect(badge('a')).toBe('Veröffentlicht');
expect(mocks.myPublications).toHaveBeenCalledTimes(1);
});
it('Löschen einer veröffentlichten Karte weist darauf hin, dass die Veröffentlichung bleibt', async () => {
await mountWith(pubMap(published('g1', 'a')));
button(tileFor('a'), 'Löschen').click();
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
expect(dialog()!.textContent).toContain('bleibt in der Community-Galerie');
button(dialog()!, 'Abbrechen').click();
await vi.waitFor(() => expect(dialog()).toBeNull());
button(tileFor('b'), 'Löschen').click();
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
expect(dialog()!.textContent).not.toContain('Community-Galerie');
});
});
describe('Meine Karten: Veröffentlichen', () => {
const start = async () => {
mocks.list.mockResolvedValue([card('a', 'Alpha'), card('b', 'Beta')]);
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(2));
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
};
it('„Veröffentlichen“ öffnet den Dialog: leere Felder, nie die E-Mail im Anzeigenamen', async () => {
await start();
await openPublish('a');
expect(dlgInput('title').value).toBe('');
expect(dlgInput('displayName').value).toBe('');
expect(dlgInput('displayName').placeholder).toBe('Anonym');
expect(publishDialogEl()!.textContent).not.toContain('a@b.de');
expect(publishDialogEl()!.textContent).toContain('„Alpha“');
expect(mocks.publishSavedCard).not.toHaveBeenCalled();
});
it('Abbrechen veröffentlicht nichts, Fokus zurück auf „Veröffentlichen“', async () => {
await start();
await openPublish('a');
button(publishDialogEl()!, 'Abbrechen').click();
await vi.waitFor(() => expect(publishDialogEl()).toBeNull());
expect(mocks.publishSavedCard).not.toHaveBeenCalled();
await vi.waitFor(() => expect(document.activeElement).toBe(button(tileFor('a'), 'Veröffentlichen')));
});
it('ohne Pflicht-Checkbox wird nichts veröffentlicht', async () => {
await start();
await openPublish('a');
dlgInput('title').value = 'Mein Motiv';
sendForm();
expect(dlgError()).toBe('Bitte bestätige die Erklärung zum Bild.');
expect(mocks.publishSavedCard).not.toHaveBeenCalled();
});
it('Erfolg: Badge „Veröffentlicht“ und „Ansehen“ erscheinen sofort, ohne die Liste neu zu laden', async () => {
mocks.publishSavedCard.mockResolvedValue({ id: 'g9' });
await start();
await openPublish('a');
fillDialog('Mein Motiv', { issuer: 'Beispielbank', tags: 'Visa, Gold', displayName: 'Kenso' });
sendForm();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(mocks.publishSavedCard).toHaveBeenCalledWith(
expect.objectContaining({ id: 'a', name: 'Alpha' }),
{ title: 'Mein Motiv', issuer: 'Beispielbank', tags: 'Visa, Gold', displayName: 'Kenso' },
);
expect(publishDialogEl()).toBeNull();
const link = tileFor('a').querySelector<HTMLAnchorElement>('a[data-action="view"]')!;
expect(link.textContent).toBe('Ansehen');
expect(link.getAttribute('href')).toBe('/community/g9');
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Ansehen', 'Zurückziehen', 'Löschen']);
// Alle Knöpfe der Kachel sind wieder bedienbar, die andere Kachel ist unberührt.
expect(button(tileFor('a'), 'Zurückziehen').disabled).toBe(false);
expect(labels(tileFor('b'))).toContain('Veröffentlichen');
expect(mocks.list).toHaveBeenCalledTimes(1);
expect(mocks.myPublications).toHaveBeenCalledTimes(1);
// Der Fokus geht auf „Ansehen“ (der auslösende Knopf ist weg).
expect(document.activeElement).toBe(link);
});
it('während des Veröffentlichens sind die Kachel-Knöpfe gesperrt und der Dialog lässt sich nicht schließen', async () => {
let finish!: (r: { id: string }) => void;
mocks.publishSavedCard.mockReturnValue(new Promise((r) => (finish = r)));
await start();
await openPublish('a');
fillDialog();
sendForm();
expect(button(publishDialogEl()!, 'Veröffentlichen').disabled).toBe(true);
expect(button(tileFor('a'), 'Löschen').disabled).toBe(true);
expect(tileFor('a').getAttribute('aria-busy')).toBe('true');
finish({ id: 'g9' });
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(tileFor('a').hasAttribute('aria-busy')).toBe(false);
});
it('Fehler (400): Text im Dialog, kein Status geändert, keine Nachprüfung, neuer Versuch möglich', async () => {
mocks.publishSavedCard.mockRejectedValueOnce(new ApiError(400, 'Der Titel enthält ungültige Zeichen.'));
await start();
await openPublish('a');
fillDialog();
sendForm();
await vi.waitFor(() => expect(dlgError()).toBe('Veröffentlichen fehlgeschlagen – Der Titel enthält ungültige Zeichen.'));
expect(publishDialogEl()?.open).toBe(true);
expect(badge('a')).toBe('');
expect(mocks.myPublications).toHaveBeenCalledTimes(1);
mocks.publishSavedCard.mockResolvedValueOnce({ id: 'g9' });
sendForm();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
});
it('409: „Diese Karte ist schon veröffentlicht.“, der Status wird neu geholt und zeigt „Veröffentlicht“', async () => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(409, 'Diese Karte ist schon veröffentlicht.'));
await start();
await openPublish('a');
fillDialog();
mocks.myPublications.mockResolvedValue(new Map([['a', published('g1', 'a')]]));
sendForm();
await vi.waitFor(() => expect(publishDialogEl()!.textContent).toContain('Diese Karte ist schon veröffentlicht.'));
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(mocks.myPublications).toHaveBeenCalledTimes(2);
button(publishDialogEl()!, 'Schließen').click();
await vi.waitFor(() => expect(publishDialogEl()).toBeNull());
expect(tileFor('a').querySelector<HTMLAnchorElement>('a[data-action="view"]')!.getAttribute('href')).toBe('/community/g1');
expect(mocks.publishSavedCard).toHaveBeenCalledOnce();
});
describe('Ausgang offen (503, 429, 408, 5xx): Nachprüfung vor der Fehlermeldung', () => {
it('war die Karte doch veröffentlicht: Erfolgszustand statt Fehler', async () => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await start();
await openPublish('a');
fillDialog();
mocks.myPublications.mockResolvedValue(new Map([['a', published('g1', 'a')]]));
sendForm();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(publishDialogEl()).toBeNull();
expect(tileFor('a').querySelector<HTMLAnchorElement>('a[data-action="view"]')!.getAttribute('href')).toBe('/community/g1');
expect(tileFor('a').querySelector('.card-msg')!.textContent).toBe('');
expect(mocks.myPublications).toHaveBeenCalledTimes(2);
});
it.each([408, 429, 500, 504])('auch bei %i wird nachgeprüft', async (code) => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(code, 'Offen.'));
await start();
await openPublish('a');
fillDialog();
mocks.myPublications.mockResolvedValue(new Map([['a', published('g1', 'a')]]));
sendForm();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
});
it('war sie es nicht: Fehlermeldung im Dialog, Status unverändert', async () => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await start();
await openPublish('a');
fillDialog();
sendForm();
await vi.waitFor(() =>
expect(dlgError()).toBe('Veröffentlichen fehlgeschlagen – Der Dienst ist gerade nicht erreichbar.'),
);
expect(mocks.myPublications).toHaveBeenCalledTimes(2);
expect(badge('a')).toBe('');
expect(publishDialogEl()?.open).toBe(true);
});
it('scheitert auch die Nachprüfung: die ursprüngliche Meldung bleibt', async () => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await start();
await openPublish('a');
fillDialog();
mocks.myPublications.mockRejectedValue(new TypeError('offline'));
sendForm();
await vi.waitFor(() =>
expect(dlgError()).toBe('Veröffentlichen fehlgeschlagen – Der Dienst ist gerade nicht erreichbar.'),
);
expect(badge('a')).toBe('');
});
it('die Nachprüfung findet nur die Veröffentlichung dieser Karte (eine andere zählt nicht)', async () => {
mocks.publishSavedCard.mockRejectedValue(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await start();
await openPublish('a');
fillDialog();
mocks.myPublications.mockResolvedValue(new Map([['b', published('g2', 'b')]]));
sendForm();
await vi.waitFor(() => expect(dlgError()).toContain('Der Dienst ist gerade nicht erreichbar.'));
expect(badge('a')).toBe('');
expect(badge('b')).toBe('Veröffentlicht');
});
});
it('abgelaufene Sitzung (401): Anmelden, dann wird veröffentlicht', async () => {
mocks.publishSavedCard
.mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { code: 401 }))
.mockResolvedValueOnce({ id: 'g9' });
await start();
await openPublish('a');
fillDialog();
sendForm();
await vi.waitFor(() => expect(badge('a')).toBe('Veröffentlicht'));
expect(mocks.requireLogin).toHaveBeenCalledOnce();
expect(mocks.publishSavedCard).toHaveBeenCalledTimes(2);
});
it('Anmeldung abgebrochen: Hinweis im Dialog, nichts veröffentlicht', async () => {
mocks.requireLogin.mockResolvedValue(false);
mocks.publishSavedCard.mockRejectedValue(Object.assign(new Error('Unauthorized'), { code: 401 }));
await start();
await openPublish('a');
fillDialog();
sendForm();
await vi.waitFor(() => expect(dlgError()).toBe('Bitte melde dich an, um fortzufahren.'));
expect(badge('a')).toBe('');
});
it('die Seite wird verlassen: der Dialog geht zu, eine spätere Antwort ändert nichts mehr', async () => {
let finish!: (r: { id: string }) => void;
mocks.publishSavedCard.mockReturnValue(new Promise((r) => (finish = r)));
await start();
await openPublish('a');
fillDialog();
sendForm();
unmount?.();
unmount = null;
expect(publishDialogEl()).toBeNull();
finish({ id: 'g9' });
await new Promise((r) => setTimeout(r, 0));
expect(root.querySelector('.card-badge')).toBeNull();
});
it('die Seite wird verlassen, solange der Dialog offen ist: er geht zu, nichts wird veröffentlicht', async () => {
await start();
await openPublish('a');
unmount?.();
unmount = null;
expect(publishDialogEl()).toBeNull();
expect(mocks.publishSavedCard).not.toHaveBeenCalled();
});
it('Titel und Namen erscheinen als Text, nie als HTML', async () => {
mocks.list.mockResolvedValue([card('a', '<img src=x onerror=alert(1)>')]);
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
await openPublish('a');
expect(publishDialogEl()!.querySelector('img')).toBeNull();
expect(publishDialogEl()!.textContent).toContain('<img src=x onerror=alert(1)>');
});
});
describe('Meine Karten: Zurückziehen', () => {
const start = async (patch: Partial<GalleryCard> = {}) => {
mocks.list.mockResolvedValue([card('a', 'Alpha'), card('b', 'Beta')]);
mocks.myPublications.mockResolvedValue(new Map([['a', published('g1', 'a', patch)]]));
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(2));
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Zurückziehen'));
};
const openConfirm = async () => {
button(tileFor('a'), 'Zurückziehen').click();
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
};
it('fragt nach, zieht dann die Veröffentlichung (Galerie-ID) zurück; die Kachel zeigt wieder „Veröffentlichen“', async () => {
mocks.withdrawCard.mockResolvedValue(undefined);
await start();
await openConfirm();
expect(dialog()!.querySelector('h2')!.textContent).toBe('Karte „Mein Motiv“ zurückziehen?');
expect(dialog()!.textContent).toContain('Deine private Karte bleibt erhalten.');
expect(mocks.withdrawCard).not.toHaveBeenCalled();
button(dialog()!, 'Zurückziehen').click();
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
expect(mocks.withdrawCard).toHaveBeenCalledWith('g1');
expect(badge('a')).toBe('');
expect(labels(tileFor('a'))).toEqual(['Öffnen', 'Herunterladen', 'Umbenennen', 'Veröffentlichen', 'Löschen']);
expect(dialog()).toBeNull();
expect(mocks.list).toHaveBeenCalledTimes(1);
expect(mocks.myPublications).toHaveBeenCalledTimes(1);
// Fokus auf dem neuen „Veröffentlichen“-Knopf.
expect(document.activeElement).toBe(button(tileFor('a'), 'Veröffentlichen'));
});
it('ausgeblendete Kacheln behalten „Zurückziehen“: danach lässt sich neu veröffentlichen', async () => {
mocks.withdrawCard.mockResolvedValue(undefined);
await start({ hidden: true, reportCount: 3 });
expect(badge('a')).toBe('Ausgeblendet (gemeldet)');
await openConfirm();
button(dialog()!, 'Zurückziehen').click();
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
expect(mocks.withdrawCard).toHaveBeenCalledWith('g1');
expect(badge('a')).toBe('');
});
it('„Abbrechen“ zieht nichts zurück', async () => {
await start();
await openConfirm();
button(dialog()!, 'Abbrechen').click();
await vi.waitFor(() => expect(dialog()).toBeNull());
expect(mocks.withdrawCard).not.toHaveBeenCalled();
expect(badge('a')).toBe('Veröffentlicht');
await vi.waitFor(() => expect(button(tileFor('a'), 'Zurückziehen').disabled).toBe(false));
});
it('Fehler: Text des Dienstes an der Kachel, Status bleibt', async () => {
mocks.withdrawCard.mockRejectedValue(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.'));
await start();
await openConfirm();
button(dialog()!, 'Zurückziehen').click();
await vi.waitFor(() =>
expect(tileFor('a').querySelector('.card-msg')!.textContent).toBe('Der Dienst ist gerade nicht erreichbar.'),
);
expect(badge('a')).toBe('Veröffentlicht');
expect(button(tileFor('a'), 'Zurückziehen').disabled).toBe(false);
});
it('die Veröffentlichung gibt es schon nicht mehr (404): Ziel erreicht, die Kachel zeigt „Veröffentlichen“', async () => {
mocks.withdrawCard.mockRejectedValue(new ApiError(404, 'Diese Karte gibt es nicht.'));
await start();
await openConfirm();
button(dialog()!, 'Zurückziehen').click();
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
expect(tileFor('a').querySelector('.card-msg')!.textContent).toBe('');
});
it('abgelaufene Sitzung (401): Anmelden, dann wird zurückgezogen', async () => {
mocks.withdrawCard
.mockRejectedValueOnce(new ApiError(401, 'Bitte melde dich an.'))
.mockResolvedValueOnce(undefined);
await start();
await openConfirm();
button(dialog()!, 'Zurückziehen').click();
await vi.waitFor(() => expect(labels(tileFor('a'))).toContain('Veröffentlichen'));
expect(mocks.requireLogin).toHaveBeenCalledOnce();
expect(mocks.withdrawCard).toHaveBeenCalledTimes(2);
});
it('Seite verlassen mit offener Rückfrage: nichts wird zurückgezogen', async () => {
await start();
await openConfirm();
unmount?.();
unmount = null;
expect(dialog()).toBeNull();
await new Promise((r) => setTimeout(r, 0));
expect(mocks.withdrawCard).not.toHaveBeenCalled();
});
});