516 lines
23 KiB
TypeScript
516 lines
23 KiB
TypeScript
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);
|
||
});
|
||
});
|