fix(cards): Sitzung, Entwurf und Datei-Aufräumen beim Öffnen und Speichern

- Gäste/abgelaufene Sitzung: Appwrite antwortet mit 404; withLogin und
  openSavedCard melden dann an (Dialog, einmal wiederholen) statt
  „nicht gefunden“ zu zeigen.
- Öffnen einer Karte fragt vor dem Verwerfen eines ungespeicherten Entwurfs
  nach; ein fehlgeschlagenes Öffnen setzt einen gefüllten Editor nicht zurück.
- Update ohne Bildwechsel schickt kein imageFileId mehr (veralteter Stand
  zeigte sonst auf eine gelöschte Datei).
- Uploads werden nur nach eindeutiger Ablehnung (4xx ohne 408/429) gelöscht,
  nicht bei Netzfehler, 5xx oder 429.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 11:01:28 +00:00
1 parent 09de06e797
commit 0c51689dfd
7 files changed
+546 -57

No files matched your search

+69
View File
@@ -279,6 +279,75 @@ describe('Meine Karten: Öffnen und Herunterladen', () => {
});
});
describe('Meine Karten: Sitzung weg, Appwrite antwortet mit 404 statt 401', () => {
const fileGone = () => Object.assign(new Error('File not found'), { code: 404, type: 'storage_file_not_found' });
const rowGone = () => Object.assign(new Error('Row not found'), { code: 404, type: 'row_not_found' });
/** Beim Laden der Seite ist der Nutzer noch da; danach (Nutzerprüfung nach dem 404) nicht mehr. */
const sessionEndsAfterLoad = () => mocks.getCurrentUser.mockResolvedValueOnce(user).mockResolvedValue(null);
it('Vorschau: erst anmelden, dann wird die Vorschau geladen (statt „Keine Vorschau“)', async () => {
sessionEndsAfterLoad();
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
mocks.loadPreview.mockReset();
mocks.loadPreview
.mockRejectedValueOnce(fileGone())
.mockImplementation(async () => new Blob(['p'], { type: 'image/webp' }));
mount();
await vi.waitFor(() => expect(tileFor('a').querySelector('img')).not.toBeNull());
expect(mocks.requireLogin).toHaveBeenCalledOnce();
expect(mocks.loadPreview).toHaveBeenCalledTimes(2);
});
it('Herunterladen: erst anmelden, dann wird das Bild geladen (statt „Das Bild dieser Karte fehlt.“)', async () => {
const png = new Blob(['png'], { type: 'image/png' });
const image = { source: {}, w: 3000, h: 2000 };
sessionEndsAfterLoad();
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
mocks.loadImage.mockRejectedValueOnce(fileGone()).mockResolvedValueOnce(new Blob(['i'], { type: 'image/jpeg' }));
mocks.decodeImage.mockResolvedValue(image);
mocks.loadCardAssets.mockResolvedValue({ image, network: null, brand: null });
mocks.exportCard.mockResolvedValue(png);
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
button(tileFor('a'), 'Herunterladen').click();
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledWith(png, 'aircard-alpha.png'));
expect(mocks.requireLogin).toHaveBeenCalledOnce();
expect(tileFor('a').querySelector('.card-msg')!.textContent).toBe('');
});
it('Löschen: die Karte gilt nicht als „schon gelöscht“, sondern wird nach dem Anmelden wirklich gelöscht', async () => {
sessionEndsAfterLoad();
mocks.list.mockResolvedValue([card('a', 'Alpha'), card('b', 'Beta')]);
mocks.remove.mockRejectedValueOnce(rowGone()).mockResolvedValueOnce(undefined);
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(2));
await vi.waitFor(() => expect(button(tileFor('b'), 'Löschen').disabled).toBe(false));
button(tileFor('b'), 'Löschen').click();
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
button(dialog()!, 'Löschen').click();
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
expect(mocks.requireLogin).toHaveBeenCalledOnce();
expect(mocks.remove).toHaveBeenCalledTimes(2);
});
it('Löschen, Anmeldung abgebrochen: die Karte bleibt, Hinweis an der Kachel', async () => {
sessionEndsAfterLoad();
mocks.requireLogin.mockResolvedValue(false);
mocks.list.mockResolvedValue([card('a', 'Alpha')]);
mocks.remove.mockRejectedValue(rowGone());
mount();
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
await vi.waitFor(() => expect(button(tileFor('a'), 'Löschen').disabled).toBe(false));
button(tileFor('a'), 'Löschen').click();
await vi.waitFor(() => expect(dialog()?.open).toBe(true));
button(dialog()!, 'Löschen').click();
await vi.waitFor(() =>
expect(tileFor('a').querySelector('.card-msg')!.textContent).toBe('Bitte melde dich an, um fortzufahren.'),
);
expect(tiles()).toHaveLength(1);
});
});
describe('Meine Karten: Umbenennen', () => {
const startRename = async () => {
mocks.list.mockResolvedValue([card('a', 'Alpha'), card('b', 'Beta')]);