Files
AirCardIOS/web/src/main.test.ts
T

516 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
type TestUser = { $id: string; email: string };
// Der Einstieg läuft mit echtem Router, echter Kopfzeile, Banner, Fußzeile und Konto-Menü; Seiten, Anmeldung und Appwrite sind Attrappen.
// Vom Benutzernamen läuft das Echte (Merker in sessionStorage); ersetzt sind nur das Lesen des Profils und der Dialog.
const mocks = vi.hoisted(() => {
const page = () => vi.fn((..._args: unknown[]) => vi.fn()); // baut eine Seite auf und gibt deren Aufräum-Funktion zurück
return {
setLocale: vi.fn(),
getCurrentUser: vi.fn(),
/** Der angemeldete Nutzer, wie `currentUser`/`currentUserId` ihn nennen. */
user: null as TestUser | null,
authListeners: new Set<(user: TestUser | null) => void>(),
/** Die Hörer von `onLogout`: sie hören nur das Abmelden durch den Nutzer, nicht jede Änderung des Nutzers. */
logoutListeners: new Set<() => void>(),
loadMyUsername: vi.fn(),
usernameDialog: vi.fn(),
pages: { editor: page(), cards: page(), community: page(), reset: page(), home: page() },
};
});
vi.mock('./account/appwrite', () => ({ client: { setLocale: mocks.setLocale } }));
vi.mock('./account/auth', () => ({
getCurrentUser: mocks.getCurrentUser,
currentUser: () => mocks.user,
currentUserId: () => mocks.user?.$id ?? null,
logout: vi.fn(),
onAuthChange: (fn: (user: TestUser | null) => void) => {
mocks.authListeners.add(fn);
return () => mocks.authListeners.delete(fn);
},
onLogout: (fn: () => void) => {
mocks.logoutListeners.add(fn);
return () => mocks.logoutListeners.delete(fn);
},
}));
vi.mock('./account/login-dialog', () => ({ requireLogin: vi.fn() }));
vi.mock('./account/username-dialog', () => ({ usernameDialog: mocks.usernameDialog }));
vi.mock('./editor/auth-reset', () => ({ bindEditorToAuth: vi.fn() }));
vi.mock('./editor/editor-page', () => ({ mountEditor: mocks.pages.editor }));
vi.mock('./pages/cards-page', () => ({ mountCardsPage: mocks.pages.cards }));
vi.mock('./pages/community-page', () => ({ mountCommunityPage: mocks.pages.community }));
vi.mock('./pages/reset-page', () => ({ mountResetPage: mocks.pages.reset }));
vi.mock('./pages/home-page', () => ({ mountHomePage: mocks.pages.home }));
const DE_FOOTER = 'Inoffizielle Community-Seite für die App AirCard-iOS';
const EN_FOOTER = 'Unofficial community site for the AirCard-iOS app';
/** Abonnenten von onAuthChange nach dem Start: das Konto-Menü und die Abfrage des Benutzernamens (main.ts); ein Sprachwechsel ändert die Zahl nicht. */
const AUTH_LISTENERS = 2;
const slot = () => document.getElementById('account-slot') as HTMLElement;
const langButton = () => document.querySelector<HTMLButtonElement>('button.lang-btn')!;
const navTexts = () => [...document.querySelectorAll('nav.app-nav a[data-link]')].map((a) => a.textContent);
// Der Anmelden-Knopf des Konto-Menüs in der Sprache der Seite („Anmelden“ auf Deutsch, „Sign in“ nach dem Wechsel auf Englisch).
const signIn = (label = 'Anmelden') => [...slot().querySelectorAll('button')].find((b) => b.textContent === label);
/** Startet main.ts frisch (es läuft beim Import) auf der Adresse `path`; `storedLang` ist die gespeicherte Sprachwahl (null: keine). */
async function start(path = '/', storedLang: string | null = 'de') {
vi.resetModules();
// Das Profil: nur das Lesen im Netz ist ersetzt. Neu nach jedem Zurücksetzen, damit jede Kopie von main.ts ihren eigenen Merker
// „schon gefragt“ im Speicher hat (ein `vi.mock` bliebe bei der ersten Kopie von profile.ts stehen).
vi.doMock('./account/profile', async () => ({
...(await vi.importActual<typeof import('./account/profile')>('./account/profile')),
loadMyUsername: mocks.loadMyUsername,
}));
localStorage.clear();
if (storedLang) localStorage.setItem('aircard.lang', storedLang);
history.replaceState(null, '', path);
document.body.innerHTML =
'<div id="donate-banner"></div>' +
'<header id="app-header"><a class="brand" href="/" data-link><span>AirCard-IOS</span></a><div id="account-slot"></div></header>' +
'<main id="app-root"></main>' +
'<footer id="app-footer"></footer>';
await import('./main');
}
beforeEach(() => {
mocks.setLocale.mockReset();
mocks.getCurrentUser.mockReset().mockResolvedValue(null); // Gast: der Anmeldestatus ist „abgemeldet“
mocks.user = null;
mocks.authListeners.clear();
mocks.logoutListeners.clear();
mocks.loadMyUsername.mockReset().mockResolvedValue(null); // das Konto hat keinen Namen
mocks.usernameDialog.mockReset().mockResolvedValue(null); // „Später“
sessionStorage.clear();
for (const page of Object.values(mocks.pages)) page.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
history.replaceState(null, '', '/');
});
describe('Start', () => {
it('Rahmen in der gespeicherten Sprache; der Appwrite-Client bekommt sie; die Seite der Adresse wird aufgebaut', async () => {
await start('/gestalten');
await vi.waitFor(() => expect(signIn()).toBeDefined());
expect(document.documentElement.lang).toBe('de');
expect(mocks.setLocale).toHaveBeenCalledExactlyOnceWith('de');
expect(navTexts()).toEqual(['Community', 'Gestalten', 'Meine Karten']);
expect(document.querySelector('.donate-banner-link')?.textContent).toBe('Unterstützen →');
expect(document.getElementById('app-footer')?.textContent).toContain(DE_FOOTER);
expect(langButton().textContent).toBe('EN');
expect(document.title).toBe('Gestalten – AirCard-IOS');
expect(mocks.pages.editor).toHaveBeenCalledTimes(1);
});
it('ohne gespeicherte Wahl entscheidet die Browsersprache; der Start speichert nichts', async () => {
vi.stubGlobal('navigator', { languages: ['en-US'], language: 'en-US' });
await start('/karten', null);
expect(document.documentElement.lang).toBe('en');
expect(mocks.setLocale).toHaveBeenCalledExactlyOnceWith('en');
expect(navTexts()).toEqual(['Community', 'Create', 'My cards']);
expect(document.getElementById('app-footer')?.textContent).toContain(EN_FOOTER);
expect(document.title).toBe('My cards – AirCard-IOS');
expect(langButton().textContent).toBe('DE');
expect(localStorage.getItem('aircard.lang')).toBeNull();
});
});
describe('Sprachwechsel', () => {
it('Kopfzeile, Banner, Fußzeile und Titel wechseln ohne Neuladen; der Client bekommt die Sprache', async () => {
await start('/gestalten');
const root = document.getElementById('app-root');
langButton().click();
expect(document.documentElement.lang).toBe('en');
expect(localStorage.getItem('aircard.lang')).toBe('en');
expect(mocks.setLocale).toHaveBeenLastCalledWith('en');
expect(navTexts()).toEqual(['Community', 'Create', 'My cards']);
expect(document.querySelector('.donate-banner-link')?.textContent).toBe('Support us →');
expect(document.getElementById('app-footer')?.textContent).toContain(EN_FOOTER);
expect(langButton().textContent).toBe('DE');
expect(document.title).toBe('Create – AirCard-IOS');
expect(document.getElementById('app-root')).toBe(root); // dasselbe Dokument, die Seite wurde nicht neu geladen
expect(location.pathname).toBe('/gestalten'); // die Adresse bleibt deutsch und unverändert
langButton().click(); // und zurück
expect(document.documentElement.lang).toBe('de');
expect(mocks.setLocale).toHaveBeenLastCalledWith('de');
expect(navTexts()).toEqual(['Community', 'Gestalten', 'Meine Karten']);
expect(document.getElementById('app-footer')?.textContent).toContain(DE_FOOTER);
});
it('die aktuelle Seite wird aufgeräumt und im selben Container neu gebaut; der Editor-Store bleibt dasselbe Objekt', async () => {
const cleanup = vi.fn();
mocks.pages.editor.mockImplementation(() => cleanup);
await start('/gestalten');
const [root, store] = mocks.pages.editor.mock.calls[0] as [HTMLElement, { get(): { cardName: string }; set(patch: object): void }];
store.set({ cardName: 'Mein Entwurf' }); // ein ungespeicherter Entwurf
langButton().click();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(mocks.pages.editor).toHaveBeenCalledTimes(2);
const [rootAfter, storeAfter] = mocks.pages.editor.mock.calls[1] as [HTMLElement, typeof store];
expect(rootAfter).toBe(root);
expect(storeAfter).toBe(store);
expect(storeAfter.get().cardName).toBe('Mein Entwurf');
});
it('ein geschlossenes Spenden-Banner kommt beim Sprachwechsel nicht zurück, auch wenn der Speicher gesperrt ist', async () => {
await start('/');
const denied = () => {
throw new DOMException('blockiert', 'SecurityError');
};
vi.stubGlobal('localStorage', { getItem: denied, setItem: denied });
document.querySelector<HTMLButtonElement>('.donate-banner-close')!.click();
expect(document.querySelector('.donate-banner')).toBeNull();
langButton().click();
expect(document.documentElement.lang).toBe('en');
expect(document.querySelector('.donate-banner')).toBeNull();
});
it('der unberührte Standardname der Karte wechselt mit der Sprache, schon bevor die Seite neu aufgebaut wird; ein eigener Name bleibt', async () => {
const seen: string[] = [];
mocks.pages.editor.mockImplementation((_root: unknown, store: unknown) => {
seen.push((store as { get(): { cardName: string } }).get().cardName);
return vi.fn();
});
await start('/gestalten');
const store = mocks.pages.editor.mock.calls[0][1] as { get(): { cardName: string }; set(patch: object): void };
expect(store.get().cardName).toBe('Meine Karte');
langButton().click();
expect(store.get().cardName).toBe('My card');
expect(seen).toEqual(['Meine Karte', 'My card']); // die neu gebaute Seite sieht den Namen der neuen Sprache
store.set({ cardName: 'Mein Urlaub' });
langButton().click(); // zurück auf Deutsch
expect(store.get().cardName).toBe('Mein Urlaub');
});
it('das Konto-Menü wird neu gebaut und zeigt sich sofort, wenn der Anmeldestatus schon bekannt ist', async () => {
await start('/');
await vi.waitFor(() => expect(signIn()).toBeDefined());
expect(mocks.authListeners.size).toBe(AUTH_LISTENERS);
langButton().click();
expect(signIn('Sign in')).toBeDefined(); // kein leerer Platz bis zur nächsten Änderung, jetzt auf Englisch
expect(mocks.authListeners.size).toBe(AUTH_LISTENERS); // das alte Menü hat sich abgemeldet
expect(mocks.getCurrentUser).toHaveBeenCalledTimes(1); // der Status wird nicht noch einmal geholt
});
it('kommt der Wechsel vor dem Anmeldestatus, bleibt das neue Konto-Menü leer, bis er da ist', async () => {
let answer!: (user: null) => void;
mocks.getCurrentUser.mockReturnValue(new Promise<null>((resolve) => (answer = resolve)));
await start('/');
expect(slot().childElementCount).toBe(0);
langButton().click();
expect(slot().childElementCount).toBe(0);
expect(mocks.authListeners.size).toBe(AUTH_LISTENERS);
answer(null);
await vi.waitFor(() => expect(signIn('Sign in')).toBeDefined());
});
it('der Fokus bleibt auf dem Sprachknopf, auch wenn die Kopfzeile neu gebaut wird', async () => {
await start('/');
const old = langButton();
old.focus();
expect(document.activeElement).toBe(old);
old.click();
expect(langButton()).not.toBe(old);
expect(document.activeElement).toBe(langButton());
});
it('ohne Fokus auf dem Sprachknopf wird kein Fokus gesetzt', async () => {
await start('/');
(document.activeElement as HTMLElement | null)?.blur();
langButton().click();
expect(document.activeElement).toBe(document.body);
});
});
describe('Benutzername abfragen (Spec § 5)', () => {
const kenso: TestUser = { $id: 'u1', email: 'kenso@example.com' };
const PROMPTED_KEY = 'aircard.usernamePrompted';
/** Das Konto ist schon beim Start angemeldet (Sitzung im Browser). */
const sessionAtStart = (user: TestUser = kenso) => {
mocks.user = user;
mocks.getCurrentUser.mockResolvedValue(user);
};
/** Der Nutzer meldet sich an: erst steht er fest, dann hören es alle Abonnenten (wie auth.ts). */
const userSignsIn = (user: TestUser = kenso) => {
mocks.user = user;
for (const fn of [...mocks.authListeners]) fn(user);
};
/** Der Nutzer meldet sich ab: wie in auth.ts erst der Nutzer weg (onAuthChange), dann die Abmelde-Hörer (onLogout). */
const userLogsOut = () => {
mocks.user = null;
for (const fn of [...mocks.authListeners]) fn(null);
for (const fn of [...mocks.logoutListeners]) fn();
};
const settle = () => new Promise((resolve) => setTimeout(resolve, 10));
it('Start mit Sitzung, Konto ohne Namen: der Dialog „prompt“ erscheint genau einmal; der Merker liegt in sessionStorage', async () => {
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
expect(mocks.usernameDialog).toHaveBeenCalledWith({ mode: 'prompt' });
expect(sessionStorage.getItem(PROMPTED_KEY)).toBe('1');
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
it('der Merker steht schon, wenn der Dialog erscheint (ein zweiter Auslöser, solange er offen ist, fragt nicht noch einmal)', async () => {
let flagWhenShown: string | null | undefined;
let close!: () => void;
mocks.usernameDialog.mockImplementation(() => {
flagWhenShown = sessionStorage.getItem(PROMPTED_KEY);
return new Promise<null>((resolve) => (close = () => resolve(null)));
});
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
expect(flagWhenShown).toBe('1');
userSignsIn(); // während der Dialog offen ist
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
close();
await settle();
userSignsIn(); // auch nach „Später“
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
it('Start mit Sitzung, Konto mit Namen: kein Dialog, der Merker bleibt leer', async () => {
mocks.loadMyUsername.mockResolvedValue('Kenso');
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalled());
await settle();
expect(mocks.usernameDialog).not.toHaveBeenCalled();
expect(sessionStorage.getItem(PROMPTED_KEY)).toBeNull();
});
it('Gast: nichts wird gelesen und nichts gefragt', async () => {
await start('/');
await vi.waitFor(() => expect(signIn()).toBeDefined());
await settle();
expect(mocks.loadMyUsername).not.toHaveBeenCalled();
expect(mocks.usernameDialog).not.toHaveBeenCalled();
});
it('Anmeldung nach dem Start, Konto ohne Namen: „prompt“ genau einmal; eine weitere Anmeldung in derselben Sitzung fragt nicht noch einmal', async () => {
await start('/');
await vi.waitFor(() => expect(signIn()).toBeDefined());
userSignsIn();
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
expect(mocks.usernameDialog).toHaveBeenCalledWith({ mode: 'prompt' });
userSignsIn({ $id: 'u1', email: 'kenso@example.com' }); // z. B. ein Labelwechsel des Kontos
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
it('Abmelden nimmt den Merker weg: ein zweites Konto ohne Namen im selben Tab wird gefragt', async () => {
const mia: TestUser = { $id: 'u2', email: 'mia@example.com' };
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
expect(sessionStorage.getItem(PROMPTED_KEY)).toBe('1');
userLogsOut();
expect(sessionStorage.getItem(PROMPTED_KEY)).toBeNull();
userSignsIn(mia);
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
expect(sessionStorage.getItem(PROMPTED_KEY)).toBe('1'); // und für dieses Konto gilt es wieder
userSignsIn(mia);
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(2);
});
it('auch dasselbe Konto wird nach dem Abmelden und erneuten Anmelden (weiter ohne Namen) wieder gefragt', async () => {
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
userLogsOut();
userSignsIn(kenso);
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
});
it('nur das Abmelden räumt den Merker weg, nicht jede Änderung des Nutzers (das Anmelden beim Registrieren räumt eine alte Sitzung weg)', async () => {
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
mocks.user = null;
for (const fn of [...mocks.authListeners]) fn(null); // abgelaufene Sitzung: kein onLogout
expect(sessionStorage.getItem(PROMPTED_KEY)).toBe('1');
});
it('Abmelden ohne gesetzten Merker ist harmlos', async () => {
mocks.loadMyUsername.mockResolvedValue('Kenso');
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
expect(() => userLogsOut()).not.toThrow();
expect(sessionStorage.getItem(PROMPTED_KEY)).toBeNull();
});
it('ist sessionStorage gesperrt, wirft das Abmelden nichts', async () => {
const denied = () => {
throw new DOMException('blockiert', 'SecurityError');
};
vi.stubGlobal('sessionStorage', { getItem: denied, setItem: denied, removeItem: denied });
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
expect(() => userLogsOut()).not.toThrow();
userSignsIn(); // der Merker im Speicher ist mit weg: gefragt wird wieder
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
});
it('Anmeldung, Konto mit Namen: kein Dialog', async () => {
mocks.loadMyUsername.mockResolvedValue('Kenso');
await start('/');
userSignsIn();
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
await settle();
expect(mocks.usernameDialog).not.toHaveBeenCalled();
});
it('Abmelden löst nichts aus', async () => {
mocks.loadMyUsername.mockResolvedValue('Kenso');
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
mocks.user = null;
for (const fn of [...mocks.authListeners]) fn(null);
await settle();
expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1);
expect(mocks.usernameDialog).not.toHaveBeenCalled();
});
it('Start und Anmeldemeldung für dasselbe Konto zugleich (wie im echten Start): ein Lesen, ein Dialog', async () => {
mocks.loadMyUsername.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve(null), 5)));
mocks.getCurrentUser.mockImplementation(async () => {
userSignsIn(); // getCurrentUser meldet den neuen Nutzer an alle Abonnenten, bevor es ihn zurückgibt
return kenso;
});
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
await settle();
expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1);
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
it('Neuladen in derselben Sitzung: der Merker aus sessionStorage hält die Abfrage zurück, der Name wird trotzdem gelesen', async () => {
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
mocks.authListeners.clear(); // die erste Seite ist weg
mocks.logoutListeners.clear();
mocks.loadMyUsername.mockClear();
await start('/');
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
it('eine neue Sitzung (anderer Tab) fragt wieder', async () => {
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
sessionStorage.clear(); // neuer Tab = neue sessionStorage
mocks.authListeners.clear();
mocks.logoutListeners.clear();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
});
it('der Name lässt sich nicht lesen: kein Dialog (das Konto könnte einen haben), kein Merker, kein Absturz', async () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
mocks.loadMyUsername.mockRejectedValue(new TypeError('Failed to fetch'));
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(error).toHaveBeenCalled());
expect(mocks.usernameDialog).not.toHaveBeenCalled();
expect(sessionStorage.getItem(PROMPTED_KEY)).toBeNull();
expect(slot().childElementCount).toBeGreaterThan(0); // die Seite steht
// Beim nächsten Versuch (neue Anmeldemeldung) wird gefragt, wenn der Name lesbar ist.
mocks.loadMyUsername.mockResolvedValue(null);
userSignsIn();
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
error.mockRestore();
});
it('scheitert der Dialog beim Öffnen, bleibt es bei einer Meldung in der Konsole', async () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
mocks.usernameDialog.mockRejectedValue(new Error('showModal kaputt'));
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(error).toHaveBeenCalled());
expect(slot().childElementCount).toBeGreaterThan(0);
error.mockRestore();
});
it('wird der Nutzer abgemeldet, während sein Name gelesen wird, erscheint kein Dialog', async () => {
let answer!: (name: string | null) => void;
mocks.loadMyUsername.mockReturnValue(new Promise((resolve) => (answer = resolve)));
await start('/');
userSignsIn();
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
mocks.user = null; // abgemeldet, bevor die Antwort da ist
answer(null);
await settle();
expect(mocks.usernameDialog).not.toHaveBeenCalled();
expect(sessionStorage.getItem(PROMPTED_KEY)).toBeNull();
});
it('wechselt das Konto, während gelesen wird, wird das andere Konto gefragt, nicht das alte', async () => {
const mia: TestUser = { $id: 'u2', email: 'mia@example.com' };
const answers: Array<(name: string | null) => void> = [];
mocks.loadMyUsername.mockImplementation(() => new Promise((resolve) => answers.push(resolve)));
await start('/');
userSignsIn(kenso);
await vi.waitFor(() => expect(answers).toHaveLength(1));
userSignsIn(mia); // Kontowechsel, bevor die erste Antwort da ist
await vi.waitFor(() => expect(answers).toHaveLength(2));
answers[0](null); // die Antwort für das alte Konto: ohne Wirkung
await settle();
expect(mocks.usernameDialog).not.toHaveBeenCalled();
answers[1](null);
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
});
it('ist sessionStorage gesperrt, wirft nichts, und gefragt wird einmal pro Seitenaufruf', async () => {
const denied = () => {
throw new DOMException('blockiert', 'SecurityError');
};
vi.stubGlobal('sessionStorage', { getItem: denied, setItem: denied, removeItem: denied });
sessionAtStart();
await start('/');
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
userSignsIn();
await settle();
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
});
});