blob: dd112a41f602828ccb8eedd93e9a081cf9c64821 [file]
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
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 {
alignMarkdownTokens,
attachThreadsToRows,
computeSequenceDiff,
parseMarkdownBlocks,
reconstructFileContent,
tokenizeWords,
} from './markdown-diff-util';
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('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(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(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(rows[0].leftHtml!, 'Follow this structure');
assert.include(rows[0].rightHtml!, 'diff-highlight-add');
assert.include(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(rows[0].leftHtml!, 'diff-highlight-del');
assert.include(rows[0].leftHtml!, 'old code');
assert.include(rows[0].rightHtml!, 'diff-highlight-add');
assert.include(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);
});
});
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');
});
});
});