feat(cards): Karten speichern und Meine Karten
- account/cards.ts: Kartendienst (neu/bestehend speichern mit Aufräumen halber Uploads, Umbenennen, Löschen, Laden) über ein schmales CardsApi-Interface, Appwrite-Anschluss mit 22.4.1-Objektparametern, fetchFile über client.prepareRequest (Projekt-Header + Cookie, kein getFileView) - account/with-login.ts: bei 401 Nutzer neu prüfen, einmal anmelden, wiederholen - editor/save.ts: Speichern-Ablauf (ein Lauf zur Zeit, 401 -> Login -> einmal wiederholen, deutsche Fehlermeldung) - Editor: Speichern mit Schnappschuss und 480-px-Vorschau, Karte öffnen über ?karte=<id>, Bildwechsel während des Speicherns gesperrt - pages/cards-page.ts: /karten mit Kacheln, Öffnen, Herunterladen, Umbenennen, Löschen (eigener Dialog), Abmelde- und Leer-Ansicht - Tests für Dienst, withLogin, Speichern-Ablauf, Karten-Seite und Editor-Anbindung Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a147d3b496
commit
ba4d9f76d4
14 files changed
+2849
-16
No files matched your search
@@ -0,0 +1,453 @@
|
||||
// Speichern und Öffnen im Editor (Karten-Dienst, Anmeldung und Renderer sind ersetzt).
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { User } from '../account/auth';
|
||||
import type { SavedCard } from '../account/cards';
|
||||
import { DEFAULT_CONFIG } from '../card/config';
|
||||
import type { CardImage } from '../card/render';
|
||||
import { createStore, initialEditorState, type EditorStore } from './state';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
save: vi.fn(),
|
||||
loadImage: vi.fn(),
|
||||
getCurrentUser: vi.fn(),
|
||||
requireLogin: vi.fn(),
|
||||
decodeImage: vi.fn(),
|
||||
loadCardAssets: vi.fn(),
|
||||
exportCard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../account/appwrite', () => ({ account: {}, client: {}, tables: {}, storage: {}, IDS: {}, APPWRITE_ENDPOINT: '' }));
|
||||
vi.mock('../account/auth', () => ({ getCurrentUser: mocks.getCurrentUser, currentUserId: () => 'u1', onAuthChange: () => () => {} }));
|
||||
vi.mock('../account/login-dialog', () => ({ requireLogin: mocks.requireLogin }));
|
||||
vi.mock('../account/cards', () => ({
|
||||
cardsService: () => ({ get: mocks.get, save: mocks.save, loadImage: mocks.loadImage }),
|
||||
}));
|
||||
vi.mock('../card/render', () => ({
|
||||
drawCard: vi.fn(),
|
||||
loadCardAssets: mocks.loadCardAssets,
|
||||
exportCard: mocks.exportCard,
|
||||
}));
|
||||
vi.mock('./upload', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('./upload')>()),
|
||||
decodeImage: mocks.decodeImage,
|
||||
}));
|
||||
|
||||
import { mountEditor } from './editor-page';
|
||||
|
||||
const user: User = { $id: 'u1', email: 'a@b.de' };
|
||||
const image = (w = 3000, h = 2000): CardImage => ({ source: {} as CanvasImageSource, w, h });
|
||||
const png = () => new Blob(['png'], { type: 'image/png' });
|
||||
|
||||
const savedCard = (patch: Partial<SavedCard> = {}): SavedCard => ({
|
||||
id: 'k1',
|
||||
name: 'Urlaub',
|
||||
imageFileId: 'img1',
|
||||
previewFileId: 'prev1',
|
||||
updatedAt: '2026-10-04T12:00:00.000Z',
|
||||
config: { ...structuredClone(DEFAULT_CONFIG), template: 'minimal', crop: { x: 99999, y: -99999, zoom: 2 } },
|
||||
...patch,
|
||||
});
|
||||
|
||||
let root: HTMLElement;
|
||||
let store: EditorStore;
|
||||
let unmount: (() => void) | null;
|
||||
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(mocks)) fn.mockReset();
|
||||
vi.stubGlobal('requestAnimationFrame', () => 0);
|
||||
vi.stubGlobal('cancelAnimationFrame', () => {});
|
||||
history.replaceState(null, '', '/');
|
||||
document.body.innerHTML = '<main id="app-root"></main>';
|
||||
root = document.getElementById('app-root') as HTMLElement;
|
||||
store = createStore(initialEditorState());
|
||||
mocks.getCurrentUser.mockResolvedValue(user);
|
||||
mocks.requireLogin.mockResolvedValue(true);
|
||||
mocks.loadCardAssets.mockResolvedValue({ image: null, network: {}, brand: {} });
|
||||
mocks.exportCard.mockResolvedValue(new Blob(['webp'], { type: 'image/webp' }));
|
||||
unmount = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmount?.();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const mount = () => {
|
||||
unmount = mountEditor(root, store);
|
||||
};
|
||||
const saveButton = () => root.querySelector<HTMLButtonElement>('.btn-save')!;
|
||||
const replaceButton = () => [...root.querySelectorAll('button')].find((b) => b.textContent === 'Bild ersetzen') as HTMLButtonElement;
|
||||
const fileInput = () => root.querySelector<HTMLInputElement>('input[type="file"]')!;
|
||||
const errorText = () => root.querySelector('.msg-error')!.textContent;
|
||||
const warningText = () => root.querySelector('.msg-warning')!.textContent;
|
||||
const noticeText = () => root.querySelector('.msg-notice')!.textContent;
|
||||
|
||||
/** Der Editor mit einem gewählten Bild, bereit zum Speichern. */
|
||||
function withImage(blob = png(), img = image()): { blob: Blob; img: CardImage } {
|
||||
store.set({ image: img, imageBlob: blob, imageChanged: true });
|
||||
return { blob, img };
|
||||
}
|
||||
|
||||
describe('Speichern', () => {
|
||||
it('speichert eine neue Karte mit Schnappschuss, Vorschau und Nutzer-Prüfung', async () => {
|
||||
const { blob } = withImage();
|
||||
store.set({ cardName: 'Mein Urlaub', config: { ...store.get().config, network: 'jcb' } });
|
||||
const result = savedCard({ id: 'neu', name: 'Mein Urlaub' });
|
||||
mocks.save.mockResolvedValue(result);
|
||||
mount();
|
||||
expect(saveButton().textContent).toBe('Speichern');
|
||||
|
||||
saveButton().click();
|
||||
expect(saveButton().disabled).toBe(true);
|
||||
expect(saveButton().textContent).toBe('Speichert …');
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
|
||||
expect(mocks.getCurrentUser).toHaveBeenCalled();
|
||||
expect(mocks.exportCard).toHaveBeenCalledWith(expect.objectContaining({ network: 'jcb' }), expect.anything(), {
|
||||
width: 480,
|
||||
type: 'image/webp',
|
||||
quality: 0.85,
|
||||
});
|
||||
const input = mocks.save.mock.calls[0][0];
|
||||
expect(input).toMatchObject({ name: 'Mein Urlaub', image: blob, imageChanged: true, previous: null });
|
||||
expect(input.config.network).toBe('jcb');
|
||||
expect(input.preview.type).toBe('image/webp');
|
||||
|
||||
const s = store.get();
|
||||
expect(s.saved).toEqual({
|
||||
id: 'neu',
|
||||
name: 'Mein Urlaub',
|
||||
imageFileId: 'img1',
|
||||
previewFileId: 'prev1',
|
||||
updatedAt: '2026-10-04T12:00:00.000Z',
|
||||
});
|
||||
expect(s.imageChanged).toBe(false);
|
||||
expect(s.saving).toBe(false);
|
||||
expect(saveButton().disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('„Gespeichert“ verschwindet nach zwei Sekunden', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
||||
withImage();
|
||||
mocks.save.mockResolvedValue(savedCard());
|
||||
mount();
|
||||
saveButton().click();
|
||||
// vi.waitFor würde die falschen Uhren selbst vorstellen; deshalb nur die Zusagen abarbeiten.
|
||||
for (let i = 0; i < 50 && saveButton().textContent !== 'Gespeichert'; i += 1) await vi.advanceTimersByTimeAsync(0);
|
||||
expect(saveButton().textContent).toBe('Gespeichert');
|
||||
await vi.advanceTimersByTimeAsync(1999);
|
||||
expect(saveButton().textContent).toBe('Gespeichert');
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(saveButton().textContent).toBe('Speichern');
|
||||
});
|
||||
|
||||
it('bestehende Karte: previous und imageChanged gehen an den Dienst, danach ist imageChanged false', async () => {
|
||||
withImage();
|
||||
const ref = { id: 'k1', name: 'Urlaub', imageFileId: 'img0', previewFileId: 'prev0', updatedAt: '2026-10-03T12:00:00.000Z' };
|
||||
store.set({ saved: ref, cardName: 'Urlaub', imageChanged: false });
|
||||
mocks.save.mockResolvedValue(savedCard({ imageFileId: 'img0', previewFileId: 'prev9' }));
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(mocks.save.mock.calls[0][0]).toMatchObject({ previous: ref, imageChanged: false });
|
||||
expect(store.get().saved?.previewFileId).toBe('prev9');
|
||||
});
|
||||
|
||||
it('ohne Bild: Meldung, kein Speichern', () => {
|
||||
mount();
|
||||
// Der Knopf ist ohne Bild gesperrt; der Aufruf von außen (z. B. Tastatur-Enter in einem Formular) wird trotzdem abgefangen.
|
||||
saveButton().disabled = false;
|
||||
saveButton().click();
|
||||
expect(errorText()).toBe('Bitte zuerst ein Bild wählen.');
|
||||
expect(mocks.save).not.toHaveBeenCalled();
|
||||
expect(mocks.getCurrentUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Doppelklick speichert nur einmal', async () => {
|
||||
withImage();
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.save.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mount();
|
||||
saveButton().click();
|
||||
saveButton().disabled = false; // selbst wenn der Klick den Knopf erreicht
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(mocks.save).toHaveBeenCalled());
|
||||
finish(savedCard());
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(mocks.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('nicht angemeldet: Login-Dialog, nach dem Anmelden wird gespeichert; abgebrochen: nichts', async () => {
|
||||
withImage();
|
||||
mocks.getCurrentUser.mockResolvedValue(null);
|
||||
mocks.requireLogin.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
mocks.save.mockResolvedValue(savedCard());
|
||||
mount();
|
||||
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Speichern'));
|
||||
expect(mocks.requireLogin).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.save).not.toHaveBeenCalled();
|
||||
expect(store.get().saving).toBe(false);
|
||||
expect(errorText()).toBe('');
|
||||
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(mocks.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Sitzung abgelaufen (401): einmal anmelden und genau einmal wiederholen', async () => {
|
||||
withImage();
|
||||
mocks.save
|
||||
.mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { code: 401 }))
|
||||
.mockResolvedValueOnce(savedCard());
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(mocks.requireLogin).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.save).toHaveBeenCalledTimes(2);
|
||||
expect(errorText()).toBe('');
|
||||
});
|
||||
|
||||
it('Fehler: Meldung mit Präfix, Editor-Zustand bleibt, erneutes Speichern geht', async () => {
|
||||
const { blob } = withImage();
|
||||
mocks.save.mockRejectedValueOnce(new TypeError('offline')).mockResolvedValueOnce(savedCard());
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(errorText()).toBe('Speichern fehlgeschlagen – Keine Verbindung zum Server.'));
|
||||
expect(saveButton().disabled).toBe(false);
|
||||
expect(saveButton().textContent).toBe('Speichern');
|
||||
expect(store.get().imageBlob).toBe(blob);
|
||||
expect(store.get().saved).toBeNull();
|
||||
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(errorText()).toBe('');
|
||||
});
|
||||
|
||||
it('während des Speicherns sind „Bild ersetzen“, die Dateiwahl und Ablegen gesperrt', async () => {
|
||||
const { blob, img } = withImage();
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.save.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(mocks.save).toHaveBeenCalled());
|
||||
expect(replaceButton().disabled).toBe(true);
|
||||
expect(fileInput().disabled).toBe(true);
|
||||
|
||||
// Ein Bild, das jetzt abgelegt wird, wird ignoriert.
|
||||
const file = new File(['x'], 'neu.png', { type: 'image/png' });
|
||||
const drop = new Event('drop', { bubbles: true, cancelable: true }) as Event & { dataTransfer: unknown };
|
||||
drop.dataTransfer = { types: ['Files'], files: [file] };
|
||||
root.querySelector('.editor')!.dispatchEvent(drop);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.decodeImage).not.toHaveBeenCalled();
|
||||
expect(store.get().imageBlob).toBe(blob);
|
||||
expect(store.get().image).toBe(img);
|
||||
|
||||
finish(savedCard());
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
expect(replaceButton().disabled).toBe(false);
|
||||
expect(fileInput().disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('nach dem Speichern darf das Bild wieder ersetzt werden und die Karte bleibt „bestehend“', async () => {
|
||||
withImage();
|
||||
mocks.save.mockResolvedValue(savedCard());
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(saveButton().textContent).toBe('Gespeichert'));
|
||||
mocks.decodeImage.mockResolvedValue(image(1000, 1000));
|
||||
const file = new File(['x'], 'neu.png', { type: 'image/png' });
|
||||
const drop = new Event('drop', { bubbles: true, cancelable: true }) as Event & { dataTransfer: unknown };
|
||||
drop.dataTransfer = { types: ['Files'], files: [file] };
|
||||
root.querySelector('.editor')!.dispatchEvent(drop);
|
||||
await vi.waitFor(() => expect(store.get().imageChanged).toBe(true));
|
||||
expect(store.get().saved?.id).toBe('k1');
|
||||
});
|
||||
|
||||
it('ein Speichern, das erst fertig wird, nachdem eine andere Karte geöffnet wurde, überschreibt deren Zustand nicht', async () => {
|
||||
withImage();
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.save.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mount();
|
||||
saveButton().click();
|
||||
await vi.waitFor(() => expect(mocks.save).toHaveBeenCalled());
|
||||
// Eine andere Karte übernimmt den Editor (z. B. Öffnen aus „Meine Karten“).
|
||||
const otherBlob = png();
|
||||
const otherRef = { id: 'andere', name: 'Andere', imageFileId: 'i', previewFileId: 'p', updatedAt: '2026-10-04T12:00:00.000Z' };
|
||||
store.set({ imageBlob: otherBlob, saved: otherRef, cardName: 'Andere', imageChanged: false });
|
||||
finish(savedCard({ id: 'neu' }));
|
||||
await vi.waitFor(() => expect(store.get().saving).toBe(false));
|
||||
expect(store.get().saved).toEqual(otherRef);
|
||||
expect(store.get().cardName).toBe('Andere');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Karte öffnen (/?karte=<id>)', () => {
|
||||
const open = (id: string) => history.replaceState(null, '', `/?karte=${id}`);
|
||||
|
||||
it('lädt Karte und Bild, begrenzt den Ausschnitt und setzt den Zustand', async () => {
|
||||
open('k1');
|
||||
const card = savedCard();
|
||||
const img = image(3000, 2000);
|
||||
const blob = new Blob(['i'], { type: 'image/jpeg' });
|
||||
mocks.get.mockResolvedValue(card);
|
||||
mocks.loadImage.mockResolvedValue(blob);
|
||||
mocks.decodeImage.mockResolvedValue(img);
|
||||
mount();
|
||||
expect(noticeText()).toBe('Karte wird geladen …');
|
||||
|
||||
await vi.waitFor(() => expect(store.get().saved?.id).toBe('k1'));
|
||||
expect(mocks.get).toHaveBeenCalledWith('k1');
|
||||
expect(mocks.loadImage).toHaveBeenCalledWith(card);
|
||||
expect(mocks.decodeImage).toHaveBeenCalledWith(blob);
|
||||
const s = store.get();
|
||||
expect(s.image).toBe(img);
|
||||
expect(s.imageBlob).toBe(blob);
|
||||
expect(s.imageChanged).toBe(false);
|
||||
expect(s.cardName).toBe('Urlaub');
|
||||
expect(s.config.template).toBe('minimal');
|
||||
expect(s.config.crop.zoom).toBe(2);
|
||||
expect(Math.abs(s.config.crop.x)).toBeLessThan(99999);
|
||||
expect(Math.abs(s.config.crop.y)).toBeLessThan(99999);
|
||||
expect(s.saved).toEqual({
|
||||
id: 'k1',
|
||||
name: 'Urlaub',
|
||||
imageFileId: 'img1',
|
||||
previewFileId: 'prev1',
|
||||
updatedAt: '2026-10-04T12:00:00.000Z',
|
||||
});
|
||||
expect(noticeText()).toBe('');
|
||||
expect(errorText()).toBe('');
|
||||
});
|
||||
|
||||
it('lädt nicht erneut, wenn der Editor die Karte schon hält', async () => {
|
||||
open('k1');
|
||||
store.set({ saved: { id: 'k1', name: 'Urlaub', imageFileId: 'i', previewFileId: 'p', updatedAt: 'x' } });
|
||||
mount();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ohne ?karte wird nichts geladen', async () => {
|
||||
mount();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fehlt die Bilddatei, öffnet die Karte ohne Bild mit Hinweis', async () => {
|
||||
open('k1');
|
||||
mocks.get.mockResolvedValue(savedCard());
|
||||
mocks.loadImage.mockRejectedValue(Object.assign(new Error('weg'), { code: 404, type: 'storage_file_not_found' }));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(store.get().saved?.id).toBe('k1'));
|
||||
const s = store.get();
|
||||
expect(s.image).toBeNull();
|
||||
expect(s.imageBlob).toBeNull();
|
||||
expect(s.imageChanged).toBe(false);
|
||||
expect(s.cardName).toBe('Urlaub');
|
||||
expect(s.config.template).toBe('minimal');
|
||||
expect(warningText()).toBe('Das Bild dieser Karte fehlt.');
|
||||
expect(noticeText()).toBe('');
|
||||
expect(mocks.decodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Karte nicht gefunden: Meldung und leerer Editor', async () => {
|
||||
open('gibtsnicht');
|
||||
store.set({ image: image(), imageBlob: png(), cardName: 'Altlast' });
|
||||
mocks.get.mockRejectedValue(Object.assign(new Error('nope'), { code: 404, type: 'row_not_found' }));
|
||||
mount();
|
||||
await vi.waitFor(() =>
|
||||
expect(errorText()).toBe('Diese Karte gibt es nicht (mehr) oder sie gehört nicht zu deinem Konto.'),
|
||||
);
|
||||
expect(store.get()).toEqual(initialEditorState());
|
||||
expect(noticeText()).toBe('');
|
||||
});
|
||||
|
||||
it('Anmeldung abgebrochen: Hinweis, Editor unverändert', async () => {
|
||||
open('k1');
|
||||
mocks.get.mockRejectedValue(Object.assign(new Error('Unauthorized'), { code: 401 }));
|
||||
mocks.requireLogin.mockResolvedValue(false);
|
||||
mount();
|
||||
await vi.waitFor(() => expect(warningText()).toBe('Melde dich an, um diese Karte zu öffnen.'));
|
||||
expect(store.get().saved).toBeNull();
|
||||
expect(noticeText()).toBe('');
|
||||
});
|
||||
|
||||
it('Sitzung abgelaufen, Anmeldung klappt: Karte wird danach geöffnet', async () => {
|
||||
open('k1');
|
||||
mocks.get
|
||||
.mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { code: 401 }))
|
||||
.mockResolvedValueOnce(savedCard());
|
||||
mocks.loadImage.mockResolvedValue(png());
|
||||
mocks.decodeImage.mockResolvedValue(image());
|
||||
mount();
|
||||
await vi.waitFor(() => expect(store.get().saved?.id).toBe('k1'));
|
||||
expect(mocks.requireLogin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('anderer Fehler (Netz): Meldung, Editor unverändert', async () => {
|
||||
open('k1');
|
||||
mocks.get.mockRejectedValue(new TypeError('offline'));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(errorText()).toBe('Keine Verbindung zum Server.'));
|
||||
expect(store.get().saved).toBeNull();
|
||||
});
|
||||
|
||||
it('ein Bild, das sich nicht dekodieren lässt, meldet das, ohne die Karte zu übernehmen', async () => {
|
||||
open('k1');
|
||||
mocks.get.mockResolvedValue(savedCard());
|
||||
mocks.loadImage.mockResolvedValue(png());
|
||||
mocks.decodeImage.mockRejectedValue(new Error('Das Bild konnte nicht gelesen werden.'));
|
||||
mount();
|
||||
await vi.waitFor(() => expect(errorText()).toBe('Das Bild konnte nicht gelesen werden.'));
|
||||
expect(store.get().saved).toBeNull();
|
||||
});
|
||||
|
||||
it('verlässt der Nutzer die Seite während des Ladens, wird nichts mehr übernommen', async () => {
|
||||
open('k1');
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.get.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mount();
|
||||
unmount?.();
|
||||
unmount = null;
|
||||
finish(savedCard());
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.loadImage).not.toHaveBeenCalled();
|
||||
expect(store.get().saved).toBeNull();
|
||||
});
|
||||
|
||||
it('wählt der Nutzer während des Ladens selbst ein Bild, gewinnt dieses', async () => {
|
||||
open('k1');
|
||||
let finish!: (c: SavedCard) => void;
|
||||
mocks.get.mockReturnValue(new Promise<SavedCard>((r) => (finish = r)));
|
||||
mocks.decodeImage.mockResolvedValue(image(500, 500));
|
||||
mount();
|
||||
const file = new File(['x'], 'neu.png', { type: 'image/png' });
|
||||
const drop = new Event('drop', { bubbles: true, cancelable: true }) as Event & { dataTransfer: unknown };
|
||||
drop.dataTransfer = { types: ['Files'], files: [file] };
|
||||
root.querySelector('.editor')!.dispatchEvent(drop);
|
||||
await vi.waitFor(() => expect(store.get().image?.w).toBe(500));
|
||||
finish(savedCard());
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.loadImage).not.toHaveBeenCalled();
|
||||
expect(store.get().saved).toBeNull();
|
||||
expect(noticeText()).toBe('');
|
||||
});
|
||||
|
||||
it('eine unbrauchbare Datei bricht das Laden der Karte nicht ab', async () => {
|
||||
open('k1');
|
||||
mocks.get.mockResolvedValue(savedCard());
|
||||
mocks.loadImage.mockResolvedValue(png());
|
||||
mocks.decodeImage.mockResolvedValue(image());
|
||||
mount();
|
||||
const file = new File(['x'], 'neu.gif', { type: 'image/gif' });
|
||||
const drop = new Event('drop', { bubbles: true, cancelable: true }) as Event & { dataTransfer: unknown };
|
||||
drop.dataTransfer = { types: ['Files'], files: [file] };
|
||||
root.querySelector('.editor')!.dispatchEvent(drop);
|
||||
await vi.waitFor(() => expect(store.get().saved?.id).toBe('k1'));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user