import { e2ePassword, E2E_USER } from '../lib/auth.mjs'; /** * Signing in and out (#177), driven through the real screen: the rest of the suite gets its * session handed to the browser as a cookie, so this is the only step that sees the sign-in * screen at all. * * It runs last and hands the editor back the way it found it: signed in, with the fixture open. */ export async function login(t) { const { check, cdp, sleep, evaluate, json, locate, find, press, typeText, mouse, shot, baseUrl, projectId, } = t; const password = await e2ePassword(); /** Loads the page without handing the browser a session first. */ const openSignedOut = async () => { await cdp.send('Network.clearBrowserCookies'); await cdp.send('Page.navigate', { url: `${baseUrl}?project=${projectId}` }); await sleep(3000); }; /** * Waits for the page to be showing one of the two screens. Signing in and out each mean a round * trip to the server – deadlines guessed for that are how a check becomes a coin toss. */ const showing = // The editor counts as up once the project is in it, not when its hook appears: the hook is // installed before the project is fetched, and in a chained run that fetch is long enough for // a check in between to read the empty project the editor starts out with. { editor: `__gm?.state?.project?.id === ${JSON.stringify(projectId)}`, login: 'Boolean(window.__gm?.login)', }; const waitForScreen = async (which) => { for (let waited = 0; waited < 15000; waited += 250) { try { if (await evaluate(`Boolean(${showing[which]})`)) return true; } catch { // "Inspected target navigated or closed": only `openSignedOut` navigates here, and asking // across it is not a reason to give up. } await sleep(250); } return false; }; await openSignedOut(); const screenHook = await json( '({ login: Boolean(window.__gm?.login), editor: Boolean(window.__gm?.state) })', ); check( 'without a session the sign-in screen stands where the editor would', screenHook.login && !screenHook.editor && Boolean(await find('Sign in')), screenHook, ); // The user name alone: refused by the form itself, before anything is sent. await typeText(E2E_USER); await press('Enter'); await press('Enter'); check( 'an empty password is refused without asking the server', (await json('__gm.login()')).message === 'Enter your password', await json('__gm.login()'), ); await typeText('not-the-password'); await press('Enter'); await sleep(600); // What the message says about a *wrong* password is checked in the unit tests: the live server // sits behind a firewall that answers every non-2xx with its own 418 page, so the wording of the // API's 401 only survives where nothing sits in between. That it is refused at all, and that // nobody gets in, holds everywhere – and that is what this check is about. check( 'a wrong password is refused and leaves the screen standing', (await json('__gm.login()')).message !== '' && Boolean(await find('Sign in')) && !(await evaluate('Boolean(window.__gm?.state)')), await json('__gm.login()'), ); // Select-all first: the failed attempt left its text in the field. await press('a', { ctrl: true }); await typeText(password); const bullets = '•'.repeat(password.length); check( 'the password is drawn as bullets, never as itself', Boolean(await find(bullets)) && (await find(password)) === null, { bullets: bullets.length }, ); await shot('45-login'); await press('Enter'); await waitForScreen('editor'); const loaded = await json( '({ editor: Boolean(window.__gm?.state), project: __gm?.state?.project?.name ?? null, title: document.title })', ); check( 'the right pair opens the editor, with the account in the window title', loaded.editor && loaded.project === 'E2E fixture' && loaded.title.endsWith(E2E_USER), loaded, ); // A mark on the window and on the canvas element: both survive a teardown and neither survives a // reload, which is what signing out must not do (#177). 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); await waitForScreen('login'); check( 'Logout ends the session and comes back to the sign-in screen', Boolean(await find('Sign in')) && !(await evaluate('Boolean(window.__gm?.state)')), await json('({ login: Boolean(window.__gm?.login), editor: Boolean(window.__gm?.state) })'), ); const stayedUp = await json( "({ window: window.__stayedUp ?? null, canvas: document.querySelector('canvas').dataset.stayedUp ?? null })", ); check( 'signing out does not reload the page: the same canvas stays, only what it draws changes', stayedUp.window === 'mark' && stayedUp.canvas === 'mark', stayedUp, ); // Back in, so the suite ends on a signed-in editor like every other step leaves it. await locate('Sign in'); await typeText(E2E_USER); await press('Enter'); await typeText(password); await press('Enter'); await waitForScreen('editor'); check( 'signing in again brings the editor back, with the project in it', (await json('__gm?.state?.project?.name ?? null')) === 'E2E fixture', await json('__gm?.state?.project?.name ?? null'), ); // 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 })'); check( 'the editor that came back is laid out across the window', placed.logout !== null && placed.logout > placed.width / 2, placed, ); }