Skip to content

Commit 8b02a9d

Browse files
Harden frontend test isolation per PR #86 review
Mock showConfirm in export tests, verify in-flight export button state, key session fetch mocks by session ID, tighten file_read assertion, add global stub cleanup across test files, and await clipboard write assertion defensively.
1 parent 1694bcc commit 8b02a9d

6 files changed

Lines changed: 54 additions & 22 deletions

File tree

static/js/app.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { state } from './shared/state.js';
33

44
const showProjects = vi.fn();
@@ -19,13 +19,21 @@ vi.mock('./shared/theme.js', () => ({
1919
}));
2020

2121
describe('router (app.js)', () => {
22+
const origScrollTo = window.scrollTo;
23+
const origScrollIntoView = Element.prototype.scrollIntoView;
24+
2225
beforeAll(async () => {
2326
window.scrollTo = vi.fn();
2427
Element.prototype.scrollIntoView = vi.fn();
2528
await import('./app.js');
2629
document.dispatchEvent(new Event('DOMContentLoaded'));
2730
});
2831

32+
afterAll(() => {
33+
window.scrollTo = origScrollTo;
34+
Element.prototype.scrollIntoView = origScrollIntoView;
35+
});
36+
2937
beforeEach(() => {
3038
document.body.innerHTML = '<div id="content"></div><span id="footer-year"></span>';
3139
state.currentProject = null;

static/js/export.test.js

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2-
import { bulkExport, downloadSession } from './export.js';
32

43
vi.mock('./projects.js', () => ({ showProjects: vi.fn() }));
4+
vi.mock('./shared/utils.js', async (importOriginal) => {
5+
const actual = await importOriginal();
6+
return {
7+
...actual,
8+
showConfirm: vi.fn((_, cb) => { cb(); }),
9+
};
10+
});
511

12+
import { bulkExport, downloadSession } from './export.js';
613
import { showProjects } from './projects.js';
714

815
const mockWritable = {
@@ -30,30 +37,28 @@ describe('export', () => {
3037
});
3138

3239
afterEach(() => {
40+
vi.unstubAllGlobals();
3341
vi.restoreAllMocks();
3442
});
3543

36-
async function confirmExport() {
37-
const ok = document.querySelector('.confirm-ok');
38-
expect(ok).not.toBeNull();
39-
ok.click();
40-
await vi.waitFor(() => expect(fetch).toHaveBeenCalled());
41-
}
42-
4344
it('bulkExport shows progress then completes on success', async () => {
44-
fetch.mockResolvedValue({
45+
let resolveFetch;
46+
const pending = new Promise((resolve) => { resolveFetch = resolve; });
47+
fetch.mockImplementation(() => pending.then(() => ({
4548
ok: true,
4649
headers: { get: () => 'application/zip' },
4750
blob: () => Promise.resolve(new Blob(['zip'], { type: 'application/zip' })),
48-
});
51+
})));
4952

5053
bulkExport('all');
5154
const btn = document.getElementById('btn-export-all');
52-
expect(btn.disabled).toBe(false);
53-
await confirmExport();
55+
await vi.waitFor(() => expect(btn.disabled).toBe(true));
56+
expect(btn.textContent).toContain('Exporting');
57+
58+
resolveFetch();
59+
await vi.waitFor(() => expect(btn.disabled).toBe(false));
5460

5561
expect(btn.textContent.trim()).toBe('Export all');
56-
expect(btn.disabled).toBe(false);
5762
expect(mockHandle.createWritable).toHaveBeenCalled();
5863
expect(showProjects).toHaveBeenCalled();
5964
expect(fetch).toHaveBeenCalledWith('/api/export', expect.objectContaining({
@@ -71,7 +76,6 @@ describe('export', () => {
7176
});
7277

7378
bulkExport('all');
74-
await confirmExport();
7579
await vi.waitFor(() => expect(document.querySelector('.toast-error')).not.toBeNull());
7680

7781
expect(document.querySelector('.toast-error').textContent).toContain('export failed');
@@ -86,7 +90,6 @@ describe('export', () => {
8690
});
8791

8892
bulkExport('incremental');
89-
await confirmExport();
9093
await vi.waitFor(() => expect(document.querySelector('.toast-error')).not.toBeNull());
9194

9295
expect(document.querySelector('.toast-error').textContent).toContain('Export failed: 403');

static/js/projects.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { showProjects } from './projects.js';
33

44
// ProjectDict[] — mirrors models/project.py.
@@ -25,6 +25,10 @@ describe('showProjects', () => {
2525
vi.stubGlobal('fetch', vi.fn());
2626
});
2727

28+
afterEach(() => {
29+
vi.unstubAllGlobals();
30+
});
31+
2832
it('renders project cards from the API response', async () => {
2933
fetch.mockImplementation((url) => {
3034
if (url === '/api/projects') {

static/js/render/tool_result/file_read.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ describe('renderFileReadResult', () => {
1919
file_path: 'README.md',
2020
});
2121
expect(html).toContain('Read: README.md');
22-
expect(html).not.toContain('lines');
22+
expect(html).not.toMatch(/\d+ lines/);
2323
});
2424
});

static/js/search.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { showSearchPage, doSearch } from './search.js';
33

44
// SearchHitDict[] — mirrors models/search.py (no highlight wrapper in search.js; snippets are escaped text only).
@@ -28,6 +28,10 @@ describe('search page', () => {
2828
window.location.hash = '';
2929
});
3030

31+
afterEach(() => {
32+
vi.unstubAllGlobals();
33+
});
34+
3135
it('showSearchPage renders the search UI and sets hash', () => {
3236
showSearchPage();
3337
expect(window.location.hash).toBe('#search');

static/js/sessions.test.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { state } from './shared/state.js';
33
import { showWorkspace, loadSession, selectSession, copyAll } from './sessions.js';
44

@@ -62,7 +62,16 @@ function mockWorkspaceFetch() {
6262
return Promise.resolve({ ok: true, json: () => Promise.resolve(SESSION_LIST) });
6363
}
6464
if (url.startsWith('/api/sessions/alpha/')) {
65-
return Promise.resolve({ ok: true, json: () => Promise.resolve(SESSION_DETAIL) });
65+
const sessionId = decodeURIComponent(url.split('/').pop());
66+
const row = SESSION_LIST.find((s) => s.id === sessionId) ?? SESSION_LIST[0];
67+
return Promise.resolve({
68+
ok: true,
69+
json: () => Promise.resolve({
70+
...SESSION_DETAIL,
71+
session_id: sessionId,
72+
title: row.title,
73+
}),
74+
});
6675
}
6776
return Promise.reject(new Error(`unexpected fetch: ${url}`));
6877
});
@@ -78,6 +87,10 @@ describe('sessions workspace', () => {
7887
window.location.hash = '';
7988
});
8089

90+
afterEach(() => {
91+
vi.unstubAllGlobals();
92+
});
93+
8194
it('showWorkspace populates the sidebar with session entries', async () => {
8295
mockWorkspaceFetch();
8396
await showWorkspace('alpha');
@@ -142,6 +155,6 @@ describe('sessions workspace', () => {
142155

143156
copyAll();
144157

145-
expect(writeText).toHaveBeenCalledWith('Line one\nLine two');
158+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('Line one\nLine two'));
146159
});
147160
});

0 commit comments

Comments
 (0)