From d50db9b8df6eec4a1e7d5f43627d7ac5381bbd6a Mon Sep 17 00:00:00 2001 From: Valentin Date: Sat, 16 May 2026 10:23:56 +0000 Subject: [PATCH 1/5] test(markdown): add renderer and endpoint expectations --- tests/e2e/markdown.test.js | 169 ++++++++ .../playwrightAiSnapshotCapabilities.test.js | 37 ++ tests/helpers/client.js | 7 + tests/helpers/testSite.js | 69 ++++ tests/unit/markdown.test.js | 364 ++++++++++++++++++ 5 files changed, 646 insertions(+) create mode 100644 tests/e2e/markdown.test.js create mode 100644 tests/e2e/playwrightAiSnapshotCapabilities.test.js create mode 100644 tests/unit/markdown.test.js diff --git a/tests/e2e/markdown.test.js b/tests/e2e/markdown.test.js new file mode 100644 index 0000000..cb0dfb7 --- /dev/null +++ b/tests/e2e/markdown.test.js @@ -0,0 +1,169 @@ +import { createClient } from '../helpers/client.js'; +import { MAX_MARKDOWN_CHARS } from '../../lib/markdown.js'; +import { getSharedEnv } from './sharedEnv.js'; + +describe('Markdown endpoint (e2e)', () => { + let serverUrl; + let testSiteUrl; + + beforeAll(() => { + const env = getSharedEnv(); + serverUrl = env.serverUrl; + testSiteUrl = env.testSiteUrl; + }); + + test('document view is the default and returns readable Markdown without refs or controls', async () => { + const client = createClient(serverUrl); + try { + const { tabId } = await client.createTab(`${testSiteUrl}/markdown-fixture`); + + const result = await client.getMarkdown(tabId); + + expect(result.url).toContain('/markdown-fixture'); + expect(result.view).toBe('document'); + expect(typeof result.markdown).toBe('string'); + expect(result.markdown).toContain('# Release notes'); + expect(result.markdown).toContain('Read the [migration guide](/guide?keep=yes) before upgrading.'); + expect(result.markdown).toContain('- Build parser'); + expect(result.markdown).toContain('| Metric | Value |'); + expect(result.markdown).toContain('| Revenue | €10M |'); + expect(result.markdown).toContain('npm install @askjo/camofox-browser'); + expect(result.markdown).toContain('![Architecture diagram]'); + expect(result.markdown).not.toMatch(/\[e\d+\]/); + expect(result.markdown).not.toContain('[ref=e'); + expect(result.markdown).not.toContain(' { + const client = createClient(serverUrl); + try { + const { tabId } = await client.createTab(`${testSiteUrl}/markdown-fixture`); + + const result = await client.getMarkdown(tabId, { view: 'agent' }); + + expect(result.view).toBe('agent'); + expect(result.markdown).toContain('[migration guide](/guide?keep=yes)'); + expect(result.markdown).toMatch(/\[migration guide\]\(\/guide\?keep=yes\)\[e\d+\]/); + expect(result.markdown).toMatch(/ + Docs + + `); + + expect(typeof page.ariaSnapshot).toBe('function'); + const normal = await page.locator('body').ariaSnapshot({ timeout: 5000 }); + const ai = await page.ariaSnapshot({ mode: 'ai', timeout: 5000 }); + + expect(normal).toContain('AI Snapshot Capability'); + expect(normal).not.toMatch(/\[ref=e\d+\]/); + expect(ai).toContain('AI Snapshot Capability'); + expect(ai).toMatch(/\[ref=e\d+\]/); + expect(ai).toMatch(/button "Click me" \[ref=e\d+\]/); + expect(ai).toMatch(/link "Docs" \[ref=e\d+\]/); + } finally { + await browser.close().catch(() => {}); + } + }, 60000); +}); diff --git a/tests/helpers/client.js b/tests/helpers/client.js index 692341d..9a64f27 100644 --- a/tests/helpers/client.js +++ b/tests/helpers/client.js @@ -95,6 +95,13 @@ class BrowserClient { if (options.offset) params.append('offset', String(options.offset)); return this.request('GET', `/tabs/${tabId}/snapshot?${params}`); } + + async getMarkdown(tabId, options = {}) { + const params = new URLSearchParams({ userId: this.userId }); + if (options.view) params.append('view', options.view); + if (options.offset) params.append('offset', String(options.offset)); + return this.request('GET', `/tabs/${tabId}/markdown?${params}`); + } async click(tabId, options) { return this.request('POST', `/tabs/${tabId}/click`, { userId: this.userId, ...options }); diff --git a/tests/helpers/testSite.js b/tests/helpers/testSite.js index 97ad14a..c745001 100644 --- a/tests/helpers/testSite.js +++ b/tests/helpers/testSite.js @@ -270,6 +270,75 @@ function createTestApp() { `); }); + // Page with rich semantic content for /markdown endpoint tests + app.get('/markdown-fixture', (req, res) => { + res.send(` + + Markdown Fixture + +
+ Home + +
+
+
+

Release notes

+

Read the migration guide before upgrading.

+
    +
  • Build parser
  • +
  • +
+ + + +
MetricValue
Revenue€10M
+
npm install @askjo/camofox-browser
+ Architecture diagram + + + + + +

+
+
+ + + `); + }); + + app.get('/large-markdown-page', (req, res) => { + const count = parseInt(req.query.count) || 700; + const items = Array.from({ length: count }, (_, i) => + `
+

Article ${i}

+

Markdown endpoint long-form paragraph ${i}. This sentence makes each article large enough to force deterministic pagination.

+ Read article ${i} +
` + ).join('\n'); + res.send(` + + Large Markdown + +
+

Large Markdown Page

+ ${items} +
+ + + `); + }); + // Page with scrollable content app.get('/scroll', (req, res) => { const items = Array.from({ length: 100 }, (_, i) => `

Item ${i}

`).join('\n'); diff --git a/tests/unit/markdown.test.js b/tests/unit/markdown.test.js new file mode 100644 index 0000000..96b442b --- /dev/null +++ b/tests/unit/markdown.test.js @@ -0,0 +1,364 @@ +import { + MAX_MARKDOWN_CHARS, + parseAriaSnapshot, + renderAgentMarkdown, + renderDocumentMarkdown, + renderMarkdownFromAriaSnapshot, + windowMarkdown, +} from '../../lib/markdown.js'; + +describe('deterministic aria snapshot markdown renderer', () => { + function expectNoRefs(markdown) { + expect(markdown).not.toContain('[ref=e'); + expect(markdown).not.toMatch(/\[e\d+\]/); + expect(markdown).not.toContain('no ref'); + expect(markdown).not.toContain('[cursor=pointer]'); + expect(markdown).not.toContain('[nth='); + } + + test('empty input returns empty Markdown', () => { + expect(renderDocumentMarkdown('')).toBe(''); + expect(renderDocumentMarkdown(null)).toBe(''); + expect(renderAgentMarkdown(undefined)).toBe(''); + }); + + test('renders common browser content shapes in document mode', () => { + const raw = [ + '- heading "Quarterly results" [level=1]', + '- paragraph "Revenue rose 12 percent year over year."', + '- list:', + ' - listitem "Europe revenue accelerated"', + '- link "Investor presentation" [ref=e1]:', + ' - /url: https://example.test/investors.pdf', + '- button "Subscribe" [ref=e2] [disabled]', + '- textbox "Search articles" [ref=e3]: value: "earnings"', + '- img "CEO speaking at the annual meeting" [ref=e4]', + '- table "Financial summary":', + ' - row:', + ' - columnheader "Metric"', + ' - columnheader "Value"', + ' - row:', + ' - cell "Revenue"', + ' - cell "€10M"', + '- code "print(\\"ok\\")"', + ].join('\n'); + + const result = renderDocumentMarkdown(raw, { title: 'Results' }); + + expect(result).toContain('# Quarterly results'); + expect(result).toContain('Revenue rose 12 percent year over year.'); + expect(result).toContain('- Europe revenue accelerated'); + expect(result).toContain('[Investor presentation](https://example.test/investors.pdf)'); + expect(result).toContain('![CEO speaking at the annual meeting]'); + expect(result).toContain('| Metric | Value |'); + expect(result).toContain('| Revenue | €10M |'); + expect(result).toContain('```\nprint("ok")\n```'); + expect(result).not.toContain('Subscribe'); + expect(result).not.toContain('Search articles'); + expectNoRefs(result); + }); + + test('prepends title only when no matching heading exists', () => { + expect(renderDocumentMarkdown('- paragraph "Body."', { title: 'Article' })).toBe('# Article\n\nBody.'); + expect(renderDocumentMarkdown('- heading "Article" [level=1]\n- paragraph "Body."', { title: 'Article' })) + .toBe('# Article\n\nBody.'); + }); + + test('parses YAML-quoted role keys with colons', () => { + const raw = [ + '- main:', + ' - \'heading "feat(web): local Camofox #123" [level=1]\':', + ' - paragraph "Body"', + ].join('\n'); + + expect(renderDocumentMarkdown(raw, { title: 'feat(web): local Camofox #123' })) + .toBe('# feat(web): local Camofox #123\n\nBody'); + }); + + test('parses nested aria snapshot indentation and slash properties', () => { + const raw = [ + '- main:', + ' - article "Story":', + ' - heading "Title" [level=1]', + ' - paragraph:', + ' - text: "Hello "', + ' - link "world":', + ' - /url: /world', + ' - paragraph "Sibling outside article"', + ].join('\n'); + + const root = parseAriaSnapshot(raw); + const main = root.children[0]; + const article = main.children[0]; + const paragraph = article.children[1]; + const link = paragraph.children[1]; + + expect(main.role).toBe('main'); + expect(article.role).toBe('article'); + expect(link.role).toBe('link'); + expect(link.props.url).toBe('/world'); + expect(main.children[1].label).toBe('Sibling outside article'); + }); + + test('preserves anchor URLs and links without URLs while stripping refs', () => { + const anchor = '- link "Contenu" [ref=e1]:\n - /url: "#content"'; + const noUrl = '- link "Edition abonnés" [ref=e7]'; + + expect(renderDocumentMarkdown(anchor)).toBe('[Contenu](#content)'); + expect(renderDocumentMarkdown(noUrl)).toBe('Edition abonnés'); + }); + + test('strips ref/cursor/nth artifacts inside labels but preserves numeric citations', () => { + const raw = [ + '- paragraph:', + ' - text: "Hermes is mentioned in [1] and [12]. Read "', + ' - link "[e247]Hermes [cursor=pointer]" [ref=e247] [nth=0]:', + ' - /url: /wiki/Hermes', + ' - text: " now."', + ].join('\n'); + + const result = renderDocumentMarkdown(raw); + + expect(result).toContain('[1]'); + expect(result).toContain('[12]'); + expect(result).toContain('[Hermes](/wiki/Hermes)'); + expectNoRefs(result); + }); + + test('document and agent share readable path but document omits refs and controls', () => { + const raw = [ + '- paragraph:', + ' - text: "Read "', + ' - link "Docs" [ref=e1]:', + ' - /url: /docs', + ' - text: " now."', + '- button "Subscribe" [ref=e2]', + '- textbox "Email" [ref=e3]: value: "alice@example.test"', + ].join('\n'); + + expect(renderDocumentMarkdown(raw)).toBe('Read [Docs](/docs) now.'); + const agent = renderAgentMarkdown(raw); + expect(agent).toContain('Read [Docs](/docs)[e1] now.'); + expect(agent).toContain('