gitforest 0.1.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 (256) hide show
  1. package/.bunignore +7 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/CLAUDE.md +111 -0
  4. package/CONTRIBUTING.md +145 -0
  5. package/README.md +168 -0
  6. package/bun.lock +267 -0
  7. package/bunfig.toml +15 -0
  8. package/cli +0 -0
  9. package/config/gitforest.example.yaml +94 -0
  10. package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
  11. package/docs/ai/VERIFICATION_REPORT.md +87 -0
  12. package/docs/ai/architecture.md +169 -0
  13. package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
  14. package/docs/ai/checks/check-2025-12-02.md +55 -0
  15. package/docs/ai/checks/test-verification-report.md +85 -0
  16. package/docs/ai/implementation-guide.md +776 -0
  17. package/docs/ai/research/gitty-codebase-analysis.md +221 -0
  18. package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
  19. package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
  20. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
  21. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
  22. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
  23. package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
  24. package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
  25. package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
  26. package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
  27. package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
  28. package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
  29. package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
  30. package/docs/ai/tickets/TASK-sitrep.md +28 -0
  31. package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
  32. package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
  33. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
  34. package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
  35. package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
  36. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
  37. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
  38. package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
  39. package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
  40. package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
  41. package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
  42. package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
  43. package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
  44. package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
  45. package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
  46. package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
  47. package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
  48. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
  49. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
  50. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
  51. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
  52. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
  53. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
  54. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
  55. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
  56. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
  57. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
  58. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
  59. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
  60. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
  61. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
  62. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
  63. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
  64. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
  65. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
  66. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
  67. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
  68. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
  69. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
  70. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
  71. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
  72. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
  73. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
  74. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
  75. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
  76. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
  77. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
  78. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
  79. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
  80. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
  81. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
  82. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
  83. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
  84. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
  85. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
  86. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
  87. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
  88. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
  89. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
  90. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
  91. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
  92. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
  93. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
  94. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
  95. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
  96. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
  97. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
  98. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
  99. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
  100. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
  101. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
  102. package/docs/ai/tkt-001-fix-database-error.md +217 -0
  103. package/docs/ai/ui-enhancement-plan.md +562 -0
  104. package/package.json +50 -0
  105. package/src/app.tsx +43 -0
  106. package/src/cli/config.ts +94 -0
  107. package/src/cli/formatters.ts +632 -0
  108. package/src/cli/index.ts +583 -0
  109. package/src/components/CloneDialog.tsx +137 -0
  110. package/src/components/ColumnHeader.tsx +128 -0
  111. package/src/components/CommandPalette.tsx +120 -0
  112. package/src/components/ConfirmDialog.tsx +105 -0
  113. package/src/components/ErrorBoundary.tsx +128 -0
  114. package/src/components/FilterBar.tsx +71 -0
  115. package/src/components/FilterOptionsOverlay.tsx +131 -0
  116. package/src/components/HelpOverlay.tsx +120 -0
  117. package/src/components/Layout.tsx +379 -0
  118. package/src/components/MarkdownRenderer.tsx +127 -0
  119. package/src/components/ProgressBar.tsx +53 -0
  120. package/src/components/ProjectItem.tsx +143 -0
  121. package/src/components/ProjectList.tsx +90 -0
  122. package/src/components/RepoDetailModal.tsx +367 -0
  123. package/src/components/StatusBar.tsx +188 -0
  124. package/src/components/UnifiedProjectItem.tsx +436 -0
  125. package/src/components/ViewModeIndicator.tsx +37 -0
  126. package/src/components/onboarding/CompleteStep.tsx +82 -0
  127. package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
  128. package/src/components/onboarding/DirectoriesStep.tsx +847 -0
  129. package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
  130. package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
  131. package/src/components/onboarding/OnboardingWizard.tsx +130 -0
  132. package/src/components/onboarding/WelcomeStep.tsx +69 -0
  133. package/src/config/loader.ts +263 -0
  134. package/src/config/onboarding.ts +67 -0
  135. package/src/constants.ts +96 -0
  136. package/src/db/index.ts +147 -0
  137. package/src/db/schema.ts +70 -0
  138. package/src/git/commands.ts +283 -0
  139. package/src/git/index.ts +2 -0
  140. package/src/git/operations.ts +93 -0
  141. package/src/git/service.ts +539 -0
  142. package/src/git/status.ts +84 -0
  143. package/src/git/types.ts +5 -0
  144. package/src/github/auth.ts +311 -0
  145. package/src/github/cache.ts +231 -0
  146. package/src/github/cli.ts +22 -0
  147. package/src/github/unified.ts +415 -0
  148. package/src/hooks/useBackgroundFetch.ts +76 -0
  149. package/src/hooks/useConfirmDialogActions.ts +120 -0
  150. package/src/hooks/useKeyBindings.ts +656 -0
  151. package/src/hooks/useProjects.ts +47 -0
  152. package/src/hooks/useUnifiedRepos.ts +317 -0
  153. package/src/index.tsx +494 -0
  154. package/src/operations/batch.ts +280 -0
  155. package/src/operations/commands.ts +140 -0
  156. package/src/operations/index.ts +37 -0
  157. package/src/scanner/index.ts +424 -0
  158. package/src/scanner/markers.ts +43 -0
  159. package/src/scanner/submodules.ts +61 -0
  160. package/src/services/git.ts +484 -0
  161. package/src/services/github.ts +676 -0
  162. package/src/services/index.ts +28 -0
  163. package/src/services/types.ts +99 -0
  164. package/src/state/actions.ts +175 -0
  165. package/src/state/reducer.ts +294 -0
  166. package/src/state/store.tsx +216 -0
  167. package/src/state/types.ts +8 -0
  168. package/src/types/index.ts +383 -0
  169. package/src/ui/theme.ts +44 -0
  170. package/src/utils/array.ts +14 -0
  171. package/src/utils/debug.ts +38 -0
  172. package/src/utils/errors.ts +17 -0
  173. package/src/utils/index.ts +8 -0
  174. package/src/utils/markdown.ts +230 -0
  175. package/src/utils/project-utils.ts +129 -0
  176. package/src/utils/rate-limiter.ts +134 -0
  177. package/src/utils/retry.ts +147 -0
  178. package/src/utils/timeout.ts +56 -0
  179. package/test/integration/app.isolated.tsx +240 -0
  180. package/test/integration/cli-commands.test.ts +287 -0
  181. package/test/integration/cli-validation.test.ts +264 -0
  182. package/test/integration/git-operations.test.ts +218 -0
  183. package/test/integration/scanner.test.ts +228 -0
  184. package/test/preload.ts +18 -0
  185. package/test/unit/cli/commands.test.ts +13 -0
  186. package/test/unit/cli/formatters.test.ts +1116 -0
  187. package/test/unit/cli/github-commands.test.ts +12 -0
  188. package/test/unit/components/CloneDialog.test.tsx +240 -0
  189. package/test/unit/components/ColumnHeader.test.tsx +128 -0
  190. package/test/unit/components/CommandPalette.test.tsx +355 -0
  191. package/test/unit/components/ConfirmDialog.test.tsx +111 -0
  192. package/test/unit/components/ErrorBoundary.test.tsx +139 -0
  193. package/test/unit/components/FilterBar.test.tsx +43 -0
  194. package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
  195. package/test/unit/components/HelpOverlay.test.tsx +90 -0
  196. package/test/unit/components/Layout.test.tsx +328 -0
  197. package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
  198. package/test/unit/components/ProgressBar.test.tsx +138 -0
  199. package/test/unit/components/ProjectItem.test.tsx +182 -0
  200. package/test/unit/components/ProjectList.test.tsx +311 -0
  201. package/test/unit/components/RepoDetailModal.test.tsx +445 -0
  202. package/test/unit/components/StatusBar.test.tsx +112 -0
  203. package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
  204. package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
  205. package/test/unit/components/test-utils.tsx +63 -0
  206. package/test/unit/config/loader.test.ts +692 -0
  207. package/test/unit/db/database.test.ts +978 -0
  208. package/test/unit/db/index.test.ts +314 -0
  209. package/test/unit/fixtures/setup.ts +186 -0
  210. package/test/unit/git/commands-untested.test.ts +205 -0
  211. package/test/unit/git/commands.test.ts +269 -0
  212. package/test/unit/git/operations.test.ts +322 -0
  213. package/test/unit/git/status.test.ts +219 -0
  214. package/test/unit/github/auth.test.ts +317 -0
  215. package/test/unit/github/cache.test.ts +1028 -0
  216. package/test/unit/github/cli.test.ts +135 -0
  217. package/test/unit/github/unified.test.ts +1201 -0
  218. package/test/unit/graceful-shutdown.test.ts +83 -0
  219. package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
  220. package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
  221. package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
  222. package/test/unit/hooks/useProjects.test.tsx +186 -0
  223. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
  224. package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
  225. package/test/unit/mocks/config.ts +109 -0
  226. package/test/unit/mocks/git-service.ts +274 -0
  227. package/test/unit/mocks/github-service.ts +250 -0
  228. package/test/unit/mocks/index.ts +72 -0
  229. package/test/unit/mocks/project.ts +148 -0
  230. package/test/unit/mocks/state-mocks.ts +187 -0
  231. package/test/unit/mocks/unified.ts +169 -0
  232. package/test/unit/operations/batch.test.ts +216 -0
  233. package/test/unit/operations/commands.test.ts +550 -0
  234. package/test/unit/scanner/errors.test.ts +297 -0
  235. package/test/unit/scanner/index.test.ts +1011 -0
  236. package/test/unit/scanner/markers.test.ts +150 -0
  237. package/test/unit/scanner/submodules.test.ts +99 -0
  238. package/test/unit/services/git-errors.test.ts +190 -0
  239. package/test/unit/services/git.test.ts +442 -0
  240. package/test/unit/services/github-errors.test.ts +293 -0
  241. package/test/unit/services/github.test.ts +200 -0
  242. package/test/unit/state/actions.test.ts +217 -0
  243. package/test/unit/state/reducer.test.ts +745 -0
  244. package/test/unit/state/store.test.tsx +711 -0
  245. package/test/unit/types/commands.test.ts +220 -0
  246. package/test/unit/types/schema.test.ts +179 -0
  247. package/test/unit/utils/array.test.ts +73 -0
  248. package/test/unit/utils/debug.test.ts +23 -0
  249. package/test/unit/utils/errors.test.ts +295 -0
  250. package/test/unit/utils/markdown.test.ts +163 -0
  251. package/test/unit/utils/project-utils.test.ts +756 -0
  252. package/test/unit/utils/rate-limiter.test.ts +256 -0
  253. package/test/unit/utils/retry.test.ts +165 -0
  254. package/test/unit/utils/strip-ansi.ts +13 -0
  255. package/test/unit/utils/timeout.test.ts +93 -0
  256. package/tsconfig.json +29 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Tests for FilterOptionsOverlay 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 { FilterOptionsOverlay } from "../../../src/components/FilterOptionsOverlay.tsx";
9
+ import { StoreProvider } from "../../../src/state/store.tsx";
10
+
11
+ // Helper to render with store provider
12
+ function renderWithStore(ui: React.ReactNode) {
13
+ return render(<StoreProvider>{ui}</StoreProvider>);
14
+ }
15
+
16
+ describe("FilterOptionsOverlay", () => {
17
+ test("renders title", () => {
18
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
19
+
20
+ expect(lastFrame()).toContain("Filter Options");
21
+ });
22
+
23
+ describe("Quick Filters section", () => {
24
+ test("shows all filter options with keyboard shortcuts", () => {
25
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
26
+
27
+ expect(lastFrame()).toContain("Quick Filters:");
28
+
29
+ // Check all filter options are present
30
+ expect(lastFrame()).toContain("0");
31
+ expect(lastFrame()).toContain("All repos");
32
+
33
+ expect(lastFrame()).toContain("1");
34
+ expect(lastFrame()).toContain("Dirty (uncommitted)");
35
+
36
+ expect(lastFrame()).toContain("2");
37
+ expect(lastFrame()).toContain("Unpushed commits");
38
+
39
+ expect(lastFrame()).toContain("3");
40
+ expect(lastFrame()).toContain("No remote configured");
41
+
42
+ expect(lastFrame()).toContain("4");
43
+ expect(lastFrame()).toContain("GitHub-only");
44
+
45
+ expect(lastFrame()).toContain("5");
46
+ expect(lastFrame()).toContain("Local-only");
47
+
48
+ expect(lastFrame()).toContain("6");
49
+ expect(lastFrame()).toContain("Private repos");
50
+
51
+ expect(lastFrame()).toContain("7");
52
+ expect(lastFrame()).toContain("Public repos");
53
+
54
+ expect(lastFrame()).toContain("8");
55
+ expect(lastFrame()).toContain("Archived");
56
+
57
+ expect(lastFrame()).toContain("9");
58
+ expect(lastFrame()).toContain("Forks");
59
+ });
60
+
61
+ test("shows active filter with checkmark", () => {
62
+ // Mock a custom state where "dirty" filter is active
63
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
64
+
65
+ // Default state has "all" filter active
66
+ expect(lastFrame()).toContain("All repos");
67
+ // Should contain checkmark for active filter
68
+ const frame = lastFrame();
69
+ expect(frame && (frame.includes("✓") || frame.includes("✓"))).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe("View Modes section", () => {
74
+ test("shows view mode options with Tab instruction", () => {
75
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
76
+
77
+ expect(lastFrame()).toContain("View Modes:");
78
+ expect(lastFrame()).toContain("(Tab to cycle)");
79
+
80
+ // Check all view modes are present
81
+ expect(lastFrame()).toContain("local");
82
+ expect(lastFrame()).toContain("github");
83
+ expect(lastFrame()).toContain("combined");
84
+ });
85
+
86
+ test("highlights current view mode", () => {
87
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
88
+
89
+ // Default view mode should be highlighted
90
+ // Since we can't easily test colors, we verify the text is present
91
+ expect(lastFrame()).toContain("local");
92
+ });
93
+ });
94
+
95
+ describe("Sort section", () => {
96
+ test("shows sort options with 's' instruction", () => {
97
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
98
+
99
+ expect(lastFrame()).toContain("Sort:");
100
+ expect(lastFrame()).toContain("(s to cycle)");
101
+
102
+ // Check all sort options are present
103
+ expect(lastFrame()).toContain("status");
104
+ expect(lastFrame()).toContain("name");
105
+ expect(lastFrame()).toContain("lastActivity");
106
+ expect(lastFrame()).toContain("stars");
107
+ expect(lastFrame()).toContain("size");
108
+ });
109
+
110
+ test("highlights current sort field", () => {
111
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
112
+
113
+ // Default sort field should be highlighted
114
+ expect(lastFrame()).toContain("status");
115
+ });
116
+ });
117
+
118
+ describe("Search section", () => {
119
+ test("shows search functionality", () => {
120
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
121
+
122
+ expect(lastFrame()).toContain("Search:");
123
+ expect(lastFrame()).toContain("/");
124
+ expect(lastFrame()).toContain("Start text search (by name, description, path)");
125
+ });
126
+ });
127
+
128
+ describe("Language Filter section", () => {
129
+ test("shows language filter instructions", () => {
130
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
131
+
132
+ expect(lastFrame()).toContain("Language Filter:");
133
+ expect(lastFrame()).toContain("Type language name to filter (e.g., typescript, rust)");
134
+ });
135
+
136
+ test("shows current language filter when set", () => {
137
+ // We can't easily mock the state with the current setup
138
+ // But we can verify the structure is there
139
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
140
+
141
+ expect(lastFrame()).toContain("Current:");
142
+ expect(lastFrame()).toContain("[none]");
143
+ });
144
+ });
145
+
146
+ describe("Footer", () => {
147
+ test("shows close instruction", () => {
148
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
149
+
150
+ expect(lastFrame()).toContain("Press any key to close");
151
+ });
152
+ });
153
+
154
+ describe("Visual layout", () => {
155
+ test("has proper border styling", () => {
156
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
157
+
158
+ // Check for border characters (this is a simplified check)
159
+ const frame = lastFrame();
160
+ // The component should render with border styling
161
+ expect(frame).toBeTruthy();
162
+ expect(frame && frame.length).toBeGreaterThan(0);
163
+ });
164
+
165
+ test("has proper section spacing", () => {
166
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
167
+
168
+ const frame = lastFrame();
169
+
170
+ // Verify all sections are present
171
+ expect(frame).toContain("Filter Options");
172
+ expect(frame).toContain("Quick Filters:");
173
+ expect(frame).toContain("View Modes:");
174
+ expect(frame).toContain("Sort:");
175
+ expect(frame).toContain("Search:");
176
+ expect(frame).toContain("Language Filter:");
177
+ });
178
+ });
179
+
180
+ describe("Keyboard shortcuts", () => {
181
+ test("shows all keyboard shortcuts", () => {
182
+ const { lastFrame } = renderWithStore(<FilterOptionsOverlay />);
183
+
184
+ const frame = lastFrame();
185
+
186
+ // Check numbers 0-9 are shown for quick filters
187
+ for (let i = 0; i <= 9; i++) {
188
+ expect(frame).toContain(i.toString());
189
+ }
190
+
191
+ // Check other shortcuts
192
+ expect(frame).toContain("/"); // Search
193
+ expect(frame).toContain("s"); // Sort
194
+ expect(frame).toContain("Tab"); // View mode cycle
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Tests for HelpOverlay 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 { HelpOverlay } from "../../../src/components/HelpOverlay.tsx";
9
+
10
+ describe("HelpOverlay", () => {
11
+ test("renders keyboard shortcuts header", () => {
12
+ const { lastFrame } = render(<HelpOverlay />);
13
+
14
+ expect(lastFrame()).toContain("Keyboard Shortcuts");
15
+ });
16
+
17
+ describe("navigation section", () => {
18
+ test("shows navigation keys", () => {
19
+ const { lastFrame } = render(<HelpOverlay />);
20
+
21
+ expect(lastFrame()).toContain("Navigation");
22
+ expect(lastFrame()).toContain("Move down");
23
+ expect(lastFrame()).toContain("Move up");
24
+ expect(lastFrame()).toContain("Go to top");
25
+ expect(lastFrame()).toContain("Go to bottom");
26
+ });
27
+ });
28
+
29
+ describe("selection section", () => {
30
+ test("shows selection keys", () => {
31
+ const { lastFrame } = render(<HelpOverlay />);
32
+
33
+ expect(lastFrame()).toContain("Selection");
34
+ expect(lastFrame()).toContain("Toggle selection");
35
+ expect(lastFrame()).toContain("Select all");
36
+ });
37
+ });
38
+
39
+ describe("git operations section", () => {
40
+ test("shows git operation keys", () => {
41
+ const { lastFrame } = render(<HelpOverlay />);
42
+
43
+ expect(lastFrame()).toContain("Git Operations");
44
+ expect(lastFrame()).toContain("Push selected");
45
+ expect(lastFrame()).toContain("Pull all repos");
46
+ // Text may wrap across lines - check for key parts
47
+ expect(lastFrame()).toContain("Fetch all");
48
+ expect(lastFrame()).toContain("Init git");
49
+ });
50
+ });
51
+
52
+ describe("github section", () => {
53
+ test("shows github keys", () => {
54
+ const { lastFrame } = render(<HelpOverlay />);
55
+
56
+ expect(lastFrame()).toContain("GitHub");
57
+ // Text may wrap - check for key parts
58
+ expect(lastFrame()).toContain("Create GitHub");
59
+ expect(lastFrame()).toContain("Archive GitHub");
60
+ });
61
+ });
62
+
63
+ describe("view section", () => {
64
+ test("shows view keys", () => {
65
+ const { lastFrame } = render(<HelpOverlay />);
66
+
67
+ expect(lastFrame()).toContain("View");
68
+ expect(lastFrame()).toContain("Filter projects");
69
+ expect(lastFrame()).toContain("Cycle sort field");
70
+ expect(lastFrame()).toContain("Refresh");
71
+ });
72
+ });
73
+
74
+ describe("general section", () => {
75
+ test("shows general keys", () => {
76
+ const { lastFrame } = render(<HelpOverlay />);
77
+
78
+ expect(lastFrame()).toContain("General");
79
+ expect(lastFrame()).toContain("Toggle help");
80
+ expect(lastFrame()).toContain("Cancel");
81
+ expect(lastFrame()).toContain("Quit");
82
+ });
83
+ });
84
+
85
+ test("shows close instruction", () => {
86
+ const { lastFrame } = render(<HelpOverlay />);
87
+
88
+ expect(lastFrame()).toContain("Press any key to close");
89
+ });
90
+ });
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Tests for Layout component
3
+ *
4
+ * Uses TestStoreProvider for dependency injection instead of mock.module()
5
+ */
6
+
7
+ import { describe, test, expect } from "bun:test";
8
+ import React from "react";
9
+ import { render } from "ink-testing-library";
10
+ import { Layout } from "../../../src/components/Layout.tsx";
11
+ import { StoreProvider, TestStoreProvider } from "../../../src/state/store.tsx";
12
+ import type { GitforestConfig, UnifiedAppState, AppMode, UnifiedRepo } from "../../../src/types/index.ts";
13
+ import { createMockUnifiedAppState, createMockUnifiedRepo, createLocalOnlyUnifiedRepo, createGitHubOnlyUnifiedRepo, createSyncedUnifiedRepo, createMockGitHubRepoInfo } from "../mocks/index.ts";
14
+
15
+ /**
16
+ * Mock config for testing
17
+ */
18
+ const mockConfig: GitforestConfig = {
19
+ directories: [{ path: "/test", maxDepth: 3 }],
20
+ scan: { ignore: [], includeHidden: false, concurrency: 4 },
21
+ github: { defaultVisibility: "private" },
22
+ display: { showSubmodules: true, showNonGitProjects: true, sortBy: "status", sortDirection: "desc" },
23
+ cache: {
24
+ ttlSeconds: 300,
25
+ githubTtlSeconds: 600,
26
+ enableBackgroundRefresh: true,
27
+ backgroundRefreshIntervalSeconds: 300,
28
+ },
29
+ commands: [
30
+ { name: "Test Command", key: "t", command: "echo test", confirm: false, background: false }
31
+ ],
32
+ };
33
+
34
+ /**
35
+ * Mock refresh function
36
+ */
37
+ const mockRefresh = async () => {};
38
+
39
+ /**
40
+ * Mock clone function
41
+ */
42
+ const mockClone = async (_repos: any[], _targetDir: string, _useSSH: boolean) => {};
43
+
44
+ /**
45
+ * Track dispatch calls
46
+ */
47
+ let dispatchCalls: any[] = [];
48
+ const mockDispatch = (action: any) => {
49
+ dispatchCalls.push(action);
50
+ };
51
+
52
+ /**
53
+ * Helper to create a mock state with specific mode and repos
54
+ */
55
+ function createStateForMode(mode: AppMode, repos: UnifiedRepo[] = []): UnifiedAppState {
56
+ return createMockUnifiedAppState({
57
+ mode,
58
+ isLoading: false,
59
+ unifiedRepos: repos,
60
+ // Add specific dialog states based on mode
61
+ confirmDialog: mode === "confirm" ? {
62
+ operation: "setup" as const,
63
+ title: "Test Confirm",
64
+ message: "Are you sure?",
65
+ items: ["item1", "item2"],
66
+ projectPaths: ["/test/path1", "/test/path2"],
67
+ showVisibilityToggle: false,
68
+ } : null,
69
+ cloneDialog: mode === "clone" ? {
70
+ repos: [createMockUnifiedRepo({ name: "test-repo" })],
71
+ directories: [{ path: "/test/dir", maxDepth: 3 }],
72
+ selectedDirIndex: 0,
73
+ useSSH: false,
74
+ } : null,
75
+ detailModal: mode === "detail" ? {
76
+ repo: createMockUnifiedRepo({ name: "detail-repo" }),
77
+ readmeContent: "# Test README",
78
+ readmeLoading: false,
79
+ readmeError: null,
80
+ readmeScrollOffset: 0,
81
+ } : null,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Helper to render Layout with TestStoreProvider (no mock.module needed)
87
+ */
88
+ function renderLayoutWithState(state: UnifiedAppState) {
89
+ // Clear previous dispatch calls
90
+ dispatchCalls = [];
91
+
92
+ return render(
93
+ <TestStoreProvider initialState={state} dispatch={mockDispatch}>
94
+ <Layout config={mockConfig} onRefresh={mockRefresh} onClone={mockClone} />
95
+ </TestStoreProvider>
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Helper to render Layout with store provider (for normal mode tests)
101
+ */
102
+ function renderLayout() {
103
+ return render(
104
+ <StoreProvider>
105
+ <Layout config={mockConfig} onRefresh={mockRefresh} />
106
+ </StoreProvider>
107
+ );
108
+ }
109
+
110
+ describe("Layout", () => {
111
+
112
+ describe("header", () => {
113
+ test("shows gitforest title", () => {
114
+ const { lastFrame } = renderLayout();
115
+
116
+ expect(lastFrame()).toContain("gitforest");
117
+ });
118
+
119
+ test("shows subtitle", () => {
120
+ const { lastFrame } = renderLayout();
121
+
122
+ expect(lastFrame()).toContain("Git Repository Manager");
123
+ });
124
+ });
125
+
126
+ describe("filter bar", () => {
127
+ test("shows filter bar", () => {
128
+ const { lastFrame } = renderLayout();
129
+
130
+ expect(lastFrame()).toContain("Filter:");
131
+ });
132
+ });
133
+
134
+ describe("initial state", () => {
135
+ test("shows loading spinner initially", () => {
136
+ const { lastFrame } = renderLayout();
137
+
138
+ // Initial state has isLoading: true
139
+ expect(lastFrame()).toContain("Scanning projects");
140
+ });
141
+ });
142
+
143
+ describe("mode rendering", () => {
144
+ test("renders HelpOverlay when mode is 'help'", () => {
145
+ const state = createStateForMode("help");
146
+ const { lastFrame } = renderLayoutWithState(state);
147
+
148
+ expect(lastFrame()).toContain("Keyboard Shortcuts");
149
+ expect(lastFrame()).toContain("Navigation");
150
+ expect(lastFrame()).toContain("Selection");
151
+ });
152
+
153
+ test("renders CommandPalette when mode is 'command-palette'", () => {
154
+ const state = createStateForMode("command-palette");
155
+ const { lastFrame } = renderLayoutWithState(state);
156
+
157
+ expect(lastFrame()).toContain("Command Palette");
158
+ expect(lastFrame()).toContain("Available Commands");
159
+ expect(lastFrame()).toContain("[t] Test Command");
160
+ });
161
+
162
+ test("renders ConfirmDialog when mode is 'confirm'", () => {
163
+ const state = createStateForMode("confirm");
164
+ const { lastFrame } = renderLayoutWithState(state);
165
+
166
+ expect(lastFrame()).toContain("Test Confirm");
167
+ expect(lastFrame()).toContain("Are you sure?");
168
+ expect(lastFrame()).toContain("item1");
169
+ expect(lastFrame()).toContain("item2");
170
+ });
171
+
172
+ test("renders CloneDialog when mode is 'clone'", () => {
173
+ const state = createStateForMode("clone");
174
+ const { lastFrame } = renderLayoutWithState(state);
175
+
176
+ expect(lastFrame()).toContain("Clone GitHub Repository");
177
+ expect(lastFrame()).toContain("test-repo");
178
+ expect(lastFrame()).toContain("Target directory");
179
+ });
180
+
181
+ test("renders RepoDetailModal when mode is 'detail'", () => {
182
+ const state = createStateForMode("detail");
183
+ const { lastFrame } = renderLayoutWithState(state);
184
+
185
+ expect(lastFrame()).toContain("detail-repo");
186
+ expect(lastFrame()).toContain("Test README");
187
+ });
188
+
189
+ test("renders filter-options placeholder when mode is 'filter-options'", () => {
190
+ const state = createStateForMode("filter-options");
191
+ const { lastFrame } = renderLayoutWithState(state);
192
+
193
+ expect(lastFrame()).toContain("Filter Options Overlay - To be implemented");
194
+ });
195
+
196
+ test("renders main layout when mode is 'normal'", () => {
197
+ const state = createStateForMode("normal");
198
+ const { lastFrame } = renderLayoutWithState(state);
199
+
200
+ expect(lastFrame()).toContain("gitforest");
201
+ expect(lastFrame()).toContain("Git Repository Manager");
202
+ expect(lastFrame()).toContain("Filter:");
203
+ expect(lastFrame()).toContain("No repositories found");
204
+ });
205
+ });
206
+
207
+ describe("StatusBar props", () => {
208
+ test("passes correct counts to StatusBar", () => {
209
+ const repos = [
210
+ createMockUnifiedRepo({
211
+ name: "dirty-repo",
212
+ local: { status: { isDirty: true } } as any
213
+ }),
214
+ createMockUnifiedRepo({
215
+ name: "ahead-repo",
216
+ local: { status: { isAhead: true } } as any
217
+ }),
218
+ createLocalOnlyUnifiedRepo("local-only"),
219
+ createGitHubOnlyUnifiedRepo("github-only"),
220
+ createSyncedUnifiedRepo("synced"),
221
+ ];
222
+
223
+ const state = createStateForMode("normal", repos);
224
+ const { lastFrame } = renderLayoutWithState(state);
225
+
226
+ // Check that StatusBar receives the correct counts
227
+ expect(lastFrame()).toContain("5 repos");
228
+ expect(lastFrame()).toContain("1 synced");
229
+ expect(lastFrame()).toContain("3 local");
230
+ expect(lastFrame()).toContain("1 remote");
231
+ expect(lastFrame()).toContain("1 dirty");
232
+ expect(lastFrame()).toContain("1 unpushed");
233
+ });
234
+ });
235
+
236
+ describe("dialog handlers", () => {
237
+ test("handleCloneConfirm calls onClone with correct params", async () => {
238
+ const state = createStateForMode("clone");
239
+ const { lastFrame } = renderLayoutWithState(state);
240
+
241
+ // The dialog should be rendered
242
+ expect(lastFrame()).toContain("Clone GitHub Repository");
243
+ expect(lastFrame()).toContain("test-repo");
244
+ expect(lastFrame()).toContain("Esc/n to cancel");
245
+ });
246
+
247
+ test("handleCloneSelectDir dispatches UPDATE_CLONE_DIALOG", () => {
248
+ const state = createStateForMode("clone");
249
+ const { lastFrame } = renderLayoutWithState(state);
250
+
251
+ expect(lastFrame()).toContain("Target directory");
252
+ // Directory selection would be tested in CloneDialog component tests
253
+ });
254
+
255
+ test("handleCloneToggleSSH toggles SSH setting", () => {
256
+ const state = createStateForMode("clone");
257
+ const { lastFrame } = renderLayoutWithState(state);
258
+
259
+ expect(lastFrame()).toContain("Protocol:");
260
+ expect(lastFrame()).toContain("SSH");
261
+ expect(lastFrame()).toContain("HTTPS");
262
+ // SSH toggle would be tested in CloneDialog component tests
263
+ });
264
+ });
265
+
266
+ describe("detail modal handlers", () => {
267
+ test("handleDetailClose would dispatch HIDE_DETAIL_MODAL", () => {
268
+ const state = createStateForMode("detail");
269
+ const { lastFrame } = renderLayoutWithState(state);
270
+
271
+ expect(lastFrame()).toContain("detail-repo");
272
+ // Close handler would be tested in RepoDetailModal component tests
273
+ });
274
+
275
+ test("handleDetailAction handles browser action", () => {
276
+ const repo = createMockUnifiedRepo({
277
+ name: "browser-repo",
278
+ github: createMockGitHubRepoInfo({
279
+ name: "browser-repo",
280
+ htmlUrl: "https://github.com/test/browser-repo"
281
+ })
282
+ });
283
+ const state = createStateForMode("detail");
284
+ state.detailModal!.repo = repo;
285
+
286
+ const { lastFrame } = renderLayoutWithState(state);
287
+
288
+ expect(lastFrame()).toContain("browser-repo");
289
+ // Browser action would open the URL (tested in integration tests)
290
+ });
291
+
292
+ test("handleDetailAction handles editor action", () => {
293
+ const repo = createMockUnifiedRepo({
294
+ name: "editor-repo",
295
+ localPath: "/test/editor-repo"
296
+ });
297
+ const state = createStateForMode("detail");
298
+ state.detailModal!.repo = repo;
299
+
300
+ const { lastFrame } = renderLayoutWithState(state);
301
+
302
+ expect(lastFrame()).toContain("editor-repo");
303
+ // Editor action would open the editor (tested in integration tests)
304
+ });
305
+
306
+ test("handleDetailScroll dispatches UPDATE_DETAIL_MODAL", () => {
307
+ const state = createStateForMode("detail");
308
+ const { lastFrame } = renderLayoutWithState(state);
309
+
310
+ expect(lastFrame()).toContain("Test README");
311
+ // Scroll handler would be tested in RepoDetailModal component tests
312
+ });
313
+
314
+ test("handleDetailCommand executes command", () => {
315
+ const repo = createMockUnifiedRepo({
316
+ name: "command-repo",
317
+ localPath: "/test/command-repo"
318
+ });
319
+ const state = createStateForMode("detail");
320
+ state.detailModal!.repo = repo;
321
+
322
+ const { lastFrame } = renderLayoutWithState(state);
323
+
324
+ expect(lastFrame()).toContain("command-repo");
325
+ // Command execution would be tested in RepoDetailModal component tests
326
+ });
327
+ });
328
+ });
@@ -0,0 +1,45 @@
1
+ import { test, expect } from "bun:test";
2
+ import React from "react";
3
+ import { render } from "ink-testing-library";
4
+ import { MarkdownRenderer } from "../../../src/components/MarkdownRenderer";
5
+
6
+ test("MarkdownRenderer renders basic markdown", () => {
7
+ const markdown = `# Hello World
8
+
9
+ This is **bold** and *italic* text.`;
10
+
11
+ const { lastFrame } = render(
12
+ React.createElement(MarkdownRenderer, { content: markdown })
13
+ );
14
+
15
+ const output = lastFrame();
16
+ expect(output).toContain("Hello World");
17
+ expect(output).toContain("bold");
18
+ expect(output).toContain("italic");
19
+ });
20
+
21
+ test("MarkdownRenderer handles code blocks", () => {
22
+ const markdown = "\`\`\`javascript\nconst x = 1;\n\`\`\`";
23
+
24
+ const { lastFrame } = render(
25
+ React.createElement(MarkdownRenderer, { content: markdown })
26
+ );
27
+
28
+ const output = lastFrame();
29
+ expect(output).toContain("const x = 1;");
30
+ });
31
+
32
+ test("MarkdownRenderer handles lists", () => {
33
+ const markdown = `- Item 1
34
+ - Item 2
35
+ - Item 3`;
36
+
37
+ const { lastFrame } = render(
38
+ React.createElement(MarkdownRenderer, { content: markdown })
39
+ );
40
+
41
+ const output = lastFrame();
42
+ expect(output).toContain("Item 1");
43
+ expect(output).toContain("Item 2");
44
+ expect(output).toContain("Item 3");
45
+ });