fix(account): Abmelden nimmt den Merker „Benutzername schon gefragt“ weg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c55912ebe
commit
53bcecf3cb
2 files changed
+80
-2
No files matched your search
@@ -12,6 +12,8 @@ const mocks = vi.hoisted(() => {
|
|||||||
/** Der angemeldete Nutzer, wie `currentUser`/`currentUserId` ihn nennen. */
|
/** Der angemeldete Nutzer, wie `currentUser`/`currentUserId` ihn nennen. */
|
||||||
user: null as TestUser | null,
|
user: null as TestUser | null,
|
||||||
authListeners: new Set<(user: TestUser | null) => void>(),
|
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(),
|
loadMyUsername: vi.fn(),
|
||||||
usernameDialog: vi.fn(),
|
usernameDialog: vi.fn(),
|
||||||
pages: { editor: page(), cards: page(), community: page(), reset: page(), home: page() },
|
pages: { editor: page(), cards: page(), community: page(), reset: page(), home: page() },
|
||||||
@@ -28,6 +30,10 @@ vi.mock('./account/auth', () => ({
|
|||||||
mocks.authListeners.add(fn);
|
mocks.authListeners.add(fn);
|
||||||
return () => mocks.authListeners.delete(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/login-dialog', () => ({ requireLogin: vi.fn() }));
|
||||||
vi.mock('./account/username-dialog', () => ({ usernameDialog: mocks.usernameDialog }));
|
vi.mock('./account/username-dialog', () => ({ usernameDialog: mocks.usernameDialog }));
|
||||||
@@ -75,6 +81,7 @@ beforeEach(() => {
|
|||||||
mocks.getCurrentUser.mockReset().mockResolvedValue(null); // Gast: der Anmeldestatus ist „abgemeldet“
|
mocks.getCurrentUser.mockReset().mockResolvedValue(null); // Gast: der Anmeldestatus ist „abgemeldet“
|
||||||
mocks.user = null;
|
mocks.user = null;
|
||||||
mocks.authListeners.clear();
|
mocks.authListeners.clear();
|
||||||
|
mocks.logoutListeners.clear();
|
||||||
mocks.loadMyUsername.mockReset().mockResolvedValue(null); // das Konto hat keinen Namen
|
mocks.loadMyUsername.mockReset().mockResolvedValue(null); // das Konto hat keinen Namen
|
||||||
mocks.usernameDialog.mockReset().mockResolvedValue(null); // „Später“
|
mocks.usernameDialog.mockReset().mockResolvedValue(null); // „Später“
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
@@ -251,6 +258,12 @@ describe('Benutzername abfragen (Spec § 5)', () => {
|
|||||||
mocks.user = user;
|
mocks.user = user;
|
||||||
for (const fn of [...mocks.authListeners]) fn(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));
|
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 () => {
|
it('Start mit Sitzung, Konto ohne Namen: der Dialog „prompt“ erscheint genau einmal; der Merker liegt in sessionStorage', async () => {
|
||||||
@@ -313,6 +326,64 @@ describe('Benutzername abfragen (Spec § 5)', () => {
|
|||||||
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
|
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 () => {
|
it('Anmeldung, Konto mit Namen: kein Dialog', async () => {
|
||||||
mocks.loadMyUsername.mockResolvedValue('Kenso');
|
mocks.loadMyUsername.mockResolvedValue('Kenso');
|
||||||
await start('/');
|
await start('/');
|
||||||
@@ -353,6 +424,7 @@ describe('Benutzername abfragen (Spec § 5)', () => {
|
|||||||
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
|
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
mocks.authListeners.clear(); // die erste Seite ist weg
|
mocks.authListeners.clear(); // die erste Seite ist weg
|
||||||
|
mocks.logoutListeners.clear();
|
||||||
mocks.loadMyUsername.mockClear();
|
mocks.loadMyUsername.mockClear();
|
||||||
await start('/');
|
await start('/');
|
||||||
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
|
await vi.waitFor(() => expect(mocks.loadMyUsername).toHaveBeenCalledTimes(1));
|
||||||
@@ -367,6 +439,7 @@ describe('Benutzername abfragen (Spec § 5)', () => {
|
|||||||
|
|
||||||
sessionStorage.clear(); // neuer Tab = neue sessionStorage
|
sessionStorage.clear(); // neuer Tab = neue sessionStorage
|
||||||
mocks.authListeners.clear();
|
mocks.authListeners.clear();
|
||||||
|
mocks.logoutListeners.clear();
|
||||||
await start('/');
|
await start('/');
|
||||||
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
|
await vi.waitFor(() => expect(mocks.usernameDialog).toHaveBeenCalledTimes(2));
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-2
@@ -2,8 +2,8 @@ import '@fontsource-variable/nunito-sans';
|
|||||||
import './styles.css';
|
import './styles.css';
|
||||||
import { mountAccountMenu, type AccountMenu } from './account/account-menu';
|
import { mountAccountMenu, type AccountMenu } from './account/account-menu';
|
||||||
import { client } from './account/appwrite';
|
import { client } from './account/appwrite';
|
||||||
import { currentUserId, getCurrentUser, onAuthChange } from './account/auth';
|
import { currentUserId, getCurrentUser, onAuthChange, onLogout } from './account/auth';
|
||||||
import { loadMyUsername, markUsernamePrompted, usernamePrompted } from './account/profile';
|
import { clearUsernamePrompted, loadMyUsername, markUsernamePrompted, usernamePrompted } from './account/profile';
|
||||||
import { usernameDialog } from './account/username-dialog';
|
import { usernameDialog } from './account/username-dialog';
|
||||||
import { DONATE_URL } from './config';
|
import { DONATE_URL } from './config';
|
||||||
import { currentLang, onLangChange, setLang } from './i18n/i18n';
|
import { currentLang, onLangChange, setLang } from './i18n/i18n';
|
||||||
@@ -94,6 +94,11 @@ onAuthChange((user) => {
|
|||||||
if (user) void checkUsername(user.$id);
|
if (user) void checkUsername(user.$id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Der Merker gilt je Anmeldung: wer sich abmeldet, nimmt ihn mit, und ein weiteres Konto ohne Namen im selben Tab wird gefragt. Nur beim
|
||||||
|
// Abmelden, nicht bei jeder Änderung des Nutzers: das Anmelden beim Registrieren räumt auch eine alte Sitzung weg (onAuthChange(null)), und
|
||||||
|
// die Registrierung hat den Merker da schon gesetzt (login-dialog.ts).
|
||||||
|
onLogout(clearUsernamePrompted);
|
||||||
|
|
||||||
// Der Anmeldestatus wird einmal beim Start geholt; schlägt das fehl (außer 401, das getCurrentUser selbst als „abgemeldet“ meldet),
|
// Der Anmeldestatus wird einmal beim Start geholt; schlägt das fehl (außer 401, das getCurrentUser selbst als „abgemeldet“ meldet),
|
||||||
// gilt der Nutzer als abgemeldet.
|
// gilt der Nutzer als abgemeldet.
|
||||||
if (accountSlot) {
|
if (accountSlot) {
|
||||||
|
|||||||
Reference in new issue
Block a user