@akiojin/gwt 2.2.0 → 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 +2 -2
- package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
- package/dist/cli/ui/components/common/Input.js +4 -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 +3 -3
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +55 -50
- 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 +261 -153
- 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 +12 -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 +92 -75
- 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,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @vitest-environment happy-dom
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
5
|
-
import { render } from
|
|
6
|
-
import React, { useState } from
|
|
7
|
-
import { Window } from
|
|
8
|
-
import { Select, type SelectItem } from
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
import React, { useState } from "react";
|
|
7
|
+
import { Window } from "happy-dom";
|
|
8
|
+
import { Select, type SelectItem } from "../../../components/common/Select.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* T082-2: React.memo optimization tests
|
|
@@ -16,7 +16,7 @@ import { Select, type SelectItem } from '../../../components/common/Select.js';
|
|
|
16
16
|
* The actual functionality works correctly in production.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
describe.skip(
|
|
19
|
+
describe.skip("Select Component React.memo (T082-2)", () => {
|
|
20
20
|
beforeEach(() => {
|
|
21
21
|
const window = new Window();
|
|
22
22
|
globalThis.window = window as any;
|
|
@@ -24,15 +24,15 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
24
24
|
vi.clearAllMocks();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it(
|
|
27
|
+
it("should not re-render when items array reference changes but content is the same", () => {
|
|
28
28
|
const onSelect = vi.fn();
|
|
29
29
|
const renderCount = 0;
|
|
30
30
|
|
|
31
31
|
// Wrapper component to track renders
|
|
32
32
|
function TestWrapper() {
|
|
33
33
|
const [items, setItems] = useState<SelectItem[]>([
|
|
34
|
-
{ label:
|
|
35
|
-
{ label:
|
|
34
|
+
{ label: "Item 1", value: "1" },
|
|
35
|
+
{ label: "Item 2", value: "2" },
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
const [counter, setCounter] = useState(0);
|
|
@@ -40,23 +40,20 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
40
40
|
return (
|
|
41
41
|
<div>
|
|
42
42
|
<div data-testid="counter">{counter}</div>
|
|
43
|
-
<Select
|
|
44
|
-
items={items}
|
|
45
|
-
onSelect={onSelect}
|
|
46
|
-
/>
|
|
43
|
+
<Select items={items} onSelect={onSelect} />
|
|
47
44
|
<button
|
|
48
45
|
data-testid="same-content"
|
|
49
46
|
onClick={() => {
|
|
50
47
|
// Create new array with same content
|
|
51
48
|
setItems([
|
|
52
|
-
{ label:
|
|
53
|
-
{ label:
|
|
49
|
+
{ label: "Item 1", value: "1" },
|
|
50
|
+
{ label: "Item 2", value: "2" },
|
|
54
51
|
]);
|
|
55
52
|
}}
|
|
56
53
|
/>
|
|
57
54
|
<button
|
|
58
55
|
data-testid="increment"
|
|
59
|
-
onClick={() => setCounter(c => c + 1)}
|
|
56
|
+
onClick={() => setCounter((c) => c + 1)}
|
|
60
57
|
/>
|
|
61
58
|
</div>
|
|
62
59
|
);
|
|
@@ -65,19 +62,19 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
65
62
|
const { getByTestId } = render(<TestWrapper />);
|
|
66
63
|
|
|
67
64
|
// Click "same-content" button to trigger re-render with same items
|
|
68
|
-
const sameContentButton = getByTestId(
|
|
65
|
+
const sameContentButton = getByTestId("same-content") as HTMLButtonElement;
|
|
69
66
|
sameContentButton.click();
|
|
70
67
|
|
|
71
68
|
// With React.memo, Select should not re-render if items content is the same
|
|
72
69
|
// Without React.memo, this test would show that Select re-renders unnecessarily
|
|
73
70
|
});
|
|
74
71
|
|
|
75
|
-
it(
|
|
72
|
+
it("should re-render when items content actually changes", () => {
|
|
76
73
|
const onSelect = vi.fn();
|
|
77
74
|
|
|
78
75
|
function TestWrapper() {
|
|
79
76
|
const [items, setItems] = useState<SelectItem[]>([
|
|
80
|
-
{ label:
|
|
77
|
+
{ label: "Item 1", value: "1" },
|
|
81
78
|
]);
|
|
82
79
|
|
|
83
80
|
return (
|
|
@@ -86,10 +83,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
86
83
|
<button
|
|
87
84
|
data-testid="add-item"
|
|
88
85
|
onClick={() => {
|
|
89
|
-
setItems([
|
|
90
|
-
...items,
|
|
91
|
-
{ label: 'Item 2', value: '2' },
|
|
92
|
-
]);
|
|
86
|
+
setItems([...items, { label: "Item 2", value: "2" }]);
|
|
93
87
|
}}
|
|
94
88
|
/>
|
|
95
89
|
</div>
|
|
@@ -99,35 +93,40 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
99
93
|
const { getByTestId, container } = render(<TestWrapper />);
|
|
100
94
|
|
|
101
95
|
// Initially should have 1 item
|
|
102
|
-
expect(container.textContent).toContain(
|
|
103
|
-
expect(container.textContent).not.toContain(
|
|
96
|
+
expect(container.textContent).toContain("Item 1");
|
|
97
|
+
expect(container.textContent).not.toContain("Item 2");
|
|
104
98
|
|
|
105
99
|
// Click "add-item" button
|
|
106
|
-
const addButton = getByTestId(
|
|
100
|
+
const addButton = getByTestId("add-item") as HTMLButtonElement;
|
|
107
101
|
addButton.click();
|
|
108
102
|
|
|
109
103
|
// Should now have 2 items (Select should re-render)
|
|
110
|
-
expect(container.textContent).toContain(
|
|
111
|
-
expect(container.textContent).toContain(
|
|
104
|
+
expect(container.textContent).toContain("Item 1");
|
|
105
|
+
expect(container.textContent).toContain("Item 2");
|
|
112
106
|
});
|
|
113
107
|
|
|
114
|
-
it(
|
|
108
|
+
it("should not re-render when other props are the same", () => {
|
|
115
109
|
const onSelect = vi.fn();
|
|
116
110
|
|
|
117
111
|
function TestWrapper() {
|
|
118
112
|
const [items] = useState<SelectItem[]>([
|
|
119
|
-
{ label:
|
|
120
|
-
{ label:
|
|
113
|
+
{ label: "Item 1", value: "1" },
|
|
114
|
+
{ label: "Item 2", value: "2" },
|
|
121
115
|
]);
|
|
122
116
|
const [unrelatedState, setUnrelatedState] = useState(0);
|
|
123
117
|
|
|
124
118
|
return (
|
|
125
119
|
<div>
|
|
126
120
|
<div data-testid="unrelated">{unrelatedState}</div>
|
|
127
|
-
<Select
|
|
121
|
+
<Select
|
|
122
|
+
items={items}
|
|
123
|
+
onSelect={onSelect}
|
|
124
|
+
limit={10}
|
|
125
|
+
disabled={false}
|
|
126
|
+
/>
|
|
128
127
|
<button
|
|
129
128
|
data-testid="update-unrelated"
|
|
130
|
-
onClick={() => setUnrelatedState(s => s + 1)}
|
|
129
|
+
onClick={() => setUnrelatedState((s) => s + 1)}
|
|
131
130
|
/>
|
|
132
131
|
</div>
|
|
133
132
|
);
|
|
@@ -136,22 +135,22 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
136
135
|
const { getByTestId } = render(<TestWrapper />);
|
|
137
136
|
|
|
138
137
|
// Update unrelated state
|
|
139
|
-
const updateButton = getByTestId(
|
|
138
|
+
const updateButton = getByTestId("update-unrelated") as HTMLButtonElement;
|
|
140
139
|
updateButton.click();
|
|
141
140
|
|
|
142
141
|
// Verify unrelated state changed
|
|
143
|
-
expect(getByTestId(
|
|
142
|
+
expect(getByTestId("unrelated").textContent).toBe("1");
|
|
144
143
|
|
|
145
144
|
// With React.memo, Select should not re-render because its props haven't changed
|
|
146
145
|
});
|
|
147
146
|
|
|
148
|
-
it(
|
|
147
|
+
it("should re-render when limit prop changes", () => {
|
|
149
148
|
const onSelect = vi.fn();
|
|
150
149
|
const items: SelectItem[] = [
|
|
151
|
-
{ label:
|
|
152
|
-
{ label:
|
|
153
|
-
{ label:
|
|
154
|
-
{ label:
|
|
150
|
+
{ label: "Item 1", value: "1" },
|
|
151
|
+
{ label: "Item 2", value: "2" },
|
|
152
|
+
{ label: "Item 3", value: "3" },
|
|
153
|
+
{ label: "Item 4", value: "4" },
|
|
155
154
|
];
|
|
156
155
|
|
|
157
156
|
function TestWrapper() {
|
|
@@ -160,10 +159,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
160
159
|
return (
|
|
161
160
|
<div>
|
|
162
161
|
<Select items={items} onSelect={onSelect} limit={limit} />
|
|
163
|
-
<button
|
|
164
|
-
data-testid="change-limit"
|
|
165
|
-
onClick={() => setLimit(3)}
|
|
166
|
-
/>
|
|
162
|
+
<button data-testid="change-limit" onClick={() => setLimit(3)} />
|
|
167
163
|
</div>
|
|
168
164
|
);
|
|
169
165
|
}
|
|
@@ -172,25 +168,23 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
172
168
|
|
|
173
169
|
// Initially should show 2 items (limit=2)
|
|
174
170
|
const initialText = container.textContent;
|
|
175
|
-
expect(initialText).toContain(
|
|
176
|
-
expect(initialText).toContain(
|
|
171
|
+
expect(initialText).toContain("Item 1");
|
|
172
|
+
expect(initialText).toContain("Item 2");
|
|
177
173
|
|
|
178
174
|
// Change limit
|
|
179
|
-
const changeLimitButton = getByTestId(
|
|
175
|
+
const changeLimitButton = getByTestId("change-limit") as HTMLButtonElement;
|
|
180
176
|
changeLimitButton.click();
|
|
181
177
|
|
|
182
178
|
// Should now show 3 items (Select should re-render)
|
|
183
179
|
const updatedText = container.textContent;
|
|
184
|
-
expect(updatedText).toContain(
|
|
185
|
-
expect(updatedText).toContain(
|
|
186
|
-
expect(updatedText).toContain(
|
|
180
|
+
expect(updatedText).toContain("Item 1");
|
|
181
|
+
expect(updatedText).toContain("Item 2");
|
|
182
|
+
expect(updatedText).toContain("Item 3");
|
|
187
183
|
});
|
|
188
184
|
|
|
189
|
-
it(
|
|
185
|
+
it("should re-render when disabled prop changes", () => {
|
|
190
186
|
const onSelect = vi.fn();
|
|
191
|
-
const items: SelectItem[] = [
|
|
192
|
-
{ label: 'Item 1', value: '1' },
|
|
193
|
-
];
|
|
187
|
+
const items: SelectItem[] = [{ label: "Item 1", value: "1" }];
|
|
194
188
|
|
|
195
189
|
function TestWrapper() {
|
|
196
190
|
const [disabled, setDisabled] = useState(false);
|
|
@@ -200,7 +194,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
200
194
|
<Select items={items} onSelect={onSelect} disabled={disabled} />
|
|
201
195
|
<button
|
|
202
196
|
data-testid="toggle-disabled"
|
|
203
|
-
onClick={() => setDisabled(d => !d)}
|
|
197
|
+
onClick={() => setDisabled((d) => !d)}
|
|
204
198
|
/>
|
|
205
199
|
</div>
|
|
206
200
|
);
|
|
@@ -209,24 +203,24 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
209
203
|
const { getByTestId } = render(<TestWrapper />);
|
|
210
204
|
|
|
211
205
|
// Toggle disabled
|
|
212
|
-
const toggleButton = getByTestId(
|
|
206
|
+
const toggleButton = getByTestId("toggle-disabled") as HTMLButtonElement;
|
|
213
207
|
toggleButton.click();
|
|
214
208
|
|
|
215
209
|
// Select should re-render with new disabled prop
|
|
216
210
|
});
|
|
217
211
|
|
|
218
|
-
it(
|
|
212
|
+
it("should use custom comparison for items array", () => {
|
|
219
213
|
const onSelect = vi.fn();
|
|
220
214
|
|
|
221
215
|
// Two arrays with same content but different references
|
|
222
216
|
const items1: SelectItem[] = [
|
|
223
|
-
{ label:
|
|
224
|
-
{ label:
|
|
217
|
+
{ label: "Item 1", value: "1" },
|
|
218
|
+
{ label: "Item 2", value: "2" },
|
|
225
219
|
];
|
|
226
220
|
|
|
227
221
|
const items2: SelectItem[] = [
|
|
228
|
-
{ label:
|
|
229
|
-
{ label:
|
|
222
|
+
{ label: "Item 1", value: "1" },
|
|
223
|
+
{ label: "Item 2", value: "2" },
|
|
230
224
|
];
|
|
231
225
|
|
|
232
226
|
// Verify they're different references
|
|
@@ -245,10 +239,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
245
239
|
return (
|
|
246
240
|
<div>
|
|
247
241
|
<Select items={items} onSelect={onSelect} />
|
|
248
|
-
<button
|
|
249
|
-
data-testid="swap-items"
|
|
250
|
-
onClick={() => setItems(items2)}
|
|
251
|
-
/>
|
|
242
|
+
<button data-testid="swap-items" onClick={() => setItems(items2)} />
|
|
252
243
|
</div>
|
|
253
244
|
);
|
|
254
245
|
}
|
|
@@ -256,7 +247,7 @@ describe.skip('Select Component React.memo (T082-2)', () => {
|
|
|
256
247
|
const { getByTestId } = render(<TestWrapper />);
|
|
257
248
|
|
|
258
249
|
// Swap to items2 (same content, different reference)
|
|
259
|
-
const swapButton = getByTestId(
|
|
250
|
+
const swapButton = getByTestId("swap-items") as HTMLButtonElement;
|
|
260
251
|
swapButton.click();
|
|
261
252
|
|
|
262
253
|
// With custom comparison in React.memo, Select should not re-render
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* @vitest-environment happy-dom
|
|
3
3
|
*/
|
|
4
4
|
/* eslint-disable no-control-regex */
|
|
5
|
-
import { describe, it, expect, vi } from
|
|
6
|
-
import { render } from
|
|
7
|
-
import React from
|
|
8
|
-
import { Select } from
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { render } from "ink-testing-library";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Select } from "../../../components/common/Select.js";
|
|
9
9
|
|
|
10
10
|
// Helper to wait for async updates
|
|
11
11
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -15,37 +15,41 @@ interface TestItem {
|
|
|
15
15
|
value: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
describe(
|
|
18
|
+
describe("Select", () => {
|
|
19
19
|
const mockItems: TestItem[] = [
|
|
20
|
-
{ label:
|
|
21
|
-
{ label:
|
|
22
|
-
{ label:
|
|
23
|
-
{ label:
|
|
24
|
-
{ label:
|
|
20
|
+
{ label: "Option 1", value: "opt1" },
|
|
21
|
+
{ label: "Option 2", value: "opt2" },
|
|
22
|
+
{ label: "Option 3", value: "opt3" },
|
|
23
|
+
{ label: "Option 4", value: "opt4" },
|
|
24
|
+
{ label: "Option 5", value: "opt5" },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
describe(
|
|
28
|
-
it(
|
|
27
|
+
describe("Rendering", () => {
|
|
28
|
+
it("should render all items", () => {
|
|
29
29
|
const onSelect = vi.fn();
|
|
30
|
-
const { lastFrame } = render(
|
|
30
|
+
const { lastFrame } = render(
|
|
31
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
32
|
+
);
|
|
31
33
|
|
|
32
|
-
expect(lastFrame()).toContain(
|
|
33
|
-
expect(lastFrame()).toContain(
|
|
34
|
-
expect(lastFrame()).toContain(
|
|
34
|
+
expect(lastFrame()).toContain("Option 1");
|
|
35
|
+
expect(lastFrame()).toContain("Option 2");
|
|
36
|
+
expect(lastFrame()).toContain("Option 3");
|
|
35
37
|
});
|
|
36
38
|
|
|
37
|
-
it(
|
|
39
|
+
it("should highlight first item by default", () => {
|
|
38
40
|
const onSelect = vi.fn();
|
|
39
|
-
const { lastFrame } = render(
|
|
41
|
+
const { lastFrame } = render(
|
|
42
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
43
|
+
);
|
|
40
44
|
|
|
41
45
|
// Cyan color code indicates selected item
|
|
42
|
-
expect(lastFrame()).toContain(
|
|
46
|
+
expect(lastFrame()).toContain("›");
|
|
43
47
|
});
|
|
44
48
|
|
|
45
|
-
it(
|
|
49
|
+
it("should highlight item at initialIndex", () => {
|
|
46
50
|
const onSelect = vi.fn();
|
|
47
51
|
const { lastFrame } = render(
|
|
48
|
-
<Select items={mockItems} onSelect={onSelect} initialIndex={2}
|
|
52
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={2} />,
|
|
49
53
|
);
|
|
50
54
|
|
|
51
55
|
const output = lastFrame();
|
|
@@ -54,121 +58,135 @@ describe('Select', () => {
|
|
|
54
58
|
expect(selectedCount).toBe(1);
|
|
55
59
|
});
|
|
56
60
|
|
|
57
|
-
it(
|
|
61
|
+
it("should render with empty items array", () => {
|
|
58
62
|
const onSelect = vi.fn();
|
|
59
63
|
const { lastFrame } = render(<Select items={[]} onSelect={onSelect} />);
|
|
60
64
|
|
|
61
65
|
expect(lastFrame()).toBeDefined();
|
|
62
66
|
});
|
|
63
67
|
|
|
64
|
-
it(
|
|
68
|
+
it("should respect limit prop for scrolling", () => {
|
|
65
69
|
const onSelect = vi.fn();
|
|
66
|
-
const { lastFrame } = render(
|
|
70
|
+
const { lastFrame } = render(
|
|
71
|
+
<Select items={mockItems} onSelect={onSelect} limit={3} />,
|
|
72
|
+
);
|
|
67
73
|
|
|
68
74
|
const output = lastFrame();
|
|
69
75
|
// Should only show 3 items when limit is 3
|
|
70
|
-
expect(output).toContain(
|
|
71
|
-
expect(output).toContain(
|
|
72
|
-
expect(output).toContain(
|
|
76
|
+
expect(output).toContain("Option 1");
|
|
77
|
+
expect(output).toContain("Option 2");
|
|
78
|
+
expect(output).toContain("Option 3");
|
|
73
79
|
// Option 4 and 5 should not be visible initially
|
|
74
|
-
expect(output).not.toContain(
|
|
75
|
-
expect(output).not.toContain(
|
|
80
|
+
expect(output).not.toContain("Option 4");
|
|
81
|
+
expect(output).not.toContain("Option 5");
|
|
76
82
|
});
|
|
77
83
|
});
|
|
78
84
|
|
|
79
|
-
describe(
|
|
80
|
-
it(
|
|
85
|
+
describe("Navigation - No Looping (Critical Feature)", () => {
|
|
86
|
+
it("should implement boundary checks to prevent looping", () => {
|
|
81
87
|
// Unit test: verify the logic used in implementation
|
|
82
88
|
// Math.max(0, current - 1) - prevents going below 0
|
|
83
89
|
// Math.min(items.length - 1, current + 1) - prevents going above max
|
|
84
90
|
|
|
85
91
|
const onSelect = vi.fn();
|
|
86
|
-
const { lastFrame } = render(
|
|
92
|
+
const { lastFrame } = render(
|
|
93
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
94
|
+
);
|
|
87
95
|
|
|
88
96
|
// Verify component renders (implementation uses Math.max/min for boundaries)
|
|
89
97
|
expect(lastFrame()).toBeDefined();
|
|
90
|
-
expect(lastFrame()).toContain(
|
|
98
|
+
expect(lastFrame()).toContain("Option 1");
|
|
91
99
|
});
|
|
92
100
|
|
|
93
|
-
it(
|
|
101
|
+
it("should start at first item by default", () => {
|
|
94
102
|
const onSelect = vi.fn();
|
|
95
|
-
const { lastFrame } = render(
|
|
103
|
+
const { lastFrame } = render(
|
|
104
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
105
|
+
);
|
|
96
106
|
|
|
97
107
|
const output = lastFrame();
|
|
98
108
|
// First line should have the selection indicator
|
|
99
|
-
const lines = output.split(
|
|
100
|
-
expect(lines[0]).toContain(
|
|
101
|
-
expect(lines[0]).toContain(
|
|
109
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
110
|
+
expect(lines[0]).toContain("›");
|
|
111
|
+
expect(lines[0]).toContain("Option 1");
|
|
102
112
|
});
|
|
103
113
|
|
|
104
|
-
it(
|
|
114
|
+
it("should respect initialIndex without looping", () => {
|
|
105
115
|
const onSelect = vi.fn();
|
|
106
116
|
const { lastFrame } = render(
|
|
107
|
-
<Select items={mockItems} onSelect={onSelect} initialIndex={4}
|
|
117
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={4} />,
|
|
108
118
|
);
|
|
109
119
|
|
|
110
120
|
// Should start at last item (index 4)
|
|
111
121
|
const output = lastFrame();
|
|
112
|
-
expect(output).toContain(
|
|
113
|
-
expect(output).toContain(
|
|
122
|
+
expect(output).toContain("Option 5");
|
|
123
|
+
expect(output).toContain("›");
|
|
114
124
|
});
|
|
115
125
|
|
|
116
|
-
it(
|
|
126
|
+
it("should handle initialIndex at 0", () => {
|
|
117
127
|
const onSelect = vi.fn();
|
|
118
128
|
const { lastFrame } = render(
|
|
119
|
-
<Select items={mockItems} onSelect={onSelect} initialIndex={0}
|
|
129
|
+
<Select items={mockItems} onSelect={onSelect} initialIndex={0} />,
|
|
120
130
|
);
|
|
121
131
|
|
|
122
132
|
const output = lastFrame();
|
|
123
|
-
const lines = output.split(
|
|
124
|
-
expect(lines[0]).toContain(
|
|
133
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
134
|
+
expect(lines[0]).toContain("Option 1");
|
|
125
135
|
});
|
|
126
136
|
});
|
|
127
137
|
|
|
128
|
-
describe(
|
|
129
|
-
it(
|
|
138
|
+
describe("Navigation - Input Handling", () => {
|
|
139
|
+
it("should use useInput hook for keyboard handling", () => {
|
|
130
140
|
// Verify component accepts keyboard input by checking it renders properly
|
|
131
141
|
const onSelect = vi.fn();
|
|
132
|
-
const { stdin } = render(
|
|
142
|
+
const { stdin } = render(
|
|
143
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
144
|
+
);
|
|
133
145
|
|
|
134
146
|
// Component should handle input without errors
|
|
135
|
-
expect(() => stdin.write(
|
|
136
|
-
expect(() => stdin.write(
|
|
137
|
-
expect(() => stdin.write(
|
|
138
|
-
expect(() => stdin.write(
|
|
147
|
+
expect(() => stdin.write("\u001B[B")).not.toThrow();
|
|
148
|
+
expect(() => stdin.write("\u001B[A")).not.toThrow();
|
|
149
|
+
expect(() => stdin.write("j")).not.toThrow();
|
|
150
|
+
expect(() => stdin.write("k")).not.toThrow();
|
|
139
151
|
});
|
|
140
152
|
|
|
141
|
-
it(
|
|
153
|
+
it("should support vim-style navigation keys (j/k)", () => {
|
|
142
154
|
const onSelect = vi.fn();
|
|
143
|
-
const { stdin } = render(
|
|
155
|
+
const { stdin } = render(
|
|
156
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
157
|
+
);
|
|
144
158
|
|
|
145
159
|
// Should accept j and k keys
|
|
146
|
-
expect(() => stdin.write(
|
|
147
|
-
expect(() => stdin.write(
|
|
160
|
+
expect(() => stdin.write("j")).not.toThrow();
|
|
161
|
+
expect(() => stdin.write("k")).not.toThrow();
|
|
148
162
|
});
|
|
149
163
|
|
|
150
|
-
it(
|
|
164
|
+
it("should support arrow keys", () => {
|
|
151
165
|
const onSelect = vi.fn();
|
|
152
|
-
const { stdin } = render(
|
|
166
|
+
const { stdin } = render(
|
|
167
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
168
|
+
);
|
|
153
169
|
|
|
154
170
|
// Should accept arrow keys
|
|
155
|
-
expect(() => stdin.write(
|
|
156
|
-
expect(() => stdin.write(
|
|
171
|
+
expect(() => stdin.write("\u001B[A")).not.toThrow(); // Up
|
|
172
|
+
expect(() => stdin.write("\u001B[B")).not.toThrow(); // Down
|
|
157
173
|
});
|
|
158
174
|
});
|
|
159
175
|
|
|
160
|
-
describe(
|
|
161
|
-
it(
|
|
176
|
+
describe("Selection", () => {
|
|
177
|
+
it("should call onSelect when Enter is pressed", () => {
|
|
162
178
|
const onSelect = vi.fn();
|
|
163
|
-
const { stdin } = render(
|
|
179
|
+
const { stdin } = render(
|
|
180
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
181
|
+
);
|
|
164
182
|
|
|
165
|
-
stdin.write(
|
|
183
|
+
stdin.write("\r"); // Enter key
|
|
166
184
|
|
|
167
185
|
// Should be called at least once
|
|
168
186
|
expect(onSelect).toHaveBeenCalled();
|
|
169
187
|
});
|
|
170
188
|
|
|
171
|
-
it(
|
|
189
|
+
it("should pass selected item to onSelect callback", () => {
|
|
172
190
|
const onSelect = vi.fn();
|
|
173
191
|
render(<Select items={mockItems} onSelect={onSelect} initialIndex={2} />);
|
|
174
192
|
|
|
@@ -177,67 +195,79 @@ describe('Select', () => {
|
|
|
177
195
|
expect(onSelect).toBeInstanceOf(Function);
|
|
178
196
|
});
|
|
179
197
|
|
|
180
|
-
it(
|
|
198
|
+
it("should handle Enter key without errors", () => {
|
|
181
199
|
const onSelect = vi.fn();
|
|
182
|
-
const { stdin } = render(
|
|
200
|
+
const { stdin } = render(
|
|
201
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
202
|
+
);
|
|
183
203
|
|
|
184
|
-
expect(() => stdin.write(
|
|
204
|
+
expect(() => stdin.write("\r")).not.toThrow();
|
|
185
205
|
});
|
|
186
206
|
});
|
|
187
207
|
|
|
188
|
-
describe(
|
|
189
|
-
it(
|
|
208
|
+
describe("Scrolling with limit", () => {
|
|
209
|
+
it("should implement offset-based scrolling logic", () => {
|
|
190
210
|
// Verify limit prop is accepted and used for slicing
|
|
191
211
|
const onSelect = vi.fn();
|
|
192
|
-
const { lastFrame } = render(
|
|
212
|
+
const { lastFrame } = render(
|
|
213
|
+
<Select items={mockItems} onSelect={onSelect} limit={3} />,
|
|
214
|
+
);
|
|
193
215
|
|
|
194
216
|
const output = lastFrame();
|
|
195
217
|
// Should show limited items initially
|
|
196
|
-
expect(output).toContain(
|
|
197
|
-
expect(output).toContain(
|
|
198
|
-
expect(output).toContain(
|
|
218
|
+
expect(output).toContain("Option 1");
|
|
219
|
+
expect(output).toContain("Option 2");
|
|
220
|
+
expect(output).toContain("Option 3");
|
|
199
221
|
});
|
|
200
222
|
|
|
201
|
-
it(
|
|
223
|
+
it("should handle limit smaller than items length", () => {
|
|
202
224
|
const onSelect = vi.fn();
|
|
203
|
-
const { lastFrame } = render(
|
|
225
|
+
const { lastFrame } = render(
|
|
226
|
+
<Select items={mockItems} onSelect={onSelect} limit={2} />,
|
|
227
|
+
);
|
|
204
228
|
|
|
205
229
|
const output = lastFrame();
|
|
206
|
-
const lines = output.split(
|
|
230
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
207
231
|
// Should only show 2 items
|
|
208
232
|
expect(lines.length).toBeLessThanOrEqual(2);
|
|
209
233
|
});
|
|
210
234
|
|
|
211
|
-
it(
|
|
235
|
+
it("should handle limit larger than items length", () => {
|
|
212
236
|
const onSelect = vi.fn();
|
|
213
|
-
const { lastFrame } = render(
|
|
237
|
+
const { lastFrame } = render(
|
|
238
|
+
<Select items={mockItems} onSelect={onSelect} limit={100} />,
|
|
239
|
+
);
|
|
214
240
|
|
|
215
241
|
// Should show all items without error
|
|
216
242
|
const output = lastFrame();
|
|
217
|
-
expect(output).toContain(
|
|
218
|
-
expect(output).toContain(
|
|
243
|
+
expect(output).toContain("Option 1");
|
|
244
|
+
expect(output).toContain("Option 5");
|
|
219
245
|
});
|
|
220
246
|
});
|
|
221
247
|
|
|
222
|
-
describe(
|
|
223
|
-
it(
|
|
248
|
+
describe("Key propagation (Critical Feature)", () => {
|
|
249
|
+
it("should not interfere with other keys like q", () => {
|
|
224
250
|
const onSelect = vi.fn();
|
|
225
|
-
const { stdin } = render(
|
|
251
|
+
const { stdin } = render(
|
|
252
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
253
|
+
);
|
|
226
254
|
|
|
227
255
|
// Press q key (should be ignored by Select and propagate to parent)
|
|
228
|
-
stdin.write(
|
|
256
|
+
stdin.write("q");
|
|
229
257
|
|
|
230
258
|
// onSelect should not be called
|
|
231
259
|
expect(onSelect).not.toHaveBeenCalled();
|
|
232
260
|
});
|
|
233
261
|
|
|
234
|
-
it(
|
|
262
|
+
it("should not interfere with other keys like m, n, c", () => {
|
|
235
263
|
const onSelect = vi.fn();
|
|
236
|
-
const { stdin } = render(
|
|
264
|
+
const { stdin } = render(
|
|
265
|
+
<Select items={mockItems} onSelect={onSelect} />,
|
|
266
|
+
);
|
|
237
267
|
|
|
238
|
-
stdin.write(
|
|
239
|
-
stdin.write(
|
|
240
|
-
stdin.write(
|
|
268
|
+
stdin.write("m");
|
|
269
|
+
stdin.write("n");
|
|
270
|
+
stdin.write("c");
|
|
241
271
|
|
|
242
272
|
// None of these should trigger selection
|
|
243
273
|
expect(onSelect).not.toHaveBeenCalled();
|