/**
* Unit tests for lib/diff-parser.js
*/
import test from 'brittle'
import { parseDiff, parseCommitInfo, getDiffStats, getStatusLabel, toSideBySide } from '../../lib/diff-parser.js'
import { sampleDiffs } from '../helpers/fixtures.js'
// ========================================
// parseDiff tests
// ========================================
test('parseDiff - returns empty array for empty input', async (t) => {
t.alike(parseDiff(''), [])
t.alike(parseDiff(null), [])
t.alike(parseDiff(undefined), [])
})
test('parseDiff - parses single file addition', async (t) => {
const result = parseDiff(sampleDiffs.singleFileAdd)
t.is(result.length, 1)
t.is(result[0].status, 'added')
t.is(result[0].newPath, 'newfile.js')
t.is(result[0].additions, 3)
t.is(result[0].deletions, 0)
t.is(result[0].isBinary, false)
t.is(result[0].hunks.length, 1)
})
test('parseDiff - parses single file modification', async (t) => {
const result = parseDiff(sampleDiffs.singleFileModify)
t.is(result.length, 1)
t.is(result[0].status, 'modified')
t.is(result[0].oldPath, 'file.js')
t.is(result[0].newPath, 'file.js')
t.ok(result[0].additions >= 1)
t.ok(result[0].deletions >= 1)
})
test('parseDiff - parses file deletion', async (t) => {
const result = parseDiff(sampleDiffs.fileDelete)
t.is(result.length, 1)
t.is(result[0].status, 'deleted')
t.is(result[0].oldPath, 'oldfile.js')
t.is(result[0].deletions, 2)
t.is(result[0].additions, 0)
})
test('parseDiff - parses file rename', async (t) => {
const result = parseDiff(sampleDiffs.fileRename)
t.is(result.length, 1)
t.is(result[0].status, 'renamed')
t.is(result[0].oldPath, 'old-name.js')
t.is(result[0].newPath, 'new-name.js')
})
test('parseDiff - parses multiple files', async (t) => {
const result = parseDiff(sampleDiffs.multipleFiles)
t.is(result.length, 2)
t.is(result[0].newPath, 'file1.js')
t.is(result[1].newPath, 'file2.js')
})
test('parseDiff - handles binary files', async (t) => {
const result = parseDiff(sampleDiffs.binaryFile)
t.is(result.length, 1)
t.is(result[0].isBinary, true)
t.is(result[0].newPath, 'image.png')
})
test('parseDiff - parses hunk headers correctly', async (t) => {
const diff = `diff --git a/test.js b/test.js
index abc..def 100644
--- a/test.js
+++ b/test.js
@@ -10,5 +10,6 @@ function test() {
context line
-deleted line
+added line
context line`
const result = parseDiff(diff)
t.is(result.length, 1)
t.is(result[0].hunks.length, 1)
t.is(result[0].hunks[0].oldStart, 10)
t.is(result[0].hunks[0].newStart, 10)
})
test('parseDiff - tracks line numbers correctly', async (t) => {
const diff = `diff --git a/test.js b/test.js
index abc..def 100644
--- a/test.js
+++ b/test.js
@@ -1,3 +1,4 @@
line1
+new line
line2
line3`
const result = parseDiff(diff)
const lines = result[0].hunks[0].lines
// First line is context at line 1
t.is(lines[0].type, 'ctx')
t.is(lines[0].oldLineNo, 1)
t.is(lines[0].newLineNo, 1)
// Second line is addition at new line 2
t.is(lines[1].type, 'add')
t.is(lines[1].oldLineNo, null)
t.is(lines[1].newLineNo, 2)
// Third line is context at old line 2, new line 3
t.is(lines[2].type, 'ctx')
t.is(lines[2].oldLineNo, 2)
t.is(lines[2].newLineNo, 3)
})
// ========================================
// parseCommitInfo tests
// ========================================
test('parseCommitInfo - returns null for empty input', async (t) => {
t.is(parseCommitInfo(''), null)
t.is(parseCommitInfo(null), null)
})
test('parseCommitInfo - parses commit info correctly', async (t) => {
const raw = `abc123def456
parent1 parent2
John Doe
john@example.com
1700000000
Fix the bug
This is the commit body.
Multiple lines are supported.`
const result = parseCommitInfo(raw)
t.is(result.hash, 'abc123def456')
t.is(result.shortHash, 'abc123d')
t.alike(result.parents, ['parent1', 'parent2'])
t.is(result.author, 'John Doe')
t.is(result.email, 'john@example.com')
t.is(result.subject, 'Fix the bug')
t.ok(result.body.includes('This is the commit body'))
})
test('parseCommitInfo - handles single parent', async (t) => {
const raw = `abc123
def456
Author
author@test.com
1700000000
Subject`
const result = parseCommitInfo(raw)
t.alike(result.parents, ['def456'])
})
test('parseCommitInfo - handles root commit (no parents)', async (t) => {
const raw = `abc123
Author
author@test.com
1700000000
Initial commit`
const result = parseCommitInfo(raw)
t.alike(result.parents, [])
})
// ========================================
// getDiffStats tests
// ========================================
test('getDiffStats - calculates stats correctly', async (t) => {
const files = [
{ additions: 10, deletions: 5 },
{ additions: 20, deletions: 3 },
{ additions: 0, deletions: 15 }
]
const stats = getDiffStats(files)
t.is(stats.filesChanged, 3)
t.is(stats.additions, 30)
t.is(stats.deletions, 23)
t.is(stats.total, 53)
})
test('getDiffStats - handles empty array', async (t) => {
const stats = getDiffStats([])
t.is(stats.filesChanged, 0)
t.is(stats.additions, 0)
t.is(stats.deletions, 0)
t.is(stats.total, 0)
})
// ========================================
// getStatusLabel tests
// ========================================
test('getStatusLabel - returns correct labels', async (t) => {
t.is(getStatusLabel('added').label, 'Added')
t.is(getStatusLabel('deleted').label, 'Deleted')
t.is(getStatusLabel('modified').label, 'Modified')
t.is(getStatusLabel('renamed').label, 'Renamed')
})
test('getStatusLabel - returns modified for unknown status', async (t) => {
t.is(getStatusLabel('unknown').label, 'Modified')
t.is(getStatusLabel('').label, 'Modified')
})
// ========================================
// toSideBySide tests
// ========================================
test('toSideBySide - handles context lines', async (t) => {
const lines = [
{ type: 'ctx', content: 'line1', oldLineNo: 1, newLineNo: 1 },
{ type: 'ctx', content: 'line2', oldLineNo: 2, newLineNo: 2 }
]
const result = toSideBySide(lines)
t.is(result.length, 2)
t.is(result[0].left.content, 'line1')
t.is(result[0].right.content, 'line1')
})
test('toSideBySide - pairs deletions with additions', async (t) => {
const lines = [
{ type: 'del', content: 'old', oldLineNo: 1, newLineNo: null },
{ type: 'add', content: 'new', oldLineNo: null, newLineNo: 1 }
]
const result = toSideBySide(lines)
t.is(result.length, 1)
t.is(result[0].left.content, 'old')
t.is(result[0].right.content, 'new')
})
test('toSideBySide - handles unmatched additions', async (t) => {
const lines = [
{ type: 'add', content: 'new1', oldLineNo: null, newLineNo: 1 },
{ type: 'add', content: 'new2', oldLineNo: null, newLineNo: 2 }
]
const result = toSideBySide(lines)
t.is(result.length, 2)
t.is(result[0].left, null)
t.is(result[0].right.content, 'new1')
t.is(result[1].left, null)
t.is(result[1].right.content, 'new2')
})
test('toSideBySide - handles mixed changes', async (t) => {
const lines = [
{ type: 'ctx', content: 'context', oldLineNo: 1, newLineNo: 1 },
{ type: 'del', content: 'deleted1', oldLineNo: 2, newLineNo: null },
{ type: 'del', content: 'deleted2', oldLineNo: 3, newLineNo: null },
{ type: 'add', content: 'added1', oldLineNo: null, newLineNo: 2 },
{ type: 'ctx', content: 'more context', oldLineNo: 4, newLineNo: 3 }
]
const result = toSideBySide(lines)
t.is(result.length, 4)
// Context
t.is(result[0].left.content, 'context')
t.is(result[0].right.content, 'context')
// Deletion + Addition paired
t.is(result[1].left.content, 'deleted1')
t.is(result[1].right.content, 'added1')
// Unmatched deletion
t.is(result[2].left.content, 'deleted2')
t.is(result[2].right, null)
// Context
t.is(result[3].left.content, 'more context')
})