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(' + + +`; +} + +export const getMarkdownFiles = (content: string): Record => { + const md = content || '# No content provided'; return { - 'content.md': content || '# No content provided', - 'App.tsx': wrapMarkdownRenderer(content), - 'index.tsx': dedent(`import React, { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./styles.css"; -import "./markdown.css"; - -import App from "./App"; - -const root = createRoot(document.getElementById("root")); -root.render(); -;`), - '/components/ui/MarkdownRenderer.tsx': markdownRenderer, - 'markdown.css': markdownCSS, + 'content.md': md, + 'index.html': generateMarkdownHtml(md), }; };