gitforest 0.1.0 → 1.0.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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/github/auth.ts +3 -3
  4. package/src/utils/debug.ts +4 -4
  5. package/.bunignore +0 -7
  6. package/.github/workflows/ci.yml +0 -73
  7. package/CLAUDE.md +0 -111
  8. package/CONTRIBUTING.md +0 -145
  9. package/bun.lock +0 -267
  10. package/bunfig.toml +0 -15
  11. package/cli +0 -0
  12. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  13. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  14. package/docs/ai/architecture.md +0 -169
  15. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  16. package/docs/ai/checks/check-2025-12-02.md +0 -55
  17. package/docs/ai/checks/test-verification-report.md +0 -85
  18. package/docs/ai/implementation-guide.md +0 -776
  19. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  20. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  21. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  22. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  23. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  25. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  26. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  27. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  28. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  29. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  30. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  31. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  32. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  33. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  34. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  35. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  36. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  37. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  38. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  40. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  41. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  42. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  43. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  44. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  45. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  46. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  47. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  49. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  50. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  51. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  52. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  55. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  57. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  58. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  59. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  60. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  61. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  62. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  63. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  64. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  65. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  66. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  67. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  68. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  69. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  70. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  71. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  72. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  73. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  74. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  75. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  76. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  77. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  78. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  79. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  80. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  81. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  83. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  84. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  86. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  87. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  88. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  89. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  90. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  91. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  92. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  93. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  94. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  95. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  96. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  97. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  98. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  99. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  101. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  103. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  104. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  105. package/docs/ai/ui-enhancement-plan.md +0 -562
  106. package/test/integration/app.isolated.tsx +0 -240
  107. package/test/integration/cli-commands.test.ts +0 -287
  108. package/test/integration/cli-validation.test.ts +0 -264
  109. package/test/integration/git-operations.test.ts +0 -218
  110. package/test/integration/scanner.test.ts +0 -228
  111. package/test/preload.ts +0 -18
  112. package/test/unit/cli/commands.test.ts +0 -13
  113. package/test/unit/cli/formatters.test.ts +0 -1116
  114. package/test/unit/cli/github-commands.test.ts +0 -12
  115. package/test/unit/components/CloneDialog.test.tsx +0 -240
  116. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  117. package/test/unit/components/CommandPalette.test.tsx +0 -355
  118. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  119. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  120. package/test/unit/components/FilterBar.test.tsx +0 -43
  121. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  122. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  123. package/test/unit/components/Layout.test.tsx +0 -328
  124. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  125. package/test/unit/components/ProgressBar.test.tsx +0 -138
  126. package/test/unit/components/ProjectItem.test.tsx +0 -182
  127. package/test/unit/components/ProjectList.test.tsx +0 -311
  128. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  129. package/test/unit/components/StatusBar.test.tsx +0 -112
  130. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  131. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  132. package/test/unit/components/test-utils.tsx +0 -63
  133. package/test/unit/config/loader.test.ts +0 -692
  134. package/test/unit/db/database.test.ts +0 -978
  135. package/test/unit/db/index.test.ts +0 -314
  136. package/test/unit/fixtures/setup.ts +0 -186
  137. package/test/unit/git/commands-untested.test.ts +0 -205
  138. package/test/unit/git/commands.test.ts +0 -269
  139. package/test/unit/git/operations.test.ts +0 -322
  140. package/test/unit/git/status.test.ts +0 -219
  141. package/test/unit/github/auth.test.ts +0 -317
  142. package/test/unit/github/cache.test.ts +0 -1028
  143. package/test/unit/github/cli.test.ts +0 -135
  144. package/test/unit/github/unified.test.ts +0 -1201
  145. package/test/unit/graceful-shutdown.test.ts +0 -83
  146. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  147. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  148. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  149. package/test/unit/hooks/useProjects.test.tsx +0 -186
  150. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  151. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  152. package/test/unit/mocks/config.ts +0 -109
  153. package/test/unit/mocks/git-service.ts +0 -274
  154. package/test/unit/mocks/github-service.ts +0 -250
  155. package/test/unit/mocks/index.ts +0 -72
  156. package/test/unit/mocks/project.ts +0 -148
  157. package/test/unit/mocks/state-mocks.ts +0 -187
  158. package/test/unit/mocks/unified.ts +0 -169
  159. package/test/unit/operations/batch.test.ts +0 -216
  160. package/test/unit/operations/commands.test.ts +0 -550
  161. package/test/unit/scanner/errors.test.ts +0 -297
  162. package/test/unit/scanner/index.test.ts +0 -1011
  163. package/test/unit/scanner/markers.test.ts +0 -150
  164. package/test/unit/scanner/submodules.test.ts +0 -99
  165. package/test/unit/services/git-errors.test.ts +0 -190
  166. package/test/unit/services/git.test.ts +0 -442
  167. package/test/unit/services/github-errors.test.ts +0 -293
  168. package/test/unit/services/github.test.ts +0 -200
  169. package/test/unit/state/actions.test.ts +0 -217
  170. package/test/unit/state/reducer.test.ts +0 -745
  171. package/test/unit/state/store.test.tsx +0 -711
  172. package/test/unit/types/commands.test.ts +0 -220
  173. package/test/unit/types/schema.test.ts +0 -179
  174. package/test/unit/utils/array.test.ts +0 -73
  175. package/test/unit/utils/debug.test.ts +0 -23
  176. package/test/unit/utils/errors.test.ts +0 -295
  177. package/test/unit/utils/markdown.test.ts +0 -163
  178. package/test/unit/utils/project-utils.test.ts +0 -756
  179. package/test/unit/utils/rate-limiter.test.ts +0 -256
  180. package/test/unit/utils/retry.test.ts +0 -165
  181. package/test/unit/utils/strip-ansi.ts +0 -13
  182. package/test/unit/utils/timeout.test.ts +0 -93
  183. package/tsconfig.json +0 -29
@@ -1,138 +0,0 @@
1
- /**
2
- * Tests for ProgressBar component
3
- */
4
-
5
- import { describe, test, expect } from "bun:test";
6
- import React from "react";
7
- import { render } from "ink-testing-library";
8
- import { ProgressBar, Spinner } from "../../../src/components/ProgressBar.tsx";
9
-
10
- describe("ProgressBar", () => {
11
- describe("rendering", () => {
12
- test("renders progress bar with current/total", () => {
13
- const { lastFrame } = render(
14
- <ProgressBar label="Processing" current={5} total={10} />
15
- );
16
-
17
- expect(lastFrame()).toContain("Processing");
18
- expect(lastFrame()).toContain("5/10");
19
- });
20
-
21
- test("shows percentage correctly at 50%", () => {
22
- const { lastFrame } = render(
23
- <ProgressBar label="Loading" current={5} total={10} />
24
- );
25
-
26
- expect(lastFrame()).toContain("(50%)");
27
- // Check that the bar has correct filled/empty ratio
28
- expect(lastFrame()).toContain("[██████████░░░░░░░░░░]");
29
- });
30
-
31
- test("handles 0% progress", () => {
32
- const { lastFrame } = render(
33
- <ProgressBar label="Starting" current={0} total={10} />
34
- );
35
-
36
- expect(lastFrame()).toContain("0/10");
37
- expect(lastFrame()).toContain("(0%)");
38
- // All empty bars (24 characters total)
39
- expect(lastFrame()).toContain("[░░░░░░░░░░░░░░░░░░░░]");
40
- });
41
-
42
- test("handles 100% progress", () => {
43
- const { lastFrame } = render(
44
- <ProgressBar label="Complete" current={10} total={10} />
45
- );
46
-
47
- expect(lastFrame()).toContain("10/10");
48
- expect(lastFrame()).toContain("(100%)");
49
- // All filled bars
50
- expect(lastFrame()).toContain("[████████████████████]");
51
- });
52
-
53
- test.skip("handles over 100% progress (BUG: component fails with negative empty value)", () => {
54
- // This test is skipped because the ProgressBar component has a bug
55
- // when current > total, causing empty to be negative
56
- // TODO: Fix ProgressBar component to handle over 100% progress
57
- const { lastFrame } = render(
58
- <ProgressBar label="Overcomplete" current={15} total={10} />
59
- );
60
-
61
- const output = lastFrame();
62
- expect(output).toContain("15/10");
63
- expect(output).toContain("(150%)");
64
- // When over 100%, it should cap at 100% filled
65
- expect(output).toContain("[████████████████████]");
66
- });
67
-
68
- test("handles zero total gracefully", () => {
69
- const { lastFrame } = render(
70
- <ProgressBar label="Empty" current={0} total={0} />
71
- );
72
-
73
- expect(lastFrame()).toContain("0/0");
74
- expect(lastFrame()).toContain("(0%)");
75
- expect(lastFrame()).toContain("[░░░░░░░░░░░░░░░░░░░░]");
76
- });
77
-
78
- test("displays label if provided", () => {
79
- const { lastFrame } = render(
80
- <ProgressBar label="Downloading files" current={25} total={100} />
81
- );
82
-
83
- expect(lastFrame()).toContain("Downloading files");
84
- expect(lastFrame()).toContain("25/100");
85
- expect(lastFrame()).toContain("(25%)");
86
- });
87
-
88
- test("uses custom width when provided", () => {
89
- const { lastFrame } = render(
90
- <ProgressBar label="Test" current={5} total={10} width={10} />
91
- );
92
-
93
- // With width=10, 50% should show 5 filled and 5 empty
94
- expect(lastFrame()).toContain("[█████░░░░░]");
95
- });
96
-
97
- test("has correct colors", () => {
98
- const { lastFrame } = render(
99
- <ProgressBar label="Colored" current={3} total={10} />
100
- );
101
-
102
- const frame = lastFrame();
103
- // Just verify the output contains expected elements
104
- expect(frame).toContain("Colored");
105
- expect(frame).toContain("[██████░░░░░░░░░░░░░░]");
106
- expect(frame).toContain("3/10 (30%)");
107
- });
108
- });
109
- });
110
-
111
- describe("Spinner", () => {
112
- test("renders spinner with label", () => {
113
- const { lastFrame } = render(<Spinner label="Loading..." />);
114
-
115
- expect(lastFrame()).toContain("Loading...");
116
- // Should show one of the spinner frames
117
- const frame = lastFrame();
118
- expect(frame).toBeDefined();
119
- const hasSpinnerFrame = frame && ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].some(
120
- (spin) => frame.includes(spin)
121
- );
122
- expect(hasSpinnerFrame).toBe(true);
123
- });
124
-
125
- test("has correct colors", () => {
126
- const { lastFrame } = render(<Spinner label="Working" />);
127
-
128
- const output = lastFrame();
129
- expect(output).toBeDefined();
130
- // Verify spinner and label are present
131
- expect(output).toContain("Working");
132
- // Should show one of the spinner frames
133
- const hasSpinnerFrame = output && ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].some(
134
- (spin) => output.includes(spin)
135
- );
136
- expect(hasSpinnerFrame).toBe(true);
137
- });
138
- });
@@ -1,182 +0,0 @@
1
- /**
2
- * Tests for ProjectItem component
3
- */
4
-
5
- import { describe, test, expect } from "bun:test";
6
- import React from "react";
7
- import { render } from "ink-testing-library";
8
- import { ProjectItem } from "../../../src/components/ProjectItem.tsx";
9
- import {
10
- createMockProject,
11
- createDirtyProject,
12
- createAheadProject,
13
- createNonGitProject,
14
- createSubmoduleProject,
15
- } from "../mocks/index.ts";
16
- import { createNoRemoteStatus } from "../mocks/git-service.ts";
17
-
18
- describe("ProjectItem", () => {
19
- describe("rendering", () => {
20
- test("renders clean git project with checkmark", () => {
21
- const project = createMockProject({ name: "test-project" });
22
- const { lastFrame } = render(
23
- <ProjectItem project={project} isSelected={false} isCursor={false} />
24
- );
25
-
26
- expect(lastFrame()).toContain("test-project");
27
- expect(lastFrame()).toContain("✓");
28
- expect(lastFrame()).toContain("[ ]");
29
- });
30
-
31
- test("renders dirty project with yellow dot", () => {
32
- const project = createDirtyProject("dirty-project");
33
- const { lastFrame } = render(
34
- <ProjectItem project={project} isSelected={false} isCursor={false} />
35
- );
36
-
37
- expect(lastFrame()).toContain("dirty-project");
38
- expect(lastFrame()).toContain("●");
39
- });
40
-
41
- test("renders project with unpushed commits", () => {
42
- const project = createAheadProject("ahead-project", 5);
43
- const { lastFrame } = render(
44
- <ProjectItem project={project} isSelected={false} isCursor={false} />
45
- );
46
-
47
- expect(lastFrame()).toContain("ahead-project");
48
- expect(lastFrame()).toContain("↑5");
49
- });
50
-
51
- test("renders project with no remote", () => {
52
- const project = createMockProject({
53
- name: "no-remote-project",
54
- status: createNoRemoteStatus(),
55
- });
56
- const { lastFrame } = render(
57
- <ProjectItem project={project} isSelected={false} isCursor={false} />
58
- );
59
-
60
- expect(lastFrame()).toContain("no-remote-project");
61
- expect(lastFrame()).toContain("no-remote");
62
- });
63
-
64
- test("renders non-git project with marker", () => {
65
- const project = createNonGitProject("npm-project", "package.json");
66
- const { lastFrame } = render(
67
- <ProjectItem project={project} isSelected={false} isCursor={false} />
68
- );
69
-
70
- expect(lastFrame()).toContain("npm-project");
71
- expect(lastFrame()).toContain("[package.json]");
72
- expect(lastFrame()).toContain("-"); // Non-git status icon
73
- });
74
-
75
- test("renders submodule with indicator", () => {
76
- const project = createSubmoduleProject("my-submodule");
77
- const { lastFrame } = render(
78
- <ProjectItem project={project} isSelected={false} isCursor={false} />
79
- );
80
-
81
- expect(lastFrame()).toContain("my-submodule");
82
- expect(lastFrame()).toContain("(sub)");
83
- expect(lastFrame()).toContain("○"); // Submodule status icon
84
- });
85
- });
86
-
87
- describe("selection state", () => {
88
- test("shows selection checkbox when selected", () => {
89
- const project = createMockProject({ name: "selected-project" });
90
- const { lastFrame } = render(
91
- <ProjectItem project={project} isSelected={true} isCursor={false} />
92
- );
93
-
94
- expect(lastFrame()).toContain("[x]");
95
- });
96
-
97
- test("shows empty checkbox when not selected", () => {
98
- const project = createMockProject({ name: "unselected-project" });
99
- const { lastFrame } = render(
100
- <ProjectItem project={project} isSelected={false} isCursor={false} />
101
- );
102
-
103
- expect(lastFrame()).toContain("[ ]");
104
- });
105
- });
106
-
107
- describe("cursor state", () => {
108
- test("shows cursor indicator when at cursor", () => {
109
- const project = createMockProject({ name: "cursor-project" });
110
- const { lastFrame } = render(
111
- <ProjectItem project={project} isSelected={false} isCursor={true} />
112
- );
113
-
114
- expect(lastFrame()).toContain(">");
115
- expect(lastFrame()).toContain("cursor-project");
116
- });
117
-
118
- test("shows no cursor indicator when not at cursor", () => {
119
- const project = createMockProject({ name: "non-cursor-project" });
120
- const { lastFrame } = render(
121
- <ProjectItem project={project} isSelected={false} isCursor={false} />
122
- );
123
-
124
- // Should not have cursor indicator at position 0
125
- const frame = lastFrame()!;
126
- expect(frame.startsWith(">")).toBe(false);
127
- });
128
- });
129
-
130
- describe("last activity formatting", () => {
131
- test("shows 'today' for recent commits", () => {
132
- const project = createMockProject({
133
- name: "recent-project",
134
- status: {
135
- ...createMockProject().status!,
136
- lastLocalCommit: new Date(),
137
- },
138
- });
139
- const { lastFrame } = render(
140
- <ProjectItem project={project} isSelected={false} isCursor={false} />
141
- );
142
-
143
- expect(lastFrame()).toContain("today");
144
- });
145
-
146
- test("shows 'yesterday' for day-old commits", () => {
147
- const yesterday = new Date();
148
- yesterday.setDate(yesterday.getDate() - 1);
149
-
150
- const project = createMockProject({
151
- name: "yesterday-project",
152
- status: {
153
- ...createMockProject().status!,
154
- lastLocalCommit: yesterday,
155
- },
156
- });
157
- const { lastFrame } = render(
158
- <ProjectItem project={project} isSelected={false} isCursor={false} />
159
- );
160
-
161
- expect(lastFrame()).toContain("yesterday");
162
- });
163
-
164
- test("shows days ago for recent commits", () => {
165
- const fiveDaysAgo = new Date();
166
- fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5);
167
-
168
- const project = createMockProject({
169
- name: "old-project",
170
- status: {
171
- ...createMockProject().status!,
172
- lastLocalCommit: fiveDaysAgo,
173
- },
174
- });
175
- const { lastFrame } = render(
176
- <ProjectItem project={project} isSelected={false} isCursor={false} />
177
- );
178
-
179
- expect(lastFrame()).toContain("5d ago");
180
- });
181
- });
182
- });
@@ -1,311 +0,0 @@
1
- /**
2
- * Tests for ProjectList component
3
- */
4
-
5
- import { describe, test, expect, beforeEach, mock } from "bun:test";
6
- import React from "react";
7
- import { render } from "ink-testing-library";
8
- import { ProjectList } from "../../../src/components/ProjectList.tsx";
9
- import { StoreContext } from "../../../src/state/store.tsx";
10
- import type { UnifiedAppState, UnifiedAppAction } from "../../../src/types/index.ts";
11
- import {
12
- createMockUnifiedAppState,
13
- createMockUnifiedRepo,
14
- createLocalOnlyUnifiedRepo,
15
- createGitHubOnlyUnifiedRepo,
16
- createSyncedUnifiedRepo
17
- } from "../mocks/index.ts";
18
-
19
- /**
20
- * Helper to create a mock store context value
21
- */
22
- function createMockStoreContext(state: Partial<UnifiedAppState> = {}) {
23
- const mockDispatch = mock((action: UnifiedAppAction) => {});
24
- const mockState = createMockUnifiedAppState(state);
25
-
26
- return {
27
- state: mockState,
28
- dispatch: mockDispatch,
29
- };
30
- }
31
-
32
- /**
33
- * Helper to render ProjectList with custom store context
34
- */
35
- function renderProjectListWithContext(contextValue: { state: UnifiedAppState; dispatch: any }, height?: number) {
36
- return render(
37
- <StoreContext.Provider value={contextValue}>
38
- <ProjectList height={height} />
39
- </StoreContext.Provider>
40
- );
41
- }
42
-
43
- describe("ProjectList", () => {
44
- describe("loading state", () => {
45
- test("shows spinner when isLoading is true", () => {
46
- const context = createMockStoreContext({ isLoading: true });
47
- const { lastFrame } = renderProjectListWithContext(context);
48
-
49
- expect(lastFrame()).toContain("Scanning projects...");
50
- // Should not show headers or list during loading
51
- expect(lastFrame()).not.toContain("Name");
52
- expect(lastFrame()).not.toContain("Status");
53
- });
54
-
55
- test("shows spinner centered in specified height", () => {
56
- const context = createMockStoreContext({ isLoading: true });
57
- const { lastFrame } = renderProjectListWithContext(context, 10);
58
-
59
- // The spinner should be rendered with the specified height
60
- const output = lastFrame();
61
- expect(output).toContain("Scanning projects...");
62
- // Ink's Box component with height creates empty lines for spacing
63
- expect(output?.split('\n').length).toBeGreaterThan(5);
64
- });
65
- });
66
-
67
- describe("empty state", () => {
68
- test("shows 'No repositories found' when empty", () => {
69
- const context = createMockStoreContext({
70
- isLoading: false,
71
- unifiedRepos: []
72
- });
73
- const { lastFrame } = renderProjectListWithContext(context);
74
-
75
- expect(lastFrame()).toContain("No repositories found");
76
- // Should still show headers even when empty
77
- expect(lastFrame()).toContain("Name");
78
- // Headers use nerd font icons
79
- expect(lastFrame()).toContain(""); // repo icon
80
- expect(lastFrame()).toContain(""); // branch icon
81
- });
82
-
83
- test("shows filter hint when filterText is set", () => {
84
- const context = createMockStoreContext({
85
- isLoading: false,
86
- unifiedRepos: [],
87
- filterText: "test-filter"
88
- });
89
- const { lastFrame } = renderProjectListWithContext(context);
90
-
91
- expect(lastFrame()).toContain("No repositories found");
92
- expect(lastFrame()).toContain("Try a different filter");
93
- });
94
-
95
- test("shows GitHub token hint when viewMode is 'github' and no repos", () => {
96
- const context = createMockStoreContext({
97
- isLoading: false,
98
- unifiedRepos: [],
99
- viewMode: "github",
100
- githubRepos: []
101
- });
102
- const { lastFrame } = renderProjectListWithContext(context);
103
-
104
- expect(lastFrame()).toContain("No repositories found");
105
- expect(lastFrame()).toContain("Set GITHUB_TOKEN to see GitHub repos");
106
- });
107
-
108
- test("shows GitHub error when githubError is set", () => {
109
- const context = createMockStoreContext({
110
- isLoading: false,
111
- unifiedRepos: [],
112
- viewMode: "github",
113
- githubRepos: [],
114
- githubError: "API rate limit exceeded"
115
- });
116
- const { lastFrame } = renderProjectListWithContext(context);
117
-
118
- expect(lastFrame()).toContain("No repositories found");
119
- expect(lastFrame()).toContain("GitHub error: API rate limit exceeded");
120
- expect(lastFrame()).not.toContain("Set GITHUB_TOKEN");
121
- });
122
- });
123
-
124
- describe("list rendering", () => {
125
- test("renders correct number of visible repos", () => {
126
- const repos = [
127
- createMockUnifiedRepo({ name: "repo1" }),
128
- createMockUnifiedRepo({ name: "repo2" }),
129
- createMockUnifiedRepo({ name: "repo3" }),
130
- ];
131
-
132
- const context = createMockStoreContext({
133
- isLoading: false,
134
- unifiedRepos: repos
135
- });
136
- const { lastFrame } = renderProjectListWithContext(context, 10);
137
-
138
- // Should show all 3 repos (with headers taking 2 lines, 8 lines left for repos)
139
- expect(lastFrame()).toContain("repo1");
140
- expect(lastFrame()).toContain("repo2");
141
- expect(lastFrame()).toContain("repo3");
142
- });
143
-
144
- test("highlights cursor position correctly", () => {
145
- const repos = [
146
- createMockUnifiedRepo({ name: "repo1" }),
147
- createMockUnifiedRepo({ name: "repo2" }),
148
- createMockUnifiedRepo({ name: "repo3" }),
149
- ];
150
-
151
- const context = createMockStoreContext({
152
- isLoading: false,
153
- unifiedRepos: repos,
154
- cursorIndex: 1 // Second repo
155
- });
156
- const { lastFrame } = renderProjectListWithContext(context);
157
-
158
- // The cursor should be on the second repo (index 1)
159
- // In the actual implementation, this would show with different styling
160
- // For now we just verify the repo is rendered
161
- expect(lastFrame()).toContain("repo2");
162
- });
163
-
164
- test("shows selection state correctly", () => {
165
- const repos = [
166
- createMockUnifiedRepo({ name: "repo1" }),
167
- createMockUnifiedRepo({ name: "repo2" }),
168
- createMockUnifiedRepo({ name: "repo3" }),
169
- ];
170
-
171
- const context = createMockStoreContext({
172
- isLoading: false,
173
- unifiedRepos: repos,
174
- selectedIndices: new Set([0, 2]) // First and third repos selected
175
- });
176
- const { lastFrame } = renderProjectListWithContext(context);
177
-
178
- // All repos should be rendered
179
- expect(lastFrame()).toContain("repo1");
180
- expect(lastFrame()).toContain("repo2");
181
- expect(lastFrame()).toContain("repo3");
182
- // The actual selection indicator would be shown by UnifiedProjectItem
183
- });
184
-
185
- test("renders UnifiedProjectItem for each visible repo", () => {
186
- const repos = [
187
- createLocalOnlyUnifiedRepo("local-repo"),
188
- createGitHubOnlyUnifiedRepo("github-repo"),
189
- createSyncedUnifiedRepo("synced-repo")
190
- ];
191
-
192
- const context = createMockStoreContext({
193
- isLoading: false,
194
- unifiedRepos: repos
195
- });
196
- const { lastFrame } = renderProjectListWithContext(context);
197
-
198
- // Should render all three repos with their respective types
199
- expect(lastFrame()).toContain("local-repo");
200
- expect(lastFrame()).toContain("github-repo");
201
- expect(lastFrame()).toContain("synced-repo");
202
- });
203
-
204
- test("always renders ColumnHeader", () => {
205
- const context = createMockStoreContext({
206
- isLoading: false,
207
- unifiedRepos: [createMockUnifiedRepo({ name: "repo1" })]
208
- });
209
- const { lastFrame } = renderProjectListWithContext(context);
210
-
211
- expect(lastFrame()).toContain("Name");
212
- // Headers use nerd font icons
213
- expect(lastFrame()).toContain(""); // repo icon
214
- expect(lastFrame()).toContain(""); // branch icon
215
- });
216
-
217
- test("ColumnSeparator returns null (no visual separator)", () => {
218
- const context = createMockStoreContext({
219
- isLoading: false,
220
- unifiedRepos: [createMockUnifiedRepo({ name: "repo1" })]
221
- });
222
- const { lastFrame } = renderProjectListWithContext(context);
223
-
224
- // ColumnSeparator component returns null, so no separator line is rendered
225
- // The separation is handled by spacing in the layout
226
- expect(lastFrame()).not.toMatch(/[-─]/);
227
- });
228
- });
229
-
230
- describe("scrolling", () => {
231
- test("calculates visible range correctly with many repos", () => {
232
- const repos = Array.from({ length: 50 }, (_, i) =>
233
- createMockUnifiedRepo({ name: `repo${i}` })
234
- );
235
-
236
- const context = createMockStoreContext({
237
- isLoading: false,
238
- unifiedRepos: repos
239
- });
240
- // Height of 10 means 8 lines for repos (2 for headers)
241
- const { lastFrame } = renderProjectListWithContext(context, 10);
242
-
243
- // Should only show first 8 repos
244
- expect(lastFrame()).toContain("repo0");
245
- expect(lastFrame()).toContain("repo7");
246
- expect(lastFrame()).not.toContain("repo8");
247
- // Should show scroll indicator
248
- expect(lastFrame()).toContain("more below");
249
- });
250
-
251
- test("shows scroll indicator when more items below", () => {
252
- const repos = Array.from({ length: 20 }, (_, i) =>
253
- createMockUnifiedRepo({ name: `repo${i}` })
254
- );
255
-
256
- const context = createMockStoreContext({
257
- isLoading: false,
258
- unifiedRepos: repos
259
- });
260
- const { lastFrame } = renderProjectListWithContext(context, 8); // 6 visible repos
261
-
262
- // Should show scroll indicator with remaining count
263
- expect(lastFrame()).toContain("more below");
264
- expect(lastFrame()).toMatch(/\d+ more below/);
265
- });
266
-
267
- test("adjusts startIndex near end of list", () => {
268
- const repos = Array.from({ length: 10 }, (_, i) =>
269
- createMockUnifiedRepo({ name: `repo${i}` })
270
- );
271
-
272
- // Cursor at the end
273
- const context = createMockStoreContext({
274
- isLoading: false,
275
- unifiedRepos: repos,
276
- cursorIndex: 9 // Last repo
277
- });
278
- const { lastFrame } = renderProjectListWithContext(context, 6); // 4 visible repos
279
-
280
- // Should show the last repos
281
- expect(lastFrame()).toContain("repo9");
282
- expect(lastFrame()).toContain("repo8");
283
- expect(lastFrame()).toContain("repo7");
284
- expect(lastFrame()).toContain("repo6");
285
- expect(lastFrame()).not.toContain("repo0");
286
- });
287
-
288
- test("centers cursor in visible area", () => {
289
- const repos = Array.from({ length: 30 }, (_, i) =>
290
- createMockUnifiedRepo({ name: `repo${i}` })
291
- );
292
-
293
- // Cursor in the middle
294
- const context = createMockStoreContext({
295
- isLoading: false,
296
- unifiedRepos: repos,
297
- cursorIndex: 15
298
- });
299
- const { lastFrame } = renderProjectListWithContext(context, 10); // 8 visible repos
300
-
301
- // With cursor at 15 and 8 visible repos, startIndex should be around 11
302
- // So we should see repos 11-18
303
- expect(lastFrame()).toContain("repo11");
304
- expect(lastFrame()).toContain("repo15"); // Cursor should be visible
305
- expect(lastFrame()).toContain("repo18");
306
- // Should not see repos much earlier or later
307
- expect(lastFrame()).not.toContain("repo10");
308
- expect(lastFrame()).not.toContain("repo19");
309
- });
310
- });
311
- });