@akiojin/gwt 4.6.0 → 4.7.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/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +38 -17
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +102 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -0
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +6 -2
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts +10 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js +44 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts +14 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js +34 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts +19 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.js +107 -0
- package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -0
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +10 -3
- package/dist/cli/ui/hooks/useGitData.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/cli/ui/utils/clipboard.d.ts +7 -0
- package/dist/cli/ui/utils/clipboard.d.ts.map +1 -0
- package/dist/cli/ui/utils/clipboard.js +21 -0
- package/dist/cli/ui/utils/clipboard.js.map +1 -0
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +12 -6
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/formatter.d.ts +15 -0
- package/dist/logging/formatter.d.ts.map +1 -0
- package/dist/logging/formatter.js +81 -0
- package/dist/logging/formatter.js.map +1 -0
- package/dist/logging/reader.d.ts +12 -0
- package/dist/logging/reader.d.ts.map +1 -0
- package/dist/logging/reader.js +63 -0
- package/dist/logging/reader.js.map +1 -0
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +57 -0
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/claude.ts +51 -22
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +7 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +17 -11
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +28 -2
- package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +57 -0
- package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +102 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts +197 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +19 -9
- package/src/cli/ui/__tests__/utils/clipboard.test.ts +65 -0
- package/src/cli/ui/components/App.tsx +177 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
- package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +83 -0
- package/src/cli/ui/components/screens/LogDetailScreen.tsx +67 -0
- package/src/cli/ui/components/screens/LogListScreen.tsx +192 -0
- package/src/cli/ui/hooks/useGitData.ts +12 -3
- package/src/cli/ui/types.ts +3 -0
- package/src/cli/ui/utils/clipboard.ts +31 -0
- package/src/gemini.ts +14 -6
- package/src/index.ts +11 -2
- package/src/logging/formatter.ts +106 -0
- package/src/logging/reader.ts +76 -0
- package/src/worktree.ts +77 -0
package/src/claude.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getTerminalStreams,
|
|
7
7
|
resetTerminalModes,
|
|
8
8
|
} from "./utils/terminal.js";
|
|
9
|
-
import {
|
|
9
|
+
import { findCommand } from "./utils/command.js";
|
|
10
10
|
import { findLatestClaudeSession } from "./utils/session.js";
|
|
11
11
|
|
|
12
12
|
const CLAUDE_CLI_PACKAGE = "@anthropic-ai/claude-code@latest";
|
|
@@ -232,7 +232,9 @@ export async function launchClaudeCode(
|
|
|
232
232
|
const childStdio = createChildStdio();
|
|
233
233
|
|
|
234
234
|
// Auto-detect locally installed claude command
|
|
235
|
-
const
|
|
235
|
+
const claudeLookup = await findCommand("claude");
|
|
236
|
+
const npxLookup =
|
|
237
|
+
process.platform === "win32" ? await findCommand("npx") : null;
|
|
236
238
|
|
|
237
239
|
const execInteractive = async (
|
|
238
240
|
file: string,
|
|
@@ -258,11 +260,12 @@ export async function launchClaudeCode(
|
|
|
258
260
|
};
|
|
259
261
|
|
|
260
262
|
try {
|
|
261
|
-
if (
|
|
263
|
+
if (claudeLookup.source === "installed" && claudeLookup.path) {
|
|
264
|
+
// Use the full path to avoid PATH issues in non-interactive shells
|
|
262
265
|
console.log(
|
|
263
266
|
chalk.green(" ✨ Using locally installed claude command"),
|
|
264
267
|
);
|
|
265
|
-
await execInteractive(
|
|
268
|
+
await execInteractive(claudeLookup.path, args, {
|
|
266
269
|
cwd: worktreePath,
|
|
267
270
|
stdin: childStdio.stdin,
|
|
268
271
|
stdout: childStdio.stdout,
|
|
@@ -270,11 +273,20 @@ export async function launchClaudeCode(
|
|
|
270
273
|
env: launchEnv,
|
|
271
274
|
});
|
|
272
275
|
} else {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const useNpx = npxLookup?.source === "installed" && npxLookup?.path;
|
|
277
|
+
if (useNpx) {
|
|
278
|
+
console.log(
|
|
279
|
+
chalk.cyan(
|
|
280
|
+
" 🔄 Falling back to npx @anthropic-ai/claude-code@latest",
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(
|
|
285
|
+
chalk.cyan(
|
|
286
|
+
" 🔄 Falling back to bunx @anthropic-ai/claude-code@latest",
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
278
290
|
console.log(
|
|
279
291
|
chalk.yellow(
|
|
280
292
|
" 💡 Recommended: Install Claude Code via official method for faster startup",
|
|
@@ -294,14 +306,33 @@ export async function launchClaudeCode(
|
|
|
294
306
|
),
|
|
295
307
|
);
|
|
296
308
|
console.log("");
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
const shouldSkipDelay =
|
|
310
|
+
typeof process !== "undefined" &&
|
|
311
|
+
(process.env?.NODE_ENV === "test" || Boolean(process.env?.VITEST));
|
|
312
|
+
if (!shouldSkipDelay) {
|
|
313
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
314
|
+
}
|
|
315
|
+
if (useNpx && npxLookup?.path) {
|
|
316
|
+
await execInteractive(
|
|
317
|
+
npxLookup.path,
|
|
318
|
+
["-y", CLAUDE_CLI_PACKAGE, ...args],
|
|
319
|
+
{
|
|
320
|
+
cwd: worktreePath,
|
|
321
|
+
stdin: childStdio.stdin,
|
|
322
|
+
stdout: childStdio.stdout,
|
|
323
|
+
stderr: childStdio.stderr,
|
|
324
|
+
env: launchEnv,
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
await execInteractive("bunx", [CLAUDE_CLI_PACKAGE, ...args], {
|
|
329
|
+
cwd: worktreePath,
|
|
330
|
+
stdin: childStdio.stdin,
|
|
331
|
+
stdout: childStdio.stdout,
|
|
332
|
+
stderr: childStdio.stderr,
|
|
333
|
+
env: launchEnv,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
305
336
|
}
|
|
306
337
|
} finally {
|
|
307
338
|
childStdio.cleanup();
|
|
@@ -342,7 +373,9 @@ export async function launchClaudeCode(
|
|
|
342
373
|
|
|
343
374
|
return capturedSessionId ? { sessionId: capturedSessionId } : {};
|
|
344
375
|
} catch (error: unknown) {
|
|
345
|
-
const
|
|
376
|
+
const claudeCheck = await findCommand("claude");
|
|
377
|
+
const hasLocalClaude =
|
|
378
|
+
claudeCheck.source === "installed" && claudeCheck.path !== null;
|
|
346
379
|
let errorMessage: string;
|
|
347
380
|
const err = error as NodeJS.ErrnoException;
|
|
348
381
|
|
|
@@ -394,10 +427,6 @@ export async function launchClaudeCode(
|
|
|
394
427
|
}
|
|
395
428
|
}
|
|
396
429
|
|
|
397
|
-
async function isClaudeCommandAvailable(): Promise<boolean> {
|
|
398
|
-
return isCommandAvailable("claude");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
430
|
/**
|
|
402
431
|
* Checks whether Claude Code is available via `bunx` in the current environment.
|
|
403
432
|
*
|
|
@@ -176,7 +176,7 @@ describe("App shortcuts integration", () => {
|
|
|
176
176
|
getRepositoryRootMock.mockResolvedValue("/repo");
|
|
177
177
|
deleteBranchMock.mockResolvedValue(undefined);
|
|
178
178
|
App = (await import("../../components/App.js")).App;
|
|
179
|
-
});
|
|
179
|
+
}, 30000);
|
|
180
180
|
|
|
181
181
|
afterEach(() => {
|
|
182
182
|
useGitDataMock.mockReset();
|
|
@@ -380,9 +380,13 @@ describe("App shortcuts integration", () => {
|
|
|
380
380
|
expect(latestProps?.cleanupUI?.footerMessage?.text).toBeTruthy();
|
|
381
381
|
expect(latestProps?.cleanupUI?.indicators).toMatchObject({
|
|
382
382
|
"feature/add-new-feature": expect.objectContaining({
|
|
383
|
-
|
|
383
|
+
isSpinning: true,
|
|
384
|
+
color: "cyan",
|
|
385
|
+
}),
|
|
386
|
+
"hotfix/urgent-fix": expect.objectContaining({
|
|
387
|
+
icon: "⏳",
|
|
388
|
+
color: "yellow",
|
|
384
389
|
}),
|
|
385
|
-
"hotfix/urgent-fix": expect.objectContaining({ icon: "⏳" }),
|
|
386
390
|
});
|
|
387
391
|
|
|
388
392
|
resolveRemoveWorktree?.();
|
|
@@ -6,6 +6,7 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
6
6
|
import { render } from "ink-testing-library";
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { Select } from "../../../components/common/Select.js";
|
|
9
|
+
import { ESCAPE_SEQUENCE_TIMEOUT_MS } from "../../../hooks/useAppInput.js";
|
|
9
10
|
|
|
10
11
|
interface TestItem {
|
|
11
12
|
label: string;
|
|
@@ -285,43 +286,48 @@ describe("Select", () => {
|
|
|
285
286
|
});
|
|
286
287
|
});
|
|
287
288
|
|
|
288
|
-
describe(
|
|
289
|
-
it(
|
|
289
|
+
describe("Space/Escape handlers", () => {
|
|
290
|
+
it("should call onSpace with the currently highlighted item", async () => {
|
|
290
291
|
const onSelect = vi.fn();
|
|
291
292
|
const onSpace = vi.fn();
|
|
292
293
|
const { stdin } = render(
|
|
293
|
-
<Select items={mockItems} onSelect={onSelect} onSpace={onSpace}
|
|
294
|
+
<Select items={mockItems} onSelect={onSelect} onSpace={onSpace} />,
|
|
294
295
|
);
|
|
295
296
|
|
|
296
|
-
stdin.write(
|
|
297
|
+
stdin.write(" ");
|
|
297
298
|
await delay(10);
|
|
298
299
|
|
|
299
300
|
expect(onSpace).toHaveBeenCalledTimes(1);
|
|
300
301
|
expect(onSpace).toHaveBeenCalledWith(mockItems[0]);
|
|
301
302
|
});
|
|
302
303
|
|
|
303
|
-
it(
|
|
304
|
+
it("should not trigger onSpace when disabled", async () => {
|
|
304
305
|
const onSelect = vi.fn();
|
|
305
306
|
const onSpace = vi.fn();
|
|
306
307
|
const { stdin } = render(
|
|
307
|
-
<Select
|
|
308
|
+
<Select
|
|
309
|
+
items={mockItems}
|
|
310
|
+
onSelect={onSelect}
|
|
311
|
+
onSpace={onSpace}
|
|
312
|
+
disabled
|
|
313
|
+
/>,
|
|
308
314
|
);
|
|
309
315
|
|
|
310
|
-
stdin.write(
|
|
316
|
+
stdin.write(" ");
|
|
311
317
|
await delay(10);
|
|
312
318
|
|
|
313
319
|
expect(onSpace).not.toHaveBeenCalled();
|
|
314
320
|
});
|
|
315
321
|
|
|
316
|
-
it(
|
|
322
|
+
it("should call onEscape when escape key is pressed", async () => {
|
|
317
323
|
const onSelect = vi.fn();
|
|
318
324
|
const onEscape = vi.fn();
|
|
319
325
|
const { stdin } = render(
|
|
320
|
-
<Select items={mockItems} onSelect={onSelect} onEscape={onEscape}
|
|
326
|
+
<Select items={mockItems} onSelect={onSelect} onEscape={onEscape} />,
|
|
321
327
|
);
|
|
322
328
|
|
|
323
|
-
stdin.write(
|
|
324
|
-
await delay(
|
|
329
|
+
stdin.write("\u001B");
|
|
330
|
+
await delay(ESCAPE_SEQUENCE_TIMEOUT_MS + 20);
|
|
325
331
|
|
|
326
332
|
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
327
333
|
});
|
|
@@ -210,7 +210,7 @@ describe("BranchListScreen", () => {
|
|
|
210
210
|
/>,
|
|
211
211
|
);
|
|
212
212
|
|
|
213
|
-
const text = container.textContent ?? "";
|
|
213
|
+
const text = stripAnsi(container.textContent ?? "");
|
|
214
214
|
expect(text).toMatch(/\[ \]\s(🟢|🔴|⚪)\s(🛡|⚠)/); // state cluster with spacing
|
|
215
215
|
});
|
|
216
216
|
|
|
@@ -724,10 +724,11 @@ describe("BranchListScreen", () => {
|
|
|
724
724
|
expect(container.textContent).toContain("feature/test");
|
|
725
725
|
});
|
|
726
726
|
|
|
727
|
-
it("should disable other key bindings (c, r) while typing in filter", () => {
|
|
727
|
+
it("should disable other key bindings (c, r, l) while typing in filter", () => {
|
|
728
728
|
const onSelect = vi.fn();
|
|
729
729
|
const onCleanupCommand = vi.fn();
|
|
730
730
|
const onRefresh = vi.fn();
|
|
731
|
+
const onOpenLogs = vi.fn();
|
|
731
732
|
|
|
732
733
|
const inkApp = inkRender(
|
|
733
734
|
<BranchListScreen
|
|
@@ -736,6 +737,7 @@ describe("BranchListScreen", () => {
|
|
|
736
737
|
onSelect={onSelect}
|
|
737
738
|
onCleanupCommand={onCleanupCommand}
|
|
738
739
|
onRefresh={onRefresh}
|
|
740
|
+
onOpenLogs={onOpenLogs}
|
|
739
741
|
/>,
|
|
740
742
|
);
|
|
741
743
|
|
|
@@ -746,10 +748,34 @@ describe("BranchListScreen", () => {
|
|
|
746
748
|
act(() => {
|
|
747
749
|
inkApp.stdin.write("c");
|
|
748
750
|
inkApp.stdin.write("r");
|
|
751
|
+
inkApp.stdin.write("l");
|
|
749
752
|
});
|
|
750
753
|
|
|
751
754
|
expect(onCleanupCommand).not.toHaveBeenCalled();
|
|
752
755
|
expect(onRefresh).not.toHaveBeenCalled();
|
|
756
|
+
expect(onOpenLogs).not.toHaveBeenCalled();
|
|
757
|
+
|
|
758
|
+
inkApp.unmount();
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("should open logs on l key", () => {
|
|
762
|
+
const onSelect = vi.fn();
|
|
763
|
+
const onOpenLogs = vi.fn();
|
|
764
|
+
|
|
765
|
+
const inkApp = inkRender(
|
|
766
|
+
<BranchListScreen
|
|
767
|
+
branches={mockBranches}
|
|
768
|
+
stats={mockStats}
|
|
769
|
+
onSelect={onSelect}
|
|
770
|
+
onOpenLogs={onOpenLogs}
|
|
771
|
+
/>,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
act(() => {
|
|
775
|
+
inkApp.stdin.write("l");
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
expect(onOpenLogs).toHaveBeenCalled();
|
|
753
779
|
|
|
754
780
|
inkApp.unmount();
|
|
755
781
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
import { act } from "@testing-library/react";
|
|
6
|
+
import { render as inkRender } from "ink-testing-library";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { LogDetailScreen } from "../../../components/screens/LogDetailScreen.js";
|
|
9
|
+
import type { FormattedLogEntry } from "../../../../../logging/formatter.js";
|
|
10
|
+
|
|
11
|
+
const entry: FormattedLogEntry = {
|
|
12
|
+
id: "entry-1",
|
|
13
|
+
raw: {
|
|
14
|
+
time: "2025-12-25T10:00:00.000Z",
|
|
15
|
+
level: 30,
|
|
16
|
+
category: "cli",
|
|
17
|
+
msg: "hello",
|
|
18
|
+
},
|
|
19
|
+
timestamp: 1_767_015_200_000,
|
|
20
|
+
timeLabel: "10:00:00",
|
|
21
|
+
levelLabel: "INFO",
|
|
22
|
+
category: "cli",
|
|
23
|
+
message: "hello",
|
|
24
|
+
summary: "[10:00:00] [INFO] [cli] hello",
|
|
25
|
+
json: '{\n "msg": "hello"\n}',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("LogDetailScreen", () => {
|
|
29
|
+
it("renders formatted JSON and handles shortcuts", () => {
|
|
30
|
+
const onBack = vi.fn();
|
|
31
|
+
const onCopy = vi.fn();
|
|
32
|
+
|
|
33
|
+
const { stdin, lastFrame } = inkRender(
|
|
34
|
+
<LogDetailScreen entry={entry} onBack={onBack} onCopy={onCopy} />,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(lastFrame()).toContain('"msg": "hello"');
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
stdin.write("c");
|
|
41
|
+
});
|
|
42
|
+
expect(onCopy).toHaveBeenCalledWith(entry);
|
|
43
|
+
|
|
44
|
+
act(() => {
|
|
45
|
+
stdin.write("q");
|
|
46
|
+
});
|
|
47
|
+
expect(onBack).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("shows fallback when entry is missing", () => {
|
|
51
|
+
const { lastFrame } = inkRender(
|
|
52
|
+
<LogDetailScreen entry={null} onBack={vi.fn()} onCopy={vi.fn()} />,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(lastFrame()).toContain("ログがありません");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
import { act } from "@testing-library/react";
|
|
6
|
+
import { render as inkRender } from "ink-testing-library";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { LogListScreen } from "../../../components/screens/LogListScreen.js";
|
|
9
|
+
import type { FormattedLogEntry } from "../../../../../logging/formatter.js";
|
|
10
|
+
|
|
11
|
+
const buildEntry = (
|
|
12
|
+
overrides: Partial<FormattedLogEntry>,
|
|
13
|
+
): FormattedLogEntry => ({
|
|
14
|
+
id: "entry-1",
|
|
15
|
+
raw: {
|
|
16
|
+
time: "2025-12-25T10:00:00.000Z",
|
|
17
|
+
level: 30,
|
|
18
|
+
category: "cli",
|
|
19
|
+
msg: "hello",
|
|
20
|
+
},
|
|
21
|
+
timestamp: 1_767_015_200_000,
|
|
22
|
+
timeLabel: "10:00:00",
|
|
23
|
+
levelLabel: "INFO",
|
|
24
|
+
category: "cli",
|
|
25
|
+
message: "hello",
|
|
26
|
+
summary: "[10:00:00] [INFO] [cli] hello",
|
|
27
|
+
json: '{\n "msg": "hello"\n}',
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("LogListScreen", () => {
|
|
32
|
+
it("renders log entries and handles shortcuts", () => {
|
|
33
|
+
const entries: FormattedLogEntry[] = [
|
|
34
|
+
buildEntry({ id: "entry-1" }),
|
|
35
|
+
buildEntry({
|
|
36
|
+
id: "entry-2",
|
|
37
|
+
summary: "[10:01:00] [WARN] [server] warn",
|
|
38
|
+
levelLabel: "WARN",
|
|
39
|
+
category: "server",
|
|
40
|
+
message: "warn",
|
|
41
|
+
}),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const onSelect = vi.fn();
|
|
45
|
+
const onBack = vi.fn();
|
|
46
|
+
const onCopy = vi.fn();
|
|
47
|
+
const onPickDate = vi.fn();
|
|
48
|
+
|
|
49
|
+
const { stdin, lastFrame } = inkRender(
|
|
50
|
+
<LogListScreen
|
|
51
|
+
entries={entries}
|
|
52
|
+
loading={false}
|
|
53
|
+
error={null}
|
|
54
|
+
onBack={onBack}
|
|
55
|
+
onSelect={onSelect}
|
|
56
|
+
onCopy={onCopy}
|
|
57
|
+
onPickDate={onPickDate}
|
|
58
|
+
selectedDate="2025-12-25"
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const frame = lastFrame();
|
|
63
|
+
expect(frame).toContain("[10:00:00] [INFO] [cli] hello");
|
|
64
|
+
expect(frame).toContain("[10:01:00] [WARN] [server] warn");
|
|
65
|
+
|
|
66
|
+
act(() => {
|
|
67
|
+
stdin.write("\r");
|
|
68
|
+
});
|
|
69
|
+
expect(onSelect).toHaveBeenCalledWith(entries[0]);
|
|
70
|
+
|
|
71
|
+
act(() => {
|
|
72
|
+
stdin.write("c");
|
|
73
|
+
});
|
|
74
|
+
expect(onCopy).toHaveBeenCalledWith(entries[0]);
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
stdin.write("d");
|
|
78
|
+
});
|
|
79
|
+
expect(onPickDate).toHaveBeenCalled();
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
stdin.write("q");
|
|
83
|
+
});
|
|
84
|
+
expect(onBack).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("shows empty message when no logs", () => {
|
|
88
|
+
const { lastFrame } = inkRender(
|
|
89
|
+
<LogListScreen
|
|
90
|
+
entries={[]}
|
|
91
|
+
loading={false}
|
|
92
|
+
error={null}
|
|
93
|
+
onBack={vi.fn()}
|
|
94
|
+
onSelect={vi.fn()}
|
|
95
|
+
onCopy={vi.fn()}
|
|
96
|
+
selectedDate="2025-12-25"
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(lastFrame()).toContain("ログがありません");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
5
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
6
|
+
import { Window } from "happy-dom";
|
|
7
|
+
|
|
8
|
+
// モジュールをモック
|
|
9
|
+
vi.mock("../../../../git.js", () => ({
|
|
10
|
+
getAllBranches: vi.fn(),
|
|
11
|
+
hasUnpushedCommitsInRepo: vi.fn(),
|
|
12
|
+
getRepositoryRoot: vi.fn(),
|
|
13
|
+
fetchAllRemotes: vi.fn(),
|
|
14
|
+
collectUpstreamMap: vi.fn(),
|
|
15
|
+
getBranchDivergenceStatuses: vi.fn(),
|
|
16
|
+
hasUncommittedChanges: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../../../../worktree.js", () => ({
|
|
20
|
+
listAdditionalWorktrees: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("../../../../github.js", () => ({
|
|
24
|
+
getPullRequestByBranch: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("../../../../config/index.js", () => ({
|
|
28
|
+
getLastToolUsageMap: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { useGitData } from "../../hooks/useGitData.js";
|
|
32
|
+
import {
|
|
33
|
+
getAllBranches,
|
|
34
|
+
getRepositoryRoot,
|
|
35
|
+
fetchAllRemotes,
|
|
36
|
+
collectUpstreamMap,
|
|
37
|
+
getBranchDivergenceStatuses,
|
|
38
|
+
} from "../../../../git.js";
|
|
39
|
+
import { listAdditionalWorktrees } from "../../../../worktree.js";
|
|
40
|
+
import { getLastToolUsageMap } from "../../../../config/index.js";
|
|
41
|
+
|
|
42
|
+
const mockGetAllBranches = getAllBranches as ReturnType<typeof vi.fn>;
|
|
43
|
+
const mockGetRepositoryRoot = getRepositoryRoot as ReturnType<typeof vi.fn>;
|
|
44
|
+
const mockFetchAllRemotes = fetchAllRemotes as ReturnType<typeof vi.fn>;
|
|
45
|
+
const mockCollectUpstreamMap = collectUpstreamMap as ReturnType<typeof vi.fn>;
|
|
46
|
+
const mockGetBranchDivergenceStatuses =
|
|
47
|
+
getBranchDivergenceStatuses as ReturnType<typeof vi.fn>;
|
|
48
|
+
const mockListAdditionalWorktrees = listAdditionalWorktrees as ReturnType<
|
|
49
|
+
typeof vi.fn
|
|
50
|
+
>;
|
|
51
|
+
const mockGetLastToolUsageMap = getLastToolUsageMap as ReturnType<typeof vi.fn>;
|
|
52
|
+
|
|
53
|
+
describe("useGitData", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
// Setup happy-dom
|
|
56
|
+
const window = new Window();
|
|
57
|
+
globalThis.window = window as unknown as typeof globalThis.window;
|
|
58
|
+
globalThis.document =
|
|
59
|
+
window.document as unknown as typeof globalThis.document;
|
|
60
|
+
|
|
61
|
+
// Reset all mocks
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
|
|
64
|
+
// Default mock implementations
|
|
65
|
+
mockGetRepositoryRoot.mockResolvedValue("/mock/repo");
|
|
66
|
+
mockFetchAllRemotes.mockResolvedValue(undefined);
|
|
67
|
+
mockGetAllBranches.mockResolvedValue([
|
|
68
|
+
{
|
|
69
|
+
name: "main",
|
|
70
|
+
type: "local",
|
|
71
|
+
isCurrent: true,
|
|
72
|
+
lastCommitDate: "2025-01-01",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "develop",
|
|
76
|
+
type: "local",
|
|
77
|
+
isCurrent: false,
|
|
78
|
+
lastCommitDate: "2025-01-02",
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
mockListAdditionalWorktrees.mockResolvedValue([]);
|
|
82
|
+
mockCollectUpstreamMap.mockResolvedValue(new Map());
|
|
83
|
+
mockGetBranchDivergenceStatuses.mockResolvedValue([]);
|
|
84
|
+
mockGetLastToolUsageMap.mockResolvedValue(new Map());
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.restoreAllMocks();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("キャッシュ機構", () => {
|
|
92
|
+
it("初回マウント時にGitデータを取得する", async () => {
|
|
93
|
+
const { result } = renderHook(() => useGitData());
|
|
94
|
+
|
|
95
|
+
// 初回ロード中
|
|
96
|
+
expect(result.current.loading).toBe(true);
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(result.current.loading).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Gitデータが取得されていることを確認
|
|
103
|
+
expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(result.current.branches).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("refresh()を呼び出すとGitデータを再取得する", async () => {
|
|
108
|
+
const { result } = renderHook(() => useGitData());
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(result.current.loading).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 初回ロードで1回呼ばれている
|
|
115
|
+
expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
|
|
116
|
+
|
|
117
|
+
// refresh()を呼び出す
|
|
118
|
+
await act(async () => {
|
|
119
|
+
result.current.refresh();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(result.current.loading).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// refresh後に再度呼ばれている(forceRefresh=true)
|
|
127
|
+
expect(mockGetAllBranches).toHaveBeenCalledTimes(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("キャッシュ済みの場合、再マウント時にGitデータを再取得しない", async () => {
|
|
131
|
+
// 注: useGitData は内部で useRef を使ってキャッシュ状態を管理
|
|
132
|
+
// 同一コンポーネント内での再レンダリングではキャッシュが効く
|
|
133
|
+
const { result, rerender } = renderHook(() => useGitData());
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(result.current.loading).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 初回ロードで1回呼ばれている
|
|
140
|
+
expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
|
|
141
|
+
|
|
142
|
+
// 再レンダリング
|
|
143
|
+
rerender();
|
|
144
|
+
|
|
145
|
+
// キャッシュされているため追加の呼び出しはない
|
|
146
|
+
expect(mockGetAllBranches).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("lastUpdated", () => {
|
|
151
|
+
it("データ取得成功後にlastUpdatedが更新される", async () => {
|
|
152
|
+
const { result } = renderHook(() => useGitData());
|
|
153
|
+
|
|
154
|
+
// 初期状態ではnull
|
|
155
|
+
expect(result.current.lastUpdated).toBeNull();
|
|
156
|
+
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(result.current.loading).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ロード完了後にlastUpdatedが設定される
|
|
162
|
+
expect(result.current.lastUpdated).toBeInstanceOf(Date);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("エラーハンドリング", () => {
|
|
167
|
+
it("getAllBranchesがエラーを投げた場合、フォールバック値(空配列)が使用される", async () => {
|
|
168
|
+
// withTimeout がエラーをキャッチしてフォールバック値を返すため、
|
|
169
|
+
// エラー状態にはならず、空配列が設定される
|
|
170
|
+
mockGetAllBranches.mockRejectedValue(new Error("Git error"));
|
|
171
|
+
|
|
172
|
+
const { result } = renderHook(() => useGitData());
|
|
173
|
+
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(result.current.loading).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// エラーはキャッチされ、フォールバック値が使用される
|
|
179
|
+
expect(result.current.error).toBeNull();
|
|
180
|
+
expect(result.current.branches).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("fetchAllRemotesがエラーを投げてもローカル表示は継続される", async () => {
|
|
184
|
+
mockFetchAllRemotes.mockRejectedValue(new Error("Network error"));
|
|
185
|
+
|
|
186
|
+
const { result } = renderHook(() => useGitData());
|
|
187
|
+
|
|
188
|
+
await waitFor(() => {
|
|
189
|
+
expect(result.current.loading).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// リモート取得失敗でもローカルブランチは表示される
|
|
193
|
+
expect(result.current.branches).toHaveLength(2);
|
|
194
|
+
expect(result.current.error).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|