hololake-platform/tests/smoke/mermaid-diagram-notes.spec.ts

623 lines
23 KiB
TypeScript

import { expect, test, type Locator, type Page } from '@playwright/test'
import fs from 'fs'
import path from 'path'
import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault'
import { executeCommand, openCommandPalette } from './helpers'
import { RUNTIME_STYLE_NONCE } from '../../src/lib/runtimeStyleNonce'
import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog'
import { triggerMenuCommand, triggerShortcutCommand } from './testBridge'
let tempVaultDir: string
const FIRST_DIAGRAM = [
'```mermaid',
'flowchart LR',
' A[Draft] --> B[Saved]',
'```',
].join('\n')
const UPDATED_FIRST_DIAGRAM = FIRST_DIAGRAM.replace('B[Saved]', 'C[Published]')
const SECOND_DIAGRAM = [
'```mermaid',
'sequenceDiagram',
' Alice->>Bob: Hello',
'```',
].join('\n')
const REPORTED_THEME_DIAGRAM = [
'```mermaid',
"%%{init: {'theme':'base','themeVariables':{'primaryColor':'#dbe7ff','primaryTextColor':'#0b1220','primaryBorderColor':'#1f3a8a','lineColor':'#1f3a8a','secondaryColor':'#fff4d6','tertiaryColor':'#ffe0e0','fontSize':'14px'}}}%%",
'flowchart TD',
' A(["Employee clocks in"]) --> B{"Linked to a planned shift?"}',
'',
' B -- "Yes" --> C["Other path<br/>for scheduled shift"]',
' C --> D["Contract path<br/>for scheduled shift"]',
'',
' B -- "No (unscheduled)" --> E["other path<br/>for unscheduled clocking"]',
'',
' classDef path fill:#fff4d6,stroke:#7a5b00,stroke-width:2px,color:#3a2c00;',
' class C,D,E,F path;',
'```',
].join('\n')
const REPORTED_DATABASE_DIAGRAM = [
'```mermaid',
'flowchart TB',
' app["app<br/>api"] --> db[("DB Type<br/>rows")]',
'```',
].join('\n')
const REPORTED_GANTT_DIAGRAM = [
'```mermaid',
'gantt',
' title Project Plan',
' dateFormat YYYY-MM-DD',
' section Phase 1',
' Requirements Analysis:a1, 2026-06-24, 7d',
' Design:a2, after a1, 5d',
' section Phase 2',
' Development:b1, after a2, 14d',
' Testing & Deployment:b2, after b1, 7d',
'```',
].join('\n')
const SYSTEM_OVERVIEW_DIAGRAM = [
'```mermaid',
'flowchart TD',
' subgraph TW["Tauri v2 Window"]',
' subgraph FE["React Frontend"]',
' App["App.tsx (orchestrator)"]',
' WS["WelcomeScreen\\n(onboarding)"]',
' SB["Sidebar\\n(navigation + filters + types)"]',
' NL["NoteList / PulseView\\n(filtered list / activity)"]',
' ED["Editor\\n(BlockNote + diff + raw)"]',
' IN["Inspector\\n(metadata + relationships)"]',
' AIP["AiPanel\\n(selected CLI agent + tools)"]',
' SP["SearchPanel\\n(keyword search)"]',
' ST["StatusBar\\n(vault picker + sync + version)"]',
' CP["CommandPalette\\n(Cmd+K launcher)"]',
'',
' App --> WS & SB & NL & ED & SP & ST & CP',
' ED --> IN & AIP',
' end',
'',
' subgraph RB["Rust Backend"]',
' LIB["lib.rs → Tauri commands"]',
' VAULT["vault/"]',
' FM["frontmatter/"]',
' GIT["git/\\n(commit, sync, clone)"]',
' SETTINGS["settings.rs"]',
' SEARCH["search.rs"]',
' CLI["ai_agents.rs\\n+ claude_cli.rs"]',
' end',
'',
' subgraph EXT["External Services"]',
' CCLI["Claude / Codex / OpenCode / Pi CLI\\n(agent subprocesses)"]',
' MCP["MCP Server\\n(ws://9710, 9711)"]',
' GCLI["git CLI\\n(system executable)"]',
' REMOTE["Git remotes\\n(GitHub/GitLab/Gitea/etc.)"]',
' end',
'',
' FE -->|"Tauri IPC"| RB',
' CLI -->|"spawn subprocess"| CCLI',
' LIB -->|"register / monitor"| MCP',
' GIT -->|"clone / fetch / push / pull"| GCLI',
' GCLI -->|"network auth via user config"| REMOTE',
' end',
'',
' style FE fill:#e8f4fd,stroke:#2196f3,color:#000',
' style RB fill:#fff8e1,stroke:#ff9800,color:#000',
' style EXT fill:#f3e5f5,stroke:#9c27b0,color:#000',
'```',
].join('\n')
const INVALID_DIAGRAM = [
'```mermaid',
'not a diagram',
'```',
].join('\n')
const REPORTED_INVALID_DIAGRAM = [
'```mermaid',
'flowchart TD',
' A[Christmas] -->|Get money| B(Go shopping)',
' B --> C{Let me think}',
' C -->|One| D[Laptop]',
' C -->|Two| E[iPhone]',
' C -->|Three| F[fa:fa-car Car]',
'## ABC',
'```',
].join('\n')
test.beforeEach(async ({ page }, testInfo) => {
testInfo.setTimeout(90_000)
tempVaultDir = createFixtureVaultCopy()
fs.writeFileSync(
path.join(tempVaultDir, 'note', 'mermaid-reported.md'),
[
'---',
'Status: Active',
'Date: 2026-04-29T00:00:00',
'---',
'',
'# Mermaid Reported',
'',
REPORTED_THEME_DIAGRAM,
'',
].join('\n'),
)
fs.writeFileSync(
path.join(tempVaultDir, 'note', 'mermaid-database.md'),
[
'---',
'Status: Active',
'Date: 2026-06-21T00:00:00',
'---',
'',
'# Mermaid Database',
'',
REPORTED_DATABASE_DIAGRAM,
'',
].join('\n'),
)
fs.writeFileSync(
path.join(tempVaultDir, 'note', 'mermaid-gantt.md'),
[
'---',
'Status: Active',
'Date: 2026-06-24T00:00:00',
'---',
'',
'# Mermaid Gantt',
'',
REPORTED_GANTT_DIAGRAM,
'',
].join('\n'),
)
await openFixtureVault(page, tempVaultDir, { installedAiAgents: ['claude_code'] })
})
test.afterEach(async () => {
removeFixtureVaultCopy(tempVaultDir)
})
async function openNote(page: Page, title: string): Promise<void> {
await page.locator('[data-testid="note-list-container"]').getByText(title, { exact: true }).click()
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
}
async function toggleRawMode(page: Page, visibleSelector: '.bn-editor' | '.cm-content'): Promise<void> {
await openCommandPalette(page)
await executeCommand(page, 'Toggle Raw')
await expect(page.locator(visibleSelector)).toBeVisible({ timeout: 5_000 })
}
async function getRawEditorContent(page: Page): Promise<string> {
return page.evaluate(() => {
type CodeMirrorHost = Element & {
cmTile?: {
view?: {
state: {
doc: {
toString(): string
}
}
}
}
}
const host = document.querySelector('.cm-content') as CodeMirrorHost | null
return host?.cmTile?.view?.state.doc.toString() ?? host?.textContent ?? ''
})
}
async function setRawEditorContent(page: Page, content: string): Promise<void> {
await page.evaluate((nextContent) => {
type CodeMirrorHost = Element & {
cmTile?: {
view?: {
state: {
doc: {
length: number
}
}
dispatch(update: {
changes: {
from: number
to: number
insert: string
}
}): void
}
}
}
const host = document.querySelector('.cm-content') as CodeMirrorHost | null
const view = host?.cmTile?.view
if (!view) return
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: nextContent },
})
}, content)
}
async function expectRenderedDiagramCount(page: Page, count: number): Promise<void> {
await expect(page.locator('[data-testid="mermaid-diagram-viewport"] svg')).toHaveCount(count, { timeout: 15_000 })
}
async function reloadVault(page: Page): Promise<void> {
await triggerMenuCommand(page, APP_COMMAND_IDS.vaultReload)
await expect(page.getByText(/Vault reloaded \(\d+ entries\)/).last()).toBeVisible({
timeout: 5_000,
})
}
async function countLargeBlackFilledShapes(page: Page, diagramIndex: number): Promise<number> {
return page.locator('[data-testid="mermaid-diagram-viewport"] svg').nth(diagramIndex).evaluate((svg) => {
const shapes = Array.from(svg.querySelectorAll<SVGGraphicsElement>('rect,path,polygon'))
return shapes.filter((shape) => {
const box = shape.getBoundingClientRect()
return getComputedStyle(shape).fill === 'rgb(0, 0, 0)'
&& box.width > 12
&& box.height > 8
}).length
})
}
function mermaidSvg(page: Page, diagramIndex: number): Locator {
return page.locator('[data-testid="mermaid-diagram-viewport"] svg').nth(diagramIndex)
}
function mermaidBodyArtifacts(page: Page): Locator {
return page.locator([
'body > [data-tolaria-mermaid-render-host]',
'body > [id^="tolaria-mermaid-"]',
'body > [id^="dtolaria-mermaid-"]',
'body > [id^="itolaria-mermaid-"]',
].join(', '))
}
function mermaidNode(page: Page, diagramIndex: number, text: string): Locator {
return mermaidSvg(page, diagramIndex).locator('.node').filter({ hasText: text }).first()
}
async function computedCss(locator: Locator, property: 'background-color' | 'color' | 'fill' | 'stroke'): Promise<string> {
return locator.evaluate((element, styleProperty) => (
getComputedStyle(element).getPropertyValue(styleProperty)
), property)
}
async function switchToResolvedTheme(page: Page, mode: 'dark' | 'light'): Promise<void> {
const root = page.locator('html')
if (await root.getAttribute('data-theme') !== mode) {
await page.getByTestId('status-theme-mode').click()
}
await expect(root).toHaveAttribute('data-theme', mode)
}
async function setDocumentZoom(page: Page, percent: number): Promise<void> {
await page.evaluate((zoomPercent) => {
const factor = zoomPercent / 100
document.documentElement.style.setProperty('zoom', `${zoomPercent}%`)
document.documentElement.style.setProperty('--tolaria-overlay-zoom-factor', String(factor))
document.documentElement.style.setProperty('--tolaria-overlay-zoom-inverse', String(1 / factor))
window.dispatchEvent(new Event('laputa-zoom-change'))
}, percent)
}
async function readDialogViewportBounds(page: Page) {
return page.getByTestId('mermaid-diagram-dialog-viewport').evaluate((viewport) => {
const rect = viewport.getBoundingClientRect()
return {
width: rect.width,
height: rect.height,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
}
})
}
async function dialogViewportFitsWindow(page: Page): Promise<boolean> {
const bounds = await readDialogViewportBounds(page)
const horizontallyContained = bounds.width <= bounds.windowWidth - 16
const verticallyContained = bounds.height <= bounds.windowHeight - 16
const stillFullscreenWidth = bounds.width >= bounds.windowWidth - 96
const stillFullscreenHeight = bounds.height >= bounds.windowHeight - 96
return horizontallyContained && verticallyContained && stillFullscreenWidth && stillFullscreenHeight
}
function mermaidNodeLabel(node: Locator): Locator {
return node.locator('.nodeLabel, text').first()
}
async function readStyleNonce(svg: Locator): Promise<string | null> {
return svg.locator('style').first().evaluate((style) => {
const nonce = (style as HTMLElement).nonce
return nonce.length > 0 ? nonce : style.getAttribute('nonce')
})
}
async function labelFitsNode(node: Locator): Promise<boolean> {
return node.evaluate((element) => {
const shape = element.querySelector<SVGGraphicsElement>('rect, polygon, circle, ellipse, path')!
const label = element.querySelector<SVGGraphicsElement | HTMLElement>('.nodeLabel, text')!
const shapeBox = shape.getBoundingClientRect()
const labelBox = label.getBoundingClientRect()
return [
labelBox.width <= shapeBox.width + 2,
labelBox.height <= shapeBox.height + 2,
].every(Boolean)
})
}
async function labelIsHorizontallyCenteredInNode(node: Locator): Promise<boolean> {
return node.evaluate((element) => {
const shape = element.querySelector<SVGGraphicsElement>('rect, polygon, circle, ellipse, path')!
const label = element.querySelector<SVGGraphicsElement | HTMLElement>('.nodeLabel, text')!
const shapeBox = shape.getBoundingClientRect()
const labelBox = label.getBoundingClientRect()
const shapeCenter = shapeBox.left + shapeBox.width / 2
const labelCenter = labelBox.left + labelBox.width / 2
return Math.abs(shapeCenter - labelCenter) <= 2
})
}
async function readReportedDiagramMetrics(page: Page, diagramIndex: number) {
const svg = mermaidSvg(page, diagramIndex)
const scheduledNode = mermaidNode(page, diagramIndex, 'Other path')
const scheduledShape = scheduledNode.locator('rect, polygon, circle, ellipse, path').first()
const scheduledLabel = mermaidNodeLabel(scheduledNode)
return {
connectorStroke: await computedCss(svg.locator('.flowchart-link').first(), 'stroke'),
markerFill: await computedCss(svg.locator('marker path').first(), 'fill'),
scheduledFill: await computedCss(scheduledShape, 'fill'),
scheduledStroke: await computedCss(scheduledShape, 'stroke'),
scheduledTextFill: await computedCss(scheduledLabel, 'fill'),
foreignObjectCount: await svg.locator('foreignObject').count(),
labelsFit: [
await labelFitsNode(scheduledNode),
await labelFitsNode(mermaidNode(page, diagramIndex, 'Contract path')),
await labelFitsNode(mermaidNode(page, diagramIndex, 'unscheduled clocking')),
].every(Boolean),
styleNonce: await readStyleNonce(svg),
text: await svg.evaluate((element) => element.textContent ?? ''),
}
}
async function readDatabaseDiagramMetrics(page: Page, diagramIndex: number) {
const svg = mermaidSvg(page, diagramIndex)
const appNode = mermaidNode(page, diagramIndex, 'app')
const databaseNode = mermaidNode(page, diagramIndex, 'DB Type')
return {
rectangleLabelCentered: await labelIsHorizontallyCenteredInNode(appNode),
databaseLabelCentered: await labelIsHorizontallyCenteredInNode(databaseNode),
databaseLabelFits: await labelFitsNode(databaseNode),
foreignObjectCount: await svg.locator('foreignObject').count(),
text: await svg.evaluate((element) => element.textContent ?? ''),
}
}
async function readGanttDiagramMetrics(page: Page, diagramIndex: number) {
const svg = mermaidSvg(page, diagramIndex)
return svg.evaluate((element) => {
const viewBox = element.getAttribute('viewBox')?.split(/\s+/u).map(Number) ?? []
const [, , width, height] = viewBox
return {
roleDescription: element.getAttribute('aria-roledescription'),
width,
height,
text: element.textContent ?? '',
taskCount: element.querySelectorAll('.task').length,
}
})
}
function readNoteBFile(): string {
return fs.readFileSync(path.join(tempVaultDir, 'note', 'note-b.md'), 'utf8')
}
test('Mermaid diagrams render when opening saved notes directly', async ({ page }) => {
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await expect.poll(() => readReportedDiagramMetrics(page, 0)).toMatchObject({
connectorStroke: 'rgb(31, 58, 138)',
markerFill: 'rgb(31, 58, 138)',
scheduledFill: 'rgb(255, 244, 214)',
scheduledStroke: 'rgb(122, 91, 0)',
scheduledTextFill: 'rgb(58, 44, 0)',
foreignObjectCount: 0,
labelsFit: true,
styleNonce: RUNTIME_STYLE_NONCE,
text: expect.stringContaining('Linked to a planned shift?'),
})
})
test('Mermaid database node labels stay centered inside cylinders', async ({ page }) => {
await openNote(page, 'Mermaid Database')
await expectRenderedDiagramCount(page, 1)
await expect.poll(() => readDatabaseDiagramMetrics(page, 0)).toMatchObject({
rectangleLabelCentered: true,
databaseLabelCentered: true,
databaseLabelFits: true,
foreignObjectCount: 0,
text: expect.stringContaining('DB Type'),
})
})
test('Mermaid Gantt diagrams render with a nonzero timeline width', async ({ page }) => {
await openNote(page, 'Mermaid Gantt')
await expectRenderedDiagramCount(page, 1)
await expect.poll(() => readGanttDiagramMetrics(page, 0)).toMatchObject({
roleDescription: 'gantt',
width: expect.any(Number),
height: expect.any(Number),
taskCount: 4,
text: expect.stringContaining('Testing & Deployment'),
})
expect((await readGanttDiagramMetrics(page, 0)).width).toBeGreaterThan(0)
})
test('fullscreen Mermaid diagrams keep the active Tolaria surface in dark mode', async ({ page }) => {
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await switchToResolvedTheme(page, 'dark')
const inlineViewport = page.getByTestId('mermaid-diagram-viewport').first()
await expect.poll(() => computedCss(inlineViewport, 'background-color')).not.toBe('rgb(255, 255, 255)')
const inlineBackground = await computedCss(inlineViewport, 'background-color')
await page.getByTestId('mermaid-diagram').first().hover()
await page.getByRole('button', { name: 'Open Mermaid diagram' }).first().click()
const dialogViewport = page.getByTestId('mermaid-diagram-dialog-viewport')
await expect(dialogViewport).toBeVisible()
await expect.poll(() => computedCss(dialogViewport, 'background-color')).toBe(inlineBackground)
})
test('fullscreen Mermaid diagrams stay within the viewport while the app is zoomed', async ({ page }) => {
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await setDocumentZoom(page, 120)
await page.getByTestId('mermaid-diagram').first().hover()
await page.getByRole('button', { name: 'Open Mermaid diagram' }).first().click()
await expect(page.getByTestId('mermaid-diagram-dialog-viewport')).toBeVisible()
await expect.poll(() => dialogViewportFitsWindow(page)).toBe(true)
})
test('Mermaid diagrams stay mounted after property edits refresh frontmatter', async ({ page }) => {
const pageErrors: string[] = []
page.on('pageerror', error => pageErrors.push(error.message))
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await triggerShortcutCommand(page, APP_COMMAND_IDS.viewToggleProperties)
await expect(page.getByTestId('add-property-row')).toBeVisible()
const dateRow = page.getByTestId('editable-property').filter({ hasText: 'Date' })
await dateRow.getByTestId('date-display').click()
const nextDateLabel = await page.evaluate(() => new Date(2026, 3, 30).toLocaleDateString())
await page.locator(`button[data-day="${nextDateLabel}"]`).first().click()
await expect.poll(() => (
fs.readFileSync(path.join(tempVaultDir, 'note', 'mermaid-reported.md'), 'utf8')
)).toMatch(/Date: "?2026-04-30"?/)
await expectRenderedDiagramCount(page, 1)
await expect(page.locator('[data-testid="mermaid-diagram-viewport"]').first()).toContainText('Linked to a planned shift?')
expect(pageErrors).toEqual([])
})
test('Mermaid diagram clicks stay stable while a vault reload settles', async ({ page }) => {
const pageErrors: string[] = []
page.on('pageerror', error => pageErrors.push(error.message))
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await reloadVault(page)
await mermaidSvg(page, 0).click()
await expectRenderedDiagramCount(page, 1)
await expect(page.locator('#tolaria-fatal-render-error')).toHaveCount(0)
await expect(mermaidSvg(page, 0)).toContainText('Linked to a planned shift?')
expect(pageErrors).toEqual([])
})
test('Mermaid diagrams render, fall back, and round-trip through raw mode', async ({ page }) => {
await openNote(page, 'Note B')
await toggleRawMode(page, '.cm-content')
const originalContent = await getRawEditorContent(page)
const nextContent = `${originalContent.trimEnd()}
${FIRST_DIAGRAM}
${REPORTED_THEME_DIAGRAM}
${INVALID_DIAGRAM}
${SECOND_DIAGRAM}
${SYSTEM_OVERVIEW_DIAGRAM}
`
await setRawEditorContent(page, nextContent)
await expect.poll(readNoteBFile).toContain(FIRST_DIAGRAM)
await toggleRawMode(page, '.bn-editor')
await expectRenderedDiagramCount(page, 4)
await expect.poll(() => countLargeBlackFilledShapes(page, 3)).toBe(0)
await expect.poll(() => readReportedDiagramMetrics(page, 1)).toMatchObject({
connectorStroke: 'rgb(31, 58, 138)',
markerFill: 'rgb(31, 58, 138)',
scheduledFill: 'rgb(255, 244, 214)',
scheduledStroke: 'rgb(122, 91, 0)',
scheduledTextFill: 'rgb(58, 44, 0)',
foreignObjectCount: 0,
labelsFit: true,
styleNonce: RUNTIME_STYLE_NONCE,
text: expect.stringContaining('Linked to a planned shift?'),
})
await expect(page.locator('[data-testid="mermaid-diagram-error"]')).toHaveCount(1)
await expect(page.locator('[data-testid="mermaid-diagram-error"]')).toContainText('not a diagram')
await page.locator('[data-testid="mermaid-diagram"]').nth(3).hover()
await page.getByRole('button', { name: 'Open Mermaid diagram' }).nth(3).click()
await expect(page.locator('[data-testid="mermaid-diagram-dialog-viewport"] svg')).toBeVisible()
await page.keyboard.press('Escape')
await toggleRawMode(page, '.cm-content')
const rawAfterRichMode = await getRawEditorContent(page)
expect(rawAfterRichMode).toContain(FIRST_DIAGRAM)
expect(rawAfterRichMode).toContain(REPORTED_THEME_DIAGRAM)
expect(rawAfterRichMode).toContain(INVALID_DIAGRAM)
expect(rawAfterRichMode).toContain(SECOND_DIAGRAM)
expect(rawAfterRichMode).toContain(SYSTEM_OVERVIEW_DIAGRAM)
await setRawEditorContent(page, rawAfterRichMode.replace(FIRST_DIAGRAM, UPDATED_FIRST_DIAGRAM))
await expect.poll(readNoteBFile).toContain(UPDATED_FIRST_DIAGRAM)
await toggleRawMode(page, '.bn-editor')
await expectRenderedDiagramCount(page, 4)
await expect(page.locator('[data-testid="mermaid-diagram-viewport"]').first()).toContainText('Published')
await openNote(page, 'Note C')
await openNote(page, 'Note B')
await toggleRawMode(page, '.cm-content')
const reopenedRaw = await getRawEditorContent(page)
expect(reopenedRaw).toContain(UPDATED_FIRST_DIAGRAM)
expect(reopenedRaw).toContain(REPORTED_THEME_DIAGRAM)
expect(reopenedRaw).toContain(INVALID_DIAGRAM)
expect(reopenedRaw).toContain(SECOND_DIAGRAM)
expect(reopenedRaw).toContain(SYSTEM_OVERVIEW_DIAGRAM)
})
test('invalid reported Mermaid syntax recovers across source and AI panel rerenders', async ({ page }) => {
await openNote(page, 'Note B')
await toggleRawMode(page, '.cm-content')
const originalContent = await getRawEditorContent(page)
await setRawEditorContent(page, `${originalContent.trimEnd()}
${REPORTED_INVALID_DIAGRAM}
`)
await expect.poll(readNoteBFile).toContain(REPORTED_INVALID_DIAGRAM)
await toggleRawMode(page, '.bn-editor')
await expect(page.locator('[data-testid="mermaid-diagram-error"]')).toContainText('## ABC')
await expect(mermaidBodyArtifacts(page)).toHaveCount(0)
await expect(page.locator('#tolaria-fatal-render-error')).toHaveCount(0)
await page.getByRole('button', { name: 'Open the AI panel' }).click()
await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 5_000 })
await toggleRawMode(page, '.cm-content')
await toggleRawMode(page, '.bn-editor')
await expect(page.locator('[data-testid="mermaid-diagram-error"]')).toContainText('## ABC')
await expect(mermaidBodyArtifacts(page)).toHaveCount(0)
const input = page.getByTestId('agent-input')
await input.fill('Confirm the note is still usable.')
await page.getByTestId('agent-send').click()
await expect(page.getByTestId('ai-message').last()).toBeVisible({ timeout: 5_000 })
await expect(mermaidBodyArtifacts(page)).toHaveCount(0)
await expect(page.locator('#tolaria-fatal-render-error')).toHaveCount(0)
})