diff --git a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts
index e46a285c50..5ffd52879f 100644
--- a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts
+++ b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts
@@ -19,7 +19,7 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
expect(result.current.fileKey).toBe('content.md');
- expect(result.current.template).toBe('react-ts');
+ expect(result.current.template).toBe('static');
});
it('should handle text/plain type with content.md as fileKey', () => {
@@ -31,7 +31,7 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
expect(result.current.fileKey).toBe('content.md');
- expect(result.current.template).toBe('react-ts');
+ expect(result.current.template).toBe('static');
});
it('should include content.md in files with original markdown', () => {
@@ -46,7 +46,7 @@ describe('useArtifactProps', () => {
expect(result.current.files['content.md']).toBe(markdownContent);
});
- it('should include App.tsx with wrapped markdown renderer', () => {
+ it('should include index.html with static markdown rendering', () => {
const artifact = createArtifact({
type: 'text/markdown',
content: '# Test',
@@ -54,8 +54,8 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- expect(result.current.files['App.tsx']).toContain('MarkdownRenderer');
- expect(result.current.files['App.tsx']).toContain('import React from');
+ expect(result.current.files['index.html']).toContain('');
+ expect(result.current.files['index.html']).toContain('marked.parse');
});
it('should include all required markdown files', () => {
@@ -66,12 +66,8 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- // Check all required files are present
expect(result.current.files['content.md']).toBeDefined();
- expect(result.current.files['App.tsx']).toBeDefined();
- expect(result.current.files['index.tsx']).toBeDefined();
- expect(result.current.files['/components/ui/MarkdownRenderer.tsx']).toBeDefined();
- expect(result.current.files['markdown.css']).toBeDefined();
+ expect(result.current.files['index.html']).toBeDefined();
});
it('should escape special characters in markdown content', () => {
@@ -82,13 +78,11 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- // Original content should be preserved in content.md
expect(result.current.files['content.md']).toContain('`const x = 1;`');
expect(result.current.files['content.md']).toContain('C:\\Users');
- // App.tsx should have escaped content
- expect(result.current.files['App.tsx']).toContain('\\`');
- expect(result.current.files['App.tsx']).toContain('\\\\');
+ expect(result.current.files['index.html']).toContain('\\`');
+ expect(result.current.files['index.html']).toContain('\\\\');
});
it('should handle empty markdown content', () => {
@@ -112,7 +106,7 @@ describe('useArtifactProps', () => {
expect(result.current.files['content.md']).toBe('# No content provided');
});
- it('should provide react-markdown dependency', () => {
+ it('should have no custom dependencies for markdown (uses CDN)', () => {
const artifact = createArtifact({
type: 'text/markdown',
content: '# Test',
@@ -120,9 +114,8 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('react-markdown');
- expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-gfm');
- expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('remark-breaks');
+ const deps = result.current.sharedProps.customSetup?.dependencies ?? {};
+ expect(deps).toEqual({});
});
it('should update files when content changes', () => {
@@ -137,7 +130,6 @@ describe('useArtifactProps', () => {
expect(result.current.files['content.md']).toBe('# Original');
- // Update the artifact content
const updatedArtifact = createArtifact({
...artifact,
content: '# Updated',
@@ -201,8 +193,6 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- // Language parameter should not affect markdown handling
- // It checks the type directly, not the key
expect(result.current.fileKey).toBe('content.md');
expect(result.current.files['content.md']).toBe('# Test');
});
@@ -214,7 +204,6 @@ describe('useArtifactProps', () => {
const { result } = renderHook(() => useArtifactProps({ artifact }));
- // Should use default behavior
expect(result.current.template).toBe('static');
});
});
diff --git a/client/src/utils/__tests__/markdown.test.ts b/client/src/utils/__tests__/markdown.test.ts
index 9734e0e18a..9834f034e9 100644
--- a/client/src/utils/__tests__/markdown.test.ts
+++ b/client/src/utils/__tests__/markdown.test.ts
@@ -1,4 +1,4 @@
-import { isSafeUrl, getMarkdownFiles } from '../markdown';
+import { isSafeUrl, getMarkdownFiles, EMBEDDED_IS_SAFE_URL } from '../markdown';
describe('isSafeUrl', () => {
it('allows https URLs', () => {
@@ -68,6 +68,37 @@ describe('isSafeUrl', () => {
});
});
+describe('isSafeUrl sync verification', () => {
+ const embeddedFn = new Function('url', EMBEDDED_IS_SAFE_URL + '\nreturn isSafeUrl(url);') as (
+ url: string,
+ ) => boolean;
+
+ const cases: [string, boolean][] = [
+ ['https://example.com', true],
+ ['http://example.com', true],
+ ['mailto:a@b.com', true],
+ ['tel:+1234567890', true],
+ ['/relative', true],
+ ['./relative', true],
+ ['../up', true],
+ ['#anchor', true],
+ ['javascript:alert(1)', false],
+ [' javascript:void(0)', false],
+ ['data:text/html,x', false],
+ ['blob:http://x.com/uuid', false],
+ ['vbscript:run', false],
+ ['file:///etc/passwd', false],
+ ['custom:payload', false],
+ ['', false],
+ [' ', false],
+ ];
+
+ it.each(cases)('embedded copy matches exported isSafeUrl for %j → %s', (url, expected) => {
+ expect(embeddedFn(url)).toBe(expected);
+ expect(isSafeUrl(url)).toBe(expected);
+ });
+});
+
describe('markdown artifacts', () => {
describe('getMarkdownFiles', () => {
it('should return content.md with the original markdown content', () => {
@@ -83,46 +114,26 @@ describe('markdown artifacts', () => {
expect(files['content.md']).toBe('# No content provided');
});
- it('should include App.tsx with MarkdownRenderer component', () => {
+ it('should include index.html with static markdown rendering', () => {
const markdown = '# Test';
const files = getMarkdownFiles(markdown);
- expect(files['App.tsx']).toContain('import React from');
- expect(files['App.tsx']).toContain(
- "import MarkdownRenderer from '/components/ui/MarkdownRenderer'",
- );
- expect(files['App.tsx']).toContain('');
+ expect(files['index.html']).toContain('marked.min.js');
+ expect(files['index.html']).toContain('marked.parse');
+ expect(files['index.html']).toContain('# Test');
});
- it('should include index.tsx entry point', () => {
- const markdown = '# Test';
- const files = getMarkdownFiles(markdown);
-
- expect(files['index.tsx']).toContain('import App from "./App"');
- expect(files['index.tsx']).toContain('import "./styles.css"');
- expect(files['index.tsx']).toContain('import "./markdown.css"');
- expect(files['index.tsx']).toContain('createRoot');
+ it('should only produce content.md and index.html', () => {
+ const files = getMarkdownFiles('# Test');
+ expect(Object.keys(files).sort()).toEqual(['content.md', 'index.html']);
});
- it('should include MarkdownRenderer component file', () => {
- const markdown = '# Test';
- const files = getMarkdownFiles(markdown);
-
- expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('import ReactMarkdown from');
- expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('MarkdownRendererProps');
- expect(files['/components/ui/MarkdownRenderer.tsx']).toContain(
- 'export default MarkdownRenderer',
- );
- });
-
- it('should include markdown.css with styling', () => {
- const markdown = '# Test';
- const files = getMarkdownFiles(markdown);
-
- expect(files['markdown.css']).toContain('.markdown-body');
- expect(files['markdown.css']).toContain('list-style-type: disc');
- expect(files['markdown.css']).toContain('prefers-color-scheme: dark');
+ it('should include markdown CSS in index.html', () => {
+ const files = getMarkdownFiles('# Test');
+ expect(files['index.html']).toContain('.markdown-body');
+ expect(files['index.html']).toContain('list-style-type: disc');
+ expect(files['index.html']).toContain('prefers-color-scheme: dark');
});
describe('content escaping', () => {
@@ -130,29 +141,36 @@ describe('markdown artifacts', () => {
const markdown = 'Here is some `inline code`';
const files = getMarkdownFiles(markdown);
- expect(files['App.tsx']).toContain('\\`');
+ expect(files['index.html']).toContain('\\`');
});
it('should escape backslashes in markdown content', () => {
const markdown = 'Path: C:\\Users\\Test';
const files = getMarkdownFiles(markdown);
- expect(files['App.tsx']).toContain('\\\\');
+ expect(files['index.html']).toContain('\\\\');
});
it('should escape dollar signs in markdown content', () => {
const markdown = 'Price: $100';
const files = getMarkdownFiles(markdown);
- expect(files['App.tsx']).toContain('\\$');
+ expect(files['index.html']).toContain('\\$');
});
it('should handle code blocks with backticks', () => {
const markdown = '```js\nconsole.log("test");\n```';
const files = getMarkdownFiles(markdown);
- // Should be escaped
- expect(files['App.tsx']).toContain('\\`\\`\\`');
+ expect(files['index.html']).toContain('\\`\\`\\`');
+ });
+
+ it('should prevent in content from breaking out of the script block', () => {
+ const markdown = 'Some content with ';
+ const files = getMarkdownFiles(markdown);
+
+ expect(files['index.html']).not.toContain('
+
+