@akiojin/gwt 2.1.1 → 2.3.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.ja.md +4 -4
- package/README.md +4 -4
- package/dist/cli/ui/components/App.d.ts +4 -4
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +144 -105
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/common/Confirm.d.ts +1 -1
- package/dist/cli/ui/components/common/Confirm.d.ts.map +1 -1
- package/dist/cli/ui/components/common/Confirm.js +7 -7
- package/dist/cli/ui/components/common/Confirm.js.map +1 -1
- package/dist/cli/ui/components/common/ErrorBoundary.d.ts +1 -1
- package/dist/cli/ui/components/common/ErrorBoundary.d.ts.map +1 -1
- package/dist/cli/ui/components/common/ErrorBoundary.js +4 -4
- package/dist/cli/ui/components/common/ErrorBoundary.js.map +1 -1
- package/dist/cli/ui/components/common/Input.d.ts +7 -2
- package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
- package/dist/cli/ui/components/common/Input.js +12 -4
- package/dist/cli/ui/components/common/Input.js.map +1 -1
- package/dist/cli/ui/components/common/LoadingIndicator.d.ts +1 -1
- package/dist/cli/ui/components/common/LoadingIndicator.d.ts.map +1 -1
- package/dist/cli/ui/components/common/LoadingIndicator.js +4 -4
- package/dist/cli/ui/components/common/LoadingIndicator.js.map +1 -1
- package/dist/cli/ui/components/common/Select.d.ts +1 -1
- package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
- package/dist/cli/ui/components/common/Select.js +11 -12
- package/dist/cli/ui/components/common/Select.js.map +1 -1
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +2 -2
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +11 -11
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts +1 -1
- package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchCreatorScreen.js +39 -36
- package/dist/cli/ui/components/screens/BranchCreatorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +8 -4
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +122 -48
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -2
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +25 -25
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts +2 -2
- package/dist/cli/ui/components/screens/PRCleanupScreen.js +21 -21
- package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +1 -1
- package/dist/cli/ui/components/screens/SessionSelectorScreen.js +8 -8
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +1 -1
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +8 -8
- package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/BranchActionSelectorScreen.js +7 -4
- package/dist/cli/ui/screens/BranchActionSelectorScreen.js.map +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/client/assets/{index-V6hDu9KS.js → index-Difv1Hwu.js} +2 -2
- package/dist/client/index.html +1 -1
- package/dist/config/builtin-tools.d.ts +10 -2
- package/dist/config/builtin-tools.d.ts.map +1 -1
- package/dist/config/builtin-tools.js +40 -4
- package/dist/config/builtin-tools.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +4 -3
- package/dist/config/tools.js.map +1 -1
- package/dist/gemini.d.ts +12 -0
- package/dist/gemini.d.ts.map +1 -0
- package/dist/gemini.js +154 -0
- package/dist/gemini.js.map +1 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -1
- package/dist/qwen.d.ts +12 -0
- package/dist/qwen.d.ts.map +1 -0
- package/dist/qwen.js +154 -0
- package/dist/qwen.js.map +1 -0
- package/dist/services/git.service.d.ts.map +1 -1
- package/dist/services/git.service.js.map +1 -1
- package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
- package/dist/web/client/src/components/BranchGraph.js +1 -1
- package/dist/web/client/src/components/BranchGraph.js.map +1 -1
- package/dist/web/client/src/components/EnvEditor.d.ts.map +1 -1
- package/dist/web/client/src/components/EnvEditor.js +7 -4
- package/dist/web/client/src/components/EnvEditor.js.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +55 -18
- package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
- package/dist/web/client/src/pages/BranchListPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchListPage.js +10 -4
- package/dist/web/client/src/pages/BranchListPage.js.map +1 -1
- package/dist/web/client/src/pages/ConfigManagementPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/ConfigManagementPage.js +4 -2
- package/dist/web/client/src/pages/ConfigManagementPage.js.map +1 -1
- package/package.json +2 -1
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +69 -50
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -45
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +117 -75
- package/src/cli/ui/__tests__/components/App.test.tsx +45 -37
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +35 -22
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +22 -22
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +29 -22
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +40 -34
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +57 -66
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +121 -91
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +18 -16
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +13 -13
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +20 -20
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +38 -26
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +31 -31
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +73 -37
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +496 -75
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +38 -32
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +39 -39
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +49 -21
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +52 -28
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -48
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +111 -83
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +111 -108
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +50 -37
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +75 -76
- package/src/cli/ui/components/App.tsx +247 -150
- package/src/cli/ui/components/common/Confirm.tsx +13 -9
- package/src/cli/ui/components/common/ErrorBoundary.tsx +8 -5
- package/src/cli/ui/components/common/Input.tsx +26 -4
- package/src/cli/ui/components/common/LoadingIndicator.tsx +8 -5
- package/src/cli/ui/components/common/Select.tsx +28 -17
- package/src/cli/ui/components/parts/Header.test.tsx +5 -15
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +19 -13
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +74 -54
- package/src/cli/ui/components/screens/BranchListScreen.tsx +187 -62
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +35 -28
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +22 -22
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +8 -8
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +8 -8
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +9 -4
- package/src/cli/ui/types.ts +8 -1
- package/src/config/builtin-tools.ts +42 -4
- package/src/config/index.ts +2 -12
- package/src/config/tools.ts +16 -6
- package/src/gemini.ts +202 -0
- package/src/git.ts +2 -1
- package/src/index.ts +30 -0
- package/src/qwen.ts +208 -0
- package/src/services/git.service.ts +2 -1
- package/src/web/client/src/components/BranchGraph.tsx +3 -2
- package/src/web/client/src/components/EnvEditor.tsx +44 -11
- package/src/web/client/src/pages/BranchDetailPage.tsx +165 -54
- package/src/web/client/src/pages/BranchListPage.tsx +37 -13
- package/src/web/client/src/pages/ConfigManagementPage.tsx +28 -9
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @vitest-environment happy-dom
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from
|
|
5
|
-
import { act, render } from
|
|
6
|
-
import { render as inkRender } from
|
|
7
|
-
import React from
|
|
8
|
-
import { BranchListScreen } from
|
|
9
|
-
import type { BranchInfo, BranchItem, Statistics } from
|
|
10
|
-
import { formatBranchItem } from
|
|
11
|
-
import stringWidth from
|
|
12
|
-
import { Window } from
|
|
13
|
-
|
|
14
|
-
const stripAnsi = (value: string): string =>
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
5
|
+
import { act, render } from "@testing-library/react";
|
|
6
|
+
import { render as inkRender } from "ink-testing-library";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { BranchListScreen } from "../../../components/screens/BranchListScreen.js";
|
|
9
|
+
import type { BranchInfo, BranchItem, Statistics } from "../../../types.js";
|
|
10
|
+
import { formatBranchItem } from "../../../utils/branchFormatter.js";
|
|
11
|
+
import stringWidth from "string-width";
|
|
12
|
+
import { Window } from "happy-dom";
|
|
13
|
+
|
|
14
|
+
const stripAnsi = (value: string): string =>
|
|
15
|
+
value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
15
16
|
const stripControlSequences = (value: string): string =>
|
|
16
17
|
value.replace(/\u001b\[([0-9;?]*)([A-Za-z])/g, (_, params, command) => {
|
|
17
|
-
if (command ===
|
|
18
|
-
const count = Number(params ||
|
|
19
|
-
return
|
|
18
|
+
if (command === "C") {
|
|
19
|
+
const count = Number(params || "1");
|
|
20
|
+
return " ".repeat(Number.isNaN(count) ? 0 : count);
|
|
20
21
|
}
|
|
21
|
-
return
|
|
22
|
+
return "";
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
describe(
|
|
25
|
+
describe("BranchListScreen", () => {
|
|
25
26
|
beforeEach(() => {
|
|
26
27
|
vi.useFakeTimers();
|
|
27
28
|
// Setup happy-dom
|
|
@@ -36,25 +37,25 @@ describe('BranchListScreen', () => {
|
|
|
36
37
|
|
|
37
38
|
const mockBranches: BranchItem[] = [
|
|
38
39
|
{
|
|
39
|
-
name:
|
|
40
|
-
type:
|
|
41
|
-
branchType:
|
|
40
|
+
name: "main",
|
|
41
|
+
type: "local",
|
|
42
|
+
branchType: "main",
|
|
42
43
|
isCurrent: true,
|
|
43
|
-
icons: [
|
|
44
|
+
icons: ["⚡", "⭐"],
|
|
44
45
|
hasChanges: false,
|
|
45
|
-
label:
|
|
46
|
-
value:
|
|
46
|
+
label: "⚡ ⭐ main",
|
|
47
|
+
value: "main",
|
|
47
48
|
latestCommitTimestamp: 1_700_000_000,
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
|
-
name:
|
|
51
|
-
type:
|
|
52
|
-
branchType:
|
|
51
|
+
name: "feature/test",
|
|
52
|
+
type: "local",
|
|
53
|
+
branchType: "feature",
|
|
53
54
|
isCurrent: false,
|
|
54
|
-
icons: [
|
|
55
|
+
icons: ["✨"],
|
|
55
56
|
hasChanges: false,
|
|
56
|
-
label:
|
|
57
|
-
value:
|
|
57
|
+
label: "✨ feature/test",
|
|
58
|
+
value: "feature/test",
|
|
58
59
|
latestCommitTimestamp: 1_699_000_000,
|
|
59
60
|
},
|
|
60
61
|
];
|
|
@@ -67,46 +68,62 @@ describe('BranchListScreen', () => {
|
|
|
67
68
|
lastUpdated: new Date(),
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
it(
|
|
71
|
+
it("should render header with title", () => {
|
|
71
72
|
const onSelect = vi.fn();
|
|
72
73
|
const { getByText } = render(
|
|
73
|
-
<BranchListScreen
|
|
74
|
+
<BranchListScreen
|
|
75
|
+
branches={mockBranches}
|
|
76
|
+
stats={mockStats}
|
|
77
|
+
onSelect={onSelect}
|
|
78
|
+
/>,
|
|
74
79
|
);
|
|
75
80
|
|
|
76
81
|
expect(getByText(/gwt - Branch Selection/i)).toBeDefined();
|
|
77
82
|
});
|
|
78
83
|
|
|
79
|
-
it(
|
|
84
|
+
it("should render statistics", () => {
|
|
80
85
|
const onSelect = vi.fn();
|
|
81
86
|
const { container, getByText } = render(
|
|
82
|
-
<BranchListScreen
|
|
87
|
+
<BranchListScreen
|
|
88
|
+
branches={mockBranches}
|
|
89
|
+
stats={mockStats}
|
|
90
|
+
onSelect={onSelect}
|
|
91
|
+
/>,
|
|
83
92
|
);
|
|
84
93
|
|
|
85
|
-
expect(container.textContent).toContain(
|
|
94
|
+
expect(container.textContent).toContain("Local: 2");
|
|
86
95
|
expect(getByText(/Remote:/)).toBeDefined();
|
|
87
96
|
});
|
|
88
97
|
|
|
89
|
-
it(
|
|
98
|
+
it("should render branch list", () => {
|
|
90
99
|
const onSelect = vi.fn();
|
|
91
100
|
const { getByText } = render(
|
|
92
|
-
<BranchListScreen
|
|
101
|
+
<BranchListScreen
|
|
102
|
+
branches={mockBranches}
|
|
103
|
+
stats={mockStats}
|
|
104
|
+
onSelect={onSelect}
|
|
105
|
+
/>,
|
|
93
106
|
);
|
|
94
107
|
|
|
95
108
|
expect(getByText(/main/)).toBeDefined();
|
|
96
109
|
expect(getByText(/feature\/test/)).toBeDefined();
|
|
97
110
|
});
|
|
98
111
|
|
|
99
|
-
it(
|
|
112
|
+
it("should render footer with actions", () => {
|
|
100
113
|
const onSelect = vi.fn();
|
|
101
114
|
const { getAllByText } = render(
|
|
102
|
-
<BranchListScreen
|
|
115
|
+
<BranchListScreen
|
|
116
|
+
branches={mockBranches}
|
|
117
|
+
stats={mockStats}
|
|
118
|
+
onSelect={onSelect}
|
|
119
|
+
/>,
|
|
103
120
|
);
|
|
104
121
|
|
|
105
122
|
// Check for enter key (main screen doesn't have q key, exit is Ctrl+C only)
|
|
106
123
|
expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
|
|
107
124
|
});
|
|
108
125
|
|
|
109
|
-
it(
|
|
126
|
+
it("should handle empty branch list", () => {
|
|
110
127
|
const onSelect = vi.fn();
|
|
111
128
|
const emptyStats: Statistics = {
|
|
112
129
|
localCount: 0,
|
|
@@ -117,13 +134,13 @@ describe('BranchListScreen', () => {
|
|
|
117
134
|
};
|
|
118
135
|
|
|
119
136
|
const { container } = render(
|
|
120
|
-
<BranchListScreen branches={[]} stats={emptyStats} onSelect={onSelect}
|
|
137
|
+
<BranchListScreen branches={[]} stats={emptyStats} onSelect={onSelect} />,
|
|
121
138
|
);
|
|
122
139
|
|
|
123
140
|
expect(container).toBeDefined();
|
|
124
141
|
});
|
|
125
142
|
|
|
126
|
-
it(
|
|
143
|
+
it("should display loading indicator after the configured delay", async () => {
|
|
127
144
|
const onSelect = vi.fn();
|
|
128
145
|
const { queryByText, getByText } = render(
|
|
129
146
|
<BranchListScreen
|
|
@@ -132,11 +149,11 @@ describe('BranchListScreen', () => {
|
|
|
132
149
|
onSelect={onSelect}
|
|
133
150
|
loading={true}
|
|
134
151
|
loadingIndicatorDelay={10}
|
|
135
|
-
|
|
152
|
+
/>,
|
|
136
153
|
);
|
|
137
154
|
|
|
138
155
|
await act(async () => {
|
|
139
|
-
if (typeof (vi as any).advanceTimersByTime ===
|
|
156
|
+
if (typeof (vi as any).advanceTimersByTime === "function") {
|
|
140
157
|
(vi as any).advanceTimersByTime(10);
|
|
141
158
|
} else {
|
|
142
159
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
@@ -146,18 +163,23 @@ describe('BranchListScreen', () => {
|
|
|
146
163
|
expect(getByText(/Loading Git information/i)).toBeDefined();
|
|
147
164
|
});
|
|
148
165
|
|
|
149
|
-
it(
|
|
166
|
+
it("should display error state", () => {
|
|
150
167
|
const onSelect = vi.fn();
|
|
151
|
-
const error = new Error(
|
|
168
|
+
const error = new Error("Failed to load branches");
|
|
152
169
|
const { getByText } = render(
|
|
153
|
-
<BranchListScreen
|
|
170
|
+
<BranchListScreen
|
|
171
|
+
branches={[]}
|
|
172
|
+
stats={mockStats}
|
|
173
|
+
onSelect={onSelect}
|
|
174
|
+
error={error}
|
|
175
|
+
/>,
|
|
154
176
|
);
|
|
155
177
|
|
|
156
178
|
expect(getByText(/Error:/i)).toBeDefined();
|
|
157
179
|
expect(getByText(/Failed to load branches/i)).toBeDefined();
|
|
158
180
|
});
|
|
159
181
|
|
|
160
|
-
it(
|
|
182
|
+
it("should use terminal height for layout calculation", () => {
|
|
161
183
|
const onSelect = vi.fn();
|
|
162
184
|
|
|
163
185
|
// Mock process.stdout
|
|
@@ -165,7 +187,11 @@ describe('BranchListScreen', () => {
|
|
|
165
187
|
process.stdout.rows = 30;
|
|
166
188
|
|
|
167
189
|
const { container } = render(
|
|
168
|
-
<BranchListScreen
|
|
190
|
+
<BranchListScreen
|
|
191
|
+
branches={mockBranches}
|
|
192
|
+
stats={mockStats}
|
|
193
|
+
onSelect={onSelect}
|
|
194
|
+
/>,
|
|
169
195
|
);
|
|
170
196
|
|
|
171
197
|
expect(container).toBeDefined();
|
|
@@ -174,10 +200,14 @@ describe('BranchListScreen', () => {
|
|
|
174
200
|
process.stdout.rows = originalRows;
|
|
175
201
|
});
|
|
176
202
|
|
|
177
|
-
it(
|
|
203
|
+
it("should display branch icons", () => {
|
|
178
204
|
const onSelect = vi.fn();
|
|
179
205
|
const { getByText } = render(
|
|
180
|
-
<BranchListScreen
|
|
206
|
+
<BranchListScreen
|
|
207
|
+
branches={mockBranches}
|
|
208
|
+
stats={mockStats}
|
|
209
|
+
onSelect={onSelect}
|
|
210
|
+
/>,
|
|
181
211
|
);
|
|
182
212
|
|
|
183
213
|
// Check for icons in labels
|
|
@@ -186,34 +216,42 @@ describe('BranchListScreen', () => {
|
|
|
186
216
|
expect(getByText(/✨/)).toBeDefined(); // feature icon
|
|
187
217
|
});
|
|
188
218
|
|
|
189
|
-
it(
|
|
219
|
+
it("should render latest commit timestamp for each branch", () => {
|
|
190
220
|
const onSelect = vi.fn();
|
|
191
221
|
const { container } = render(
|
|
192
|
-
<BranchListScreen
|
|
222
|
+
<BranchListScreen
|
|
223
|
+
branches={mockBranches}
|
|
224
|
+
stats={mockStats}
|
|
225
|
+
onSelect={onSelect}
|
|
226
|
+
/>,
|
|
193
227
|
);
|
|
194
228
|
|
|
195
|
-
const textContent = container.textContent ??
|
|
229
|
+
const textContent = container.textContent ?? "";
|
|
196
230
|
const matches = textContent.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/g) ?? [];
|
|
197
231
|
expect(matches.length).toBe(mockBranches.length);
|
|
198
232
|
});
|
|
199
233
|
|
|
200
|
-
it(
|
|
201
|
-
process.env.FORCE_COLOR =
|
|
234
|
+
it("should highlight the selected branch with cyan background", async () => {
|
|
235
|
+
process.env.FORCE_COLOR = "1";
|
|
202
236
|
const onSelect = vi.fn();
|
|
203
237
|
let renderResult: ReturnType<typeof inkRender>;
|
|
204
238
|
await act(async () => {
|
|
205
239
|
renderResult = inkRender(
|
|
206
|
-
<BranchListScreen
|
|
207
|
-
|
|
240
|
+
<BranchListScreen
|
|
241
|
+
branches={mockBranches}
|
|
242
|
+
stats={mockStats}
|
|
243
|
+
onSelect={onSelect}
|
|
244
|
+
/>,
|
|
245
|
+
{ stripAnsi: false },
|
|
208
246
|
);
|
|
209
247
|
});
|
|
210
248
|
|
|
211
|
-
const frame = renderResult!.lastFrame() ??
|
|
212
|
-
expect(frame).toContain(
|
|
249
|
+
const frame = renderResult!.lastFrame() ?? "";
|
|
250
|
+
expect(frame).toContain("\u001b[46m"); // cyan background ANSI code
|
|
213
251
|
});
|
|
214
252
|
|
|
215
|
-
it(
|
|
216
|
-
process.env.FORCE_COLOR =
|
|
253
|
+
it("should align timestamps even when unpushed icon is displayed", async () => {
|
|
254
|
+
process.env.FORCE_COLOR = "1";
|
|
217
255
|
const onSelect = vi.fn();
|
|
218
256
|
|
|
219
257
|
const originalColumns = process.stdout.columns;
|
|
@@ -221,25 +259,25 @@ describe('BranchListScreen', () => {
|
|
|
221
259
|
|
|
222
260
|
const branchInfos: BranchInfo[] = [
|
|
223
261
|
{
|
|
224
|
-
name:
|
|
225
|
-
type:
|
|
226
|
-
branchType:
|
|
262
|
+
name: "feature/update-ui",
|
|
263
|
+
type: "local",
|
|
264
|
+
branchType: "feature",
|
|
227
265
|
isCurrent: false,
|
|
228
266
|
hasUnpushedCommits: true,
|
|
229
267
|
latestCommitTimestamp: 1_700_000_000,
|
|
230
268
|
},
|
|
231
269
|
{
|
|
232
|
-
name:
|
|
233
|
-
type:
|
|
234
|
-
branchType:
|
|
270
|
+
name: "origin/main",
|
|
271
|
+
type: "remote",
|
|
272
|
+
branchType: "main",
|
|
235
273
|
isCurrent: false,
|
|
236
274
|
hasUnpushedCommits: false,
|
|
237
275
|
latestCommitTimestamp: 1_699_999_000,
|
|
238
276
|
},
|
|
239
277
|
{
|
|
240
|
-
name:
|
|
241
|
-
type:
|
|
242
|
-
branchType:
|
|
278
|
+
name: "main",
|
|
279
|
+
type: "local",
|
|
280
|
+
branchType: "main",
|
|
243
281
|
isCurrent: true,
|
|
244
282
|
hasUnpushedCommits: false,
|
|
245
283
|
latestCommitTimestamp: 1_699_998_000,
|
|
@@ -247,21 +285,25 @@ describe('BranchListScreen', () => {
|
|
|
247
285
|
];
|
|
248
286
|
|
|
249
287
|
const branchesWithUnpushed: BranchItem[] = branchInfos.map((branch) =>
|
|
250
|
-
formatBranchItem(branch)
|
|
288
|
+
formatBranchItem(branch),
|
|
251
289
|
);
|
|
252
290
|
|
|
253
291
|
try {
|
|
254
292
|
let renderResult: ReturnType<typeof inkRender>;
|
|
255
293
|
await act(async () => {
|
|
256
294
|
renderResult = inkRender(
|
|
257
|
-
<BranchListScreen
|
|
258
|
-
|
|
295
|
+
<BranchListScreen
|
|
296
|
+
branches={branchesWithUnpushed}
|
|
297
|
+
stats={mockStats}
|
|
298
|
+
onSelect={onSelect}
|
|
299
|
+
/>,
|
|
300
|
+
{ stripAnsi: false },
|
|
259
301
|
);
|
|
260
302
|
});
|
|
261
303
|
|
|
262
|
-
const frame = renderResult!.lastFrame() ??
|
|
304
|
+
const frame = renderResult!.lastFrame() ?? "";
|
|
263
305
|
const timestampLines = frame
|
|
264
|
-
.split(
|
|
306
|
+
.split("\n")
|
|
265
307
|
.map((line) => stripControlSequences(stripAnsi(line)))
|
|
266
308
|
.filter((line) => /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(line));
|
|
267
309
|
|
|
@@ -274,7 +316,7 @@ describe('BranchListScreen', () => {
|
|
|
274
316
|
|
|
275
317
|
let width = 0;
|
|
276
318
|
for (const char of Array.from(beforeTimestamp)) {
|
|
277
|
-
if (char ===
|
|
319
|
+
if (char === "\u2B06" || char === "\u2601") {
|
|
278
320
|
width += 1;
|
|
279
321
|
continue;
|
|
280
322
|
}
|
|
@@ -290,4 +332,383 @@ describe('BranchListScreen', () => {
|
|
|
290
332
|
process.stdout.columns = originalColumns;
|
|
291
333
|
}
|
|
292
334
|
});
|
|
335
|
+
|
|
336
|
+
describe("Filter Mode", () => {
|
|
337
|
+
it("should always display filter input field", () => {
|
|
338
|
+
// Note: Filter input is now always visible (no need to press 'f' key)
|
|
339
|
+
const onSelect = vi.fn();
|
|
340
|
+
const { container } = render(
|
|
341
|
+
<BranchListScreen
|
|
342
|
+
branches={mockBranches}
|
|
343
|
+
stats={mockStats}
|
|
344
|
+
onSelect={onSelect}
|
|
345
|
+
/>,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Filter input field should be displayed by default
|
|
349
|
+
expect(container.textContent).toContain("Filter:");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should enter filter mode when f key is pressed", () => {
|
|
353
|
+
const onSelect = vi.fn();
|
|
354
|
+
const { container } = render(
|
|
355
|
+
<BranchListScreen
|
|
356
|
+
branches={mockBranches}
|
|
357
|
+
stats={mockStats}
|
|
358
|
+
onSelect={onSelect}
|
|
359
|
+
/>,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Initially should show prompt to press f
|
|
363
|
+
expect(container.textContent).toContain("(press f to filter)");
|
|
364
|
+
|
|
365
|
+
// Press 'f' key
|
|
366
|
+
const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
367
|
+
"keydown",
|
|
368
|
+
{ key: "f" },
|
|
369
|
+
);
|
|
370
|
+
document.dispatchEvent(fKeyEvent);
|
|
371
|
+
|
|
372
|
+
// Filter input should be active (placeholder visible)
|
|
373
|
+
// Select component should be disabled
|
|
374
|
+
expect(container).toBeDefined();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should exit filter mode and return to branch selection when Esc is pressed in filter mode", () => {
|
|
378
|
+
const onSelect = vi.fn();
|
|
379
|
+
const { container } = render(
|
|
380
|
+
<BranchListScreen
|
|
381
|
+
branches={mockBranches}
|
|
382
|
+
stats={mockStats}
|
|
383
|
+
onSelect={onSelect}
|
|
384
|
+
/>,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Enter filter mode first
|
|
388
|
+
const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
389
|
+
"keydown",
|
|
390
|
+
{ key: "f" },
|
|
391
|
+
);
|
|
392
|
+
document.dispatchEvent(fKeyEvent);
|
|
393
|
+
|
|
394
|
+
// Press Escape
|
|
395
|
+
const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
396
|
+
"keydown",
|
|
397
|
+
{ key: "Escape" },
|
|
398
|
+
);
|
|
399
|
+
document.dispatchEvent(escKeyEvent);
|
|
400
|
+
|
|
401
|
+
// Should return to branch selection mode
|
|
402
|
+
// Select should be active, Input should be inactive
|
|
403
|
+
expect(container.textContent).toContain("(press f to filter)");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should show branch list cursor highlight in filter mode", () => {
|
|
407
|
+
process.env.FORCE_COLOR = "1";
|
|
408
|
+
const onSelect = vi.fn();
|
|
409
|
+
let renderResult: ReturnType<typeof inkRender>;
|
|
410
|
+
act(() => {
|
|
411
|
+
renderResult = inkRender(
|
|
412
|
+
<BranchListScreen
|
|
413
|
+
branches={mockBranches}
|
|
414
|
+
stats={mockStats}
|
|
415
|
+
onSelect={onSelect}
|
|
416
|
+
testFilterMode={true}
|
|
417
|
+
/>,
|
|
418
|
+
{ stripAnsi: false },
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const frame = renderResult!.lastFrame() ?? "";
|
|
423
|
+
// Should contain cyan background (cursor highlight) even in filter mode
|
|
424
|
+
expect(frame).toContain("\u001b[46m");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should allow cursor movement with arrow keys in filter mode", () => {
|
|
428
|
+
const onSelect = vi.fn();
|
|
429
|
+
const { container } = render(
|
|
430
|
+
<BranchListScreen
|
|
431
|
+
branches={mockBranches}
|
|
432
|
+
stats={mockStats}
|
|
433
|
+
onSelect={onSelect}
|
|
434
|
+
testFilterMode={true}
|
|
435
|
+
/>,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Arrow keys should work in filter mode (Select component should not be disabled)
|
|
439
|
+
// This test verifies that cursor movement is possible
|
|
440
|
+
expect(container).toBeDefined();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("should allow branch selection with Enter key in filter mode", () => {
|
|
444
|
+
const onSelect = vi.fn();
|
|
445
|
+
const { container } = render(
|
|
446
|
+
<BranchListScreen
|
|
447
|
+
branches={mockBranches}
|
|
448
|
+
stats={mockStats}
|
|
449
|
+
onSelect={onSelect}
|
|
450
|
+
testFilterMode={true}
|
|
451
|
+
/>,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Simulate Enter key (this will trigger onSelect if Select is enabled)
|
|
455
|
+
// Note: Actual key event testing may not work in happy-dom environment
|
|
456
|
+
// but the component should be set up to allow selection
|
|
457
|
+
expect(container).toBeDefined();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should disable filter input cursor when in branch selection mode", () => {
|
|
461
|
+
const onSelect = vi.fn();
|
|
462
|
+
const { container } = render(
|
|
463
|
+
<BranchListScreen
|
|
464
|
+
branches={mockBranches}
|
|
465
|
+
stats={mockStats}
|
|
466
|
+
onSelect={onSelect}
|
|
467
|
+
/>,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// By default, should be in branch selection mode
|
|
471
|
+
// Filter input cursor should be disabled/hidden
|
|
472
|
+
expect(container).toBeDefined();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("should filter branches in real-time as user types", () => {
|
|
476
|
+
const onSelect = vi.fn();
|
|
477
|
+
const branches: BranchItem[] = [
|
|
478
|
+
...mockBranches,
|
|
479
|
+
{
|
|
480
|
+
name: "bugfix/issue-123",
|
|
481
|
+
type: "local",
|
|
482
|
+
branchType: "bugfix",
|
|
483
|
+
isCurrent: false,
|
|
484
|
+
icons: ["🐛"],
|
|
485
|
+
hasChanges: false,
|
|
486
|
+
label: "🐛 bugfix/issue-123",
|
|
487
|
+
value: "bugfix/issue-123",
|
|
488
|
+
latestCommitTimestamp: 1_698_000_000,
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
const { container } = render(
|
|
493
|
+
<BranchListScreen
|
|
494
|
+
branches={branches}
|
|
495
|
+
stats={mockStats}
|
|
496
|
+
onSelect={onSelect}
|
|
497
|
+
testFilterMode={true}
|
|
498
|
+
testFilterQuery="feature"
|
|
499
|
+
/>,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// Only feature/test should be visible
|
|
503
|
+
expect(container.textContent).toContain("feature/test");
|
|
504
|
+
expect(container.textContent).not.toContain("bugfix/issue-123");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should clear filter query when Esc key is pressed (with query)", () => {
|
|
508
|
+
// Note: Filter input remains visible, only the query is cleared
|
|
509
|
+
const onSelect = vi.fn();
|
|
510
|
+
const { container } = render(
|
|
511
|
+
<BranchListScreen
|
|
512
|
+
branches={mockBranches}
|
|
513
|
+
stats={mockStats}
|
|
514
|
+
onSelect={onSelect}
|
|
515
|
+
/>,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// Enter filter mode
|
|
519
|
+
const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
520
|
+
"keydown",
|
|
521
|
+
{ key: "f" },
|
|
522
|
+
);
|
|
523
|
+
document.dispatchEvent(fKeyEvent);
|
|
524
|
+
|
|
525
|
+
// Type something in filter
|
|
526
|
+
const input = container.querySelector("input");
|
|
527
|
+
if (input) {
|
|
528
|
+
input.value = "feature";
|
|
529
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Press Escape (should clear query first)
|
|
533
|
+
const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
534
|
+
"keydown",
|
|
535
|
+
{ key: "Escape" },
|
|
536
|
+
);
|
|
537
|
+
document.dispatchEvent(escKeyEvent);
|
|
538
|
+
|
|
539
|
+
// Filter input should still be visible, but query cleared
|
|
540
|
+
// All branches should be visible again
|
|
541
|
+
expect(container.textContent).toContain("Filter:");
|
|
542
|
+
expect(container.textContent).toContain("main");
|
|
543
|
+
expect(container.textContent).toContain("feature/test");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("should exit filter mode when Esc is pressed with empty query", () => {
|
|
547
|
+
const onSelect = vi.fn();
|
|
548
|
+
const { container } = render(
|
|
549
|
+
<BranchListScreen
|
|
550
|
+
branches={mockBranches}
|
|
551
|
+
stats={mockStats}
|
|
552
|
+
onSelect={onSelect}
|
|
553
|
+
/>,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// Enter filter mode
|
|
557
|
+
const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
558
|
+
"keydown",
|
|
559
|
+
{ key: "f" },
|
|
560
|
+
);
|
|
561
|
+
document.dispatchEvent(fKeyEvent);
|
|
562
|
+
|
|
563
|
+
// Press Escape with empty query (should exit filter mode)
|
|
564
|
+
const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
565
|
+
"keydown",
|
|
566
|
+
{ key: "Escape" },
|
|
567
|
+
);
|
|
568
|
+
document.dispatchEvent(escKeyEvent);
|
|
569
|
+
|
|
570
|
+
// Should return to branch selection mode
|
|
571
|
+
expect(container.textContent).toContain("(press f to filter)");
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("should perform case-insensitive search", () => {
|
|
575
|
+
const onSelect = vi.fn();
|
|
576
|
+
const { container } = render(
|
|
577
|
+
<BranchListScreen
|
|
578
|
+
branches={mockBranches}
|
|
579
|
+
stats={mockStats}
|
|
580
|
+
onSelect={onSelect}
|
|
581
|
+
testFilterMode={true}
|
|
582
|
+
testFilterQuery="FEATURE"
|
|
583
|
+
/>,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// "feature/test" should still be visible
|
|
587
|
+
expect(container.textContent).toContain("feature/test");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("should disable other key bindings (m, c, r) while typing in filter", () => {
|
|
591
|
+
// Note: Input component uses blockKeys prop to prevent c/r/m/f from
|
|
592
|
+
// triggering shortcuts while typing in the filter field
|
|
593
|
+
// This test verifies the intended behavior (though KeyboardEvent
|
|
594
|
+
// may not work correctly in test environment)
|
|
595
|
+
const onSelect = vi.fn();
|
|
596
|
+
const onNavigate = vi.fn();
|
|
597
|
+
const onCleanupCommand = vi.fn();
|
|
598
|
+
const onRefresh = vi.fn();
|
|
599
|
+
|
|
600
|
+
const { container } = render(
|
|
601
|
+
<BranchListScreen
|
|
602
|
+
branches={mockBranches}
|
|
603
|
+
stats={mockStats}
|
|
604
|
+
onSelect={onSelect}
|
|
605
|
+
onNavigate={onNavigate}
|
|
606
|
+
onCleanupCommand={onCleanupCommand}
|
|
607
|
+
onRefresh={onRefresh}
|
|
608
|
+
/>,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// Enter filter mode
|
|
612
|
+
const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
613
|
+
"keydown",
|
|
614
|
+
{ key: "f" },
|
|
615
|
+
);
|
|
616
|
+
document.dispatchEvent(fKeyEvent);
|
|
617
|
+
|
|
618
|
+
// When user types in filter, Input component blocks c/r/m/f keys
|
|
619
|
+
// Press m, c, r keys (should be blocked by Input's blockKeys)
|
|
620
|
+
["m", "c", "r"].forEach((key) => {
|
|
621
|
+
const keyEvent = new (globalThis.window as any).KeyboardEvent(
|
|
622
|
+
"keydown",
|
|
623
|
+
{ key },
|
|
624
|
+
);
|
|
625
|
+
document.dispatchEvent(keyEvent);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// None of the callbacks should be called (keys are blocked)
|
|
629
|
+
// Note: This may fail in test environment due to KeyboardEvent limitations
|
|
630
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
631
|
+
expect(onCleanupCommand).not.toHaveBeenCalled();
|
|
632
|
+
expect(onRefresh).not.toHaveBeenCalled();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("should display match count when filtering", () => {
|
|
636
|
+
const onSelect = vi.fn();
|
|
637
|
+
const branches: BranchItem[] = [
|
|
638
|
+
...mockBranches,
|
|
639
|
+
{
|
|
640
|
+
name: "feature/another",
|
|
641
|
+
type: "local",
|
|
642
|
+
branchType: "feature",
|
|
643
|
+
isCurrent: false,
|
|
644
|
+
icons: ["✨"],
|
|
645
|
+
hasChanges: false,
|
|
646
|
+
label: "✨ feature/another",
|
|
647
|
+
value: "feature/another",
|
|
648
|
+
latestCommitTimestamp: 1_698_000_000,
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
const { container } = render(
|
|
653
|
+
<BranchListScreen
|
|
654
|
+
branches={branches}
|
|
655
|
+
stats={mockStats}
|
|
656
|
+
onSelect={onSelect}
|
|
657
|
+
testFilterMode={true}
|
|
658
|
+
testFilterQuery="feature"
|
|
659
|
+
/>,
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Should show "Showing 2 of 3 branches"
|
|
663
|
+
expect(container.textContent).toMatch(/Showing\s+2\s+of\s+3/i);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("should show empty list when no branches match", () => {
|
|
667
|
+
const onSelect = vi.fn();
|
|
668
|
+
const { container } = render(
|
|
669
|
+
<BranchListScreen
|
|
670
|
+
branches={mockBranches}
|
|
671
|
+
stats={mockStats}
|
|
672
|
+
onSelect={onSelect}
|
|
673
|
+
testFilterMode={true}
|
|
674
|
+
testFilterQuery="nonexistent"
|
|
675
|
+
/>,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// Should show "Showing 0 of 2 branches"
|
|
679
|
+
expect(container.textContent).toMatch(/Showing\s+0\s+of\s+2/i);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("should search in PR titles when available", () => {
|
|
683
|
+
const onSelect = vi.fn();
|
|
684
|
+
const branchesWithPR: BranchItem[] = [
|
|
685
|
+
...mockBranches,
|
|
686
|
+
{
|
|
687
|
+
name: "feature/add-filter",
|
|
688
|
+
type: "local",
|
|
689
|
+
branchType: "feature",
|
|
690
|
+
isCurrent: false,
|
|
691
|
+
icons: ["✨", "🔀"],
|
|
692
|
+
hasChanges: false,
|
|
693
|
+
label: "✨ 🔀 feature/add-filter",
|
|
694
|
+
value: "feature/add-filter",
|
|
695
|
+
latestCommitTimestamp: 1_698_000_000,
|
|
696
|
+
openPR: { number: 123, title: "Add search filter to branch list" },
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
|
|
700
|
+
const { container } = render(
|
|
701
|
+
<BranchListScreen
|
|
702
|
+
branches={branchesWithPR}
|
|
703
|
+
stats={mockStats}
|
|
704
|
+
onSelect={onSelect}
|
|
705
|
+
testFilterMode={true}
|
|
706
|
+
testFilterQuery="search"
|
|
707
|
+
/>,
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Branch with matching PR title should be visible
|
|
711
|
+
expect(container.textContent).toContain("feature/add-filter");
|
|
712
|
+
});
|
|
713
|
+
});
|
|
293
714
|
});
|