mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* chore: enforce test directory conventions Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only. * ci: use guard and package-scoped tests Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases. * ci: align stable release guard check Use the new repository guard in stable release verification after replacing the residual-JS-only script. * chore: tighten test layout enforcement Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests. * chore: clarify no-emit test tsconfigs Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs.
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
DeckHtmlRenderer,
|
|
HtmlRenderer,
|
|
MarkdownRenderer,
|
|
ReactComponentRenderer,
|
|
RendererRegistry,
|
|
SvgRenderer,
|
|
artifactRendererRegistry,
|
|
} from '../../src/artifacts/renderer-registry';
|
|
import { renderMarkdownToSafeHtml } from '../../src/artifacts/markdown';
|
|
import type { ProjectFile } from '../../src/types';
|
|
|
|
function baseFile(overrides: Partial<ProjectFile> & Pick<ProjectFile, 'name'>): ProjectFile {
|
|
return {
|
|
path: 'artifact.html',
|
|
type: 'file',
|
|
size: 1,
|
|
mtime: Date.now(),
|
|
kind: 'html',
|
|
mime: 'text/html; charset=utf-8',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('RendererRegistry', () => {
|
|
const registry = new RendererRegistry([
|
|
ReactComponentRenderer,
|
|
DeckHtmlRenderer,
|
|
HtmlRenderer,
|
|
MarkdownRenderer,
|
|
SvgRenderer,
|
|
]);
|
|
|
|
it('resolves markdown renderer from explicit manifest', () => {
|
|
const file = baseFile({
|
|
name: 'notes.md',
|
|
kind: 'text',
|
|
mime: 'text/markdown; charset=utf-8',
|
|
artifactManifest: {
|
|
version: 1,
|
|
kind: 'markdown-document',
|
|
title: 'Notes',
|
|
entry: 'notes.md',
|
|
renderer: 'markdown',
|
|
exports: ['md', 'html'],
|
|
},
|
|
});
|
|
const match = registry.resolve({ file, isDeckHint: false });
|
|
expect(match?.renderer.id).toBe('markdown');
|
|
expect(match?.manifest.renderer).toBe('markdown');
|
|
});
|
|
|
|
it('falls back to inferred markdown manifest for .md files', () => {
|
|
const file = baseFile({
|
|
name: 'README.md',
|
|
kind: 'text',
|
|
mime: 'text/markdown; charset=utf-8',
|
|
artifactManifest: undefined,
|
|
});
|
|
const match = registry.resolve({ file, isDeckHint: false });
|
|
expect(match?.renderer.id).toBe('markdown');
|
|
expect(match?.manifest.kind).toBe('markdown-document');
|
|
});
|
|
|
|
it('resolves svg renderer from explicit manifest', () => {
|
|
const file = baseFile({
|
|
name: 'diagram.svg',
|
|
kind: 'sketch',
|
|
mime: 'image/svg+xml',
|
|
artifactManifest: {
|
|
version: 1,
|
|
kind: 'svg',
|
|
title: 'Diagram',
|
|
entry: 'diagram.svg',
|
|
renderer: 'svg',
|
|
exports: ['svg'],
|
|
},
|
|
});
|
|
const match = registry.resolve({ file, isDeckHint: false });
|
|
expect(match?.renderer.id).toBe('svg');
|
|
expect(match?.manifest.renderer).toBe('svg');
|
|
});
|
|
|
|
it('falls back to inferred svg manifest for .svg files', () => {
|
|
const file = baseFile({
|
|
name: 'logo.svg',
|
|
kind: 'sketch',
|
|
mime: 'image/svg+xml',
|
|
artifactManifest: undefined,
|
|
});
|
|
const match = registry.resolve({ file, isDeckHint: false });
|
|
expect(match?.renderer.id).toBe('svg');
|
|
expect(match?.manifest.kind).toBe('svg');
|
|
});
|
|
|
|
it('keeps unknown files on old fallback path', () => {
|
|
const file = baseFile({
|
|
name: 'archive.bin',
|
|
kind: 'binary',
|
|
mime: 'application/octet-stream',
|
|
artifactManifest: undefined,
|
|
});
|
|
expect(registry.resolve({ file, isDeckHint: false })).toBeNull();
|
|
});
|
|
|
|
it('exposes conservative streaming contract values', () => {
|
|
expect(HtmlRenderer.supportsStreaming).toBe(false);
|
|
expect(DeckHtmlRenderer.supportsStreaming).toBe(false);
|
|
|
|
expect(MarkdownRenderer.supportsStreaming).toBe(true);
|
|
expect(MarkdownRenderer.renderPartial).toBe(renderMarkdownToSafeHtml);
|
|
|
|
expect(SvgRenderer.supportsStreaming).toBe(false);
|
|
expect(SvgRenderer.renderPartial).toBeUndefined();
|
|
});
|
|
|
|
it('keeps markdown partial renderer output safe', () => {
|
|
const out = MarkdownRenderer.renderPartial?.('[<script>alert(1)</script>](https://example.com/a_b_c)') ?? '';
|
|
expect(out).toContain('<script>alert(1)</script>');
|
|
expect(out).toContain('href="https://example.com/a_b_c"');
|
|
expect(out).not.toContain('<script>');
|
|
});
|
|
|
|
it('routes JSX and TSX files to the React component renderer', () => {
|
|
expect(
|
|
artifactRendererRegistry.resolve({
|
|
file: baseFile({
|
|
name: 'Hero.jsx',
|
|
kind: 'code',
|
|
mime: 'text/javascript; charset=utf-8',
|
|
}),
|
|
isDeckHint: false,
|
|
})?.renderer.id,
|
|
).toBe('react-component');
|
|
expect(
|
|
artifactRendererRegistry.resolve({
|
|
file: baseFile({
|
|
name: 'Hero.tsx',
|
|
kind: 'code',
|
|
mime: 'text/typescript; charset=utf-8',
|
|
}),
|
|
isDeckHint: false,
|
|
})?.renderer.id,
|
|
).toBe('react-component');
|
|
});
|
|
|
|
it('prefers an explicit React manifest over the coarse code kind', () => {
|
|
expect(
|
|
artifactRendererRegistry.resolve({
|
|
file: baseFile({
|
|
name: 'entry.txt',
|
|
kind: 'text',
|
|
artifactManifest: {
|
|
version: 1,
|
|
kind: 'react-component',
|
|
title: 'Entry',
|
|
entry: 'entry.txt',
|
|
renderer: 'react-component',
|
|
exports: ['jsx', 'html', 'zip'],
|
|
},
|
|
}),
|
|
isDeckHint: false,
|
|
})?.renderer.id,
|
|
).toBe('react-component');
|
|
});
|
|
});
|