gitmaps 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +947 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +84 -75
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import type { Window } from 'happy-dom';
|
|
3
|
+
import { addRecentRepo, getRecentRepos, removeRecentRepo, renderRecentCommitsUI } from './recent-commits';
|
|
4
|
+
import { setCanvasContext } from './context';
|
|
5
|
+
import { installFetchMock, setupDomTest } from './test-dom';
|
|
6
|
+
|
|
7
|
+
describe('recent commits sidebar', () => {
|
|
8
|
+
let window: Window;
|
|
9
|
+
let cleanup: (() => void) | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
const handle = setupDomTest({
|
|
13
|
+
url: 'http://localhost:3335/',
|
|
14
|
+
html: `
|
|
15
|
+
<div id="recentCommits" style="display:none">
|
|
16
|
+
<div id="recentCommitsList"></div>
|
|
17
|
+
</div>
|
|
18
|
+
<select id="repoSelect">
|
|
19
|
+
<option value="">Choose</option>
|
|
20
|
+
<option value="C:/Code/gitmaps">gitmaps</option>
|
|
21
|
+
<option value="C:/Code/jsx-ai">jsx-ai</option>
|
|
22
|
+
</select>
|
|
23
|
+
<button id="pullBtn">Pull</button>
|
|
24
|
+
`,
|
|
25
|
+
});
|
|
26
|
+
window = handle.window;
|
|
27
|
+
cleanup = handle.cleanup;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
try {
|
|
32
|
+
(mock as any).restore?.();
|
|
33
|
+
} catch {}
|
|
34
|
+
setCanvasContext(null);
|
|
35
|
+
cleanup?.();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('normalizes legacy localStorage entries and removes malformed ones', () => {
|
|
39
|
+
localStorage.setItem(
|
|
40
|
+
'gitcanvas:recentRepos',
|
|
41
|
+
JSON.stringify([
|
|
42
|
+
'C:/Code/alpha',
|
|
43
|
+
{ path: 'C:/Code/beta', loadedAt: 'bad', commitCount: 3 },
|
|
44
|
+
{ nope: true },
|
|
45
|
+
'[object HTMLDivElement]',
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const repos = getRecentRepos();
|
|
50
|
+
expect(repos.map((repo) => repo.path)).toEqual(['C:/Code/alpha', 'C:/Code/beta']);
|
|
51
|
+
expect(repos[0]?.commitCount).toBe(0);
|
|
52
|
+
expect(repos[1]?.commitCount).toBe(3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('renders stable metadata instead of undefined/NaN values', () => {
|
|
56
|
+
localStorage.setItem(
|
|
57
|
+
'gitcanvas:recentRepos',
|
|
58
|
+
JSON.stringify([{ path: 'C:/Code/epstein-files' }]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
renderRecentCommitsUI();
|
|
62
|
+
|
|
63
|
+
const text = document.getElementById('recentCommitsList')?.textContent || '';
|
|
64
|
+
expect(text).toContain('epstein-files');
|
|
65
|
+
expect(text).toContain('0 commits · Just now');
|
|
66
|
+
expect(text).not.toContain('undefined');
|
|
67
|
+
expect(text).not.toContain('NaN');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('addRecentRepo persists object format and rerenders list', () => {
|
|
71
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
72
|
+
|
|
73
|
+
const stored = JSON.parse(localStorage.getItem('gitcanvas:recentRepos') || '[]');
|
|
74
|
+
expect(stored[0]?.path).toBe('C:/Code/gitmaps');
|
|
75
|
+
expect(stored[0]?.commitCount).toBe(12);
|
|
76
|
+
|
|
77
|
+
const text = document.getElementById('recentCommitsList')?.textContent || '';
|
|
78
|
+
expect(text).toContain('gitmaps');
|
|
79
|
+
expect(text).toContain('12 commits');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('recent repos section shows and hides as the list fills and empties', () => {
|
|
83
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
84
|
+
|
|
85
|
+
renderRecentCommitsUI();
|
|
86
|
+
expect(section.style.display).toBe('none');
|
|
87
|
+
|
|
88
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
89
|
+
expect(section.style.display).toBe('block');
|
|
90
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('gitmaps');
|
|
91
|
+
|
|
92
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
93
|
+
expect(section.style.display).toBe('block');
|
|
94
|
+
|
|
95
|
+
removeRecentRepo('C:/Code/gitmaps');
|
|
96
|
+
expect(section.style.display).toBe('block');
|
|
97
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('jsx-ai');
|
|
98
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').not.toContain('gitmaps');
|
|
99
|
+
|
|
100
|
+
removeRecentRepo('C:/Code/jsx-ai');
|
|
101
|
+
expect(section.style.display).toBe('none');
|
|
102
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('No recent commits');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('normalization back to empty hides the recent repos section', () => {
|
|
106
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
107
|
+
|
|
108
|
+
localStorage.setItem(
|
|
109
|
+
'gitcanvas:recentRepos',
|
|
110
|
+
JSON.stringify([{ path: ' ' }, '[object HTMLDivElement]', { nope: true }]),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
renderRecentCommitsUI();
|
|
114
|
+
|
|
115
|
+
expect(section.style.display).toBe('none');
|
|
116
|
+
expect(document.querySelectorAll('[data-path]').length).toBe(0);
|
|
117
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('No recent commits');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('re-adding an existing repo moves it to the front without duplicates', () => {
|
|
121
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
122
|
+
|
|
123
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
124
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
125
|
+
addRecentRepo('C:/Code/gitmaps', 13);
|
|
126
|
+
|
|
127
|
+
const repos = getRecentRepos();
|
|
128
|
+
expect(section.style.display).toBe('block');
|
|
129
|
+
expect(repos.map((repo) => repo.path)).toEqual(['C:/Code/gitmaps', 'C:/Code/jsx-ai']);
|
|
130
|
+
expect(repos[0]?.commitCount).toBe(13);
|
|
131
|
+
|
|
132
|
+
const stored = JSON.parse(localStorage.getItem('gitcanvas:recentRepos') || '[]');
|
|
133
|
+
expect(stored.map((repo: any) => repo.path)).toEqual(['C:/Code/gitmaps', 'C:/Code/jsx-ai']);
|
|
134
|
+
|
|
135
|
+
const renderedButtons = Array.from(document.querySelectorAll('[data-path]')) as HTMLButtonElement[];
|
|
136
|
+
expect(renderedButtons.map((item) => item.dataset.path)).toEqual(['C:/Code/gitmaps', 'C:/Code/jsx-ai']);
|
|
137
|
+
expect(renderedButtons.filter((item) => item.dataset.path === 'C:/Code/gitmaps')).toHaveLength(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('adding more than five repos keeps only the newest five in storage and UI order', () => {
|
|
141
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
142
|
+
const repoPaths = [
|
|
143
|
+
'C:/Code/repo-1',
|
|
144
|
+
'C:/Code/repo-2',
|
|
145
|
+
'C:/Code/repo-3',
|
|
146
|
+
'C:/Code/repo-4',
|
|
147
|
+
'C:/Code/repo-5',
|
|
148
|
+
'C:/Code/repo-6',
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
repoPaths.forEach((path, index) => addRecentRepo(path, index + 1));
|
|
152
|
+
|
|
153
|
+
const expectedPaths = [
|
|
154
|
+
'C:/Code/repo-6',
|
|
155
|
+
'C:/Code/repo-5',
|
|
156
|
+
'C:/Code/repo-4',
|
|
157
|
+
'C:/Code/repo-3',
|
|
158
|
+
'C:/Code/repo-2',
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const repos = getRecentRepos();
|
|
162
|
+
expect(section.style.display).toBe('block');
|
|
163
|
+
expect(repos).toHaveLength(5);
|
|
164
|
+
expect(repos.map((repo) => repo.path)).toEqual(expectedPaths);
|
|
165
|
+
expect(repos.map((repo) => repo.commitCount)).toEqual([6, 5, 4, 3, 2]);
|
|
166
|
+
|
|
167
|
+
const stored = JSON.parse(localStorage.getItem('gitcanvas:recentRepos') || '[]');
|
|
168
|
+
expect(stored).toHaveLength(5);
|
|
169
|
+
expect(stored.map((repo: any) => repo.path)).toEqual(expectedPaths);
|
|
170
|
+
|
|
171
|
+
const renderedButtons = Array.from(document.querySelectorAll('[data-path]')) as HTMLButtonElement[];
|
|
172
|
+
expect(renderedButtons).toHaveLength(5);
|
|
173
|
+
expect(renderedButtons.map((item) => item.dataset.path)).toEqual(expectedPaths);
|
|
174
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').not.toContain('repo-1');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('invalid recent repo JSON clears stale rendered items back to the empty state', () => {
|
|
178
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
179
|
+
|
|
180
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
181
|
+
expect(section.style.display).toBe('block');
|
|
182
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(1);
|
|
183
|
+
|
|
184
|
+
localStorage.setItem('gitcanvas:recentRepos', '{not valid json');
|
|
185
|
+
renderRecentCommitsUI();
|
|
186
|
+
|
|
187
|
+
expect(getRecentRepos()).toEqual([]);
|
|
188
|
+
expect(section.style.display).toBe('none');
|
|
189
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(0);
|
|
190
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('No recent commits');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('non-array recent repo payload clears stale rendered items back to the empty state', () => {
|
|
194
|
+
const section = document.getElementById('recentCommits') as HTMLDivElement;
|
|
195
|
+
|
|
196
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
197
|
+
expect(section.style.display).toBe('block');
|
|
198
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
localStorage.setItem('gitcanvas:recentRepos', JSON.stringify({ path: 'C:/Code/gitmaps' }));
|
|
201
|
+
renderRecentCommitsUI();
|
|
202
|
+
|
|
203
|
+
expect(getRecentRepos()).toEqual([]);
|
|
204
|
+
expect(section.style.display).toBe('none');
|
|
205
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(0);
|
|
206
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('No recent commits');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('clicking Pull without an active repo shows an error toast without entering loading state', async () => {
|
|
210
|
+
const ctx = {
|
|
211
|
+
snap: () => ({ context: { repoPath: '' } }),
|
|
212
|
+
} as any;
|
|
213
|
+
setCanvasContext(ctx);
|
|
214
|
+
|
|
215
|
+
renderRecentCommitsUI();
|
|
216
|
+
|
|
217
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
218
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
219
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
220
|
+
|
|
221
|
+
pullBtn.click();
|
|
222
|
+
await Promise.resolve();
|
|
223
|
+
|
|
224
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
225
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
226
|
+
expect(document.querySelector('.toast.error')?.textContent || '').toContain('No repository loaded');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('clicking a recent repo starts the repo reload flow for that path', async () => {
|
|
230
|
+
const ctx = {
|
|
231
|
+
actor: { send: mock(() => undefined) },
|
|
232
|
+
snap: () => ({ context: { repoPath: '', zoom: 1, offsetX: 0, offsetY: 0, commits: [] }, value: { view: 'allfiles' } }),
|
|
233
|
+
fileCards: new Map(),
|
|
234
|
+
deferredCards: new Map(),
|
|
235
|
+
changedFilePaths: new Set(),
|
|
236
|
+
positions: new Map(),
|
|
237
|
+
hiddenFiles: new Set(),
|
|
238
|
+
allFilesData: [],
|
|
239
|
+
commitFilesData: [],
|
|
240
|
+
canvas: document.createElement('div'),
|
|
241
|
+
canvasViewport: document.createElement('div'),
|
|
242
|
+
svgOverlay: null,
|
|
243
|
+
loadingOverlay: null,
|
|
244
|
+
} as any;
|
|
245
|
+
setCanvasContext(ctx);
|
|
246
|
+
|
|
247
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
248
|
+
repoSelect.value = '';
|
|
249
|
+
|
|
250
|
+
const fetchMock = mock(async (input: string) => {
|
|
251
|
+
expect(input).toBe('/api/repo/load');
|
|
252
|
+
return new Response('boom', { status: 500 });
|
|
253
|
+
});
|
|
254
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
255
|
+
|
|
256
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
257
|
+
|
|
258
|
+
const item = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
259
|
+
expect(item).toBeTruthy();
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
item.click();
|
|
263
|
+
await Promise.resolve();
|
|
264
|
+
await Promise.resolve();
|
|
265
|
+
} finally {
|
|
266
|
+
fetchHandle.restore();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
270
|
+
expect(ctx.actor.send).toHaveBeenCalledWith({ type: 'LOAD_REPO', path: 'C:/Code/gitmaps' });
|
|
271
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
});
|
|
276
|
+
expect(String(fetchMock.mock.calls[0]?.[1]?.body)).toContain('C:/Code/gitmaps');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('clicking a recent repo uses onRepoReady when provided', async () => {
|
|
280
|
+
const onRepoReady = mock(() => undefined);
|
|
281
|
+
const ctx = {
|
|
282
|
+
onRepoReady,
|
|
283
|
+
actor: { send: mock(() => undefined) },
|
|
284
|
+
snap: () => ({ context: { repoPath: '', zoom: 1, offsetX: 0, offsetY: 0, commits: [] }, value: { view: 'allfiles' } }),
|
|
285
|
+
fileCards: new Map(),
|
|
286
|
+
deferredCards: new Map(),
|
|
287
|
+
changedFilePaths: new Set(),
|
|
288
|
+
positions: new Map(),
|
|
289
|
+
hiddenFiles: new Set(),
|
|
290
|
+
allFilesData: [],
|
|
291
|
+
commitFilesData: [],
|
|
292
|
+
canvas: document.createElement('div'),
|
|
293
|
+
canvasViewport: document.createElement('div'),
|
|
294
|
+
svgOverlay: null,
|
|
295
|
+
loadingOverlay: null,
|
|
296
|
+
} as any;
|
|
297
|
+
setCanvasContext(ctx);
|
|
298
|
+
|
|
299
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
300
|
+
repoSelect.value = '';
|
|
301
|
+
|
|
302
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
303
|
+
|
|
304
|
+
const item = document.querySelector('[data-path="C:/Code/jsx-ai"]') as HTMLButtonElement;
|
|
305
|
+
expect(item).toBeTruthy();
|
|
306
|
+
|
|
307
|
+
item.click();
|
|
308
|
+
await Promise.resolve();
|
|
309
|
+
|
|
310
|
+
expect(repoSelect.value).toBe('C:/Code/jsx-ai');
|
|
311
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/jsx-ai');
|
|
312
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('rerendering the recent repo list preserves click-through behavior for newly rendered entries', async () => {
|
|
316
|
+
const onRepoReady = mock(() => undefined);
|
|
317
|
+
const ctx = {
|
|
318
|
+
onRepoReady,
|
|
319
|
+
actor: { send: mock(() => undefined) },
|
|
320
|
+
snap: () => ({ context: { repoPath: '', zoom: 1, offsetX: 0, offsetY: 0, commits: [] }, value: { view: 'allfiles' } }),
|
|
321
|
+
fileCards: new Map(),
|
|
322
|
+
deferredCards: new Map(),
|
|
323
|
+
changedFilePaths: new Set(),
|
|
324
|
+
positions: new Map(),
|
|
325
|
+
hiddenFiles: new Set(),
|
|
326
|
+
allFilesData: [],
|
|
327
|
+
commitFilesData: [],
|
|
328
|
+
canvas: document.createElement('div'),
|
|
329
|
+
canvasViewport: document.createElement('div'),
|
|
330
|
+
svgOverlay: null,
|
|
331
|
+
loadingOverlay: null,
|
|
332
|
+
} as any;
|
|
333
|
+
setCanvasContext(ctx);
|
|
334
|
+
|
|
335
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
336
|
+
repoSelect.value = '';
|
|
337
|
+
|
|
338
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
339
|
+
renderRecentCommitsUI();
|
|
340
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
341
|
+
renderRecentCommitsUI();
|
|
342
|
+
|
|
343
|
+
const item = document.querySelector('[data-path="C:/Code/jsx-ai"]') as HTMLButtonElement;
|
|
344
|
+
expect(item).toBeTruthy();
|
|
345
|
+
|
|
346
|
+
item.click();
|
|
347
|
+
await Promise.resolve();
|
|
348
|
+
|
|
349
|
+
expect(repoSelect.value).toBe('C:/Code/jsx-ai');
|
|
350
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
351
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/jsx-ai');
|
|
352
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('clicking a recent repo stays inert when there is no canvas context', async () => {
|
|
356
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
357
|
+
repoSelect.value = '';
|
|
358
|
+
|
|
359
|
+
const fetchMock = mock(async () => new Response('{}'));
|
|
360
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
361
|
+
|
|
362
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
363
|
+
|
|
364
|
+
const item = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
365
|
+
expect(item).toBeTruthy();
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
item.click();
|
|
369
|
+
await Promise.resolve();
|
|
370
|
+
} finally {
|
|
371
|
+
fetchHandle.restore();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
expect(repoSelect.value).toBe('');
|
|
375
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('malformed recent entries render empty state with no clickable repo path', () => {
|
|
379
|
+
localStorage.setItem(
|
|
380
|
+
'gitcanvas:recentRepos',
|
|
381
|
+
JSON.stringify([{ path: ' ' }, '[object HTMLDivElement]', { nope: true }]),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
renderRecentCommitsUI();
|
|
385
|
+
|
|
386
|
+
expect(document.querySelectorAll('[data-path]').length).toBe(0);
|
|
387
|
+
expect(document.getElementById('recentCommitsList')?.textContent || '').toContain('No recent commits');
|
|
388
|
+
expect((document.getElementById('recentCommits') as HTMLDivElement | null)?.style.display).toBe('none');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('clicking Pull with an active repo shows loading state, calls loadRepository, and restores the button on success', async () => {
|
|
392
|
+
let resolveLoad!: () => void;
|
|
393
|
+
const loadPromise = new Promise<void>((resolve) => {
|
|
394
|
+
resolveLoad = resolve;
|
|
395
|
+
});
|
|
396
|
+
const loadRepositoryMock = mock(() => loadPromise);
|
|
397
|
+
const originalRepoModule = require('./repo');
|
|
398
|
+
mock.module('./repo', () => ({
|
|
399
|
+
...originalRepoModule,
|
|
400
|
+
loadRepository: loadRepositoryMock,
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
const ctx = {
|
|
404
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
405
|
+
} as any;
|
|
406
|
+
setCanvasContext(ctx);
|
|
407
|
+
|
|
408
|
+
renderRecentCommitsUI();
|
|
409
|
+
|
|
410
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
pullBtn.click();
|
|
414
|
+
await Promise.resolve();
|
|
415
|
+
|
|
416
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
417
|
+
expect(pullBtn.disabled).toBeTrue();
|
|
418
|
+
expect((pullBtn.textContent || '').includes('Pulling...')).toBeTrue();
|
|
419
|
+
|
|
420
|
+
resolveLoad();
|
|
421
|
+
await Promise.resolve();
|
|
422
|
+
await Promise.resolve();
|
|
423
|
+
|
|
424
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
425
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
426
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
427
|
+
} finally {
|
|
428
|
+
(mock as any).restore?.();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('clicking Pull with an active repo restores the button and shows an error toast when loadRepository rejects', async () => {
|
|
433
|
+
let rejectLoad!: (error: Error) => void;
|
|
434
|
+
const loadPromise = new Promise<void>((_, reject) => {
|
|
435
|
+
rejectLoad = reject;
|
|
436
|
+
});
|
|
437
|
+
const loadRepositoryMock = mock(() => loadPromise);
|
|
438
|
+
const originalRepoModule = require('./repo');
|
|
439
|
+
mock.module('./repo', () => ({
|
|
440
|
+
...originalRepoModule,
|
|
441
|
+
loadRepository: loadRepositoryMock,
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
const ctx = {
|
|
445
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
446
|
+
} as any;
|
|
447
|
+
setCanvasContext(ctx);
|
|
448
|
+
|
|
449
|
+
renderRecentCommitsUI();
|
|
450
|
+
|
|
451
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
pullBtn.click();
|
|
455
|
+
await Promise.resolve();
|
|
456
|
+
|
|
457
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
458
|
+
expect(pullBtn.disabled).toBeTrue();
|
|
459
|
+
expect((pullBtn.textContent || '').includes('Pulling...')).toBeTrue();
|
|
460
|
+
|
|
461
|
+
rejectLoad(new Error('network down'));
|
|
462
|
+
await Promise.resolve();
|
|
463
|
+
await Promise.resolve();
|
|
464
|
+
|
|
465
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
466
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
467
|
+
expect(document.querySelector('.toast.error')?.textContent || '').toContain('Pull failed: network down');
|
|
468
|
+
} finally {
|
|
469
|
+
(mock as any).restore?.();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('repeated renderRecentCommitsUI calls do not double-bind the Pull button handler', async () => {
|
|
474
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
475
|
+
const originalRepoModule = require('./repo');
|
|
476
|
+
mock.module('./repo', () => ({
|
|
477
|
+
...originalRepoModule,
|
|
478
|
+
loadRepository: loadRepositoryMock,
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
const ctx = {
|
|
482
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
483
|
+
} as any;
|
|
484
|
+
setCanvasContext(ctx);
|
|
485
|
+
|
|
486
|
+
renderRecentCommitsUI();
|
|
487
|
+
renderRecentCommitsUI();
|
|
488
|
+
renderRecentCommitsUI();
|
|
489
|
+
|
|
490
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
pullBtn.click();
|
|
494
|
+
await Promise.resolve();
|
|
495
|
+
await Promise.resolve();
|
|
496
|
+
|
|
497
|
+
expect(pullBtn.dataset.bound).toBe('true');
|
|
498
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
499
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
500
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
501
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
502
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
503
|
+
} finally {
|
|
504
|
+
(mock as any).restore?.();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('replacing the Pull button DOM node and rerendering binds the handler exactly once on the new element', async () => {
|
|
509
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
510
|
+
const originalRepoModule = require('./repo');
|
|
511
|
+
mock.module('./repo', () => ({
|
|
512
|
+
...originalRepoModule,
|
|
513
|
+
loadRepository: loadRepositoryMock,
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
const ctx = {
|
|
517
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
518
|
+
} as any;
|
|
519
|
+
setCanvasContext(ctx);
|
|
520
|
+
|
|
521
|
+
renderRecentCommitsUI();
|
|
522
|
+
|
|
523
|
+
const originalPullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
524
|
+
const replacement = document.createElement('button');
|
|
525
|
+
replacement.id = 'pullBtn';
|
|
526
|
+
replacement.textContent = 'Pull';
|
|
527
|
+
originalPullBtn.replaceWith(replacement);
|
|
528
|
+
|
|
529
|
+
renderRecentCommitsUI();
|
|
530
|
+
|
|
531
|
+
const reboundPullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
expect(reboundPullBtn).toBeTruthy();
|
|
535
|
+
expect(reboundPullBtn).not.toBe(originalPullBtn);
|
|
536
|
+
expect(reboundPullBtn.dataset.bound).toBe('true');
|
|
537
|
+
|
|
538
|
+
reboundPullBtn.click();
|
|
539
|
+
await Promise.resolve();
|
|
540
|
+
await Promise.resolve();
|
|
541
|
+
|
|
542
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
543
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
544
|
+
expect(reboundPullBtn.disabled).toBeFalse();
|
|
545
|
+
expect((reboundPullBtn.textContent || '').trim()).toBe('Pull');
|
|
546
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
547
|
+
} finally {
|
|
548
|
+
(mock as any).restore?.();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('rerendering the recent repo list does not break the already-bound Pull button behavior', async () => {
|
|
553
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
554
|
+
const originalRepoModule = require('./repo');
|
|
555
|
+
mock.module('./repo', () => ({
|
|
556
|
+
...originalRepoModule,
|
|
557
|
+
loadRepository: loadRepositoryMock,
|
|
558
|
+
}));
|
|
559
|
+
|
|
560
|
+
const ctx = {
|
|
561
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
562
|
+
} as any;
|
|
563
|
+
setCanvasContext(ctx);
|
|
564
|
+
|
|
565
|
+
renderRecentCommitsUI();
|
|
566
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
567
|
+
|
|
568
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
569
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
expect(pullBtn.dataset.bound).toBe('true');
|
|
573
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(2);
|
|
574
|
+
|
|
575
|
+
pullBtn.click();
|
|
576
|
+
await Promise.resolve();
|
|
577
|
+
await Promise.resolve();
|
|
578
|
+
|
|
579
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
580
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
581
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
582
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
583
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
584
|
+
} finally {
|
|
585
|
+
(mock as any).restore?.();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('pull-button and click-through behavior both survive the same recent-list rerender sequence', async () => {
|
|
590
|
+
const onRepoReady = mock((path: string) => {
|
|
591
|
+
currentRepoPath = path;
|
|
592
|
+
});
|
|
593
|
+
let currentRepoPath = 'C:/Code/gitmaps';
|
|
594
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
595
|
+
const originalRepoModule = require('./repo');
|
|
596
|
+
mock.module('./repo', () => ({
|
|
597
|
+
...originalRepoModule,
|
|
598
|
+
loadRepository: loadRepositoryMock,
|
|
599
|
+
}));
|
|
600
|
+
|
|
601
|
+
const ctx = {
|
|
602
|
+
onRepoReady,
|
|
603
|
+
actor: { send: mock(() => undefined) },
|
|
604
|
+
snap: () => ({
|
|
605
|
+
context: { repoPath: currentRepoPath, zoom: 1, offsetX: 0, offsetY: 0, commits: [] },
|
|
606
|
+
value: { view: 'allfiles' },
|
|
607
|
+
}),
|
|
608
|
+
fileCards: new Map(),
|
|
609
|
+
deferredCards: new Map(),
|
|
610
|
+
changedFilePaths: new Set(),
|
|
611
|
+
positions: new Map(),
|
|
612
|
+
hiddenFiles: new Set(),
|
|
613
|
+
allFilesData: [],
|
|
614
|
+
commitFilesData: [],
|
|
615
|
+
canvas: document.createElement('div'),
|
|
616
|
+
canvasViewport: document.createElement('div'),
|
|
617
|
+
svgOverlay: null,
|
|
618
|
+
loadingOverlay: null,
|
|
619
|
+
} as any;
|
|
620
|
+
setCanvasContext(ctx);
|
|
621
|
+
|
|
622
|
+
renderRecentCommitsUI();
|
|
623
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
624
|
+
|
|
625
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
626
|
+
renderRecentCommitsUI();
|
|
627
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
628
|
+
renderRecentCommitsUI();
|
|
629
|
+
|
|
630
|
+
const item = document.querySelector('[data-path="C:/Code/jsx-ai"]') as HTMLButtonElement;
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
expect(pullBtn.dataset.bound).toBe('true');
|
|
634
|
+
expect(item).toBeTruthy();
|
|
635
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(2);
|
|
636
|
+
|
|
637
|
+
item.click();
|
|
638
|
+
await Promise.resolve();
|
|
639
|
+
|
|
640
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
641
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/jsx-ai');
|
|
642
|
+
expect((document.getElementById('repoSelect') as HTMLSelectElement).value).toBe('C:/Code/jsx-ai');
|
|
643
|
+
|
|
644
|
+
pullBtn.click();
|
|
645
|
+
await Promise.resolve();
|
|
646
|
+
await Promise.resolve();
|
|
647
|
+
|
|
648
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
649
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/jsx-ai');
|
|
650
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
651
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
652
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
653
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
654
|
+
} finally {
|
|
655
|
+
(mock as any).restore?.();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('multiple rerender and click cycles keep using fresh recent-repo handlers without duplicate handoffs', async () => {
|
|
660
|
+
const onRepoReady = mock((path: string) => {
|
|
661
|
+
currentRepoPath = path;
|
|
662
|
+
});
|
|
663
|
+
let currentRepoPath = '';
|
|
664
|
+
const ctx = {
|
|
665
|
+
onRepoReady,
|
|
666
|
+
actor: { send: mock(() => undefined) },
|
|
667
|
+
snap: () => ({
|
|
668
|
+
context: { repoPath: currentRepoPath, zoom: 1, offsetX: 0, offsetY: 0, commits: [] },
|
|
669
|
+
value: { view: 'allfiles' },
|
|
670
|
+
}),
|
|
671
|
+
fileCards: new Map(),
|
|
672
|
+
deferredCards: new Map(),
|
|
673
|
+
changedFilePaths: new Set(),
|
|
674
|
+
positions: new Map(),
|
|
675
|
+
hiddenFiles: new Set(),
|
|
676
|
+
allFilesData: [],
|
|
677
|
+
commitFilesData: [],
|
|
678
|
+
canvas: document.createElement('div'),
|
|
679
|
+
canvasViewport: document.createElement('div'),
|
|
680
|
+
svgOverlay: null,
|
|
681
|
+
loadingOverlay: null,
|
|
682
|
+
} as any;
|
|
683
|
+
setCanvasContext(ctx);
|
|
684
|
+
|
|
685
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
686
|
+
repoSelect.value = '';
|
|
687
|
+
|
|
688
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
689
|
+
let firstCycleItem = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
690
|
+
expect(firstCycleItem).toBeTruthy();
|
|
691
|
+
|
|
692
|
+
firstCycleItem.click();
|
|
693
|
+
await Promise.resolve();
|
|
694
|
+
|
|
695
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
696
|
+
expect(onRepoReady).toHaveBeenNthCalledWith(1, 'C:/Code/gitmaps');
|
|
697
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
698
|
+
|
|
699
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
700
|
+
renderRecentCommitsUI();
|
|
701
|
+
|
|
702
|
+
const secondCycleItem = document.querySelector('[data-path="C:/Code/jsx-ai"]') as HTMLButtonElement;
|
|
703
|
+
expect(secondCycleItem).toBeTruthy();
|
|
704
|
+
expect(secondCycleItem).not.toBe(firstCycleItem);
|
|
705
|
+
|
|
706
|
+
secondCycleItem.click();
|
|
707
|
+
await Promise.resolve();
|
|
708
|
+
|
|
709
|
+
expect(onRepoReady).toHaveBeenCalledTimes(2);
|
|
710
|
+
expect(onRepoReady).toHaveBeenNthCalledWith(2, 'C:/Code/jsx-ai');
|
|
711
|
+
expect(repoSelect.value).toBe('C:/Code/jsx-ai');
|
|
712
|
+
|
|
713
|
+
addRecentRepo('C:/Code/gitmaps', 13);
|
|
714
|
+
renderRecentCommitsUI();
|
|
715
|
+
|
|
716
|
+
const thirdCycleItem = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
717
|
+
expect(thirdCycleItem).toBeTruthy();
|
|
718
|
+
expect(thirdCycleItem).not.toBe(firstCycleItem);
|
|
719
|
+
|
|
720
|
+
thirdCycleItem.click();
|
|
721
|
+
await Promise.resolve();
|
|
722
|
+
|
|
723
|
+
expect(onRepoReady).toHaveBeenCalledTimes(3);
|
|
724
|
+
expect(onRepoReady).toHaveBeenNthCalledWith(3, 'C:/Code/gitmaps');
|
|
725
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
726
|
+
expect(currentRepoPath).toBe('C:/Code/gitmaps');
|
|
727
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test('removing a recent repo after repeated rerenders clears stale entries and preserves remaining click-through behavior', async () => {
|
|
731
|
+
const onRepoReady = mock((path: string) => {
|
|
732
|
+
currentRepoPath = path;
|
|
733
|
+
});
|
|
734
|
+
let currentRepoPath = '';
|
|
735
|
+
const ctx = {
|
|
736
|
+
onRepoReady,
|
|
737
|
+
actor: { send: mock(() => undefined) },
|
|
738
|
+
snap: () => ({
|
|
739
|
+
context: { repoPath: currentRepoPath, zoom: 1, offsetX: 0, offsetY: 0, commits: [] },
|
|
740
|
+
value: { view: 'allfiles' },
|
|
741
|
+
}),
|
|
742
|
+
fileCards: new Map(),
|
|
743
|
+
deferredCards: new Map(),
|
|
744
|
+
changedFilePaths: new Set(),
|
|
745
|
+
positions: new Map(),
|
|
746
|
+
hiddenFiles: new Set(),
|
|
747
|
+
allFilesData: [],
|
|
748
|
+
commitFilesData: [],
|
|
749
|
+
canvas: document.createElement('div'),
|
|
750
|
+
canvasViewport: document.createElement('div'),
|
|
751
|
+
svgOverlay: null,
|
|
752
|
+
loadingOverlay: null,
|
|
753
|
+
} as any;
|
|
754
|
+
setCanvasContext(ctx);
|
|
755
|
+
|
|
756
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
757
|
+
repoSelect.value = '';
|
|
758
|
+
|
|
759
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
760
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
761
|
+
renderRecentCommitsUI();
|
|
762
|
+
|
|
763
|
+
const firstJsxItem = document.querySelector('[data-path="C:/Code/jsx-ai"]') as HTMLButtonElement;
|
|
764
|
+
expect(firstJsxItem).toBeTruthy();
|
|
765
|
+
|
|
766
|
+
firstJsxItem.click();
|
|
767
|
+
await Promise.resolve();
|
|
768
|
+
|
|
769
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
770
|
+
expect(onRepoReady).toHaveBeenLastCalledWith('C:/Code/jsx-ai');
|
|
771
|
+
expect(repoSelect.value).toBe('C:/Code/jsx-ai');
|
|
772
|
+
|
|
773
|
+
addRecentRepo('C:/Code/gitmaps', 13);
|
|
774
|
+
renderRecentCommitsUI();
|
|
775
|
+
removeRecentRepo('C:/Code/jsx-ai');
|
|
776
|
+
renderRecentCommitsUI();
|
|
777
|
+
|
|
778
|
+
expect(document.querySelector('[data-path="C:/Code/jsx-ai"]')).toBeNull();
|
|
779
|
+
|
|
780
|
+
const remainingItem = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
781
|
+
expect(remainingItem).toBeTruthy();
|
|
782
|
+
expect(document.querySelectorAll('[data-path]')).toHaveLength(1);
|
|
783
|
+
expect(getRecentRepos().map((repo) => repo.path)).toEqual(['C:/Code/gitmaps']);
|
|
784
|
+
|
|
785
|
+
remainingItem.click();
|
|
786
|
+
await Promise.resolve();
|
|
787
|
+
|
|
788
|
+
expect(onRepoReady).toHaveBeenCalledTimes(2);
|
|
789
|
+
expect(onRepoReady).toHaveBeenNthCalledWith(2, 'C:/Code/gitmaps');
|
|
790
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
791
|
+
expect(currentRepoPath).toBe('C:/Code/gitmaps');
|
|
792
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test('after rerenders and recent-repo removal, Pull uses the surviving clicked entry as the active repo path', async () => {
|
|
796
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
797
|
+
const originalRepoModule = require('./repo');
|
|
798
|
+
mock.module('./repo', () => ({
|
|
799
|
+
...originalRepoModule,
|
|
800
|
+
loadRepository: loadRepositoryMock,
|
|
801
|
+
}));
|
|
802
|
+
|
|
803
|
+
let currentRepoPath = 'C:/Code/jsx-ai';
|
|
804
|
+
const onRepoReady = mock((path: string) => {
|
|
805
|
+
currentRepoPath = path;
|
|
806
|
+
});
|
|
807
|
+
const ctx = {
|
|
808
|
+
onRepoReady,
|
|
809
|
+
actor: { send: mock(() => undefined) },
|
|
810
|
+
snap: () => ({
|
|
811
|
+
context: { repoPath: currentRepoPath, zoom: 1, offsetX: 0, offsetY: 0, commits: [] },
|
|
812
|
+
value: { view: 'allfiles' },
|
|
813
|
+
}),
|
|
814
|
+
fileCards: new Map(),
|
|
815
|
+
deferredCards: new Map(),
|
|
816
|
+
changedFilePaths: new Set(),
|
|
817
|
+
positions: new Map(),
|
|
818
|
+
hiddenFiles: new Set(),
|
|
819
|
+
allFilesData: [],
|
|
820
|
+
commitFilesData: [],
|
|
821
|
+
canvas: document.createElement('div'),
|
|
822
|
+
canvasViewport: document.createElement('div'),
|
|
823
|
+
svgOverlay: null,
|
|
824
|
+
loadingOverlay: null,
|
|
825
|
+
} as any;
|
|
826
|
+
setCanvasContext(ctx);
|
|
827
|
+
|
|
828
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
829
|
+
repoSelect.value = '';
|
|
830
|
+
|
|
831
|
+
renderRecentCommitsUI();
|
|
832
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
833
|
+
addRecentRepo('C:/Code/jsx-ai', 5);
|
|
834
|
+
renderRecentCommitsUI();
|
|
835
|
+
removeRecentRepo('C:/Code/jsx-ai');
|
|
836
|
+
renderRecentCommitsUI();
|
|
837
|
+
|
|
838
|
+
const remainingItem = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
839
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
expect(document.querySelector('[data-path="C:/Code/jsx-ai"]')).toBeNull();
|
|
843
|
+
expect(remainingItem).toBeTruthy();
|
|
844
|
+
expect(pullBtn.dataset.bound).toBe('true');
|
|
845
|
+
|
|
846
|
+
remainingItem.click();
|
|
847
|
+
await Promise.resolve();
|
|
848
|
+
|
|
849
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
850
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
851
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
852
|
+
expect(currentRepoPath).toBe('C:/Code/gitmaps');
|
|
853
|
+
|
|
854
|
+
pullBtn.click();
|
|
855
|
+
await Promise.resolve();
|
|
856
|
+
await Promise.resolve();
|
|
857
|
+
|
|
858
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
859
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
860
|
+
expect(loadRepositoryMock).not.toHaveBeenCalledWith(ctx, 'C:/Code/jsx-ai');
|
|
861
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
862
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
863
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
864
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
865
|
+
} finally {
|
|
866
|
+
(mock as any).restore?.();
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test('removing the final recent repo after rerenders leaves Pull bound only to the active repo state', async () => {
|
|
871
|
+
const loadRepositoryMock = mock(async () => undefined);
|
|
872
|
+
const originalRepoModule = require('./repo');
|
|
873
|
+
mock.module('./repo', () => ({
|
|
874
|
+
...originalRepoModule,
|
|
875
|
+
loadRepository: loadRepositoryMock,
|
|
876
|
+
}));
|
|
877
|
+
|
|
878
|
+
let currentRepoPath = 'C:/Code/gitmaps';
|
|
879
|
+
const onRepoReady = mock((path: string) => {
|
|
880
|
+
currentRepoPath = path;
|
|
881
|
+
});
|
|
882
|
+
const ctx = {
|
|
883
|
+
onRepoReady,
|
|
884
|
+
actor: { send: mock(() => undefined) },
|
|
885
|
+
snap: () => ({
|
|
886
|
+
context: { repoPath: currentRepoPath, zoom: 1, offsetX: 0, offsetY: 0, commits: [] },
|
|
887
|
+
value: { view: 'allfiles' },
|
|
888
|
+
}),
|
|
889
|
+
fileCards: new Map(),
|
|
890
|
+
deferredCards: new Map(),
|
|
891
|
+
changedFilePaths: new Set(),
|
|
892
|
+
positions: new Map(),
|
|
893
|
+
hiddenFiles: new Set(),
|
|
894
|
+
allFilesData: [],
|
|
895
|
+
commitFilesData: [],
|
|
896
|
+
canvas: document.createElement('div'),
|
|
897
|
+
canvasViewport: document.createElement('div'),
|
|
898
|
+
svgOverlay: null,
|
|
899
|
+
loadingOverlay: null,
|
|
900
|
+
} as any;
|
|
901
|
+
setCanvasContext(ctx);
|
|
902
|
+
|
|
903
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
904
|
+
const pullBtn = document.getElementById('pullBtn') as HTMLButtonElement;
|
|
905
|
+
repoSelect.value = '';
|
|
906
|
+
|
|
907
|
+
renderRecentCommitsUI();
|
|
908
|
+
addRecentRepo('C:/Code/gitmaps', 12);
|
|
909
|
+
renderRecentCommitsUI();
|
|
910
|
+
|
|
911
|
+
const recentItem = document.querySelector('[data-path="C:/Code/gitmaps"]') as HTMLButtonElement;
|
|
912
|
+
expect(recentItem).toBeTruthy();
|
|
913
|
+
|
|
914
|
+
recentItem.click();
|
|
915
|
+
await Promise.resolve();
|
|
916
|
+
|
|
917
|
+
expect(onRepoReady).toHaveBeenCalledTimes(1);
|
|
918
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
919
|
+
expect(currentRepoPath).toBe('C:/Code/gitmaps');
|
|
920
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
921
|
+
|
|
922
|
+
removeRecentRepo('C:/Code/gitmaps');
|
|
923
|
+
renderRecentCommitsUI();
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
expect(document.querySelector('[data-path]')).toBeNull();
|
|
927
|
+
expect(getRecentRepos()).toEqual([]);
|
|
928
|
+
expect((document.getElementById('recentCommits') as HTMLDivElement).style.display).toBe('none');
|
|
929
|
+
expect(pullBtn.dataset.bound).toBe('true');
|
|
930
|
+
expect(currentRepoPath).toBe('C:/Code/gitmaps');
|
|
931
|
+
|
|
932
|
+
pullBtn.click();
|
|
933
|
+
await Promise.resolve();
|
|
934
|
+
await Promise.resolve();
|
|
935
|
+
|
|
936
|
+
expect(loadRepositoryMock).toHaveBeenCalledTimes(1);
|
|
937
|
+
expect(loadRepositoryMock).toHaveBeenCalledWith(ctx, 'C:/Code/gitmaps');
|
|
938
|
+
expect(loadRepositoryMock.mock.calls.map((call: any[]) => call[1])).not.toContain('C:/Code/jsx-ai');
|
|
939
|
+
expect(pullBtn.disabled).toBeFalse();
|
|
940
|
+
expect((pullBtn.textContent || '').trim()).toBe('Pull');
|
|
941
|
+
expect(document.querySelector('.toast.success')?.textContent || '').toContain('Pulled latest commits');
|
|
942
|
+
expect(ctx.actor.send).not.toHaveBeenCalled();
|
|
943
|
+
} finally {
|
|
944
|
+
(mock as any).restore?.();
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
});
|