@akiojin/gwt 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +10 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +43 -23
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -67
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +107 -130
- package/src/cli/ui/__tests__/components/App.test.tsx +102 -71
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +57 -30
- package/src/cli/ui/components/App.tsx +16 -3
- package/src/cli/ui/types.ts +1 -1
- package/src/index.test.ts +1 -1
- package/src/worktree.ts +54 -37
|
@@ -15,14 +15,7 @@ import { render, act, waitFor } from "@testing-library/react";
|
|
|
15
15
|
import React from "react";
|
|
16
16
|
import type { BranchItem, CleanupTarget } from "../../types.js";
|
|
17
17
|
import { Window } from "happy-dom";
|
|
18
|
-
|
|
19
|
-
import * as useScreenStateModule from "../../hooks/useScreenState.js";
|
|
20
|
-
import * as WorktreeManagerScreenModule from "../../components/screens/WorktreeManagerScreen.js";
|
|
21
|
-
import * as BranchCreatorScreenModule from "../../components/screens/BranchCreatorScreen.js";
|
|
22
|
-
import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
|
|
23
|
-
import * as worktreeModule from "../../../../worktree.ts";
|
|
24
|
-
import * as gitModule from "../../../../git.ts";
|
|
25
|
-
import { App } from "../../components/App.js";
|
|
18
|
+
let App: typeof import("../../components/App.js").App;
|
|
26
19
|
|
|
27
20
|
const navigateToMock = vi.fn();
|
|
28
21
|
const goBackMock = vi.fn();
|
|
@@ -32,49 +25,77 @@ const worktreeScreenProps: any[] = [];
|
|
|
32
25
|
const branchCreatorProps: any[] = [];
|
|
33
26
|
const branchListProps: any[] = [];
|
|
34
27
|
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
28
|
+
const useGitDataMock = vi.fn();
|
|
29
|
+
const useScreenStateMock = vi.fn();
|
|
30
|
+
const getMergedPRWorktreesMock = vi.fn();
|
|
31
|
+
const generateWorktreePathMock = vi.fn();
|
|
32
|
+
const createWorktreeMock = vi.fn();
|
|
33
|
+
const removeWorktreeMock = vi.fn();
|
|
34
|
+
const getRepositoryRootMock = vi.fn();
|
|
35
|
+
const deleteBranchMock = vi.fn();
|
|
36
|
+
|
|
37
|
+
vi.mock("../../hooks/useGitData.js", () => ({
|
|
38
|
+
useGitData: (...args: any[]) => useGitDataMock(...args),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock("../../hooks/useScreenState.js", () => ({
|
|
42
|
+
useScreenState: (...args: any[]) => useScreenStateMock(...args),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock("../../../../worktree.js", async () => {
|
|
46
|
+
const actual = await vi.importActual<
|
|
47
|
+
typeof import("../../../../worktree.js")
|
|
48
|
+
>("../../../../worktree.js");
|
|
49
|
+
return {
|
|
50
|
+
...actual,
|
|
51
|
+
getMergedPRWorktrees: getMergedPRWorktreesMock,
|
|
52
|
+
generateWorktreePath: generateWorktreePathMock,
|
|
53
|
+
createWorktree: createWorktreeMock,
|
|
54
|
+
removeWorktree: removeWorktreeMock,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
vi.mock("../../../../git.js", async () => {
|
|
59
|
+
const actual =
|
|
60
|
+
await vi.importActual<typeof import("../../../../git.js")>(
|
|
61
|
+
"../../../../git.js",
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
...actual,
|
|
65
|
+
getRepositoryRoot: getRepositoryRootMock,
|
|
66
|
+
deleteBranch: deleteBranchMock,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
vi.mock("../../components/screens/WorktreeManagerScreen.js", () => {
|
|
71
|
+
return {
|
|
72
|
+
WorktreeManagerScreen: (props: any) => {
|
|
73
|
+
worktreeScreenProps.push(props);
|
|
74
|
+
return React.createElement("div", null, "WorktreeManagerScreenMock");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
vi.mock("../../components/screens/BranchCreatorScreen.js", () => {
|
|
80
|
+
return {
|
|
81
|
+
BranchCreatorScreen: (props: any) => {
|
|
82
|
+
branchCreatorProps.push(props);
|
|
83
|
+
return React.createElement("div", null, "BranchCreatorScreenMock");
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
vi.mock("../../components/screens/BranchListScreen.js", () => {
|
|
89
|
+
return {
|
|
90
|
+
BranchListScreen: (props: any) => {
|
|
91
|
+
branchListProps.push(props);
|
|
92
|
+
return React.createElement("div", null, "BranchListScreenMock");
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
75
96
|
|
|
76
97
|
describe("App shortcuts integration", () => {
|
|
77
|
-
beforeEach(() => {
|
|
98
|
+
beforeEach(async () => {
|
|
78
99
|
if (typeof globalThis.document === "undefined") {
|
|
79
100
|
const window = new Window();
|
|
80
101
|
globalThis.window = window as any;
|
|
@@ -86,7 +107,7 @@ describe("App shortcuts integration", () => {
|
|
|
86
107
|
navigateToMock.mockClear();
|
|
87
108
|
goBackMock.mockClear();
|
|
88
109
|
resetMock.mockClear();
|
|
89
|
-
|
|
110
|
+
useGitDataMock.mockReturnValue({
|
|
90
111
|
branches: [],
|
|
91
112
|
worktrees: [
|
|
92
113
|
{
|
|
@@ -99,26 +120,14 @@ describe("App shortcuts integration", () => {
|
|
|
99
120
|
error: null,
|
|
100
121
|
refresh: vi.fn(),
|
|
101
122
|
lastUpdated: null,
|
|
102
|
-
}));
|
|
103
|
-
useScreenStateSpy.mockImplementation(() => ({
|
|
104
|
-
currentScreen: "worktree-manager",
|
|
105
|
-
navigateTo: navigateToMock as _Mock,
|
|
106
|
-
goBack: goBackMock as _Mock,
|
|
107
|
-
reset: resetMock as _Mock,
|
|
108
|
-
}));
|
|
109
|
-
worktreeManagerScreenSpy.mockImplementation((props: any) => {
|
|
110
|
-
worktreeScreenProps.push(props);
|
|
111
|
-
return React.createElement(originalWorktreeManagerScreen, props);
|
|
112
|
-
});
|
|
113
|
-
branchCreatorScreenSpy.mockImplementation((props: any) => {
|
|
114
|
-
branchCreatorProps.push(props);
|
|
115
|
-
return React.createElement(originalBranchCreatorScreen, props);
|
|
116
123
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
useScreenStateMock.mockReturnValue({
|
|
125
|
+
currentScreen: "worktree-manager",
|
|
126
|
+
navigateTo: navigateToMock as Mock,
|
|
127
|
+
goBack: goBackMock as Mock,
|
|
128
|
+
reset: resetMock as Mock,
|
|
120
129
|
});
|
|
121
|
-
|
|
130
|
+
getMergedPRWorktreesMock.mockResolvedValue([
|
|
122
131
|
{
|
|
123
132
|
branch: "feature/add-new-feature",
|
|
124
133
|
cleanupType: "worktree-and-branch",
|
|
@@ -152,44 +161,26 @@ describe("App shortcuts integration", () => {
|
|
|
152
161
|
isAccessible: true,
|
|
153
162
|
},
|
|
154
163
|
] as CleanupTarget[]);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
generateWorktreePathMock.mockResolvedValue("/worktrees/new-branch");
|
|
165
|
+
createWorktreeMock.mockResolvedValue(undefined);
|
|
166
|
+
removeWorktreeMock.mockResolvedValue(undefined);
|
|
167
|
+
getRepositoryRootMock.mockResolvedValue("/repo");
|
|
168
|
+
deleteBranchMock.mockResolvedValue(undefined);
|
|
169
|
+
App = (await import("../../components/App.js")).App;
|
|
160
170
|
});
|
|
161
171
|
|
|
162
172
|
afterEach(() => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
useGitDataSpy.mockImplementation(originalUseGitData);
|
|
175
|
-
useScreenStateSpy.mockImplementation(originalUseScreenState);
|
|
176
|
-
worktreeManagerScreenSpy.mockImplementation(
|
|
177
|
-
originalWorktreeManagerScreen as any,
|
|
178
|
-
);
|
|
179
|
-
branchCreatorScreenSpy.mockImplementation(
|
|
180
|
-
originalBranchCreatorScreen as any,
|
|
181
|
-
);
|
|
182
|
-
branchListScreenSpy.mockImplementation(originalBranchListScreen as any);
|
|
183
|
-
getMergedPRWorktreesSpy.mockImplementation(
|
|
184
|
-
originalGetMergedPRWorktrees as any,
|
|
185
|
-
);
|
|
186
|
-
generateWorktreePathSpy.mockImplementation(
|
|
187
|
-
originalGenerateWorktreePath as any,
|
|
188
|
-
);
|
|
189
|
-
createWorktreeSpy.mockImplementation(originalCreateWorktree as any);
|
|
190
|
-
removeWorktreeSpy.mockImplementation(originalRemoveWorktree as any);
|
|
191
|
-
getRepositoryRootSpy.mockImplementation(originalGetRepositoryRoot as any);
|
|
192
|
-
deleteBranchSpy.mockImplementation(originalDeleteBranch as any);
|
|
173
|
+
useGitDataMock.mockReset();
|
|
174
|
+
useScreenStateMock.mockReset();
|
|
175
|
+
getMergedPRWorktreesMock.mockReset();
|
|
176
|
+
generateWorktreePathMock.mockReset();
|
|
177
|
+
createWorktreeMock.mockReset();
|
|
178
|
+
removeWorktreeMock.mockReset();
|
|
179
|
+
getRepositoryRootMock.mockReset();
|
|
180
|
+
deleteBranchMock.mockReset();
|
|
181
|
+
worktreeScreenProps.length = 0;
|
|
182
|
+
branchCreatorProps.length = 0;
|
|
183
|
+
branchListProps.length = 0;
|
|
193
184
|
});
|
|
194
185
|
|
|
195
186
|
it("navigates to AI tool selector when worktree is selected", () => {
|
|
@@ -209,11 +200,11 @@ describe("App shortcuts integration", () => {
|
|
|
209
200
|
const onExit = vi.fn();
|
|
210
201
|
|
|
211
202
|
// Update screen state mock to branch-creator for this test
|
|
212
|
-
|
|
203
|
+
useScreenStateMock.mockReturnValue({
|
|
213
204
|
currentScreen: "branch-creator",
|
|
214
|
-
navigateTo: navigateToMock as
|
|
215
|
-
goBack: goBackMock as
|
|
216
|
-
reset: resetMock as
|
|
205
|
+
navigateTo: navigateToMock as Mock,
|
|
206
|
+
goBack: goBackMock as Mock,
|
|
207
|
+
reset: resetMock as Mock,
|
|
217
208
|
});
|
|
218
209
|
|
|
219
210
|
render(<App onExit={onExit} />);
|
|
@@ -225,7 +216,7 @@ describe("App shortcuts integration", () => {
|
|
|
225
216
|
await onCreate("feature/new-branch");
|
|
226
217
|
});
|
|
227
218
|
|
|
228
|
-
expect(
|
|
219
|
+
expect(createWorktreeMock).toHaveBeenCalledWith(
|
|
229
220
|
expect.objectContaining({
|
|
230
221
|
branchName: "feature/new-branch",
|
|
231
222
|
isNewBranch: true,
|
|
@@ -243,25 +234,25 @@ describe("App shortcuts integration", () => {
|
|
|
243
234
|
let resolveRemoveWorktree: (() => void) | undefined;
|
|
244
235
|
let resolveDeleteBranch: (() => void) | undefined;
|
|
245
236
|
|
|
246
|
-
|
|
237
|
+
removeWorktreeMock.mockImplementationOnce(
|
|
247
238
|
() =>
|
|
248
239
|
new Promise<void>((resolve) => {
|
|
249
240
|
resolveRemoveWorktree = resolve;
|
|
250
241
|
}),
|
|
251
242
|
);
|
|
252
243
|
|
|
253
|
-
|
|
244
|
+
deleteBranchMock.mockImplementationOnce(
|
|
254
245
|
() =>
|
|
255
246
|
new Promise<void>((resolve) => {
|
|
256
247
|
resolveDeleteBranch = resolve;
|
|
257
248
|
}),
|
|
258
249
|
);
|
|
259
250
|
|
|
260
|
-
|
|
251
|
+
useScreenStateMock.mockReturnValue({
|
|
261
252
|
currentScreen: "branch-list",
|
|
262
|
-
navigateTo: navigateToMock as
|
|
263
|
-
goBack: goBackMock as
|
|
264
|
-
reset: resetMock as
|
|
253
|
+
navigateTo: navigateToMock as Mock,
|
|
254
|
+
goBack: goBackMock as Mock,
|
|
255
|
+
reset: resetMock as Mock,
|
|
265
256
|
});
|
|
266
257
|
|
|
267
258
|
render(<App onExit={onExit} />);
|
|
@@ -299,11 +290,11 @@ describe("App shortcuts integration", () => {
|
|
|
299
290
|
|
|
300
291
|
resolveDeleteBranch?.();
|
|
301
292
|
|
|
302
|
-
expect(
|
|
293
|
+
expect(removeWorktreeMock).toHaveBeenCalledWith(
|
|
303
294
|
"/worktrees/feature-add-new-feature",
|
|
304
295
|
true,
|
|
305
296
|
);
|
|
306
|
-
expect(
|
|
297
|
+
expect(deleteBranchMock).toHaveBeenCalledWith(
|
|
307
298
|
"feature/add-new-feature",
|
|
308
299
|
true,
|
|
309
300
|
);
|
|
@@ -339,17 +330,3 @@ describe("App shortcuts integration", () => {
|
|
|
339
330
|
}
|
|
340
331
|
});
|
|
341
332
|
});
|
|
342
|
-
|
|
343
|
-
afterAll(() => {
|
|
344
|
-
useGitDataSpy.mockRestore();
|
|
345
|
-
useScreenStateSpy.mockRestore();
|
|
346
|
-
worktreeManagerScreenSpy.mockRestore();
|
|
347
|
-
branchCreatorScreenSpy.mockRestore();
|
|
348
|
-
branchListScreenSpy.mockRestore();
|
|
349
|
-
getMergedPRWorktreesSpy.mockRestore();
|
|
350
|
-
generateWorktreePathSpy.mockRestore();
|
|
351
|
-
createWorktreeSpy.mockRestore();
|
|
352
|
-
removeWorktreeSpy.mockRestore();
|
|
353
|
-
getRepositoryRootSpy.mockRestore();
|
|
354
|
-
deleteBranchSpy.mockRestore();
|
|
355
|
-
});
|
|
@@ -12,25 +12,59 @@ import {
|
|
|
12
12
|
} from "vitest";
|
|
13
13
|
import { act, render } from "@testing-library/react";
|
|
14
14
|
import React from "react";
|
|
15
|
-
import { App } from "../../components/App.js";
|
|
16
15
|
import { Window } from "happy-dom";
|
|
17
16
|
import type { BranchInfo } from "../../types.js";
|
|
18
|
-
import
|
|
17
|
+
import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
|
|
19
18
|
|
|
20
19
|
const mockRefresh = vi.fn();
|
|
21
|
-
|
|
22
|
-
const
|
|
20
|
+
let App: typeof import("../../components/App.js").App;
|
|
21
|
+
const branchListProps: BranchListScreenProps[] = [];
|
|
22
|
+
const useGitDataMock = vi.fn();
|
|
23
|
+
|
|
24
|
+
vi.mock("../../hooks/useGitData.js", () => ({
|
|
25
|
+
useGitData: (...args: any[]) => useGitDataMock(...args),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("../../components/screens/BranchListScreen.js", () => {
|
|
29
|
+
return {
|
|
30
|
+
BranchListScreen: (props: BranchListScreenProps) => {
|
|
31
|
+
branchListProps.push(props);
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<div>gwt - Branch Selection</div>
|
|
35
|
+
<div>Local: {props.stats?.localCount ?? 0}</div>
|
|
36
|
+
<div>Remote: {props.stats?.remoteCount ?? 0}</div>
|
|
37
|
+
<div>Worktrees: {props.stats?.worktreeCount ?? 0}</div>
|
|
38
|
+
<div>Changes: {props.stats?.changesCount ?? 0}</div>
|
|
39
|
+
{props.loading && <div>Loading Git information</div>}
|
|
40
|
+
{props.error && <div>Error: {props.error.message}</div>}
|
|
41
|
+
{!props.loading && !props.error && props.branches.length === 0 && (
|
|
42
|
+
<div>No branches found</div>
|
|
43
|
+
)}
|
|
44
|
+
<ul>
|
|
45
|
+
{props.branches.map((branch) => (
|
|
46
|
+
<li
|
|
47
|
+
key={branch.name}
|
|
48
|
+
>{`${branch.icons?.join("") ?? ""} ${branch.name}`}</li>
|
|
49
|
+
))}
|
|
50
|
+
</ul>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
23
56
|
|
|
24
57
|
describe("App", () => {
|
|
25
|
-
beforeEach(() => {
|
|
58
|
+
beforeEach(async () => {
|
|
26
59
|
// Setup happy-dom
|
|
27
60
|
const window = new Window();
|
|
28
61
|
globalThis.window = window as any;
|
|
29
62
|
globalThis.document = window.document as any;
|
|
30
63
|
|
|
31
64
|
vi.clearAllMocks();
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
useGitDataMock.mockReset();
|
|
66
|
+
branchListProps.length = 0;
|
|
67
|
+
App = (await import("../../components/App.js")).App;
|
|
34
68
|
});
|
|
35
69
|
|
|
36
70
|
const mockBranches: BranchInfo[] = [
|
|
@@ -49,61 +83,61 @@ describe("App", () => {
|
|
|
49
83
|
];
|
|
50
84
|
|
|
51
85
|
it("should render BranchListScreen when data is loaded", () => {
|
|
52
|
-
|
|
86
|
+
useGitDataMock.mockImplementation(() => ({
|
|
53
87
|
branches: mockBranches,
|
|
54
88
|
loading: false,
|
|
55
89
|
error: null,
|
|
56
90
|
worktrees: [],
|
|
57
91
|
refresh: mockRefresh,
|
|
58
|
-
});
|
|
92
|
+
}));
|
|
59
93
|
|
|
60
94
|
const onExit = vi.fn();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(
|
|
66
|
-
expect(
|
|
95
|
+
render(<App onExit={onExit} />);
|
|
96
|
+
|
|
97
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
98
|
+
const props = branchListProps.at(-1);
|
|
99
|
+
expect(props?.loading).toBe(false);
|
|
100
|
+
expect(props?.error).toBeNull();
|
|
101
|
+
const branchNames = props?.branches.map((b) => b.name);
|
|
102
|
+
expect(branchNames).toContain("main");
|
|
103
|
+
expect(branchNames).toContain("feature/test");
|
|
67
104
|
});
|
|
68
105
|
|
|
69
106
|
it("should show loading state initially", async () => {
|
|
70
|
-
|
|
107
|
+
useGitDataMock.mockImplementation(() => ({
|
|
71
108
|
branches: [],
|
|
72
109
|
loading: true,
|
|
73
110
|
error: null,
|
|
74
111
|
worktrees: [],
|
|
75
112
|
refresh: mockRefresh,
|
|
76
|
-
});
|
|
113
|
+
}));
|
|
77
114
|
|
|
78
115
|
const onExit = vi.fn();
|
|
79
|
-
|
|
80
|
-
<App onExit={onExit} loadingIndicatorDelay={10} />,
|
|
81
|
-
);
|
|
116
|
+
render(<App onExit={onExit} loadingIndicatorDelay={10} />);
|
|
82
117
|
|
|
83
|
-
expect(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(getByText(/Loading Git information/i)).toBeDefined();
|
|
118
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
119
|
+
const props = branchListProps.at(-1);
|
|
120
|
+
expect(props?.loading).toBe(true);
|
|
121
|
+
expect(props?.loadingIndicatorDelay).toBe(10);
|
|
90
122
|
});
|
|
91
123
|
|
|
92
124
|
it("should show error state when Git data fails to load", () => {
|
|
93
125
|
const error = new Error("Failed to fetch branches");
|
|
94
|
-
|
|
126
|
+
useGitDataMock.mockImplementation(() => ({
|
|
95
127
|
branches: [],
|
|
96
128
|
loading: false,
|
|
97
129
|
error,
|
|
98
130
|
worktrees: [],
|
|
99
131
|
refresh: mockRefresh,
|
|
100
|
-
});
|
|
132
|
+
}));
|
|
101
133
|
|
|
102
134
|
const onExit = vi.fn();
|
|
103
|
-
|
|
135
|
+
render(<App onExit={onExit} />);
|
|
104
136
|
|
|
105
|
-
expect(
|
|
106
|
-
|
|
137
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
138
|
+
const props = branchListProps.at(-1);
|
|
139
|
+
expect(props?.error?.message).toBe("Failed to fetch branches");
|
|
140
|
+
expect(props?.loading).toBe(false);
|
|
107
141
|
});
|
|
108
142
|
|
|
109
143
|
it("should calculate statistics from branches", () => {
|
|
@@ -133,67 +167,71 @@ describe("App", () => {
|
|
|
133
167
|
},
|
|
134
168
|
];
|
|
135
169
|
|
|
136
|
-
|
|
170
|
+
useGitDataMock.mockImplementation(() => ({
|
|
137
171
|
branches: branchesWithWorktree,
|
|
138
172
|
loading: false,
|
|
139
173
|
error: null,
|
|
140
174
|
worktrees: [],
|
|
141
175
|
refresh: mockRefresh,
|
|
142
|
-
});
|
|
176
|
+
}));
|
|
143
177
|
|
|
144
178
|
const onExit = vi.fn();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
expect(
|
|
150
|
-
expect(
|
|
151
|
-
expect(
|
|
152
|
-
expect(getByText(/Worktrees:/)).toBeDefined();
|
|
179
|
+
render(<App onExit={onExit} />);
|
|
180
|
+
|
|
181
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
182
|
+
const props = branchListProps.at(-1);
|
|
183
|
+
expect(props?.stats.localCount).toBe(2);
|
|
184
|
+
expect(props?.stats.remoteCount).toBe(1);
|
|
185
|
+
expect(props?.stats.worktreeCount).toBe(1);
|
|
153
186
|
});
|
|
154
187
|
|
|
155
|
-
it("should
|
|
156
|
-
|
|
188
|
+
it("should render branch selection without triggering exit", () => {
|
|
189
|
+
useGitDataMock.mockImplementation(() => ({
|
|
157
190
|
branches: mockBranches,
|
|
158
191
|
loading: false,
|
|
159
192
|
error: null,
|
|
160
193
|
worktrees: [],
|
|
161
194
|
refresh: mockRefresh,
|
|
162
|
-
});
|
|
195
|
+
}));
|
|
163
196
|
|
|
164
197
|
const onExit = vi.fn();
|
|
165
198
|
const { container } = render(<App onExit={onExit} />);
|
|
166
199
|
|
|
167
200
|
expect(container).toBeDefined();
|
|
201
|
+
expect(onExit).not.toHaveBeenCalled();
|
|
168
202
|
// Note: Testing actual selection requires simulating user input,
|
|
169
203
|
// which is covered in integration tests
|
|
170
204
|
});
|
|
171
205
|
|
|
172
206
|
it("should handle empty branch list", () => {
|
|
173
|
-
|
|
207
|
+
useGitDataMock.mockImplementation(() => ({
|
|
174
208
|
branches: [],
|
|
175
209
|
loading: false,
|
|
176
210
|
error: null,
|
|
177
211
|
worktrees: [],
|
|
178
212
|
refresh: mockRefresh,
|
|
179
|
-
});
|
|
213
|
+
}));
|
|
180
214
|
|
|
181
215
|
const onExit = vi.fn();
|
|
182
|
-
|
|
216
|
+
render(<App onExit={onExit} />);
|
|
183
217
|
|
|
184
|
-
expect(
|
|
218
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
219
|
+
const props = branchListProps.at(-1);
|
|
220
|
+
expect(props?.branches).toHaveLength(0);
|
|
221
|
+
expect(props?.loading).toBe(false);
|
|
222
|
+
expect(props?.error).toBeNull();
|
|
185
223
|
});
|
|
186
224
|
|
|
187
225
|
it("should wrap with ErrorBoundary", () => {
|
|
188
226
|
// This test verifies ErrorBoundary is present
|
|
189
227
|
// Actual error catching is tested separately
|
|
190
|
-
|
|
228
|
+
useGitDataMock.mockImplementation(() => ({
|
|
191
229
|
branches: mockBranches,
|
|
192
230
|
loading: false,
|
|
193
231
|
error: null,
|
|
194
232
|
worktrees: [],
|
|
195
233
|
refresh: mockRefresh,
|
|
196
|
-
});
|
|
234
|
+
}));
|
|
197
235
|
|
|
198
236
|
const onExit = vi.fn();
|
|
199
237
|
const { container } = render(<App onExit={onExit} />);
|
|
@@ -202,30 +240,32 @@ describe("App", () => {
|
|
|
202
240
|
});
|
|
203
241
|
|
|
204
242
|
it("should format branch items with icons", () => {
|
|
205
|
-
|
|
243
|
+
useGitDataMock.mockImplementation(() => ({
|
|
206
244
|
branches: mockBranches,
|
|
207
245
|
loading: false,
|
|
208
246
|
error: null,
|
|
209
247
|
worktrees: [],
|
|
210
248
|
refresh: mockRefresh,
|
|
211
|
-
});
|
|
249
|
+
}));
|
|
212
250
|
|
|
213
251
|
const onExit = vi.fn();
|
|
214
|
-
|
|
252
|
+
render(<App onExit={onExit} />);
|
|
215
253
|
|
|
216
|
-
|
|
217
|
-
|
|
254
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
255
|
+
const props = branchListProps.at(-1);
|
|
256
|
+
const main = props?.branches.find((b: any) => b.name === "main");
|
|
257
|
+
expect(main?.icons).toContain("⚡");
|
|
218
258
|
});
|
|
219
259
|
|
|
220
260
|
describe("BranchActionSelectorScreen integration", () => {
|
|
221
261
|
it("should show BranchActionSelectorScreen after branch selection", () => {
|
|
222
|
-
|
|
262
|
+
useGitDataMock.mockImplementation(() => ({
|
|
223
263
|
branches: mockBranches,
|
|
224
264
|
loading: false,
|
|
225
265
|
error: null,
|
|
226
266
|
worktrees: [],
|
|
227
267
|
refresh: mockRefresh,
|
|
228
|
-
});
|
|
268
|
+
}));
|
|
229
269
|
|
|
230
270
|
const onExit = vi.fn();
|
|
231
271
|
const { container } = render(<App onExit={onExit} />);
|
|
@@ -235,13 +275,13 @@ describe("App", () => {
|
|
|
235
275
|
});
|
|
236
276
|
|
|
237
277
|
it('should navigate to AI tool selector when "use existing" is selected', () => {
|
|
238
|
-
|
|
278
|
+
useGitDataMock.mockImplementation(() => ({
|
|
239
279
|
branches: mockBranches,
|
|
240
280
|
loading: false,
|
|
241
281
|
error: null,
|
|
242
282
|
worktrees: [],
|
|
243
283
|
refresh: mockRefresh,
|
|
244
|
-
});
|
|
284
|
+
}));
|
|
245
285
|
|
|
246
286
|
const onExit = vi.fn();
|
|
247
287
|
const { container } = render(<App onExit={onExit} />);
|
|
@@ -251,13 +291,13 @@ describe("App", () => {
|
|
|
251
291
|
});
|
|
252
292
|
|
|
253
293
|
it('should navigate to branch creator when "create new" is selected', () => {
|
|
254
|
-
|
|
294
|
+
useGitDataMock.mockImplementation(() => ({
|
|
255
295
|
branches: mockBranches,
|
|
256
296
|
loading: false,
|
|
257
297
|
error: null,
|
|
258
298
|
worktrees: [],
|
|
259
299
|
refresh: mockRefresh,
|
|
260
|
-
});
|
|
300
|
+
}));
|
|
261
301
|
|
|
262
302
|
const onExit = vi.fn();
|
|
263
303
|
const { container } = render(<App onExit={onExit} />);
|
|
@@ -266,13 +306,4 @@ describe("App", () => {
|
|
|
266
306
|
expect(container).toBeDefined();
|
|
267
307
|
});
|
|
268
308
|
});
|
|
269
|
-
|
|
270
|
-
afterEach(() => {
|
|
271
|
-
useGitDataSpy.mockReset();
|
|
272
|
-
useGitDataSpy.mockImplementation(originalUseGitData);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
afterAll(() => {
|
|
277
|
-
useGitDataSpy.mockRestore();
|
|
278
309
|
});
|