clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / edge / test / svg.test.ts
svg.test.ts 5.3K
1import { describe, expect, it } from 'vitest'2import { sanitizeAndDecorateSvg } from '../src/runtime/svg'34describe('SVG normalization', () => {5 it('removes active content and external links', () => {6 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 20" onload="alert(1)"><script>alert(1)</script><a href="https://example.com"><text>x</text></a></svg>'7 const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'pikchr')8 expect(result).not.toContain('onload')9 expect(result).not.toContain('<script')10 expect(result).not.toContain('https://example.com')11 expect(result).toContain('<text>x</text>')12 })1314 it('preserves CDATA labels and styles used by Ditaa, Symbolator, and TikZ', () => {15 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><style><![CDATA[.label { font-family: Helvetica; } @keyframes dashdraw { from { stroke-dashoffset: 10; } } .animated { animation: dashdraw 2s linear infinite; }]]></style><text class="label"><![CDATA[Alice Agent]]></text></svg>'16 const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'ditaa')17 expect(result).toContain('.label { font-family: Helvetica; }')18 expect(result).toContain('@keyframes dashdraw')19 expect(result).toContain('animation: dashdraw')20 expect(result).toContain('<text class="label">Alice Agent</text>')21 })2223 it('preserves a narrow safe subset of Mermaid XHTML labels', () => {24 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><foreignObject width="100" height="20"><div xmlns="http://www.w3.org/1999/xhtml" style="text-align:center"><span><strong>Alice</strong> Agent</span><script>alert(1)</script><img src="https://example.com/x"></img></div></foreignObject></svg>'25 const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'mermaid')26 expect(result).toContain('<foreignObject')27 expect(result).toContain('<strong>Alice</strong> Agent')28 expect(result).not.toContain('<script')29 expect(result).not.toContain('<img')30 expect(result).not.toContain('https://example.com')31 })3233 it('replaces renderer canvas backdrops without removing internal white shapes', () => {34 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 120 80"><rect x="-10" y="-10" width="100%" height="100%" fill="white"></rect><g><rect x="0" y="0" width="40" height="20" fill="white" stroke="black"></rect></g></svg>'35 const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '#f4f4f4', padding: 0, frame: false }, 'symbolator')36 expect(result).not.toContain('width="100%"')37 expect(result).toContain('<rect x="0" y="0" width="40" height="20" fill="white" stroke="black"></rect>')38 expect(result).toContain('background-color:#f4f4f4')39 expect(result).toContain('fill="#f4f4f4"')40 })4142 it('removes nested D2, Svgbob, and GraphViz canvas backdrops', () => {43 const d2 = sanitizeAndDecorateSvg(44 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80"><svg viewBox="-10 -10 120 80"><rect class="backdrop" x="-10" y="-10" width="120" height="80" fill="#FFFFFF"></rect><rect x="0" y="0" width="20" height="20" fill="#FFFFFF" stroke="#000000"></rect></svg></svg>',45 { title: '', description: '' },46 { background: '#f4f4f4', padding: 0, frame: false },47 'd2',48 )49 expect(d2).not.toContain('class="backdrop"')50 expect(d2).toContain('width="20" height="20" fill="#FFFFFF" stroke="#000000"')5152 const graphviz = sanitizeAndDecorateSvg(53 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><g><polygon fill="white" stroke="none" points="0,0 20,0 20,20 0,20"></polygon><polygon fill="white" stroke="black" points="2,2 4,2 4,4 2,4"></polygon></g></svg>',54 { title: '', description: '' },55 { background: '#f4f4f4', padding: 0, frame: false },56 'graphviz',57 )58 expect(graphviz).not.toContain('fill="white" stroke="none"')59 expect(graphviz).toContain('fill="white" stroke="black"')60 })6162 it('applies metadata and presentation after sanitization', () => {63 const source = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 10 20"><path d="M0 0"/></svg>'64 const result = sanitizeAndDecorateSvg(65 source,66 { title: 'A & B', description: 'Diagram <description>' },67 { background: '#ffffff', padding: 4, frame: true },68 'pikchr',69 )70 expect(result).toContain('viewBox="-4 -4 18 28"')71 expect(result).toContain('width="18"')72 expect(result).toContain('height="28"')73 expect(result).toContain('<title>A &amp; B</title>')74 expect(result).toContain('<desc>Diagram &lt;description&gt;</desc>')75 expect(result).toContain('fill="#ffffff"')76 expect(result).toContain('vector-effect="non-scaling-stroke"')77 expect(result).toContain('x="-3.5" y="-3.5" width="17" height="27" fill="none"')78 })7980 it('rejects unsafe stylesheet URLs', () => {81 expect(() => sanitizeAndDecorateSvg(82 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><style>rect{fill:url(https://example.com/a)}</style></svg>',83 { title: '', description: '' },84 { background: '', padding: 0, frame: false },85 'pikchr',86 )).toThrow('unsafe CSS')87 })88})