Skip to content

Commit c7a456a

Browse files
committed
test: expand coverage to 97% lines / 92% functions
Add unit tests covering ~420 new cases across the webview, extension backend, and shared helpers. Drives coverage from 60% lines / 81% functions to 97% / 92%, with every source file now hit by at least one test. Backend (src/git): - git-parser edge cases: stash refs, malformed input, branch ahead/behind variants, diff hunk defaults, octal/escape path unescaping - git-service.extra: rootPath, submodule status/update, LFS lock/unlock/ ls-files (including expected-error swallowing), git-flow shortcuts, getFlowBranches, argument validation Webview modals (24 components): - Newly tested: Pull, Fetch, Push/PushTag, Reset, Revert, Stash 4-pack, DeleteBranch, FastForward, all small confirmation modals, git-flow Init/Start/Finish, AddWorktree, RemoveWorktree, TagDetails, etc. - Extended: Modal overlay/Enter handling, AddWorktree branch dropdown, CheckoutCommit branch picker, SetUpstream toggle/remote-switch flow Webview components: - Common: SearchBar (debounce, filters, backdrops), Reflog (context menu, action filters, ref switching), StatsView, ActivityLog, ImageDiff (swipe drag, onion slider), FileTreeBrowser, CommitHoverCard, BisectBanner skip action, ContextMenu submenu - Commit: CommitDetails (file tree, diff modes, LFS, context menu, parent hover preview, resize, scroll sync) - Layout: Toolbar (flow finish/start/init, repo switch, badges), BottomPanel - Rebase: InteractiveRebase (drag-reorder, move buttons, squash guard, action menu) - App.svelte: message dispatch, keyboard shortcuts, conflict/rebase banner actions, error bar, all showModal branches, modal callback payloads, resize handle
1 parent 436c8db commit c7a456a

46 files changed

Lines changed: 5857 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
parseLog,
4+
parseRefs,
5+
parseBranches,
6+
parseTags,
7+
parseRemotes,
8+
parseStashList,
9+
parseDiff,
10+
parseWorktreeList,
11+
} from '../git-parser';
12+
13+
describe('parseRefs — edge cases', () => {
14+
it('handles stash refs (refs/stash and stash)', () => {
15+
expect(parseRefs('refs/stash')).toEqual([{ type: 'stash', name: 'stash' }]);
16+
expect(parseRefs('stash')).toEqual([{ type: 'stash', name: 'stash' }]);
17+
});
18+
19+
it('skips blank entries between commas', () => {
20+
expect(parseRefs('main, ,origin/main', ['origin'])).toEqual([
21+
{ type: 'branch', name: 'main' },
22+
{ type: 'remote-branch', name: 'main', remote: 'origin' },
23+
]);
24+
});
25+
26+
it('whitespace-only refStr returns empty', () => {
27+
expect(parseRefs(' ')).toEqual([]);
28+
});
29+
});
30+
31+
describe('parseLog — edge cases', () => {
32+
it('record without refs field produces empty refs array', () => {
33+
const raw = '\x01h\x00h\x00A\x00a@x.com\x002024-01-01\x00A\x00a@x.com\x002024-01-01\x00msg\x00\x00';
34+
const result = parseLog(raw);
35+
expect(result[0].refs).toEqual([]);
36+
});
37+
38+
it('record with whitespace-only refs field produces empty refs array', () => {
39+
const raw = '\x01h\x00h\x00A\x00a@x.com\x002024-01-01\x00A\x00a@x.com\x002024-01-01\x00msg\x00\x00 ';
40+
const result = parseLog(raw);
41+
expect(result[0].refs).toEqual([]);
42+
});
43+
44+
it('parses commit body when present', () => {
45+
const raw = '\x01h\x00h\x00A\x00a@x.com\x002024-01-01\x00A\x00a@x.com\x002024-01-01\x00subject\x00\x00\x00body line\n\nmore';
46+
const result = parseLog(raw);
47+
expect(result[0].body).toBe('body line\n\nmore');
48+
});
49+
});
50+
51+
describe('parseBranches — edge cases', () => {
52+
it('parses only-ahead and only-behind correctly', () => {
53+
const onlyAhead = parseBranches('*main\x00h\x00origin/main\x00ahead 3');
54+
expect(onlyAhead[0].ahead).toBe(3);
55+
expect(onlyAhead[0].behind).toBe(0);
56+
57+
const onlyBehind = parseBranches(' feat\x00h\x00origin/feat\x00behind 5');
58+
expect(onlyBehind[0].ahead).toBe(0);
59+
expect(onlyBehind[0].behind).toBe(5);
60+
});
61+
62+
it('drops entries whose name is empty', () => {
63+
const raw = ' \x00\x00\x00\n main\x00h\x00\x00\x00refs/heads/main';
64+
const result = parseBranches(raw);
65+
expect(result).toHaveLength(1);
66+
expect(result[0].name).toBe('main');
67+
});
68+
69+
it('treats empty upstream field as undefined', () => {
70+
const raw = ' feat\x00h\x00\x00';
71+
expect(parseBranches(raw)[0].upstream).toBeUndefined();
72+
});
73+
});
74+
75+
describe('parseTags — edge cases', () => {
76+
it('annotated tag with body but no subject becomes undefined message', () => {
77+
// body-only is treated like no subject → no message
78+
const raw = 'v1\x00h\x00tag\x00\x00body only\x01';
79+
expect(parseTags(raw)[0].message).toBeUndefined();
80+
});
81+
});
82+
83+
describe('parseRemotes — edge cases', () => {
84+
it('ignores malformed lines without a (fetch)/(push) suffix', () => {
85+
const raw = 'garbage line without suffix\norigin\thttps://x.git (fetch)';
86+
const result = parseRemotes(raw);
87+
expect(result).toHaveLength(1);
88+
expect(result[0].fetchUrl).toBe('https://x.git');
89+
});
90+
});
91+
92+
describe('parseStashList — edge cases', () => {
93+
it('returns index 0 when refStr lacks the stash@{N} pattern', () => {
94+
const raw = 'malformed\x00msg\x00date\x00\x00';
95+
expect(parseStashList(raw)[0].index).toBe(0);
96+
});
97+
98+
it('parentHash and hash are undefined when omitted', () => {
99+
const raw = 'stash@{0}\x00msg\x00date';
100+
const result = parseStashList(raw);
101+
expect(result[0].parentHash).toBeUndefined();
102+
expect(result[0].hash).toBeUndefined();
103+
});
104+
});
105+
106+
describe('parseDiff — edge cases', () => {
107+
it('hunk header without explicit oldLines/newLines defaults to 1', () => {
108+
const raw = `diff --git a/f.ts b/f.ts
109+
@@ -1 +1 @@
110+
-x
111+
+y`;
112+
const result = parseDiff(raw);
113+
expect(result[0].hunks[0].oldLines).toBe(1);
114+
expect(result[0].hunks[0].newLines).toBe(1);
115+
});
116+
117+
it('non-image binary diff sets isImage=false', () => {
118+
const raw = `diff --git a/blob.bin b/blob.bin
119+
Binary files a/blob.bin and b/blob.bin differ`;
120+
const result = parseDiff(raw);
121+
expect(result[0].isBinary).toBe(true);
122+
expect(result[0].isImage).toBe(false);
123+
});
124+
125+
it('content lines before a hunk header are skipped (no currentHunk)', () => {
126+
const raw = `diff --git a/f.ts b/f.ts
127+
some preamble
128+
+ orphan add
129+
@@ -1 +1 @@
130+
-x
131+
+y`;
132+
const result = parseDiff(raw);
133+
expect(result[0].hunks).toHaveLength(1);
134+
// Orphan "+ orphan add" must not appear among the parsed lines.
135+
const contents = result[0].hunks[0].lines.map(l => l.content);
136+
expect(contents).not.toContain(' orphan add');
137+
});
138+
139+
it('treats blank lines inside a hunk as context', () => {
140+
const raw = `diff --git a/f.ts b/f.ts
141+
@@ -1,3 +1,3 @@
142+
a
143+
144+
c`;
145+
const result = parseDiff(raw);
146+
const ctxLines = result[0].hunks[0].lines.filter(l => l.type === 'context');
147+
expect(ctxLines).toHaveLength(3);
148+
expect(ctxLines[1].content).toBe('');
149+
});
150+
151+
it('image extensions are detected case-insensitively', () => {
152+
const raw = `diff --git a/Logo.PNG b/Logo.PNG
153+
Binary files a/Logo.PNG and b/Logo.PNG differ`;
154+
expect(parseDiff(raw)[0].isImage).toBe(true);
155+
});
156+
});
157+
158+
describe('parseWorktreeList — edge cases', () => {
159+
it('skips blocks without a worktree path', () => {
160+
const raw = 'worktree /repo/main\nHEAD h\nbranch refs/heads/main\n\n\nHEAD h2\nbranch refs/heads/other\n';
161+
const result = parseWorktreeList(raw);
162+
expect(result).toHaveLength(1);
163+
expect(result[0].path).toBe('/repo/main');
164+
});
165+
166+
it('handles "locked <reason>" and "prunable <reason>" prefixed forms', () => {
167+
const raw = 'worktree /repo/main\nHEAD h0\nbranch refs/heads/main\n\n' +
168+
'worktree /tmp/wt\nHEAD h1\nbranch refs/heads/x\nlocked because reasons\nprunable old worktree\n';
169+
const result = parseWorktreeList(raw);
170+
expect(result[1].locked).toBe(true);
171+
expect(result[1].prunable).toBe(true);
172+
});
173+
});
174+
175+
describe('parseDiff — git path unescaping edge cases', () => {
176+
it('decodes \\n, \\r, \\\\ and \\" escape forms in quoted paths', () => {
177+
const raw = [
178+
'diff --git "a/has\\nnewline.txt" "b/has\\nnewline.txt"',
179+
'@@ -1 +1 @@',
180+
'-x',
181+
'+y',
182+
].join('\n');
183+
expect(parseDiff(raw)[0].file).toBe('has\nnewline.txt');
184+
185+
const raw2 = [
186+
'diff --git "a/back\\\\slash.txt" "b/back\\\\slash.txt"',
187+
'@@ -1 +1 @@',
188+
'-x',
189+
'+y',
190+
].join('\n');
191+
expect(parseDiff(raw2)[0].file).toBe('back\\slash.txt');
192+
193+
const raw3 = [
194+
'diff --git "a/has\\rreturn.txt" "b/has\\rreturn.txt"',
195+
'@@ -1 +1 @@',
196+
'-x',
197+
'+y',
198+
].join('\n');
199+
expect(parseDiff(raw3)[0].file).toBe('has\rreturn.txt');
200+
201+
const raw4 = [
202+
'diff --git "a/quoted\\".txt" "b/quoted\\".txt"',
203+
'@@ -1 +1 @@',
204+
'-x',
205+
'+y',
206+
].join('\n');
207+
expect(parseDiff(raw4)[0].file).toBe('quoted".txt');
208+
});
209+
210+
it('falls through unknown escape sequences to the literal character', () => {
211+
// \b is not in the recognised switch — should yield the literal "b".
212+
const raw = [
213+
'diff --git "a/foo\\bbar.txt" "b/foo\\bbar.txt"',
214+
'@@ -1 +1 @@',
215+
'-x',
216+
'+y',
217+
].join('\n');
218+
expect(parseDiff(raw)[0].file).toBe('foobbar.txt');
219+
});
220+
});

0 commit comments

Comments
 (0)