Changing the password: the Logout button at the far right of the toolbar became an Account menu that names the signed-in account in an entry of its own that cannot be picked, with Change password... above Logout, and the new dialog (src/editor/auth/PasswordDialog.ts) asks for the current password, the new one and its repetition, all three as bullets, and stays standing when the server refuses so that it can say why, unlike the other form dialogs; the rules for empty, too short, new equal to old and a differing repetition sit as the pure function passwordProblem() next to loginProblem(), the API gained POST ?r=auth/password which answers 204, insists on the current password although a session is there, enforces a minimum length of 8 in UserStore::MIN_PASSWORD_LENGTH and again in the client so the mask can answer without asking the server, and the new SessionStore::destroyAllFor($user, $keep) ends every other session of the account while the tab that changed the password keeps its own; the placeholders admin/admin and user/user stay as they are, which the length check does not touch because it only runs when a password is set through the API; e2e 45 grew by 8 checks for the menu, the dialog, both refusals and signing in with the old and the new password, and it sets the e2e account back at the end - a run dying in between leaves it on <password>-tmp, which a make-user.php call repairs (#182)
game-maker / gate (push) Successful in 2m4s
game-maker / suite (push) Successful in 7m18s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 13:46:40 +02:00
1 parent 1ef497fa17
commit c830c7a3aa
15 files changed
+473 -18

No files matched your search

+124 -8
View File
@@ -19,12 +19,19 @@ export async function login(t) {
press,
typeText,
mouse,
clickText,
shot,
baseUrl,
projectId,
} = t;
const password = await e2ePassword();
/**
* What the password is set to in between (#182). It is set back before the step ends; should a
* run die between the two, the account is repaired with
* `make-user.php e2e <the password in data/e2e-password.txt>`.
*/
const tempPassword = `${password}-tmp`;
/** Loads the page without handing the browser a session first. */
const openSignedOut = async () => {
@@ -124,11 +131,30 @@ export async function login(t) {
await evaluate("window.__stayedUp = 'mark'; document.querySelector('canvas').dataset.stayedUp = 'mark'");
// Pressed by hand, not with `clickText`: that one waits for a drawn frame afterwards, and the
// screen the button brings up is built before that frame is due.
const logout = await locate('Logout');
await mouse('mouseMoved', logout.x, logout.y);
await mouse('mousePressed', logout.x, logout.y);
await mouse('mouseReleased', logout.x, logout.y);
// screen Logout brings up is built before that frame is due.
const pressAt = async (spot) => {
await mouse('mouseMoved', spot.x, spot.y);
await mouse('mousePressed', spot.x, spot.y);
await mouse('mouseReleased', spot.x, spot.y);
};
/** The account menu at the far right of the toolbar (#182), which holds Logout since. */
const openAccountMenu = async () => {
await pressAt(await locate('Account'));
await sleep(300);
};
await openAccountMenu();
const accountMenu = await json('({ rows: __gm.menuRows(), enabled: __gm.menuEnabled() })');
check(
'the account menu says who is signed in, as an entry that cannot be picked',
accountMenu.rows[0] === `Signed in as ${E2E_USER}` &&
accountMenu.enabled[0] === false &&
accountMenu.rows.includes('Change password…') &&
accountMenu.rows.includes('Logout'),
accountMenu,
);
await pressAt(await locate('Logout'));
await waitForScreen('login');
check(
'Logout ends the session and comes back to the sign-in screen',
@@ -162,11 +188,101 @@ export async function login(t) {
);
// The rebuilt editor has to be laid out, not sitting unlaid-out in the corner: `resize` lays out
// only when the size changed, and the sign-in screen has set `root` to the size of the window
// already (see `App.ts`). The Logout button is the far right of the toolbar, so it says so.
const placed = await json('({ logout: __gm.locate("Logout")?.x ?? null, width: window.innerWidth })');
// already (see `App.ts`). The account button is the far right of the toolbar, so it says so.
const placed = await json('({ account: __gm.locate("Account")?.x ?? null, width: window.innerWidth })');
check(
'the editor that came back is laid out across the window',
placed.logout !== null && placed.logout > placed.width / 2,
placed.account !== null && placed.account > placed.width / 2,
placed,
);
// Changing the password (#182), through the real dialog. It runs here, at the very end, because
// it takes the account's password away from `data/e2e-password.txt` for a moment.
const openPasswordDialog = async () => {
await openAccountMenu();
await clickText('Change password…');
await sleep(300);
};
/**
* Fills the three fields of a freshly opened dialog and submits. Freshly opened is the point: a
* refused attempt puts the caret into the field it is about, so typing on would fill the wrong
* one – every attempt here starts from Escape and a new dialog.
*/
const fillPasswords = async (current, next, repeat) => {
await typeText(current);
await press('Enter');
await typeText(next);
await press('Enter');
await typeText(repeat);
await press('Enter');
await sleep(800);
};
await openPasswordDialog();
check('the account menu opens the Change password dialog', (await json('__gm.passwordDialog()')) !== null, {
dialog: await json('__gm.passwordDialog()'),
});
await fillPasswords(password, 'short', 'short');
check(
'a new password below the minimum is refused without asking the server',
(await json('__gm.passwordDialog()'))?.message === 'The new password needs at least 8 characters',
await json('__gm.passwordDialog()'),
);
await press('Escape');
await openPasswordDialog();
await fillPasswords('not-the-password', tempPassword, tempPassword);
check(
'a wrong current password is refused and leaves the dialog standing',
(await json('__gm.passwordDialog()'))?.message === 'Wrong current password',
await json('__gm.passwordDialog()'),
);
await shot('45-password');
await press('Escape');
await openPasswordDialog();
await fillPasswords(password, tempPassword, tempPassword);
const changed = await json('({ dialog: __gm.passwordDialog(), notice: __gm.state.notice })');
check(
'the right current password changes it: the dialog closes and the toolbar says so',
changed.dialog === null && changed.notice.startsWith('Password changed'),
changed,
);
// The proof that the new password is the one that counts now: out through the menu, in with it.
await openAccountMenu();
await pressAt(await locate('Logout'));
await waitForScreen('login');
await typeText(E2E_USER);
await press('Enter');
await typeText(password);
await press('Enter');
await sleep(800);
check(
'the password that was replaced does not sign in any more',
(await json('__gm.login()')).message === 'Wrong user name or password' &&
!(await evaluate('Boolean(window.__gm?.state)')),
await json('__gm.login()'),
);
await press('a', { ctrl: true });
await typeText(tempPassword);
await press('Enter');
await waitForScreen('editor');
check(
'the new password signs in',
await evaluate(`__gm?.state?.project?.id === ${JSON.stringify(projectId)}`),
await json('({ project: __gm?.state?.project?.id ?? null })'),
);
// Back to the password the repo's tools know, so the next run starts where this one did.
await openPasswordDialog();
await fillPasswords(tempPassword, password, password);
const restored = await json('({ dialog: __gm.passwordDialog(), notice: __gm.state.notice })');
check(
'the account is handed back with the password the check tools carry',
restored.dialog === null && restored.notice.startsWith('Password changed'),
restored,
);
}