open-design/apps/web/tests/runtime/markdown.test.tsx
chaoxiaoche dc2cb6b371
feat(web): render code comment directives (#2871)
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-25 11:09:59 +00:00

300 lines
12 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.

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderToStaticMarkup } from 'react-dom/server';
import { renderMarkdown } from '../../src/runtime/markdown';
function html(input: string): string {
return renderToStaticMarkup(<>{renderMarkdown(input)}</>);
}
describe('renderMarkdown', () => {
let writeTextMock: ReturnType<typeof vi.fn>;
let originalClipboard: PropertyDescriptor | undefined;
beforeEach(() => {
originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
writeTextMock = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: writeTextMock,
},
});
});
afterEach(() => {
if (originalClipboard) {
Object.defineProperty(navigator, 'clipboard', originalClipboard);
} else {
delete (navigator as { clipboard?: Clipboard }).clipboard;
}
cleanup();
vi.clearAllMocks();
});
it('autolinks bare https URLs without breaking on underscores in query params', () => {
// OAuth-style URL with underscores in `response_type`, `client_id`,
// `code_challenge`, `code_challenge_method`. The previous renderer
// greedily matched `_..._` as italic and shredded the URL into pieces.
const url =
'https://mcp.higgsfield.ai/oauth2/authorize?response_type=code&client_id=abc&code_challenge=xyz&code_challenge_method=S256';
// HTML attribute encoding swaps `&` for `&amp;` — compare against the
// encoded form rather than the raw URL we passed in.
const encoded = url.replace(/&/g, '&amp;');
const out = html(`Open this link: ${url}`);
expect(out).toContain(`href="${encoded}"`);
expect(out).toContain(`>${encoded}</a>`);
// The italic <em> tag should NOT have been emitted from the URL fragments.
expect(out).not.toContain('<em>');
});
it('keeps italic working in regular prose', () => {
const out = html('A word with _emphasis_ here.');
expect(out).toContain('<em>emphasis</em>');
});
it('renders explicit [text](url) markdown links', () => {
const out = html('Click [here](https://example.com/page) to continue.');
expect(out).toContain('<a class="md-link"');
expect(out).toContain('href="https://example.com/page"');
expect(out).toContain('>here</a>');
});
it('marks bare URLs with the bare-link class so CSS can break them mid-string', () => {
const out = html('See https://example.com/very/long/path?with=long&query=string');
expect(out).toContain('md-link-bare');
});
it.each([
{
name: 'ascii comma',
input: 'Open https://example.com/demo, please.',
href: 'https://example.com/demo',
rendered: '>https://example.com/demo</a>,',
excludedHref: 'https://example.com/demo,',
},
{
name: 'fullwidth full stop',
input: 'Visit https://example.com/final。',
href: 'https://example.com/final',
rendered: '>https://example.com/final</a>。',
excludedHref: 'https://example.com/final。',
},
{
name: 'wrapped in fullwidth parens',
input: 'https://example.com/a',
href: 'https://example.com/a',
rendered: '<a class="md-link md-link-bare" href="https://example.com/a"',
trailing: '>https://example.com/a</a>',
excludedHref: 'https://example.com/a',
},
{
name: 'lone trailing CJK quote',
input: 'https://example.com/b」',
href: 'https://example.com/b',
rendered: '>https://example.com/b</a>」',
excludedHref: 'https://example.com/b」',
},
{
name: 'stacked CJK punctuation',
input: 'https://example.com/c。',
href: 'https://example.com/c',
rendered: '>https://example.com/c</a>。)',
excludedHref: 'https://example.com/c。',
},
{
name: 'no trailing punctuation',
input: 'https://example.com/path',
href: 'https://example.com/path',
rendered: '>https://example.com/path</a>',
excludedHref: '',
},
])('keeps bare autolink punctuation handling stable: $name', ({ input, href, rendered, excludedHref, trailing }) => {
const out = html(input);
expect(out).toContain(`href="${href}"`);
expect(out).toContain(rendered);
if (trailing) {
expect(out).toContain(trailing);
}
if (excludedHref) {
expect(out).not.toContain(`href="${excludedHref}"`);
}
});
it('does not autolink inside inline code spans', () => {
const out = html('Use `https://example.com/x` literally.');
// The URL should appear inside a <code> tag, not turned into an anchor.
expect(out).toContain('<code class="md-inline-code">https://example.com/x</code>');
});
it('adds copy controls to fenced code blocks', () => {
const out = html('```tsx\nexport const ok = true;\n```');
expect(out).toContain('class="md-code-block"');
expect(out).toContain('class="md-code-actions"');
expect(out).toContain('class="md-code-action"');
expect(out).toContain('>Copy</button>');
expect(out).toContain('export const ok = true;');
});
it('copies fenced code block contents', async () => {
render(<>{renderMarkdown('```css\n.card { color: red; }\n```')}</>);
fireEvent.click(screen.getByRole('button', { name: 'Copy' }));
await waitFor(() => {
expect(writeTextMock).toHaveBeenCalledWith('.card { color: red; }');
});
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Copied!' })).toBeTruthy();
});
});
it('renders Codex code-comment directives as annotation cards', () => {
const out = html(
'Before\n' +
'::code-comment{title="[P2] Guard empty state" body="This should check the empty queue before reading the first task." file="/repo/apps/web/src/Chat.tsx" start=42 end=44 priority=2}\n' +
'After',
);
expect(out).toContain('Before');
expect(out).toContain('class="md-code-comment"');
expect(out).toContain('[P2] Guard empty state');
expect(out).toContain('This should check the empty queue before reading the first task.');
expect(out).toContain('/repo/apps/web/src/Chat.tsx:42-44');
expect(out).toContain('P2');
expect(out).toContain('After');
expect(out).not.toContain('::code-comment');
});
it('leaves malformed code-comment directives as text', () => {
const out = html('::code-comment{title="No file" body="Missing file"}');
expect(out).toContain('::code-comment');
expect(out).not.toContain('class="md-code-comment"');
});
it('renders a GFM pipe table with header, body, and alignment', () => {
const md = [
'| L | C | R |',
'|:---|:---:|---:|',
'| a | b | c |',
'| d | e | f |',
].join('\n');
const out = html(md);
expect(out).toContain('<div class="md-table-wrap">');
expect(out).toContain('<table class="md-table">');
expect(out).toContain('<th style="text-align:left">L</th>');
expect(out).toContain('<th style="text-align:center">C</th>');
expect(out).toContain('<th style="text-align:right">R</th>');
expect(out).toContain('<td style="text-align:left">a</td>');
expect(out).toContain('<td style="text-align:right">f</td>');
expect(out).not.toContain('<p>| L');
});
it('renders inline code and bold inside table cells', () => {
const md = ['| k | v |', '|---|---|', '| `id` | **bold** |'].join('\n');
const out = html(md);
expect(out).toContain('<code class="md-inline-code">id</code>');
expect(out).toContain('<strong>bold</strong>');
});
it('keeps escaped pipes literal inside a cell', () => {
const md = ['| a | b |', '|---|---|', '| x \\| y | z |'].join('\n');
const out = html(md);
expect(out).toContain('x | y');
});
it('breaks the preceding paragraph at a table start without a blank line', () => {
const md = ['Intro paragraph', '| a | b |', '|---|---|', '| 1 | 2 |'].join('\n');
const out = html(md);
expect(out).toContain('Intro paragraph');
expect(out).toContain('<div class="md-table-wrap">');
expect(out).toContain('<table class="md-table">');
expect(out).not.toContain('Intro paragraph\n| a');
});
it('does not promote a stray pipe-containing line to a table', () => {
const out = html('Just a line with a | pipe.');
expect(out).not.toContain('<table');
expect(out).toContain('| pipe');
});
it('treats pipes inside a backtick code span as cell content, not column boundaries', () => {
// TypeScript-style union cells contain a literal `|` inside backticks.
// The pre-review splitter ran before inline parsing and shredded such
// rows; this asserts the scan-based splitter keeps the code span whole
// (one body cell, not two).
const md = ['| status | type |', '|---|---|', '| ok | `"ready" | "done"` |'].join('\n');
const out = html(md);
expect(out).toContain('<code class="md-inline-code">&quot;ready&quot; | &quot;done&quot;</code>');
// Exactly two <td> cells in the body row — pipe inside backticks must
// not have introduced a phantom third column.
const bodyTd = (out.match(/<tbody>[\s\S]*<\/tbody>/)?.[0] ?? '').match(/<td/g) ?? [];
expect(bodyTd.length).toBe(2);
});
it('renders ![alt](url) as <img> for relative BYOK image URLs', () => {
const out = html('Here is your cat: ![cute kitten](/api/byok-image/abc-123.png)');
expect(out).toContain('<img');
expect(out).toContain('class="md-image"');
expect(out).toContain('src="/api/byok-image/abc-123.png"');
expect(out).toContain('alt="cute kitten"');
expect(out).toContain('loading="lazy"');
expect(out).toContain('referrerPolicy="no-referrer"');
// Image syntax must NOT be turned into an <a> link — `[alt](url)`
// with a leading `!` is image, not link.
expect(out).not.toContain('<a class="md-link"');
});
it('renders ![](url) with empty alt text', () => {
const out = html('![](/api/byok-image/abc.png)');
expect(out).toContain('<img');
expect(out).toContain('alt=""');
});
it('renders https image URLs', () => {
const out = html('![logo](https://example.com/logo.png)');
expect(out).toContain('<img');
expect(out).toContain('src="https://example.com/logo.png"');
});
it('renders data: image URIs', () => {
const out = html('![inline](data:image/png;base64,iVBORw0KGgo=)');
expect(out).toContain('<img');
expect(out).toContain('src="data:image/png;base64,iVBORw0KGgo="');
});
it('drops image tags with unsafe schemes and keeps alt text as plain text', () => {
const out = html('![hacked](javascript:alert(1))');
expect(out).not.toContain('<img');
expect(out).not.toContain('javascript:');
expect(out).toContain('hacked');
});
it('rejects protocol-relative image URLs (could load cross-origin)', () => {
// `//evil.com/track.png` would inherit the page protocol; not in our
// allowlist. Should fall through to alt-as-text.
const out = html('![track](//evil.com/track.png)');
expect(out).not.toContain('<img');
expect(out).toContain('track');
});
it('keeps regular [text](url) links working alongside image syntax', () => {
const out = html('Click [here](https://example.com) and look ![image](/api/byok-image/a.png)');
expect(out).toContain('<a class="md-link"');
expect(out).toContain('href="https://example.com"');
expect(out).toContain('>here</a>');
expect(out).toContain('<img');
expect(out).toContain('src="/api/byok-image/a.png"');
});
it('preserves bold + italic + code after the image regex addition', () => {
const out = html('**b** and *i* and `c` and ![a](/p.png)');
expect(out).toContain('<strong>b</strong>');
expect(out).toContain('<em>i</em>');
expect(out).toContain('<code class="md-inline-code">c</code>');
expect(out).toContain('<img');
});
});