// What the flows share: the world they run over, and how a page is read, // logged in to and written from, as a person does it. import { expect } from '@playwright/test' import { execFileSync } from 'node:child_process' import { join } from 'node:path' import { environment, root } from '../shots/world.mjs' export const world = JSON.parse(process.env.E2E_WORLD) export const who = Object.fromEntries(world.accounts.map((a) => [a.handle.split('.')[0], a])) export const url = (path) => world.base + path /// The records of `collection` in `name`'s repo, read from their PDS. export const records = async (name, collection) => { const asked = `${world.pds}/xrpc/com.atproto.repo.listRecords?repo=${who[name].did}&collection=${collection}&limit=100` return (await fetch(asked).then((r) => r.json())).records ?? [] } /// What the command line says, run as `name`: for a private repository, /// read from its space with their own session, past the pages and index. export const knotless = (name, ...args) => execFileSync(join(root, 'target/debug/knotless'), args, { env: environment(world, world.dir, name) }) .toString() .trim() /// The status `page`, logged in to no one, gets for `path`. export const seen = async (page, path) => (await page.goto(url(path))).status() /// Until the page's script has taken it over, as the root says once /// mounted: a click before that reaches no handler. export const ready = (page) => page.locator('html[data-ready]').waitFor({ state: 'attached' }) /// `page` at `path` of the pages, ready. export const go = async (page, path) => { await page.goto(path.startsWith('http') ? path : url(path)) await ready(page) } /// Until `check` passes on `page` read anew: the index follows the PDS a /// moment behind. `check` is handed an `expect` that gives up soon, so a /// page drawn before the index caught up is read again rather than waited /// on. `timeout`, for a change seen later than the index's. export const anew = (page, check, timeout = 30_000) => expect(async () => { await page.reload() await ready(page) await check(expect.configure({ timeout: 2_000 })) }).toPass({ timeout }) /// Signs in as `name` on the PDS's own page `at` has come to, and approves /// what was asked of it. export const approve = async (at, name) => { await at.waitForURL((u) => u.origin === new URL(world.pds).origin) await at.getByRole('textbox', { name: 'Password' }).fill(who[name].password) await at.getByRole('button', { name: 'Sign in' }).click() await at.getByRole('button', { name: /^(accept|authorize|allow)$/i }).click() } /// Delegates `name`'s writes to this server from the button on `page`: /// approved at their PDS, in the page of its own the button opens. export const delegate = async (page, name) => { const opening = page.context().waitForEvent('page') await page.getByRole('button', { name: 'Delegate writes to this server' }).click() const pds = await opening await approve(pds, name) // Back at the gateway, which takes the grant there. await pds.waitForURL((u) => u.origin === new URL(world.base).origin) await pds.close() await expect(page.getByRole('button', { name: /^(Delegate writes to this server|Waiting for approval…)$/ })).toHaveCount(0) } /// The status the pages answer `path` with, asked from the page, as the /// browser reaches them. export const status = (page, path) => page.evaluate((p) => fetch(p).then((r) => r.status), path) /// Switches the browser to `handle`, an account it is logged in to too. export const switchTo = async (page, handle) => { await page.getByLabel('Your account').click() await page.locator('.account .menu-item.switch', { hasText: handle }).click() await expect.poll(() => loggedInAs(page)).toBe(handle) await ready(page) } /// A comment, by what it says. export const comment = (page, text) => page.locator('.comment', { hasText: text }) /// The text of the form whose button says `label`, as editing opens one. export const editor = (page, label) => page.locator('form', { has: page.getByRole('button', { name: label }) }).locator('textarea') /// Chooses `action` from the menu of the comment that says `text`. export const choose = async (page, text, action) => { const menu = comment(page, text).locator('.action-menu') await menu.locator('summary').click() await menu.getByRole('button', { name: action, exact: true }).click() } /// Logs in as `name` from the dialog the page's header opens: the handle, /// then the PDS's own page to sign in and approve on, which sends the /// browser back to the page it started from. export const logIn = async (page, name, open = () => page.getByRole('button', { name: 'Log in' }).click()) => { await open() const dialog = page.getByRole('dialog', { name: 'Log in with your Atmosphere account' }) await dialog.getByLabel('Handle').fill(who[name].handle) const back = page.url() await dialog.getByRole('button', { name: 'Log in' }).click() await approve(page, name) await page.waitForURL(back) await ready(page) await expect(page.getByLabel('Your account')).toBeVisible() } /// Who the page is logged in as, by the account menu's head, which is /// there with the menu closed: nothing while the page reloads. export const loggedInAs = (page) => page .locator('.account .menu-head') .textContent({ timeout: 1000 }) .then((t) => t.trim(), () => '')