mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-08-24 02:24:24 +00:00
refactor git widgets to use shared git command helpers
Centralize cwd resolution and git command execution for git widgets, and expand widget-level and utility tests for failure and edge cases. Closes #176
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
resolveGitCwd,
|
||||
runGit
|
||||
} from '../git';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
};
|
||||
|
||||
describe('git utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('resolveGitCwd', () => {
|
||||
it('prefers context.data.cwd when available', () => {
|
||||
const context: RenderContext = {
|
||||
data: {
|
||||
cwd: '/repo/from/cwd',
|
||||
workspace: {
|
||||
current_dir: '/repo/from/current-dir',
|
||||
project_dir: '/repo/from/project-dir'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(resolveGitCwd(context)).toBe('/repo/from/cwd');
|
||||
});
|
||||
|
||||
it('falls back to workspace.current_dir', () => {
|
||||
const context: RenderContext = {
|
||||
data: {
|
||||
workspace: {
|
||||
current_dir: '/repo/from/current-dir',
|
||||
project_dir: '/repo/from/project-dir'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(resolveGitCwd(context)).toBe('/repo/from/current-dir');
|
||||
});
|
||||
|
||||
it('falls back to workspace.project_dir', () => {
|
||||
const context: RenderContext = { data: { workspace: { project_dir: '/repo/from/project-dir' } } };
|
||||
|
||||
expect(resolveGitCwd(context)).toBe('/repo/from/project-dir');
|
||||
});
|
||||
|
||||
it('skips empty candidate values', () => {
|
||||
const context: RenderContext = {
|
||||
data: {
|
||||
cwd: ' ',
|
||||
workspace: {
|
||||
current_dir: '',
|
||||
project_dir: '/repo/from/project-dir'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(resolveGitCwd(context)).toBe('/repo/from/project-dir');
|
||||
});
|
||||
|
||||
it('returns undefined when no candidates are available', () => {
|
||||
expect(resolveGitCwd({})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('runGit', () => {
|
||||
it('runs git command with resolved cwd and trims output', () => {
|
||||
mockExecSync.mockReturnValue(' feature/worktree \n');
|
||||
const context: RenderContext = { data: { cwd: '/tmp/repo' } };
|
||||
|
||||
const result = runGit('branch --show-current', context);
|
||||
|
||||
expect(result).toBe('feature/worktree');
|
||||
expect(mockExecSync.mock.calls[0]?.[0]).toBe('git branch --show-current');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/repo'
|
||||
});
|
||||
});
|
||||
|
||||
it('runs git command without cwd when no context directory exists', () => {
|
||||
mockExecSync.mockReturnValue('true\n');
|
||||
|
||||
const result = runGit('rev-parse --is-inside-work-tree', {});
|
||||
|
||||
expect(result).toBe('true');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when the command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('git failed'); });
|
||||
|
||||
expect(runGit('status --short', {})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInsideGitWorkTree', () => {
|
||||
it('returns true when git reports true', () => {
|
||||
mockExecSync.mockReturnValue('true\n');
|
||||
|
||||
expect(isInsideGitWorkTree({})).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when git reports false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(isInsideGitWorkTree({})).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when git command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('git failed'); });
|
||||
|
||||
expect(isInsideGitWorkTree({})).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
|
||||
export function resolveGitCwd(context: RenderContext): string | undefined {
|
||||
const candidates = [
|
||||
context.data?.cwd,
|
||||
context.data?.workspace?.current_dir,
|
||||
context.data?.workspace?.project_dir
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function runGit(command: string, context: RenderContext): string | null {
|
||||
try {
|
||||
const cwd = resolveGitCwd(context);
|
||||
const output = execSync(`git ${command}`, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
...(cwd ? { cwd } : {})
|
||||
}).trim();
|
||||
|
||||
return output.length > 0 ? output : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isInsideGitWorkTree(context: RenderContext): boolean {
|
||||
return runGit('rev-parse --is-inside-work-tree', context) === 'true';
|
||||
}
|
||||
+11
-13
@@ -1,5 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
@@ -8,6 +6,10 @@ import type {
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitBranchWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
@@ -49,23 +51,19 @@ export class GitBranchWidget implements Widget {
|
||||
return item.rawValue ? 'main' : '⎇ main';
|
||||
}
|
||||
|
||||
const branch = this.getGitBranch();
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : '⎇ no git';
|
||||
}
|
||||
|
||||
const branch = this.getGitBranch(context);
|
||||
if (branch)
|
||||
return item.rawValue ? branch : `⎇ ${branch}`;
|
||||
|
||||
return hideNoGit ? null : '⎇ no git';
|
||||
}
|
||||
|
||||
private getGitBranch(): string | null {
|
||||
try {
|
||||
const branch = execSync('git branch --show-current', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
return branch || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
private getGitBranch(context: RenderContext): string | null {
|
||||
return runGit('branch --show-current', context);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
+28
-33
@@ -1,5 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
@@ -8,6 +6,10 @@ import type {
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitChangesWidget implements Widget {
|
||||
getDefaultColor(): string { return 'yellow'; }
|
||||
@@ -49,46 +51,39 @@ export class GitChangesWidget implements Widget {
|
||||
return '(+42,-10)';
|
||||
}
|
||||
|
||||
const changes = this.getGitChanges();
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
const changes = this.getGitChanges(context);
|
||||
if (changes)
|
||||
return `(+${changes.insertions},-${changes.deletions})`;
|
||||
else
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
private getGitChanges(): { insertions: number; deletions: number } | null {
|
||||
try {
|
||||
let totalInsertions = 0;
|
||||
let totalDeletions = 0;
|
||||
private getGitChanges(context: RenderContext): { insertions: number; deletions: number } | null {
|
||||
let totalInsertions = 0;
|
||||
let totalDeletions = 0;
|
||||
|
||||
const unstagedStat = execSync('git diff --shortstat', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
const unstagedStat = runGit('diff --shortstat', context) ?? '';
|
||||
const stagedStat = runGit('diff --cached --shortstat', context) ?? '';
|
||||
|
||||
const stagedStat = execSync('git diff --cached --shortstat', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
if (unstagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(unstagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
if (stagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(stagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(stagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
return { insertions: totalInsertions, deletions: totalDeletions };
|
||||
} catch {
|
||||
return null;
|
||||
if (unstagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(unstagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
if (stagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(stagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(stagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
return { insertions: totalInsertions, deletions: totalDeletions };
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
+11
-13
@@ -1,5 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
@@ -8,6 +6,10 @@ import type {
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitRootDirWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
@@ -49,7 +51,11 @@ export class GitRootDirWidget implements Widget {
|
||||
return 'my-repo';
|
||||
}
|
||||
|
||||
const rootDir = this.getGitRootDir();
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : 'no git';
|
||||
}
|
||||
|
||||
const rootDir = this.getGitRootDir(context);
|
||||
if (rootDir) {
|
||||
return this.getRootDirName(rootDir);
|
||||
}
|
||||
@@ -57,16 +63,8 @@ export class GitRootDirWidget implements Widget {
|
||||
return hideNoGit ? null : 'no git';
|
||||
}
|
||||
|
||||
private getGitRootDir(): string | null {
|
||||
try {
|
||||
const rootDir = execSync('git rev-parse --show-toplevel', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
return rootDir || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
private getGitRootDir(context: RenderContext): string | null {
|
||||
return runGit('rev-parse --show-toplevel', context);
|
||||
}
|
||||
|
||||
private getRootDirName(rootDir: string): string {
|
||||
|
||||
+29
-19
@@ -1,5 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
@@ -7,6 +5,10 @@ import type {
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitWorktreeWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
@@ -47,31 +49,39 @@ export class GitWorktreeWidget implements Widget {
|
||||
if (context.isPreview)
|
||||
return item.rawValue ? 'main' : '𖠰 main';
|
||||
|
||||
const worktree = this.getGitWorktree();
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : '𖠰 no git';
|
||||
}
|
||||
|
||||
const worktree = this.getGitWorktree(context);
|
||||
if (worktree)
|
||||
return item.rawValue ? worktree : `𖠰 ${worktree}`;
|
||||
|
||||
return hideNoGit ? null : '𖠰 no git';
|
||||
}
|
||||
|
||||
private getGitWorktree(): string | null {
|
||||
try {
|
||||
const worktreeDir = execSync('git rev-parse --git-dir', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
// /some/path/.git or .git
|
||||
if (worktreeDir.endsWith('/.git') || worktreeDir === '.git')
|
||||
return 'main';
|
||||
|
||||
// /some/path/.git/worktrees/some-worktree or /some/path/.git/worktrees/some-dir/some-worktree
|
||||
const [, worktree] = worktreeDir.split('.git/worktrees/');
|
||||
|
||||
return worktree ?? null;
|
||||
} catch {
|
||||
private getGitWorktree(context: RenderContext): string | null {
|
||||
const worktreeDir = runGit('rev-parse --git-dir', context);
|
||||
if (!worktreeDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedGitDir = worktreeDir.replace(/\\/g, '/');
|
||||
|
||||
// /some/path/.git or .git
|
||||
if (normalizedGitDir.endsWith('/.git') || normalizedGitDir === '.git')
|
||||
return 'main';
|
||||
|
||||
// /some/path/.git/worktrees/some-worktree or /some/path/.git/worktrees/some-dir/some-worktree
|
||||
const marker = '.git/worktrees/';
|
||||
const markerIndex = normalizedGitDir.lastIndexOf(marker);
|
||||
if (markerIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const worktree = normalizedGitDir.slice(markerIndex + marker.length);
|
||||
|
||||
return worktree.length > 0 ? worktree : null;
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import { GitBranchWidget } from '../GitBranch';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
rawValue?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitBranchWidget();
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-branch',
|
||||
type: 'git-branch',
|
||||
rawValue: options.rawValue,
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('GitBranchWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render preview', () => {
|
||||
expect(render({ isPreview: true })).toBe('⎇ main');
|
||||
});
|
||||
|
||||
it('should render preview with raw value', () => {
|
||||
expect(render({ isPreview: true, rawValue: true })).toBe('main');
|
||||
});
|
||||
|
||||
it('should render branch name', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('feature/worktree');
|
||||
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('⎇ feature/worktree');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render raw branch value', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('feature/worktree');
|
||||
|
||||
expect(render({ rawValue: true })).toBe('feature/worktree');
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('⎇ no git');
|
||||
});
|
||||
|
||||
it('should hide no git when configured', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should render no git when branch lookup is empty', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(render()).toBe('⎇ no git');
|
||||
});
|
||||
|
||||
it('should render no git when command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('⎇ no git');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import { GitChangesWidget } from '../GitChanges';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitChangesWidget();
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-changes',
|
||||
type: 'git-changes',
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('GitChangesWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render preview', () => {
|
||||
expect(render({ isPreview: true })).toBe('(+42,-10)');
|
||||
});
|
||||
|
||||
it('should render combined staged and unstaged changes', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 2 insertions(+), 1 deletion(-)');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 3 insertions(+), 4 deletions(-)');
|
||||
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('(+5,-5)');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[2]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render zero counts when repo is clean', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(render()).toBe('(+0,-0)');
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
|
||||
it('should hide no git when configured', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should render no git when command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,18 @@ import { GitRootDirWidget } from '../GitRootDir';
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mockReturnValue: (value: string) => void;
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(options: { isPreview?: boolean; hideNoGit?: boolean } = {}) {
|
||||
function render(options: { cwd?: string; hideNoGit?: boolean; isPreview?: boolean } = {}) {
|
||||
const widget = new GitRootDirWidget();
|
||||
const context: RenderContext = { isPreview: options.isPreview };
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
@@ -41,29 +46,49 @@ describe('GitRootDirWidget', () => {
|
||||
});
|
||||
|
||||
it('should render root directory name', () => {
|
||||
mockExecSync.mockReturnValue('/some/path/my-repo');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/my-repo');
|
||||
|
||||
expect(render()).toBe('my-repo');
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('my-repo');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle trailing separators', () => {
|
||||
mockExecSync.mockReturnValue('/some/path/my-repo/');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/my-repo/');
|
||||
|
||||
expect(render()).toBe('my-repo');
|
||||
});
|
||||
|
||||
it('should render unix root path without returning empty output', () => {
|
||||
mockExecSync.mockReturnValue('/');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/');
|
||||
|
||||
expect(render()).toBe('/');
|
||||
});
|
||||
|
||||
it('should render windows drive root without returning empty output', () => {
|
||||
mockExecSync.mockReturnValue('C:/');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('C:/');
|
||||
|
||||
expect(render()).toBe('C:');
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('no git');
|
||||
});
|
||||
|
||||
it('should render no git when command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
|
||||
@@ -16,17 +16,28 @@ import { GitWorktreeWidget } from '../GitWorktree';
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mockReturnValue: (value: string) => void;
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(rawValue = false, isPreview = false) {
|
||||
function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
rawValue?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitWorktreeWidget();
|
||||
const context: RenderContext = { isPreview };
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-worktree',
|
||||
type: 'git-worktree',
|
||||
rawValue
|
||||
rawValue: options.rawValue,
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context);
|
||||
@@ -38,45 +49,72 @@ describe('GitWorktreeWidget', () => {
|
||||
});
|
||||
|
||||
it('should render preview', () => {
|
||||
const isPreview = true;
|
||||
const rawValue = false;
|
||||
|
||||
expect(render(rawValue, isPreview)).toBe('𖠰 main');
|
||||
expect(render({ isPreview: true })).toBe('𖠰 main');
|
||||
});
|
||||
|
||||
it('should render preview with raw value', () => {
|
||||
const isPreview = true;
|
||||
const rawValue = true;
|
||||
|
||||
expect(render(rawValue, isPreview)).toBe('main');
|
||||
expect(render({ isPreview: true, rawValue: true })).toBe('main');
|
||||
});
|
||||
|
||||
it('should render with worktree', () => {
|
||||
mockExecSync.mockReturnValue('/some/path/.git/worktrees/some-worktree');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/.git/worktrees/some-worktree');
|
||||
|
||||
expect(render()).toBe('𖠰 some-worktree');
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('𖠰 some-worktree');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with nested worktree', () => {
|
||||
mockExecSync.mockReturnValue('/some/path/.git/worktrees/some-dir/some-worktree');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/.git/worktrees/some-dir/some-worktree');
|
||||
|
||||
expect(render()).toBe('𖠰 some-dir/some-worktree');
|
||||
});
|
||||
|
||||
it('should render with no worktree', () => {
|
||||
mockExecSync.mockReturnValue('.git');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('.git');
|
||||
|
||||
expect(render()).toBe('𖠰 main');
|
||||
});
|
||||
|
||||
it('should handle windows git-dir paths', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('C:\\repo\\.git\\worktrees\\some-worktree');
|
||||
|
||||
expect(render()).toBe('𖠰 some-worktree');
|
||||
});
|
||||
|
||||
it('should render with no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('𖠰 no git');
|
||||
});
|
||||
|
||||
it('should render with no git', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('𖠰 no git');
|
||||
});
|
||||
|
||||
it('should hide no git when configured', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should render with invalid git dir', () => {
|
||||
mockExecSync.mockReturnValue('');
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(render()).toBe('𖠰 no git');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user