openpencil/tests/e2e/chat/markdown.spec.ts
Danila Poyarkov e532f616ba
refactor!: move shared primitives below dom-css and core (#771)
* refactor!: move shared primitives below dom-css and core

dom-css depended on core for color conversion, base64 helpers, text
direction, and web-font assets, so core could not use dom-css and every
caller special-cased HTML and Tailwind output.

Color conversion and management, base64 helpers, and text/layout
direction now live in scene-graph under `color`, `bytes`, and
`text-direction`. dom-css takes web-font resolution as an injected
`fonts` option and owns the font face types, so it depends only on
scene-graph and core can depend on it.

BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes`
are removed, and the direction helpers are no longer exported from
`@open-pencil/core/text`; import them from `@open-pencil/scene-graph`
subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of
`'assets'`.

* fix(tools): import color parsing from scene-graph in visual bisect

* fix(mcp): declare the scene-graph dependency

MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved
there, but only reached scene-graph through core, so isolated installs
and package checks depended on transitive resolution.

* refactor!: use js-base64 directly instead of a base64 wrapper

Base64 helpers had moved into scene-graph only to sit below dom-css,
but they are a thin wrapper over js-base64 and unrelated to the graph;
fig already called js-base64 directly.

Callers use js-base64 and check `isValid` where input comes from outside
(clipboard, imported HTML, tool arguments, the plugin API). A new
`open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and
Buffer Base64 conversions, and AGENTS.md records the convention.

BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`.

* fix(dom-css): keep images with invalid Base64 inline in HTML export

`exportHTMLBundle` accepts documents parsed from outside HTML, and
js-base64 drops characters it cannot decode, so extracting an invalid
image data URL wrote different bytes. Such images now stay inline.
2026-09-26 11:39:11 +04:00

60 lines
2.4 KiB
TypeScript

import { toUint8Array } from 'js-base64'
import { expect, test } from '#tests/helpers/chat/fixture'
test('completed responses release streaming parser history', async ({ configuredChat: chat }) => {
await chat.submit('Show a code block')
const markdown = chat.page.locator('.chat-markdown').last()
await expect(markdown).toHaveAttribute('data-chat-markdown-mode', 'static')
await expect(markdown.locator('.shiki').first()).toBeVisible()
})
test('code blocks follow light and dark themes', async ({ configuredChat: chat }) => {
await chat.submit('Show a code block')
const code = chat.assistantMessage().locator('.shiki').first()
await expect(code).toHaveCSS('background-color', 'rgb(30, 30, 30)')
await expect(code.locator('span').filter({ hasText: 'const' }).first()).not.toHaveCSS(
'color',
'rgb(240, 240, 240)'
)
await chat.page.evaluate(async () => {
const themeModulePath = '/src/app/shell/theme.ts'
const themeModule = await import(themeModulePath)
themeModule.useAppTheme().setTheme('light')
})
await chat.page.waitForFunction(() => document.documentElement.dataset.theme === 'light')
await expect(chat.page.locator('.chat-markdown').last()).toHaveClass(/light/)
await expect(code).toHaveCSS('background-color', 'rgb(255, 255, 255)')
})
test('Markdown blocks unsafe links and cross-origin images', async ({ configuredChat: chat }) => {
const approvedImageURL = new URL('/assets/approved.png', chat.page.url()).href
await chat.page.route(approvedImageURL, async (route) => {
await route.fulfill({
body: Buffer.from(
toUint8Array(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEAQH/69cbGAAAAABJRU5ErkJggg=='
)
),
contentType: 'image/png'
})
})
await chat.submit('Show unsafe Markdown')
const assistant = chat.assistantMessage()
await expect(assistant.locator('a[href^="javascript:"]')).toHaveCount(0)
await expect(assistant.locator('img[src^="data:"]')).toHaveCount(0)
await expect(assistant.getByRole('img', { name: 'approved' })).toHaveAttribute(
'src',
approvedImageURL
)
await expect(assistant.getByRole('img', { name: 'unapproved' })).toHaveCount(0)
await expect(assistant.getByRole('img', { name: 'insecure' })).toHaveCount(0)
await expect(assistant.getByRole('link', { name: 'safe' })).toHaveAttribute(
'href',
'https://openpencil.dev/'
)
})