gitforest 0.1.0 → 1.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.
- package/LICENSE +21 -0
- package/package.json +24 -4
- package/src/components/onboarding/DirectoriesStep.tsx +19 -19
- package/src/github/auth.ts +3 -3
- package/src/utils/debug.ts +4 -4
- package/.bunignore +0 -7
- package/.github/workflows/ci.yml +0 -73
- package/CLAUDE.md +0 -111
- package/CONTRIBUTING.md +0 -145
- package/bun.lock +0 -267
- package/bunfig.toml +0 -15
- package/cli +0 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
- package/docs/ai/VERIFICATION_REPORT.md +0 -87
- package/docs/ai/architecture.md +0 -169
- package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
- package/docs/ai/checks/check-2025-12-02.md +0 -55
- package/docs/ai/checks/test-verification-report.md +0 -85
- package/docs/ai/implementation-guide.md +0 -776
- package/docs/ai/research/gitty-codebase-analysis.md +0 -221
- package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
- package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
- package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
- package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
- package/docs/ai/tickets/TASK-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
- package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
- package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
- package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
- package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
- package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
- package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
- package/docs/ai/tkt-001-fix-database-error.md +0 -217
- package/docs/ai/ui-enhancement-plan.md +0 -562
- package/test/integration/app.isolated.tsx +0 -240
- package/test/integration/cli-commands.test.ts +0 -287
- package/test/integration/cli-validation.test.ts +0 -264
- package/test/integration/git-operations.test.ts +0 -218
- package/test/integration/scanner.test.ts +0 -228
- package/test/preload.ts +0 -18
- package/test/unit/cli/commands.test.ts +0 -13
- package/test/unit/cli/formatters.test.ts +0 -1116
- package/test/unit/cli/github-commands.test.ts +0 -12
- package/test/unit/components/CloneDialog.test.tsx +0 -240
- package/test/unit/components/ColumnHeader.test.tsx +0 -128
- package/test/unit/components/CommandPalette.test.tsx +0 -355
- package/test/unit/components/ConfirmDialog.test.tsx +0 -111
- package/test/unit/components/ErrorBoundary.test.tsx +0 -139
- package/test/unit/components/FilterBar.test.tsx +0 -43
- package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
- package/test/unit/components/HelpOverlay.test.tsx +0 -90
- package/test/unit/components/Layout.test.tsx +0 -328
- package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
- package/test/unit/components/ProgressBar.test.tsx +0 -138
- package/test/unit/components/ProjectItem.test.tsx +0 -182
- package/test/unit/components/ProjectList.test.tsx +0 -311
- package/test/unit/components/RepoDetailModal.test.tsx +0 -445
- package/test/unit/components/StatusBar.test.tsx +0 -112
- package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
- package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
- package/test/unit/components/test-utils.tsx +0 -63
- package/test/unit/config/loader.test.ts +0 -692
- package/test/unit/db/database.test.ts +0 -978
- package/test/unit/db/index.test.ts +0 -314
- package/test/unit/fixtures/setup.ts +0 -186
- package/test/unit/git/commands-untested.test.ts +0 -205
- package/test/unit/git/commands.test.ts +0 -269
- package/test/unit/git/operations.test.ts +0 -322
- package/test/unit/git/status.test.ts +0 -219
- package/test/unit/github/auth.test.ts +0 -317
- package/test/unit/github/cache.test.ts +0 -1028
- package/test/unit/github/cli.test.ts +0 -135
- package/test/unit/github/unified.test.ts +0 -1201
- package/test/unit/graceful-shutdown.test.ts +0 -83
- package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
- package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
- package/test/unit/hooks/useProjects.test.tsx +0 -186
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
- package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
- package/test/unit/mocks/config.ts +0 -109
- package/test/unit/mocks/git-service.ts +0 -274
- package/test/unit/mocks/github-service.ts +0 -250
- package/test/unit/mocks/index.ts +0 -72
- package/test/unit/mocks/project.ts +0 -148
- package/test/unit/mocks/state-mocks.ts +0 -187
- package/test/unit/mocks/unified.ts +0 -169
- package/test/unit/operations/batch.test.ts +0 -216
- package/test/unit/operations/commands.test.ts +0 -550
- package/test/unit/scanner/errors.test.ts +0 -297
- package/test/unit/scanner/index.test.ts +0 -1011
- package/test/unit/scanner/markers.test.ts +0 -150
- package/test/unit/scanner/submodules.test.ts +0 -99
- package/test/unit/services/git-errors.test.ts +0 -190
- package/test/unit/services/git.test.ts +0 -442
- package/test/unit/services/github-errors.test.ts +0 -293
- package/test/unit/services/github.test.ts +0 -200
- package/test/unit/state/actions.test.ts +0 -217
- package/test/unit/state/reducer.test.ts +0 -745
- package/test/unit/state/store.test.tsx +0 -711
- package/test/unit/types/commands.test.ts +0 -220
- package/test/unit/types/schema.test.ts +0 -179
- package/test/unit/utils/array.test.ts +0 -73
- package/test/unit/utils/debug.test.ts +0 -23
- package/test/unit/utils/errors.test.ts +0 -295
- package/test/unit/utils/markdown.test.ts +0 -163
- package/test/unit/utils/project-utils.test.ts +0 -756
- package/test/unit/utils/rate-limiter.test.ts +0 -256
- package/test/unit/utils/retry.test.ts +0 -165
- package/test/unit/utils/strip-ansi.ts +0 -13
- package/test/unit/utils/timeout.test.ts +0 -93
- package/tsconfig.json +0 -29
|
@@ -1,197 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,328 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
});
|