fix(passwort): Erfolgsansicht überlebt den Neuaufbau der Seite beim Sprachwechsel
Schlussreview F3. showDone() entfernt userId und secret aus der Adresse. Wechselte der Nutzer danach die Sprache, baute main.ts die Seite neu auf und sie las eine Adresse ohne Link: „Der Link ist ungültig oder abgelaufen.“ statt des Erfolgs. - Ein Stand außerhalb der Seite (`passwordChanged`, Modulvariable) merkt, dass das Passwort gesetzt wurde; ein Neuaufbau zeigt dann wieder die Erfolgsansicht (in der neuen Sprache) mit „Anmelden“. - Ein frischer Link (userId und secret in der Adresse) setzt den Stand zurück und zeigt das Formular. Gemerkt wird nur der Erfolg, ein Fehlschlag nicht. - Tests: Neuaufbau nach dem Erfolg (deutsch, mehrfach, englisch), frischer Link danach, Seite ohne Link bleibt ungültig, Fehlschlag wird nicht als Erfolg gemerkt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec4e80c62c
commit
b3c6e65801
2 files changed
+93
-1
No files matched your search
@@ -151,3 +151,85 @@ describe('mit Link', () => {
|
|||||||
expect(root.querySelector('form')).not.toBeNull();
|
expect(root.querySelector('form')).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Neuaufbau der Seite (Sprachwechsel) nach dem Erfolg', () => {
|
||||||
|
const SUCCESS = 'Passwort geändert. Du kannst dich jetzt anmelden.';
|
||||||
|
|
||||||
|
/** Setzt das Passwort mit dem Link und wartet auf die Erfolgsansicht; gibt die Aufräum-Funktion der Seite zurück. */
|
||||||
|
async function succeed(): Promise<() => void> {
|
||||||
|
withLink();
|
||||||
|
mocks.updateRecovery.mockResolvedValue({});
|
||||||
|
const unmount = mountResetPage(root);
|
||||||
|
fill('neuesPasswort1');
|
||||||
|
submit();
|
||||||
|
await vi.waitFor(() => expect(root.querySelector('[role="status"]')!.textContent).toBe(SUCCESS));
|
||||||
|
expect(location.search).toBe(''); // das Geheimnis ist aus der Adresse
|
||||||
|
return unmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('ein Neuaufbau zeigt wieder den Erfolg und „Anmelden“, nicht „Der Link ist ungültig oder abgelaufen.“', async () => {
|
||||||
|
const unmount = await succeed();
|
||||||
|
unmount(); // main.ts: der Router räumt die Seite auf und baut sie mit derselben Adresse neu auf
|
||||||
|
mountResetPage(root);
|
||||||
|
expect(root.querySelector('[role="status"]')!.textContent).toBe(SUCCESS);
|
||||||
|
expect(root.querySelector('[role="alert"]')).toBeNull();
|
||||||
|
expect(root.querySelector('form')).toBeNull();
|
||||||
|
expect(root.querySelector('h1')!.textContent).toBe('Passwort neu setzen');
|
||||||
|
expect(location.search).toBe('');
|
||||||
|
button('Anmelden').click();
|
||||||
|
expect(document.querySelector<HTMLDialogElement>('dialog.auth-dialog')!.open).toBe(true);
|
||||||
|
expect(mocks.updateRecovery).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wiederholt sich beliebig oft (mehrere Sprachwechsel hintereinander)', async () => {
|
||||||
|
let unmount = await succeed();
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
unmount();
|
||||||
|
unmount = mountResetPage(root);
|
||||||
|
expect(root.querySelector('[role="status"]')!.textContent).toBe(SUCCESS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sprachwechsel auf Englisch: der Neuaufbau zeigt den Erfolg übersetzt', async () => {
|
||||||
|
const unmount = await succeed();
|
||||||
|
const { setLang } = await import('../i18n/i18n'); // dieselbe Kopie des Moduls wie die der Seite (siehe vi.resetModules oben)
|
||||||
|
setLang('en', { persist: false });
|
||||||
|
unmount();
|
||||||
|
mountResetPage(root);
|
||||||
|
expect(root.querySelector('[role="status"]')!.textContent).toBe('Password changed. You can now sign in.');
|
||||||
|
expect(root.querySelector('[role="alert"]')).toBeNull();
|
||||||
|
expect(button('Sign in')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein frischer Link (userId und secret in der Adresse) zeigt wieder das Formular', async () => {
|
||||||
|
const unmount = await succeed();
|
||||||
|
unmount();
|
||||||
|
withLink();
|
||||||
|
mountResetPage(root);
|
||||||
|
expect(root.querySelector('form')).not.toBeNull();
|
||||||
|
expect(root.querySelector('[role="status"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ohne vorherigen Erfolg bleibt die Seite ohne Link ungültig, auch nach einem Neuaufbau', () => {
|
||||||
|
const unmount = mountResetPage(root);
|
||||||
|
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
|
||||||
|
unmount();
|
||||||
|
mountResetPage(root);
|
||||||
|
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
|
||||||
|
expect(root.querySelector('[role="status"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nur ein Erfolg wird gemerkt: nach einem Fehlschlag (ungültiger Link) zeigt die Seite ohne Link weiter „Link ungültig“', async () => {
|
||||||
|
withLink();
|
||||||
|
mocks.updateRecovery.mockRejectedValue(fail(401, 'user_invalid_token'));
|
||||||
|
const unmount = mountResetPage(root);
|
||||||
|
fill('neuesPasswort1');
|
||||||
|
submit();
|
||||||
|
await vi.waitFor(() => expect(root.querySelector('form')).toBeNull());
|
||||||
|
unmount();
|
||||||
|
history.replaceState(null, '', '/passwort-neu');
|
||||||
|
mountResetPage(root);
|
||||||
|
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
|
||||||
|
expect(root.querySelector('[role="status"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,11 +9,19 @@ import { navigate } from './router';
|
|||||||
|
|
||||||
const MISMATCH = msg('Die Passwörter stimmen nicht überein.');
|
const MISMATCH = msg('Die Passwörter stimmen nicht überein.');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Passwort wurde mit dem Link schon gesetzt. Der Stand lebt außerhalb der Seite: showDone() entfernt Kennung und Geheimnis aus der
|
||||||
|
* Adresse, und baut sich die Seite danach neu auf (Sprachwechsel, siehe main.ts), zeigte sie sonst „Der Link ist ungültig oder
|
||||||
|
* abgelaufen.“. Ein frischer Link (userId und secret in der Adresse) setzt den Stand zurück.
|
||||||
|
*/
|
||||||
|
let passwordChanged = false;
|
||||||
|
|
||||||
export function mountResetPage(root: HTMLElement): () => void {
|
export function mountResetPage(root: HTMLElement): () => void {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const userId = params.get('userId') ?? '';
|
const userId = params.get('userId') ?? '';
|
||||||
const secret = params.get('secret') ?? '';
|
const secret = params.get('secret') ?? '';
|
||||||
|
if (userId && secret) passwordChanged = false;
|
||||||
|
|
||||||
const heading = el('h1', {}, t('Passwort neu setzen'));
|
const heading = el('h1', {}, t('Passwort neu setzen'));
|
||||||
const page = el('section', { class: 'reset-page' });
|
const page = el('section', { class: 'reset-page' });
|
||||||
@@ -29,6 +37,7 @@ export function mountResetPage(root: HTMLElement): () => void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showDone(): void {
|
function showDone(): void {
|
||||||
|
passwordChanged = true;
|
||||||
// Das Geheimnis ist verbraucht; es soll nicht in der Adresszeile und im Verlauf stehen bleiben.
|
// Das Geheimnis ist verbraucht; es soll nicht in der Adresszeile und im Verlauf stehen bleiben.
|
||||||
history.replaceState(null, '', location.pathname);
|
history.replaceState(null, '', location.pathname);
|
||||||
const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-primary' }, t('Anmelden'));
|
const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-primary' }, t('Anmelden'));
|
||||||
@@ -122,7 +131,8 @@ export function mountResetPage(root: HTMLElement): () => void {
|
|||||||
page.replaceChildren(form);
|
page.replaceChildren(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId && secret) showForm();
|
if (passwordChanged) showDone();
|
||||||
|
else if (userId && secret) showForm();
|
||||||
else showInvalidLink();
|
else showInvalidLink();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in new issue
Block a user