fix(cards): Editor an Karte und Konto koppeln (Neue Karte, Abmelden, Kontowechsel)
- Editor zeigt „Bearbeitet: <Name>“, solange eine gespeicherte Karte gebunden ist, und bietet „Neue Karte“ (setzt zurück, bricht Laden ab, entfernt ?karte=; fragt bei ungespeicherten Änderungen per eigenem Dialog „Verwerfen?“) - EditorState.savedConfig als Vergleichsbasis für ungespeicherte Änderungen - bindEditorToAuth: Abmelden leert den Editor, Kontowechsel A -> B entkoppelt die Karte (Entwurf bleibt), Anmelden ohne Vorgänger und abgelaufene Sitzung behalten den Entwurf; auth.onLogout unterscheidet eigenes Abmelden von bemerktem Ablauf - Speichern nimmt den Schnappschuss pro Klick (auch für die Wiederholung nach 401) - save: row_not_found beim Update legt eine neue Karte an (inkl. Aufräumen) - Karten-Seite: Editor-Abgleich auch nach dem Verlassen der Seite, Löschen-Dialog wird beim Verlassen geschlossen, keine doppelten Vorschau-URLs - confirm-dialog als gemeinsames Modul (Löschen, Verwerfen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba4d9f76d4
commit
38b361ad7a
20 files changed
+1038
-116
No files matched your search
@@ -454,3 +454,88 @@ describe('Meine Karten: Löschen', () => {
|
||||
expect(store.get().saved).toEqual(saved);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meine Karten: Seite wird verlassen', () => {
|
||||
const savedRef = (id: string, name: string) => ({
|
||||
id,
|
||||
name,
|
||||
imageFileId: `img-${id}`,
|
||||
previewFileId: `prev-${id}`,
|
||||
updatedAt: '2026-10-04T12:00:00.000Z',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('ein offener Löschen-Dialog wird geschlossen, nichts wird gelöscht', async () => {
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
|
||||
mount();
|
||||
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
|
||||
button(tileFor('a'), 'Löschen').click();
|
||||
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
|
||||
unmount?.();
|
||||
unmount = null;
|
||||
expect(dialog()).toBeNull();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Umbenennen, das erst nach dem Verlassen fertig wird, stellt den Editor-Zustand trotzdem richtig', async () => {
|
||||
store.set({ saved: savedRef('a', 'Alpha'), cardName: 'Alpha' });
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.rename.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
|
||||
button(tileFor('a'), 'Umbenennen').click();
|
||||
const input = tileFor('a').querySelector<HTMLInputElement>('input')!;
|
||||
input.value = 'Neu';
|
||||
submit(input.closest('form')!);
|
||||
await vi.waitFor(() => expect(mocks.rename).toHaveBeenCalled());
|
||||
unmount?.();
|
||||
unmount = null;
|
||||
finish(card('a', 'Neu', { updatedAt: '2026-10-06T08:00:00.000Z' }));
|
||||
await vi.waitFor(() => expect(store.get().cardName).toBe('Neu'));
|
||||
expect(store.get().saved).toMatchObject({ id: 'a', name: 'Neu' });
|
||||
});
|
||||
|
||||
it('Löschen, das erst nach dem Verlassen fertig wird, löst den Editor trotzdem von der Karte', async () => {
|
||||
store.set({ saved: savedRef('a', 'Alpha'), savedConfig: structuredClone(DEFAULT_CONFIG) });
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
|
||||
let finish!: () => void;
|
||||
mocks.remove.mockReturnValue(new Promise<void>((r) => (finish = r)));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
|
||||
button(tileFor('a'), 'Löschen').click();
|
||||
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
|
||||
button(dialog()!, 'Löschen').click();
|
||||
await vi.waitFor(() => expect(mocks.remove).toHaveBeenCalled());
|
||||
unmount?.();
|
||||
unmount = null;
|
||||
finish();
|
||||
await vi.waitFor(() => expect(store.get().saved).toBeNull());
|
||||
expect(store.get().savedConfig).toBeNull();
|
||||
});
|
||||
|
||||
it('eine zweite Vorschau derselben Datei legt keine zweite URL an (sonst bliebe die erste ungenutzt)', async () => {
|
||||
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
|
||||
const pending: Array<(b: Blob) => void> = [];
|
||||
mocks.loadPreview.mockImplementation(() => new Promise<Blob>((r) => pending.push(r)));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(pending).toHaveLength(1));
|
||||
// Abmelden und wieder anmelden lädt die Liste neu, während die erste Vorschau noch unterwegs ist.
|
||||
for (const fn of mocks.authListeners) fn(null);
|
||||
for (const fn of mocks.authListeners) fn(user);
|
||||
await vi.waitFor(() => expect(pending).toHaveLength(2));
|
||||
pending[0](new Blob(['p'], { type: 'image/webp' }));
|
||||
await vi.waitFor(() => expect(tileFor('a').querySelector('img')).not.toBeNull());
|
||||
pending[1](new Blob(['p'], { type: 'image/webp' }));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
||||
unmount?.();
|
||||
unmount = null;
|
||||
expect(revoked).toEqual(['blob:preview-1']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user