feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,622 @@
|
|||
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)
|
||||
})
|
||||
Loading…
Reference in a new issue