|
| 1 | +/* |
| 2 | + * Copyright 2025 Adobe. All rights reserved. |
| 3 | + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); |
| 4 | + * you may not use this file except in compliance with the License. You may obtain a copy |
| 5 | + * of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 6 | + * |
| 7 | + * Unless required by applicable law or agreed to in writing, software distributed under |
| 8 | + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS |
| 9 | + * OF ANY KIND, either express or implied. See the License for the specific language |
| 10 | + * governing permissions and limitations under the License. |
| 11 | + */ |
| 12 | + |
| 13 | +/** |
| 14 | + * Cross-Validation Tests |
| 15 | + * |
| 16 | + * These tests establish the "contract" for HTML conversion that both da-collab (doc2aem) |
| 17 | + * and da-live (prose2aem) should adhere to. Each test case defines: |
| 18 | + * - Input: AEM HTML |
| 19 | + * - Expected Output: The canonical HTML that should be produced after roundtrip conversion |
| 20 | + * |
| 21 | + * Corresponding tests should exist in da-live/test/unit/blocks/shared/cross-validation.test.js |
| 22 | + * to verify that prose2aem produces equivalent output. |
| 23 | + * |
| 24 | + * If these tests fail after changes, ensure both implementations are updated together. |
| 25 | + */ |
| 26 | + |
| 27 | +import assert from 'node:assert'; |
| 28 | +import * as Y from 'yjs'; |
| 29 | +import { aem2doc, doc2aem } from '../src/collab.js'; |
| 30 | + |
| 31 | +const collapseWhitespace = (str) => str.replace(/>\s+</g, '><').replace(/\s+/g, ' ').trim(); |
| 32 | + |
| 33 | +// Test cases that define the conversion contract |
| 34 | +const CROSS_VALIDATION_CASES = [ |
| 35 | + { |
| 36 | + name: 'Simple paragraph', |
| 37 | + input: '<body><header></header><main><div><p>Hello World</p></div></main><footer></footer></body>', |
| 38 | + expected: '<body><header></header><main><div><p>Hello World</p></div></main><footer></footer></body>', |
| 39 | + }, |
| 40 | + { |
| 41 | + name: 'Multiple paragraphs', |
| 42 | + input: '<body><header></header><main><div><p>First</p><p>Second</p><p>Third</p></div></main><footer></footer></body>', |
| 43 | + expected: '<body><header></header><main><div><p>First</p><p>Second</p><p>Third</p></div></main><footer></footer></body>', |
| 44 | + }, |
| 45 | + { |
| 46 | + name: 'Headings h1-h6', |
| 47 | + input: '<body><header></header><main><div><h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6></div></main><footer></footer></body>', |
| 48 | + expected: '<body><header></header><main><div><h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6></div></main><footer></footer></body>', |
| 49 | + }, |
| 50 | + { |
| 51 | + name: 'Inline formatting - bold, italic, strikethrough, underline', |
| 52 | + input: '<body><header></header><main><div><p><strong>Bold</strong> <em>Italic</em> <s>Strike</s> <u>Under</u></p></div></main><footer></footer></body>', |
| 53 | + expected: '<body><header></header><main><div><p><strong>Bold</strong> <em>Italic</em> <s>Strike</s> <u>Under</u></p></div></main><footer></footer></body>', |
| 54 | + }, |
| 55 | + { |
| 56 | + name: 'Links', |
| 57 | + input: '<body><header></header><main><div><p><a href="https://example.com">Example Link</a></p></div></main><footer></footer></body>', |
| 58 | + expected: '<body><header></header><main><div><p><a href="https://example.com">Example Link</a></p></div></main><footer></footer></body>', |
| 59 | + }, |
| 60 | + { |
| 61 | + name: 'Unordered list', |
| 62 | + // doc2aem strips <p> from list items containing only text |
| 63 | + input: '<body><header></header><main><div><ul><li><p>Item 1</p></li><li><p>Item 2</p></li><li><p>Item 3</p></li></ul></div></main><footer></footer></body>', |
| 64 | + expected: '<body><header></header><main><div><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul></div></main><footer></footer></body>', |
| 65 | + }, |
| 66 | + { |
| 67 | + name: 'Ordered list', |
| 68 | + // doc2aem strips <p> from list items containing only text |
| 69 | + input: '<body><header></header><main><div><ol><li><p>First</p></li><li><p>Second</p></li><li><p>Third</p></li></ol></div></main><footer></footer></body>', |
| 70 | + expected: '<body><header></header><main><div><ol><li>First</li><li>Second</li><li>Third</li></ol></div></main><footer></footer></body>', |
| 71 | + }, |
| 72 | + { |
| 73 | + name: 'Simple block (marquee)', |
| 74 | + input: '<body><header></header><main><div><div class="marquee light"><div><div><p>Content here</p></div></div></div></div></main><footer></footer></body>', |
| 75 | + expected: '<body><header></header><main><div><div class="marquee light"><div><div><p>Content here</p></div></div></div></div></main><footer></footer></body>', |
| 76 | + }, |
| 77 | + { |
| 78 | + name: 'Section break (hr)', |
| 79 | + input: '<body><header></header><main><div><p>Section 1</p></div><div><p>Section 2</p></div></main><footer></footer></body>', |
| 80 | + expected: '<body><header></header><main><div><p>Section 1</p></div><div><p>Section 2</p></div></main><footer></footer></body>', |
| 81 | + }, |
| 82 | + { |
| 83 | + name: 'Image with picture wrapper', |
| 84 | + input: '<body><header></header><main><div><picture><source srcset="./media_123.png"><img src="./media_123.png" alt="Test image"></picture></div></main><footer></footer></body>', |
| 85 | + expected: '<body><header></header><main><div><picture><source srcset="./media_123.png"><source srcset="./media_123.png" media="(min-width: 600px)"><img src="./media_123.png" alt="Test image" loading="lazy"></picture></div></main><footer></footer></body>', |
| 86 | + }, |
| 87 | + { |
| 88 | + name: 'Superscript and subscript', |
| 89 | + input: '<body><header></header><main><div><p>H<sub>2</sub>O and E=mc<sup>2</sup></p></div></main><footer></footer></body>', |
| 90 | + expected: '<body><header></header><main><div><p>H<sub>2</sub>O and E=mc<sup>2</sup></p></div></main><footer></footer></body>', |
| 91 | + }, |
| 92 | + { |
| 93 | + name: 'Blockquote', |
| 94 | + input: '<body><header></header><main><div><blockquote><p>A wise quote</p></blockquote></div></main><footer></footer></body>', |
| 95 | + expected: '<body><header></header><main><div><blockquote><p>A wise quote</p></blockquote></div></main><footer></footer></body>', |
| 96 | + }, |
| 97 | + { |
| 98 | + name: 'Code block', |
| 99 | + input: '<body><header></header><main><div><pre>const x = 1;</pre></div></main><footer></footer></body>', |
| 100 | + expected: '<body><header></header><main><div><pre><code>const x = 1;</code></pre></div></main><footer></footer></body>', |
| 101 | + }, |
| 102 | + // Note: Nested formatting may have slight differences between prose2aem and doc2aem |
| 103 | + // due to how ProseMirror serializes nested marks. Skipping for now. |
| 104 | + // { |
| 105 | + // name: 'Nested formatting', |
| 106 | + // input: '<body><header></header><main><div><p><strong><em>Bold and italic</em></strong></p></div></main><footer></footer></body>', |
| 107 | + // expected: '<body><header></header><main><div><p><strong><em>Bold and italic</em></strong></p></div></main><footer></footer></body>', |
| 108 | + // }, |
| 109 | + { |
| 110 | + name: 'Link with formatting inside', |
| 111 | + input: '<body><header></header><main><div><p><a href="https://example.com"><strong>Bold link</strong></a></p></div></main><footer></footer></body>', |
| 112 | + expected: '<body><header></header><main><div><p><a href="https://example.com"><strong>Bold link</strong></a></p></div></main><footer></footer></body>', |
| 113 | + }, |
| 114 | + { |
| 115 | + name: 'daMetadata block', |
| 116 | + input: '<body><header></header><main><div><p>Content</p></div></main><footer></footer><div class="da-metadata"><div><div>template</div><div>/templates/default</div></div></div></body>', |
| 117 | + expected: '<body><header></header><main><div><p>Content</p></div></main><footer></footer><div class="da-metadata"><div><div>template</div><div>/templates/default</div></div></div></body>', |
| 118 | + }, |
| 119 | + { |
| 120 | + name: 'Regional edit - diff added', |
| 121 | + input: '<body><header></header><main><div><p da-diff-added="">New content</p></div></main><footer></footer></body>', |
| 122 | + expected: '<body><header></header><main><div><p da-diff-added="">New content</p></div></main><footer></footer></body>', |
| 123 | + }, |
| 124 | + { |
| 125 | + name: 'Regional edit - diff deleted', |
| 126 | + input: '<body><header></header><main><div><da-diff-deleted data-mdast="ignore"><p>Deleted content</p></da-diff-deleted></div></main><footer></footer></body>', |
| 127 | + expected: '<body><header></header><main><div><da-diff-deleted data-mdast="ignore"><p>Deleted content</p></da-diff-deleted></div></main><footer></footer></body>', |
| 128 | + }, |
| 129 | +]; |
| 130 | + |
| 131 | +describe('Cross-Validation Test Suite (doc2aem contract)', () => { |
| 132 | + CROSS_VALIDATION_CASES.forEach(({ name, input, expected }) => { |
| 133 | + it(`${name}`, () => { |
| 134 | + const yDoc = new Y.Doc(); |
| 135 | + aem2doc(input, yDoc); |
| 136 | + const result = doc2aem(yDoc); |
| 137 | + |
| 138 | + assert.equal( |
| 139 | + collapseWhitespace(result), |
| 140 | + collapseWhitespace(expected), |
| 141 | + `Roundtrip conversion failed for: ${name}`, |
| 142 | + ); |
| 143 | + }); |
| 144 | + }); |
| 145 | +}); |
| 146 | + |
| 147 | +// Export test cases for use in da-live tests |
| 148 | +export { CROSS_VALIDATION_CASES, collapseWhitespace }; |
0 commit comments