~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

7
118 lines · 5.3 KB Raw History
// 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(), () => '')