Files
Momento/memento-note/tests/unit/interactive-page-validate.test.ts
Antigravity 69c99e4f4f feat: page interactive, démos Play/Step et simulateur Carnot
Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 17:51:43 +00:00

125 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, test } from 'vitest'
import {
validateInteractivePage,
type PageSpecV1,
} from '../../lib/interactive-page'
const fixturePath = join(
__dirname,
'../../lib/interactive-page/fixtures/thermo-page.json'
)
function loadFixture(): PageSpecV1 {
return JSON.parse(readFileSync(fixturePath, 'utf8')) as PageSpecV1
}
function clone<T>(v: T): T {
return JSON.parse(JSON.stringify(v)) as T
}
describe('validateInteractivePage', () => {
test('thermo fixture passes (positive)', () => {
const result = validateInteractivePage(loadFixture())
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.page.id).toBe('page.thermo-test')
expect(result.page.schemaVersion).toBe(1)
expect(result.page.sections.length).toBeGreaterThanOrEqual(3)
}
})
test('rejects unknown block type', () => {
const page = clone(loadFixture()) as Record<string, unknown>
const sections = page.sections as Array<{ blocks: unknown[] }>
sections[0].blocks.push({ type: 'carousel', items: [] })
const result = validateInteractivePage(page)
expect(result.ok).toBe(false)
if (!result.ok) {
expect(
result.issues.some(
(i) =>
i.path.includes('blocks') ||
i.code === 'invalid_union_discriminator' ||
i.message.toLowerCase().includes('discriminat')
)
).toBe(true)
}
})
test('rejects hex color in non-human structural field', () => {
const page = clone(loadFixture())
// intent is enum — inject hex via overview card by forcing raw object
const raw = clone(loadFixture()) as Record<string, unknown>
const overview = raw.overview as {
cards: Array<Record<string, unknown>>
}
overview.cards[0].intent = '#ff0000'
const result = validateInteractivePage(raw)
expect(result.ok).toBe(false)
})
test('rejects duplicate section id', () => {
const page = clone(loadFixture())
page.sections[1].id = page.sections[0].id
const result = validateInteractivePage(page)
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.issues.some((i) => i.code === 'duplicate_section_id')).toBe(
true
)
}
})
test('rejects invalid embedded demo (delegation)', () => {
const page = clone(loadFixture())
const demoBlock = page.sections
.flatMap((s) => s.blocks)
.find((b) => b.type === 'demo')
expect(demoBlock?.type).toBe('demo')
if (demoBlock?.type === 'demo') {
// @ts-expect-error intentional
demoBlock.demo.acts[0].steps[0].pattern = 'notARealPattern'
}
const result = validateInteractivePage(page)
expect(result.ok).toBe(false)
if (!result.ok) {
expect(
result.issues.some(
(i) =>
i.path.includes('demo') ||
i.code.startsWith('demo_') ||
i.path.includes('pattern')
)
).toBe(true)
}
})
test('rejects table row width mismatch', () => {
const page = clone(loadFixture())
const table = page.sections
.flatMap((s) => s.blocks)
.find((b) => b.type === 'table')
expect(table?.type).toBe('table')
if (table?.type === 'table') {
table.rows[0] = ['only-one']
}
const result = validateInteractivePage(page)
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.issues.some((i) => i.code === 'table_shape')).toBe(true)
}
})
test('allows hex mentioned in human prose', () => {
const page = clone(loadFixture())
const prose = page.sections[0].blocks.find((b) => b.type === 'prose')
if (prose?.type === 'prose') {
prose.md = 'La couleur #FF0000 est un signal dalarme.'
}
const result = validateInteractivePage(page)
expect(result.ok).toBe(true)
})
})