Files
hanzo.ai/e2e/gates/copy-prompt.spec.ts

87 lines
4.1 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { serveExport } from './export'
import { SETUP } from '../../components/home/prompts'
/**
* The fold's second action hands the platform to the reader's own agent.
*
* It copies `SETUP` (components/home/prompts.ts), the one sentence every copy
* control on the page carries: read /skill.md, sign in, pick a starter, boot
* it. `/skill.md` is written by `scripts/skill.mjs` on every build out of the
* same `GET /v1/openapi.json` snapshot the mega-menu reads, so the manual the
* sentence points at cannot describe an API that has moved on.
*
* `preventDefault` has to run during the click's own dispatch, or the browser
* has already begun following the link and the page walks off to the raw
* markdown. Measured once, with the two lines the other way round: the click
* navigated to /skill.md every time and the clipboard stayed empty. A
* screenshot cannot see that — the button looks identical either way — so it
* is asserted here.
*
* And the element is a LINK before it is a button, because content on this site
* must not depend on scripting. With JS off the manual still opens.
*/
test('the templates page copies the setup prompt without leaving the page', async ({ page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
const server = await serveExport()
try {
await page.goto(`${server.url}/templates`, { waitUntil: 'load' })
// A press before hydration follows the link to the raw manual (providers `Hydrated`).
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
// `$=`: a press writes the link's address out absolute (providers `carry`),
// so the attribute reads /skill.md before it and https://…/skill.md after.
const cta = page.locator('a[data-slot="action"][href$="/skill.md"]').first()
await expect(cta).toBeVisible()
await expect(cta).toHaveText(/Copy setup prompt/)
// WATCH FOR A DOWNLOAD, and nothing else. Three candidate signals, two of
// them useless here, and each one was tried:
//
// the URL — `/skill.md` is served with no content-type (production does
// the same), so the default action SAVES the file rather than
// navigating and `page.url()` is unchanged either way. Asserting on it
// is how the first version of this gate passed against the very bug it
// was written for.
// `framenavigated` — fires on its own after hydration, measured: once,
// for the page's own URL, with no click at all. Noise.
// `download` — fires only when the default action ran. That is the whole
// difference between a cancelled click and an uncancelled one here.
const saved: string[] = []
page.on('download', (d) => saved.push(d.suggestedFilename()))
await cta.click()
await expect(cta).toHaveText(/Copied/)
// The click was cancelled: nothing reached the download manager.
expect(saved).toEqual([])
const copied = await page.evaluate(() => navigator.clipboard.readText())
expect(copied).toBe(SETUP)
expect(copied.startsWith('Read https://hanzo.ai/skill.md')).toBe(true)
// …and it goes back, so a second reader of the same page is not told a
// copy happened that did not.
await expect(cta).toHaveText(/Copy setup prompt/, { timeout: 6000 })
} finally {
await server.close()
}
})
test('with no scripting it is still a working link', async ({ browser }) => {
const server = await serveExport()
const ctx = await browser.newContext({ javaScriptEnabled: false })
try {
const page = await ctx.newPage()
await page.goto(`${server.url}/templates`, { waitUntil: 'load' })
const cta = page.locator('a[data-slot="action"][href="/skill.md"]').first()
await expect(cta).toBeVisible()
await expect(cta).toHaveText(/Copy setup prompt/)
// The href is the fallback, so it has to be the real file and not a hash.
const res = await page.request.get(server.url + '/skill.md')
expect(res.status()).toBe(200)
expect((await res.text()).startsWith('# Hanzo')).toBe(true)
} finally {
await ctx.close()
await server.close()
}
})