visitor

tcmts.dev.devenv.visitor · #pqdpfpa6 · mjs file

// A visitor on the registry, in headless Chrome: sign in as maya.test (the real PDS's sign-in and consent pages), then
// star alice.test's geo and take the star back, saying what the page and the PDS show at each step.
import puppeteer from 'puppeteer-core'
import fs from 'node:fs'
import { CHROME, sayYes } from './yes.mjs'

const registry = process.argv[2] ?? 'http://127.0.0.1:7402'
const env = JSON.parse(fs.readFileSync(new URL('./env.json', import.meta.url), 'utf8'))
const maya = env.accounts['maya.test'].did
const alice = env.accounts['alice.test'].did
const step = (s) => console.log(`-- ${s}`)
const b = await puppeteer.launch({ executablePath: CHROME, headless: true })
const p = await b.newPage()
const errors = []
p.on('pageerror', (e) => errors.push(String(e)))
p.on('console', (m) => m.type() === 'error' && !/^Failed to load resource/.test(m.text()) && errors.push(m.text()))
p.on('response', async (r) => r.status() >= 400 && errors.push(`${r.status()} ${r.request().method()} ${r.url().split('?')[0]}: ${(await r.text().catch(() => '')).slice(0, 120)}`))
const text = (sel) => p.$eval(sel, (e) => e.textContent).catch(() => '')
const until = async (what, fn, ms = 30_000) => {
  try { await p.waitForFunction(fn, { timeout: ms }) } catch (e) {
    await p.screenshot({ path: `${new URL('./shots/', import.meta.url).pathname}visitor-${what.replace(/\W+/g, '-')}.png`, fullPage: true })
    throw new Error(`${what}: not seen at ${p.url()} (${errors.join(' | ')})`)
  }
}
const starRecord = async () => (await fetch(`${env.pds}/xrpc/com.atproto.repo.getRecord?repo=${maya}&collection=codes.tcm.star&rkey=${encodeURIComponent(`${alice}:geo`)}`)).status
try {
  await p.goto(`${registry}/@alice.test/geo`, { waitUntil: 'networkidle0' })
  await until('the sign-in button', () => document.querySelector('.account button')?.textContent === 'Sign in')
  step('signing in as maya.test')
  await p.click('.account button')
  await p.waitForSelector('.signin-pop input')
  await p.type('.signin-pop input', 'maya.test')
  await p.click('.signin-pop button[type=submit]')
  await p.waitForFunction((pds) => location.href.startsWith(pds), { timeout: 30_000 }, env.pds).catch(async () => {
    throw new Error(`not sent to the PDS: at ${p.url()}: ${await text('.account')} (${errors.join(' | ')})`)
  })
  await sayYes(p, 'registry')
  await until('signed in', () => /@maya\.test/.test(document.querySelector('.account .avatar-btn')?.getAttribute('aria-label') ?? ''))
  step(`signed in, back at ${p.url()}: ${await p.$eval('.account .avatar-btn', (b) => b.getAttribute('aria-label'))}`)
  step(`star button: ${await text('.star')}`)
  await p.click('.star button')
  await until('starred', () => /Starred|Couldn't/.test((document.querySelector('.star')?.textContent ?? '') + (document.querySelector('.toasts')?.textContent ?? '')))
  step(`after starring: ${await text('.star')}; the PDS's getRecord: ${await starRecord()}`)
  await p.click('.star button')
  await until('unstarred', () => /☆ Star|Couldn't/.test((document.querySelector('.star')?.textContent ?? '') + (document.querySelector('.toasts')?.textContent ?? '')))
  step(`after taking it back: ${await text('.star')}; the PDS's getRecord: ${await starRecord()}`)
  await p.goto(`${registry}/?show=following`, { waitUntil: 'networkidle0' })
  step(`People I follow: ${(await p.evaluate(() => document.querySelector('.narrow')?.innerText ?? '')).replace(/\s+/g, ' ').slice(0, 300)}`)
  // libraries she uses: from a codebase she serves (app, which holds geo), given to the registry
  const codebase = process.argv[3]
  if (codebase) {
    await p.goto(`${registry}/?show=mine`, { waitUntil: 'networkidle0' })
    step(`Libraries I use, before: ${await p.$$eval('.ev', (x) => x.length)} item(s)`)
    await p.type('.codebaseform input', codebase)
    await p.click('.codebaseform button')
    await until('libraries from her codebase', () => document.querySelectorAll('.ev').length > 0)
    step(`Libraries I use, with ${codebase}: ${await p.$$eval('.ev', (x) => x.map((e) => e.innerText.split('\n')[0]).join(' | '))}`)
  }
  step(`page errors: ${errors.length ? errors.join(' | ') : 'none'}`)
} finally {
  await b.close()
}

used by