| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import {assert} from '@open-wc/testing'; | |
| import {render, TemplateResult} from 'lit'; | |
| import '../../../test/common-test-setup'; | |
| import {DiffInfo} from '../../../types/diff'; | |
| import {CommentSide, Side} from '../../../constants/constants'; | |
| import { | |
| createComment, | |
| createCommentThread, | |
| createDiff, | |
| } from '../../../test/test-data-generators'; | |
| import {CommentThread, UrlEncodedCommentId} from '../../../types/common'; | |
| import { | |
| alignHomologousItems, | |
| alignMarkdownTokens, | |
| attachThreadsToRows, | |
| coalesceInlineDiffSegments, | |
| computeNormalizedEditDistance, | |
| computeSequenceDiff, | |
| parseMarkdownBlocks, | |
| reconstructFileContent, | |
| renderBlockquoteDiff, | |
| renderCodeBlockDiff, | |
| renderInlineCodeSpans, | |
| renderInlineMarkdown, | |
| renderInlineTextDiff, | |
| renderListDiff, | |
| renderTableDiff, | |
| sanitizeMarkdownUrl, | |
| tokenizeCodeWords, | |
| tokenizeWords, | |
| } from './markdown-diff-util'; | |
| function renderToHtmlString(template?: TemplateResult): string { | |
| if (!template) return ''; | |
| const container = document.createElement('div'); | |
| render(template, container); | |
| return container.innerHTML.replace(/<!--[\s\S]*?-->/g, ''); | |
| } | |
| suite('markdown-diff-util tests', () => { | |
| test('reconstructFileContent', () => { | |
| const diff: DiffInfo = { | |
| ...createDiff(), | |
| content: [ | |
| {ab: ['# Header', '']}, | |
| {a: ['Old line'], b: ['New line']}, | |
| {ab: ['', 'Footer']}, | |
| ], | |
| }; | |
| assert.equal( | |
| reconstructFileContent(diff, Side.LEFT), | |
| '# Header\n\nOld line\n\nFooter' | |
| ); | |
| assert.equal( | |
| reconstructFileContent(diff, Side.RIGHT), | |
| '# Header\n\nNew line\n\nFooter' | |
| ); | |
| }); | |
| test('parseMarkdownBlocks', () => { | |
| const md = '# Title\n\nParagraph text.\n\n```js\nconsole.log(1);\n```'; | |
| const tokens = parseMarkdownBlocks(md); | |
| assert.equal(tokens.length, 3); | |
| assert.equal(tokens[0].type, 'heading'); | |
| assert.equal(tokens[1].type, 'paragraph'); | |
| assert.equal(tokens[2].type, 'code'); | |
| }); | |
| test('tokenizeWords', () => { | |
| const words = tokenizeWords('Hello world! How are you?'); | |
| assert.deepEqual(words, [ | |
| 'Hello', | |
| ' ', | |
| 'world', | |
| '!', | |
| ' ', | |
| 'How', | |
| ' ', | |
| 'are', | |
| ' ', | |
| 'you', | |
| '?', | |
| ]); | |
| }); | |
| test('tokenizeWords keeps markdown links and inline code spans atomic and separates adjacent punctuation', () => { | |
| const words = tokenizeWords( | |
| 'Located at [`scripts/run_experiment.py`](scripts/run_experiment.py) (`<SKILL_DIR>`, `--workspace-root`, `` `code` ``).' | |
| ); | |
| assert.include( | |
| words, | |
| '[`scripts/run_experiment.py`](scripts/run_experiment.py)' | |
| ); | |
| assert.include(words, '`<SKILL_DIR>`'); | |
| assert.include(words, '`--workspace-root`'); | |
| assert.include(words, '`` `code` ``'); | |
| assert.equal( | |
| words[4], | |
| '[`scripts/run_experiment.py`](scripts/run_experiment.py)' | |
| ); | |
| }); | |
| test('renderInlineCodeSpans converts backtick blocks to <code> and escapes HTML', () => { | |
| const rendered = renderToHtmlString( | |
| renderInlineCodeSpans( | |
| 'Relative to `SKILL.md` (`<SKILL_DIR>`), `--workspace-root`, `$ANDROID_BUILD_TOP`, `.repo/`, and `` `code` ``.' | |
| ) | |
| ); | |
| assert.equal( | |
| rendered, | |
| 'Relative to <code>SKILL.md</code> (<code><SKILL_DIR></code>), <code>--workspace-root</code>, <code>$ANDROID_BUILD_TOP</code>, <code>.repo/</code>, and <code>`code`</code>.' | |
| ); | |
| }); | |
| test('tokenizeWords and renderInlineTextDiff handle autolinks and nested links/code inside formatted text', () => { | |
| const words = tokenizeWords( | |
| 'Check **old `flag` and [Docs](https://example.com)** and <https://auto.example.com>.' | |
| ); | |
| assert.include(words, '`flag`'); | |
| assert.include(words, '[Docs](https://example.com)'); | |
| assert.include(words, '<https://auto.example.com>'); | |
| const {leftHtml, rightHtml} = renderInlineTextDiff( | |
| 'Check **old `flag` and [Docs](https://example.com)** and <https://auto.example.com/v1>.', | |
| 'Check **new `flag` and [Docs](https://example.com)** and <https://auto.example.com/v2>.', | |
| 'p' | |
| ); | |
| assert.include( | |
| renderToHtmlString(leftHtml), | |
| '<strong><del class="diff-highlight-del">old</del> <code>flag</code> and <a href="https://example.com">Docs</a></strong>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(rightHtml), | |
| '<strong><ins class="diff-highlight-add">new</ins> <code>flag</code> and <a href="https://example.com">Docs</a></strong>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(leftHtml), | |
| '<del class="diff-highlight-del"><a href="https://auto.example.com/v1">https://auto.example.com/v1</a></del>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(rightHtml), | |
| '<ins class="diff-highlight-add"><a href="https://auto.example.com/v2">https://auto.example.com/v2</a></ins>' | |
| ); | |
| }); | |
| test('renderInlineMarkdown converts markdown links and code spans to HTML and escapes attributes', () => { | |
| const rendered = renderToHtmlString( | |
| renderInlineMarkdown( | |
| 'Located at [`scripts/run_experiment.py`](scripts/run_experiment.py) and [Docs](https://example.com/docs "Documentation") (relative to `SKILL.md`).' | |
| ) | |
| ); | |
| assert.equal( | |
| rendered, | |
| 'Located at <a href="scripts/run_experiment.py"><code>scripts/run_experiment.py</code></a> and <a href="https://example.com/docs">Docs</a> (relative to <code>SKILL.md</code>).' | |
| ); | |
| }); | |
| test('renderInlineMarkdown handles parenthesized URLs, angle-bracket URLs, bracketed code labels, and ignores images', () => { | |
| const rendered = renderToHtmlString( | |
| renderInlineMarkdown( | |
| 'See [Wiki](https://en.wikipedia.org/wiki/Fn_(math)), [Angle](<https://example.com/path>), [`arr[0]`](https://example.com/arr), and .' | |
| ) | |
| ); | |
| assert.equal( | |
| rendered, | |
| 'See <a href="https://en.wikipedia.org/wiki/Fn_(math)">Wiki</a>, <a href="https://example.com/path">Angle</a>, <a href="https://example.com/arr"><code>arr[0]</code></a>, and .' | |
| ); | |
| }); | |
| test('sanitizeMarkdownUrl and Lit templates harden against XSS and unsafe URL schemes', () => { | |
| assert.equal( | |
| sanitizeMarkdownUrl('javascript:alert(1)'), | |
| 'about:invalid#zClosurez' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl(' JAVASCRIPT:alert(1) '), | |
| 'about:invalid#zClosurez' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl('data:text/html,<script>alert(1)</script>'), | |
| 'about:invalid#zClosurez' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl('file:///etc/passwd'), | |
| 'about:invalid#zClosurez' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl('http://example.com/safe'), | |
| 'http://example.com/safe' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl('https://example.com/safe'), | |
| 'https://example.com/safe' | |
| ); | |
| assert.equal( | |
| sanitizeMarkdownUrl('scripts/run_experiment.py'), | |
| 'scripts/run_experiment.py' | |
| ); | |
| const rendered = renderToHtmlString( | |
| renderInlineMarkdown( | |
| 'Click [evil](javascript:alert(1)) or <img src=x onerror=alert(1)>' | |
| ) | |
| ); | |
| assert.include(rendered, '<a href="about:invalid#zClosurez">evil</a>'); | |
| assert.include(rendered, '<img src=x onerror=alert(1)>'); | |
| }); | |
| test('renderInlineTextDiff renders single backtick code blocks and markdown links inside modified paragraphs', () => { | |
| const textA = | |
| 'The generalized runner script is located at [run_experiment.py](https://example.com/tools/build_experimentation/scripts/run_experiment.py).'; | |
| const textB = | |
| 'The generalized runner script is located at [`scripts/run_experiment.py`](scripts/run_experiment.py) (relative to this `SKILL.md` directory, `<SKILL_DIR>`). It automatically resolves the workspace root from `--workspace-root`, `workspace_root` in the JSON config, `$ANDROID_BUILD_TOP`, or the nearest ancestor directory containing `.repo/`.'; | |
| const {leftHtml, rightHtml} = renderInlineTextDiff(textA, textB, 'p'); | |
| const leftStr = renderToHtmlString(leftHtml); | |
| const rightStr = renderToHtmlString(rightHtml); | |
| assert.include( | |
| leftStr, | |
| '<a href="https://example.com/tools/build_experimentation/scripts/run_experiment.py">run_experiment.py</a>' | |
| ); | |
| assert.include( | |
| rightStr, | |
| '<a href="scripts/run_experiment.py"><code>scripts/run_experiment.py</code></a>' | |
| ); | |
| assert.include(rightStr, '<code>SKILL.md</code>'); | |
| assert.include(rightStr, '<code><SKILL_DIR></code>'); | |
| assert.include(rightStr, '<code>--workspace-root</code>'); | |
| assert.include(rightStr, '<code>workspace_root</code>'); | |
| assert.include(rightStr, '<code>$ANDROID_BUILD_TOP</code>'); | |
| assert.include(rightStr, '<code>.repo/</code>'); | |
| }); | |
| test('computeNormalizedEditDistance and coalesceInlineDiffSegments follow delta-style edit inference', () => { | |
| assert.equal( | |
| computeNormalizedEditDistance( | |
| tokenizeWords('same sentence'), | |
| tokenizeWords('same sentence') | |
| ), | |
| 0 | |
| ); | |
| assert.equal( | |
| computeNormalizedEditDistance( | |
| tokenizeWords('alpha beta'), | |
| tokenizeWords('gamma delta') | |
| ), | |
| 1 | |
| ); | |
| const rawSegments = computeSequenceDiff( | |
| tokenizeWords('Run old legacy command now.'), | |
| tokenizeWords('Run new modern command now.') | |
| ); | |
| const coalesced = coalesceInlineDiffSegments(rawSegments); | |
| // The space between 'old'/'legacy' and 'new'/'modern' is coalesced into a single edit span. | |
| assert.deepEqual(coalesced, [ | |
| {text: 'Run ', type: 'common'}, | |
| {text: 'old legacy', type: 'deleted'}, | |
| {text: 'new modern', type: 'added'}, | |
| {text: ' command now.', type: 'common'}, | |
| ]); | |
| }); | |
| test('alignHomologousItems pairs modified items across insertions', () => { | |
| const itemsA = ['Configure timeout in default.yaml']; | |
| const itemsB = [ | |
| 'Brand new prerequisite step', | |
| 'Configure timeout in production.yaml', | |
| ]; | |
| const aligned = alignHomologousItems(itemsA, itemsB); | |
| assert.equal(aligned.length, 2); | |
| assert.equal(aligned[0].type, 'added'); | |
| assert.equal(aligned[1].type, 'modified'); | |
| if (aligned[1].type === 'modified') { | |
| assert.equal(aligned[1].textA, 'Configure timeout in default.yaml'); | |
| assert.equal(aligned[1].textB, 'Configure timeout in production.yaml'); | |
| } | |
| }); | |
| test('renderCodeBlockDiff performs within-line token diffing on homologous code lines', () => { | |
| const tokensA = parseMarkdownBlocks( | |
| '```bash\npython3 tools/build_experimentation/scripts/run_experiment.py --config path/to/config.json\n```' | |
| ); | |
| const tokensB = parseMarkdownBlocks( | |
| '```bash\npython3 <SKILL_DIR>/scripts/run_experiment.py --config path/to/config.json\n```' | |
| ); | |
| assert.deepEqual(tokenizeCodeWords('python3 <SKILL_DIR>/run.py --flag'), [ | |
| 'python3', | |
| ' ', | |
| '<SKILL_DIR>', | |
| '/', | |
| 'run', | |
| '.', | |
| 'py', | |
| ' ', | |
| '--flag', | |
| ]); | |
| const {leftHtml, rightHtml} = renderCodeBlockDiff( | |
| tokensA[0] as never, | |
| tokensB[0] as never | |
| ); | |
| const leftStr = renderToHtmlString(leftHtml); | |
| const rightStr = renderToHtmlString(rightHtml); | |
| assert.include(leftStr, 'class="diff-code-line-del"'); | |
| assert.include( | |
| leftStr, | |
| '<del class="diff-highlight-del">tools/build_experimentation</del>' | |
| ); | |
| assert.include(rightStr, 'class="diff-code-line-add"'); | |
| assert.include( | |
| rightStr, | |
| '<ins class="diff-highlight-add"><SKILL_DIR></ins>' | |
| ); | |
| }); | |
| test('renderListDiff, renderBlockquoteDiff, and renderTableDiff highlight within-element changes and emit placeholders', () => { | |
| const listA = parseMarkdownBlocks( | |
| '- Enable `--legacy-auth` on startup\n- Unchanged item' | |
| )[0]; | |
| const listB = parseMarkdownBlocks( | |
| '- Enable `--oauth2-auth` on startup\n- Unchanged item\n- Added item' | |
| )[0]; | |
| const listDiff = renderListDiff(listA as never, listB as never); | |
| const listLeftStr = renderToHtmlString(listDiff.leftHtml); | |
| const listRightStr = renderToHtmlString(listDiff.rightHtml); | |
| assert.include( | |
| listLeftStr, | |
| '<del class="diff-highlight-del"><code>--legacy-auth</code></del>' | |
| ); | |
| assert.include( | |
| listLeftStr, | |
| '<li class="diff-list-item-placeholder" aria-hidden="true"></li>' | |
| ); | |
| assert.include( | |
| listRightStr, | |
| '<ins class="diff-highlight-add"><code>--oauth2-auth</code></ins>' | |
| ); | |
| assert.include(listRightStr, 'class="diff-list-item-add"'); | |
| const quoteA = parseMarkdownBlocks('> Removed in Q3.')[0]; | |
| const quoteB = parseMarkdownBlocks('> Removed in Q4.')[0]; | |
| const quoteDiff = renderBlockquoteDiff(quoteA as never, quoteB as never); | |
| assert.include( | |
| renderToHtmlString(quoteDiff.leftHtml), | |
| '<blockquote><p>Removed in <del class="diff-highlight-del">Q3</del>.</p></blockquote>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(quoteDiff.rightHtml), | |
| '<blockquote><p>Removed in <ins class="diff-highlight-add">Q4</ins>.</p></blockquote>' | |
| ); | |
| const tableA = parseMarkdownBlocks( | |
| '| Flag | Default |\n| --- | --- |\n| `--threads` | `4` |' | |
| )[0]; | |
| const tableB = parseMarkdownBlocks( | |
| '| Flag | Default | Notes |\n| --- | --- | --- |\n| `--threads` | `8` | Fast |\n| `--verbose` | `true` | Logs |' | |
| )[0]; | |
| const tableDiff = renderTableDiff(tableA as never, tableB as never); | |
| const tableLeftStr = renderToHtmlString(tableDiff.leftHtml); | |
| const tableRightStr = renderToHtmlString(tableDiff.rightHtml); | |
| assert.include( | |
| tableLeftStr, | |
| '<th class="diff-table-cell-placeholder" aria-hidden="true"></th>' | |
| ); | |
| assert.include(tableLeftStr, '<td><code>--threads</code></td>'); | |
| assert.include( | |
| tableLeftStr, | |
| '<td><del class="diff-highlight-del"><code>4</code></del></td>' | |
| ); | |
| assert.include( | |
| tableLeftStr, | |
| '<td class="diff-table-cell-placeholder" aria-hidden="true"></td>' | |
| ); | |
| assert.include( | |
| tableLeftStr, | |
| '<tr class="diff-table-row-placeholder" aria-hidden="true">' | |
| ); | |
| assert.include( | |
| tableRightStr, | |
| '<td><ins class="diff-highlight-add"><code>8</code></ins></td>' | |
| ); | |
| assert.include(tableRightStr, '<tr class="diff-table-row-add">'); | |
| }); | |
| test('handles combined bold+italic delimiters, backslash escapes, nested lists, ordered list start, and multi-paragraph blockquotes', () => { | |
| assert.deepEqual(tokenizeWords('***old italic***'), [ | |
| '*', | |
| '**', | |
| 'old', | |
| ' ', | |
| 'italic', | |
| '**', | |
| '*', | |
| ]); | |
| const boldItalicDiff = renderInlineTextDiff( | |
| 'Prefix ***old italic*** suffix.', | |
| 'Prefix ***new italic*** suffix.', | |
| 'p' | |
| ); | |
| assert.include( | |
| renderToHtmlString(boldItalicDiff.leftHtml), | |
| '<em><strong><del class="diff-highlight-del">old</del> italic</strong></em>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(boldItalicDiff.rightHtml), | |
| '<em><strong><ins class="diff-highlight-add">new</ins> italic</strong></em>' | |
| ); | |
| assert.equal( | |
| renderToHtmlString( | |
| renderInlineMarkdown('Escaped \\*star\\* and \\[brackets\\]') | |
| ), | |
| 'Escaped *star* and [brackets]' | |
| ); | |
| const nestedListA = parseMarkdownBlocks( | |
| '3. Parent item\n - Old child\n4. Second item' | |
| )[0]; | |
| const nestedListB = parseMarkdownBlocks( | |
| '3. Parent item\n - New child\n4. Second item' | |
| )[0]; | |
| const nestedListDiff = renderListDiff( | |
| nestedListA as never, | |
| nestedListB as never | |
| ); | |
| const nestedLeftStr = renderToHtmlString(nestedListDiff.leftHtml); | |
| const nestedRightStr = renderToHtmlString(nestedListDiff.rightHtml); | |
| assert.include(nestedLeftStr, '<ol start="3">'); | |
| assert.match( | |
| nestedLeftStr, | |
| /<ul>\s*<li><del class="diff-highlight-del">Old<\/del> child<\/li>\s*<\/ul>/ | |
| ); | |
| assert.match( | |
| nestedRightStr, | |
| /<ul>\s*<li><ins class="diff-highlight-add">New<\/ins> child<\/li>\s*<\/ul>/ | |
| ); | |
| const multiQuoteA = parseMarkdownBlocks( | |
| '> First paragraph old.\n>\n> Second paragraph.' | |
| )[0]; | |
| const multiQuoteB = parseMarkdownBlocks( | |
| '> First paragraph new.\n>\n> Second paragraph.' | |
| )[0]; | |
| const multiQuoteDiff = renderBlockquoteDiff( | |
| multiQuoteA as never, | |
| multiQuoteB as never | |
| ); | |
| assert.include( | |
| renderToHtmlString(multiQuoteDiff.leftHtml), | |
| '<blockquote><p>First paragraph <del class="diff-highlight-del">old</del>.</p><p>Second paragraph.</p></blockquote>' | |
| ); | |
| assert.include( | |
| renderToHtmlString(multiQuoteDiff.rightHtml), | |
| '<blockquote><p>First paragraph <ins class="diff-highlight-add">new</ins>.</p><p>Second paragraph.</p></blockquote>' | |
| ); | |
| }); | |
| test('computeSequenceDiff', () => { | |
| const seqA = ['a', 'b', 'c']; | |
| const seqB = ['a', 'x', 'c']; | |
| const diff = computeSequenceDiff(seqA, seqB); | |
| assert.deepEqual(diff, [ | |
| {text: 'a', type: 'common'}, | |
| {text: 'b', type: 'deleted'}, | |
| {text: 'x', type: 'added'}, | |
| {text: 'c', type: 'common'}, | |
| ]); | |
| }); | |
| suite('alignMarkdownTokens', () => { | |
| test('identical documents produce unchanged rows', () => { | |
| const doc = '# Title\n\nParagraph 1.\n\nParagraph 2.'; | |
| const tokensA = parseMarkdownBlocks(doc); | |
| const tokensB = parseMarkdownBlocks(doc); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 3); | |
| assert.isTrue(rows.every(r => r.status === 'unchanged')); | |
| }); | |
| test('inserted block creates added row with empty left', () => { | |
| const docA = '# Title\n\nFooter.'; | |
| const docB = '# Title\n\nInserted paragraph.\n\nFooter.'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 3); | |
| assert.equal(rows[0].status, 'unchanged'); | |
| assert.equal(rows[1].status, 'added'); | |
| assert.isUndefined(rows[1].leftToken); | |
| assert.isDefined(rows[1].rightToken); | |
| assert.include( | |
| renderToHtmlString(rows[1].rightHtml), | |
| 'Inserted paragraph' | |
| ); | |
| assert.equal(rows[2].status, 'unchanged'); | |
| }); | |
| test('deleted block creates deleted row with empty right', () => { | |
| const docA = '# Title\n\nDeleted paragraph.\n\nFooter.'; | |
| const docB = '# Title\n\nFooter.'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 3); | |
| assert.equal(rows[0].status, 'unchanged'); | |
| assert.equal(rows[1].status, 'deleted'); | |
| assert.isDefined(rows[1].leftToken); | |
| assert.isUndefined(rows[1].rightToken); | |
| assert.include(renderToHtmlString(rows[1].leftHtml), 'Deleted paragraph'); | |
| assert.equal(rows[2].status, 'unchanged'); | |
| }); | |
| test('modified paragraph generates inline diff highlights', () => { | |
| const docA = 'Follow this structure:'; | |
| const docB = 'Follow this structure. Only the first block is required:'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 1); | |
| assert.equal(rows[0].status, 'modified'); | |
| assert.include( | |
| renderToHtmlString(rows[0].leftHtml), | |
| 'Follow this structure' | |
| ); | |
| assert.include( | |
| renderToHtmlString(rows[0].rightHtml), | |
| 'diff-highlight-add' | |
| ); | |
| assert.include( | |
| renderToHtmlString(rows[0].rightHtml), | |
| 'Only the first block is required' | |
| ); | |
| }); | |
| test('modified code block generates line diff highlights', () => { | |
| const docA = '```bash\nline 1\nold code\nline 3\n```'; | |
| const docB = '```bash\nline 1\nnew code\nline 3\n```'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 1); | |
| assert.equal(rows[0].status, 'modified'); | |
| assert.include( | |
| renderToHtmlString(rows[0].leftHtml), | |
| 'diff-highlight-del' | |
| ); | |
| assert.include(renderToHtmlString(rows[0].leftHtml), 'old code'); | |
| assert.include( | |
| renderToHtmlString(rows[0].rightHtml), | |
| 'diff-highlight-add' | |
| ); | |
| assert.include(renderToHtmlString(rows[0].rightHtml), 'new code'); | |
| }); | |
| test('line numbers are correctly assigned to tokens and rows', () => { | |
| const docA = '# Header\n\nLine 1\nLine 2'; | |
| const docB = '# Header\n\nLine 1\nLine 2 modified'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| assert.equal(tokensA[0].startLine, 1); | |
| assert.equal(tokensA[0].endLine, 1); | |
| assert.equal(tokensA[1].startLine, 3); | |
| assert.equal(tokensA[1].endLine, 4); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows[0].leftStartLine, 1); | |
| assert.equal(rows[0].leftEndLine, 1); | |
| assert.equal(rows[0].rightStartLine, 1); | |
| assert.equal(rows[0].rightEndLine, 1); | |
| assert.equal(rows[1].leftStartLine, 3); | |
| assert.equal(rows[1].leftEndLine, 4); | |
| assert.equal(rows[1].rightStartLine, 3); | |
| assert.equal(rows[1].rightEndLine, 4); | |
| }); | |
| test('does not cross-pair unrelated blocks when uA === uB > 1', () => { | |
| const docA = | |
| 'Alpha beta gamma.\n\n```bash\npython3 run_experiment.py --old-flag --config a.json\n```'; | |
| const docB = | |
| '```bash\npython3 run_experiment.py --new-flag --config a.json\n```\n\nDelta epsilon zeta.'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| assert.equal(rows.length, 3); | |
| assert.equal(rows[0].status, 'deleted'); | |
| assert.equal(rows[1].status, 'modified'); | |
| assert.equal(rows[1].leftToken?.type, 'code'); | |
| assert.equal(rows[1].rightToken?.type, 'code'); | |
| assert.equal(rows[2].status, 'added'); | |
| }); | |
| }); | |
| suite('attachThreadsToRows', () => { | |
| test('attaches line threads to matching rows and separates file-level threads', () => { | |
| const docA = '# Header\n\nParagraph 1\n\nParagraph 2'; | |
| const docB = '# Header\n\nParagraph 1 modified\n\nParagraph 2'; | |
| const tokensA = parseMarkdownBlocks(docA); | |
| const tokensB = parseMarkdownBlocks(docB); | |
| const rows = alignMarkdownTokens(tokensA, tokensB); | |
| const fileThread: CommentThread = createCommentThread([ | |
| { | |
| ...createComment(), | |
| id: 'file-1' as UrlEncodedCommentId, | |
| line: undefined, | |
| side: CommentSide.REVISION, | |
| }, | |
| ]); | |
| const rightThread: CommentThread = createCommentThread([ | |
| { | |
| ...createComment(), | |
| id: 'right-1' as UrlEncodedCommentId, | |
| line: 3, | |
| side: CommentSide.REVISION, | |
| }, | |
| ]); | |
| const leftThread: CommentThread = createCommentThread([ | |
| { | |
| ...createComment(), | |
| id: 'left-1' as UrlEncodedCommentId, | |
| line: 3, | |
| side: CommentSide.PARENT, | |
| }, | |
| ]); | |
| const result = attachThreadsToRows(rows, [ | |
| fileThread, | |
| rightThread, | |
| leftThread, | |
| ]); | |
| assert.equal(result.fileLevelThreads.length, 1); | |
| assert.equal(result.fileLevelThreads[0].rootId, 'file-1'); | |
| // Row 1 is "Paragraph 1 modified" which spans lines 3-3 | |
| assert.equal(result.rowsWithThreads[1].rightThreads.length, 1); | |
| assert.equal(result.rowsWithThreads[1].rightThreads[0].rootId, 'right-1'); | |
| assert.equal(result.rowsWithThreads[1].leftThreads.length, 1); | |
| assert.equal(result.rowsWithThreads[1].leftThreads[0].rootId, 'left-1'); | |
| }); | |
| }); | |
| }); |