Files
hanzo.ai/e2e/artifact.spec.ts
zeekay fc057768c6 What the conversation made, beside it
A fenced block in an answer is a thing rather than a sentence — a file, a
command, a page — and read inline it scrolls away with the talk. In the
column it keeps its language and a copy control, which is what somebody
came to a code block to do. That is hanzo.chat's artifacts, drawn with the
Code region @hanzo/ui already publishes.

READ FROM THE TURNS, not tracked as they arrive. The transcript is already
the record; a second list of what was made would be a second thing to keep
in step with it, and the two would disagree the first time a turn was
restored rather than streamed.

No section where nothing was made: an empty heading is worse than none.

The spec puts two blocks in one answer and a third in another, so a reader
that stopped at the first match or the first turn would fail rather than
pass on the easy case.
2026-08-30 05:25:04 -07:00

81 lines
2.7 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { signIn } from './session'
/**
* WHAT THE CONVERSATION MADE, BESIDE IT.
*
* A fenced block in an answer is a thing rather than a sentence, and read
* inline it scrolls away with the talk. In the column it keeps its language and
* a copy control — which is what somebody came to a code block to do.
*
* Two blocks in one answer and one in another, so the spec would catch a reader
* that stopped at the first match or at the first turn. Prose fences nothing, so
* a conversation that made nothing shows no section rather than an empty one.
*/
const SEED = 'gate-artifact-thread'
const TRANSCRIPT = {
id: SEED,
messages: [
{ id: 'm1', role: 'user', content: 'Two files please' },
{
id: 'm2',
role: 'assistant',
content:
'Here you go.\n\n```ts\nexport const one = 1\n```\n\nand\n\n```sh\necho two\n```\n',
},
{ id: 'm3', role: 'assistant', content: 'And a third.\n\n```py\nthree = 3\n```' },
],
}
test('the column carries every block an answer produced', async ({ page }) => {
await signIn(page)
await page.route('**/v1/agents/chat/conversations/**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(TRANSCRIPT),
}),
)
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`/chat?thread=${SEED}`)
await page.waitForLoadState('networkidle')
await page.getByRole('button', { name: 'Open side panel' }).click()
const column = page.locator('aside')
await expect(column).toContainText('Artifacts', { timeout: 15_000 })
// Every block, across both answers, each under its own language.
for (const [language, code] of [
['ts', 'export const one = 1'],
['sh', 'echo two'],
['py', 'three = 3'],
] as const) {
await expect(column, `the ${language} block`).toContainText(code)
await expect(column).toContainText(language)
}
})
test('a conversation that made nothing shows no section', async ({ page }) => {
await signIn(page)
await page.route('**/v1/agents/chat/conversations/**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: SEED,
messages: [{ id: 'm1', role: 'assistant', content: 'Just prose, no fences.' }],
}),
}),
)
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`/chat?thread=${SEED}`)
await page.waitForLoadState('networkidle')
await page.getByRole('button', { name: 'Open side panel' }).click()
const column = page.locator('aside')
await expect(column).toContainText('Answering', { timeout: 15_000 })
await expect(column, 'an empty section is worse than none').not.toContainText('Artifacts')
})