Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 157 lines 5.3 kB view raw
1/** 2 * Regression tests for #719: version-history preview shows raw ProseMirror 3 * schema XML instead of rendered document. 4 * 5 * Before the fix: `previewDiv.textContent = fragment.toString()` dumped 6 * <paragraph indent="0"><heading level="1">Title</heading>… 7 * into the preview pane. Unreadable. 8 * 9 * After: `renderYjsFragmentAsHtml(fragment)` walks the Yjs XmlFragment and 10 * emits readable HTML (h1/p/ul/li/blockquote/table etc.). 11 * 12 * These tests use duck-typed mock nodes (nodeName + getAttribute + toArray) 13 * rather than instantiating real Yjs objects — keeps the tests hermetic and 14 * documents the exact interface the renderer depends on. 15 */ 16import { describe, it, expect } from 'vitest'; 17import { renderYjsFragmentAsHtml } from '../src/version-panel.js'; 18 19function mockText(text: string) { 20 return Object.assign( 21 { toString: () => text }, 22 { constructor: { name: 'YXmlText' } }, 23 ); 24} 25 26function mockElement( 27 nodeName: string, 28 attrs: Record<string, string>, 29 children: unknown[], 30) { 31 return { 32 nodeName, 33 getAttribute: (k: string) => attrs[k], 34 toArray: () => children, 35 }; 36} 37 38function mockFragment(children: unknown[]) { 39 return { toArray: () => children }; 40} 41 42describe('#719 — renderYjsFragmentAsHtml', () => { 43 it('renders a heading as <hN> with its level', () => { 44 const frag = mockFragment([ 45 mockElement('heading', { level: '1' }, [mockText('Title')]), 46 mockElement('heading', { level: '2' }, [mockText('Sub')]), 47 mockElement('heading', { level: '3' }, [mockText('Deep')]), 48 ]); 49 const html = renderYjsFragmentAsHtml(frag); 50 expect(html).toBe('<h1>Title</h1><h2>Sub</h2><h3>Deep</h3>'); 51 }); 52 53 it('clamps heading level to 1..6', () => { 54 const frag = mockFragment([ 55 mockElement('heading', { level: '0' }, [mockText('A')]), 56 mockElement('heading', { level: '99' }, [mockText('B')]), 57 ]); 58 expect(renderYjsFragmentAsHtml(frag)).toBe('<h1>A</h1><h6>B</h6>'); 59 }); 60 61 it('renders paragraphs', () => { 62 const frag = mockFragment([ 63 mockElement('paragraph', {}, [mockText('hello')]), 64 mockElement('paragraph', {}, [mockText('world')]), 65 ]); 66 expect(renderYjsFragmentAsHtml(frag)).toBe('<p>hello</p><p>world</p>'); 67 }); 68 69 it('renders bullet lists with list items', () => { 70 const frag = mockFragment([ 71 mockElement('bulletList', {}, [ 72 mockElement('listItem', {}, [ 73 mockElement('paragraph', {}, [mockText('alpha')]), 74 ]), 75 mockElement('listItem', {}, [ 76 mockElement('paragraph', {}, [mockText('beta')]), 77 ]), 78 ]), 79 ]); 80 expect(renderYjsFragmentAsHtml(frag)).toBe( 81 '<ul><li><p>alpha</p></li><li><p>beta</p></li></ul>', 82 ); 83 }); 84 85 it('renders tables with tr/th/td', () => { 86 const frag = mockFragment([ 87 mockElement('table', {}, [ 88 mockElement('tableRow', {}, [ 89 mockElement('tableHeader', {}, [ 90 mockElement('paragraph', {}, [mockText('Name')]), 91 ]), 92 ]), 93 mockElement('tableRow', {}, [ 94 mockElement('tableCell', {}, [ 95 mockElement('paragraph', {}, [mockText('Alice')]), 96 ]), 97 ]), 98 ]), 99 ]); 100 const html = renderYjsFragmentAsHtml(frag); 101 expect(html).toContain('<table>'); 102 expect(html).toContain('<tr><th><p>Name</p></th></tr>'); 103 expect(html).toContain('<tr><td><p>Alice</p></td></tr>'); 104 }); 105 106 it('renders codeBlock as <pre><code>', () => { 107 const frag = mockFragment([ 108 mockElement('codeBlock', {}, [mockText('const x = 1')]), 109 ]); 110 expect(renderYjsFragmentAsHtml(frag)).toBe('<pre><code>const x = 1</code></pre>'); 111 }); 112 113 it('renders images with src + alt', () => { 114 const frag = mockFragment([ 115 mockElement('image', { src: 'photo.jpg', alt: 'A photo' }, []), 116 ]); 117 expect(renderYjsFragmentAsHtml(frag)).toBe('<img src="photo.jpg" alt="A photo">'); 118 }); 119 120 it('escapes HTML-special characters in text nodes', () => { 121 const frag = mockFragment([ 122 mockElement('paragraph', {}, [mockText('<script>alert(1)</script> & "x"')]), 123 ]); 124 expect(renderYjsFragmentAsHtml(frag)).toBe( 125 '<p>&lt;script&gt;alert(1)&lt;/script&gt; &amp; &quot;x&quot;</p>', 126 ); 127 }); 128 129 it('falls back to <div> for unknown node types (content still shows)', () => { 130 const frag = mockFragment([ 131 mockElement('mysteryBlock', {}, [mockText('still visible')]), 132 ]); 133 expect(renderYjsFragmentAsHtml(frag)).toBe('<div>still visible</div>'); 134 }); 135 136 it('never emits ProseMirror schema tags (e.g. <paragraph>, <heading>) in output', () => { 137 const frag = mockFragment([ 138 mockElement('heading', { level: '1' }, [mockText('Outline Test')]), 139 mockElement('paragraph', {}, [mockText('body')]), 140 mockElement('bulletList', {}, [ 141 mockElement('listItem', {}, [ 142 mockElement('paragraph', {}, [mockText('x')]), 143 ]), 144 ]), 145 ]); 146 const html = renderYjsFragmentAsHtml(frag); 147 // The regression we're guarding against: 148 expect(html).not.toMatch(/<paragraph\b/); 149 expect(html).not.toMatch(/<heading\b/); 150 expect(html).not.toMatch(/<bulletList\b/); 151 expect(html).not.toMatch(/<listItem\b/); 152 }); 153 154 it('renders an empty fragment as the empty string', () => { 155 expect(renderYjsFragmentAsHtml(mockFragment([]))).toBe(''); 156 }); 157});