@abreen/tada 1.5.1 → 1.5.4
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/build/globals.ts +1 -1
- package/package.json +3 -2
- package/src/code/index.ts +5 -4
- package/src/index.ts +1 -1
- package/src/navigate/index.ts +27 -21
- package/src/navigate/lifecycle.ts +1 -1
- package/src/navigate/style.scss +2 -2
- package/src/search/index.ts +19 -16
- package/src/toc/index.ts +1 -1
- package/src/top/index.ts +3 -3
- package/src/util.ts +7 -6
- package/templates/_nav.html +1 -0
- package/bin/validators.test.ts +0 -205
- package/build/build-manifest.test.ts +0 -369
- package/build/code.test.ts +0 -272
- package/build/colors.test.ts +0 -66
- package/build/globals.test.ts +0 -100
- package/build/json-schema.test.ts +0 -57
- package/build/log.test.ts +0 -117
- package/build/markdown-plugins.test.ts +0 -456
- package/build/pagefind.test.ts +0 -192
- package/build/pdf-text.test.ts +0 -52
- package/build/reachability.test.ts +0 -80
- package/build/serve.test.ts +0 -59
- package/build/toc-plugin.test.ts +0 -120
- package/build/utils/content-files.test.ts +0 -56
- package/build/utils/derive-theme.test.ts +0 -111
- package/build/utils/file-types.test.ts +0 -79
- package/build/utils/front-matter.test.ts +0 -77
- package/build/utils/include.test.ts +0 -117
- package/build/utils/paths.test.ts +0 -91
- package/build/utils/trace-layout.test.ts +0 -570
- package/build/utils/trace-svg.test.ts +0 -425
- package/build/utils/trace.test.ts +0 -169
- package/build/validate-config-links.test.ts +0 -239
- package/build/validate-internal-links-plugin.test.ts +0 -134
- package/build/watch-reachability-state.test.ts +0 -198
- package/fonts/google-sans-code/ttf/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/ttf/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/inter/ttf/InterVariable-Italic.ttf +0 -0
- package/src/anchor/index.test.ts +0 -64
- package/src/code/index.test.ts +0 -71
- package/src/header/index.test.ts +0 -89
- package/src/navigate/eligible.test.ts +0 -101
- package/src/navigate/index.test.ts +0 -82
- package/src/navigate/lifecycle.test.ts +0 -26
- package/src/print/index.test.ts +0 -71
- package/src/question/index.test.ts +0 -82
- package/src/search/index.test.ts +0 -203
- package/src/timezone/index.test.ts +0 -400
- package/src/toc/index.test.ts +0 -237
- package/src/top/index.test.ts +0 -46
- package/src/trace/index.test.ts +0 -554
- package/src/util.test.ts +0 -135
package/src/trace/index.test.ts
DELETED
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
-
import { JSDOM } from 'jsdom';
|
|
3
|
-
import type { TraceManifest, TraceChunkEntry } from './types';
|
|
4
|
-
import mount from './index';
|
|
5
|
-
|
|
6
|
-
function makeManifest(overrides: Partial<TraceManifest> = {}): TraceManifest {
|
|
7
|
-
return {
|
|
8
|
-
totalSteps: 3,
|
|
9
|
-
chunkSize: 10,
|
|
10
|
-
sourceFile: 'Main.java',
|
|
11
|
-
source: 'public class Main {}',
|
|
12
|
-
lineToSteps: {},
|
|
13
|
-
...overrides,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function makeChunk(
|
|
18
|
-
entries: Array<{ line?: number; stdout?: string; svg?: string }>,
|
|
19
|
-
): TraceChunkEntry[] {
|
|
20
|
-
return entries.map(e => ({
|
|
21
|
-
line: e.line ?? 1,
|
|
22
|
-
stdout: e.stdout ?? '',
|
|
23
|
-
svg: e.svg ?? '<svg></svg>',
|
|
24
|
-
}));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const defaultManifest = makeManifest();
|
|
28
|
-
const defaultChunk = makeChunk([
|
|
29
|
-
{ line: 1, stdout: '', svg: '<svg>step0</svg>' },
|
|
30
|
-
{ line: 2, stdout: 'hello', svg: '<svg>step1</svg>' },
|
|
31
|
-
{ line: 3, stdout: ' world', svg: '<svg>step2</svg>' },
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
function widgetHtml(manifestUrl = '/trace/manifest.json'): string {
|
|
35
|
-
return (
|
|
36
|
-
`<div class="trace-widget" data-trace-manifest="${manifestUrl}">` +
|
|
37
|
-
'<div class="trace-source">' +
|
|
38
|
-
'<span class="line-number" data-line="1">1</span>' +
|
|
39
|
-
'<span class="line-number" data-line="2">2</span>' +
|
|
40
|
-
'<span class="line-number" data-line="3">3</span>' +
|
|
41
|
-
'</div>' +
|
|
42
|
-
'<div class="trace-controls">' +
|
|
43
|
-
'<button class="trace-first">First</button>' +
|
|
44
|
-
'<button class="trace-prev">Prev</button>' +
|
|
45
|
-
'<span class="trace-step-counter"></span>' +
|
|
46
|
-
'<button class="trace-next">Next</button>' +
|
|
47
|
-
'<button class="trace-last">Last</button>' +
|
|
48
|
-
'</div>' +
|
|
49
|
-
'<pre class="trace-output"></pre>' +
|
|
50
|
-
'<div class="trace-diagram"></div>' +
|
|
51
|
-
'</div>'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createWindow(html: string) {
|
|
56
|
-
const dom = new JSDOM(`<body>${html}</body>`, { url: 'http://localhost/' });
|
|
57
|
-
return dom.window;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let originalFetch: typeof globalThis.fetch;
|
|
61
|
-
|
|
62
|
-
function setupFetch(responses: Record<string, unknown>): void {
|
|
63
|
-
const map = new Map(Object.entries(responses));
|
|
64
|
-
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
65
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
66
|
-
const data = map.get(url);
|
|
67
|
-
if (data === undefined) {
|
|
68
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
69
|
-
}
|
|
70
|
-
return { ok: true, json: async () => data } as Response;
|
|
71
|
-
}) as typeof fetch;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function flush(): Promise<void> {
|
|
75
|
-
for (let i = 0; i < 10; i++) {
|
|
76
|
-
await new Promise(r => setTimeout(r, 0));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function setupDefaultFetch(): void {
|
|
81
|
-
setupFetch({
|
|
82
|
-
'/trace/manifest.json': defaultManifest,
|
|
83
|
-
'/trace/chunk-0.json': defaultChunk,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe('trace', () => {
|
|
88
|
-
beforeEach(() => {
|
|
89
|
-
originalFetch = globalThis.fetch;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
afterEach(() => {
|
|
93
|
-
globalThis.fetch = originalFetch;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('returns early when no trace widgets exist', () => {
|
|
97
|
-
let fetchCalled = false;
|
|
98
|
-
globalThis.fetch = (async () => {
|
|
99
|
-
fetchCalled = true;
|
|
100
|
-
return {} as Response;
|
|
101
|
-
}) as unknown as typeof fetch;
|
|
102
|
-
|
|
103
|
-
const win = createWindow('<div>no widgets</div>');
|
|
104
|
-
mount(win);
|
|
105
|
-
expect(fetchCalled).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('returns early when widget has no data-trace-manifest', async () => {
|
|
109
|
-
let fetchCalled = false;
|
|
110
|
-
globalThis.fetch = (async () => {
|
|
111
|
-
fetchCalled = true;
|
|
112
|
-
return {} as Response;
|
|
113
|
-
}) as unknown as typeof fetch;
|
|
114
|
-
|
|
115
|
-
const win = createWindow('<div class="trace-widget"></div>');
|
|
116
|
-
mount(win);
|
|
117
|
-
await flush();
|
|
118
|
-
expect(fetchCalled).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('fetches manifest and first chunk on init', async () => {
|
|
122
|
-
const fetched: string[] = [];
|
|
123
|
-
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
124
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
125
|
-
fetched.push(url);
|
|
126
|
-
if (url.includes('manifest.json')) {
|
|
127
|
-
return { ok: true, json: async () => defaultManifest } as Response;
|
|
128
|
-
}
|
|
129
|
-
return { ok: true, json: async () => defaultChunk } as Response;
|
|
130
|
-
}) as typeof fetch;
|
|
131
|
-
|
|
132
|
-
const win = createWindow(widgetHtml());
|
|
133
|
-
mount(win);
|
|
134
|
-
await flush();
|
|
135
|
-
|
|
136
|
-
expect(fetched).toContain('/trace/manifest.json');
|
|
137
|
-
expect(fetched).toContain('/trace/chunk-0.json');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('displays step counter as 1/N', async () => {
|
|
141
|
-
setupDefaultFetch();
|
|
142
|
-
const win = createWindow(widgetHtml());
|
|
143
|
-
mount(win);
|
|
144
|
-
await flush();
|
|
145
|
-
|
|
146
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
147
|
-
expect(counter!.textContent).toBe('1/3');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('step counter min-width matches total step digits', async () => {
|
|
151
|
-
const manifest = makeManifest({ totalSteps: 100 });
|
|
152
|
-
const chunk = makeChunk(Array.from({ length: 10 }, () => ({ line: 1 })));
|
|
153
|
-
setupFetch({
|
|
154
|
-
'/trace/manifest.json': manifest,
|
|
155
|
-
'/trace/chunk-0.json': chunk,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const win = createWindow(widgetHtml());
|
|
159
|
-
mount(win);
|
|
160
|
-
await flush();
|
|
161
|
-
|
|
162
|
-
const counter = win.document.querySelector(
|
|
163
|
-
'.trace-step-counter',
|
|
164
|
-
) as HTMLElement;
|
|
165
|
-
// 3 digits * 2 + 1 = 7ch
|
|
166
|
-
expect(counter.style.minWidth).toBe('7ch');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('first and prev buttons disabled at step 0', async () => {
|
|
170
|
-
setupDefaultFetch();
|
|
171
|
-
const win = createWindow(widgetHtml());
|
|
172
|
-
mount(win);
|
|
173
|
-
await flush();
|
|
174
|
-
|
|
175
|
-
const first = win.document.querySelector(
|
|
176
|
-
'.trace-first',
|
|
177
|
-
) as HTMLButtonElement;
|
|
178
|
-
const prev = win.document.querySelector('.trace-prev') as HTMLButtonElement;
|
|
179
|
-
expect(first.disabled).toBe(true);
|
|
180
|
-
expect(prev.disabled).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('next and last buttons enabled at step 0', async () => {
|
|
184
|
-
setupDefaultFetch();
|
|
185
|
-
const win = createWindow(widgetHtml());
|
|
186
|
-
mount(win);
|
|
187
|
-
await flush();
|
|
188
|
-
|
|
189
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
190
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
191
|
-
expect(next.disabled).toBe(false);
|
|
192
|
-
expect(last.disabled).toBe(false);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test('highlights the active source line', async () => {
|
|
196
|
-
setupDefaultFetch();
|
|
197
|
-
const win = createWindow(widgetHtml());
|
|
198
|
-
mount(win);
|
|
199
|
-
await flush();
|
|
200
|
-
|
|
201
|
-
const active = win.document.querySelector('.trace-line-active');
|
|
202
|
-
expect(active).not.toBeNull();
|
|
203
|
-
expect(active!.getAttribute('data-line')).toBe('1');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test('renders initial SVG in diagram', async () => {
|
|
207
|
-
setupDefaultFetch();
|
|
208
|
-
const win = createWindow(widgetHtml());
|
|
209
|
-
mount(win);
|
|
210
|
-
await flush();
|
|
211
|
-
|
|
212
|
-
const diagram = win.document.querySelector('.trace-diagram') as HTMLElement;
|
|
213
|
-
expect(diagram.innerHTML).toBe('<svg>step0</svg>');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('renders initial stdout in output', async () => {
|
|
217
|
-
const chunk = makeChunk([
|
|
218
|
-
{ line: 1, stdout: 'initial output', svg: '<svg></svg>' },
|
|
219
|
-
{ line: 2, svg: '<svg></svg>' },
|
|
220
|
-
]);
|
|
221
|
-
setupFetch({
|
|
222
|
-
'/trace/manifest.json': makeManifest({ totalSteps: 2 }),
|
|
223
|
-
'/trace/chunk-0.json': chunk,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const win = createWindow(widgetHtml());
|
|
227
|
-
mount(win);
|
|
228
|
-
await flush();
|
|
229
|
-
|
|
230
|
-
const output = win.document.querySelector('.trace-output') as HTMLElement;
|
|
231
|
-
expect(output.textContent).toBe('initial output');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('clicking next advances to the next step', async () => {
|
|
235
|
-
setupDefaultFetch();
|
|
236
|
-
const win = createWindow(widgetHtml());
|
|
237
|
-
mount(win);
|
|
238
|
-
await flush();
|
|
239
|
-
|
|
240
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
241
|
-
next.click();
|
|
242
|
-
await flush();
|
|
243
|
-
|
|
244
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
245
|
-
expect(counter!.textContent).toBe('2/3');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test('clicking next updates source highlight', async () => {
|
|
249
|
-
setupDefaultFetch();
|
|
250
|
-
const win = createWindow(widgetHtml());
|
|
251
|
-
mount(win);
|
|
252
|
-
await flush();
|
|
253
|
-
|
|
254
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
255
|
-
next.click();
|
|
256
|
-
await flush();
|
|
257
|
-
|
|
258
|
-
const active = win.document.querySelector('.trace-line-active');
|
|
259
|
-
expect(active!.getAttribute('data-line')).toBe('2');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test('clicking next updates SVG diagram', async () => {
|
|
263
|
-
setupDefaultFetch();
|
|
264
|
-
const win = createWindow(widgetHtml());
|
|
265
|
-
mount(win);
|
|
266
|
-
await flush();
|
|
267
|
-
|
|
268
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
269
|
-
next.click();
|
|
270
|
-
await flush();
|
|
271
|
-
|
|
272
|
-
const diagram = win.document.querySelector('.trace-diagram') as HTMLElement;
|
|
273
|
-
expect(diagram.innerHTML).toBe('<svg>step1</svg>');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('output accumulates stdout across steps', async () => {
|
|
277
|
-
setupDefaultFetch();
|
|
278
|
-
const win = createWindow(widgetHtml());
|
|
279
|
-
mount(win);
|
|
280
|
-
await flush();
|
|
281
|
-
|
|
282
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
283
|
-
const output = win.document.querySelector('.trace-output') as HTMLElement;
|
|
284
|
-
|
|
285
|
-
next.click();
|
|
286
|
-
await flush();
|
|
287
|
-
expect(output.textContent).toBe('hello');
|
|
288
|
-
|
|
289
|
-
next.click();
|
|
290
|
-
await flush();
|
|
291
|
-
expect(output.textContent).toBe('hello world');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('clicking last goes to the final step', async () => {
|
|
295
|
-
setupDefaultFetch();
|
|
296
|
-
const win = createWindow(widgetHtml());
|
|
297
|
-
mount(win);
|
|
298
|
-
await flush();
|
|
299
|
-
|
|
300
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
301
|
-
last.click();
|
|
302
|
-
await flush();
|
|
303
|
-
|
|
304
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
305
|
-
expect(counter!.textContent).toBe('3/3');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test('next and last disabled at the last step', async () => {
|
|
309
|
-
setupDefaultFetch();
|
|
310
|
-
const win = createWindow(widgetHtml());
|
|
311
|
-
mount(win);
|
|
312
|
-
await flush();
|
|
313
|
-
|
|
314
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
315
|
-
last.click();
|
|
316
|
-
await flush();
|
|
317
|
-
|
|
318
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
319
|
-
expect(next.disabled).toBe(true);
|
|
320
|
-
expect(last.disabled).toBe(true);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('first and prev enabled after advancing', async () => {
|
|
324
|
-
setupDefaultFetch();
|
|
325
|
-
const win = createWindow(widgetHtml());
|
|
326
|
-
mount(win);
|
|
327
|
-
await flush();
|
|
328
|
-
|
|
329
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
330
|
-
next.click();
|
|
331
|
-
await flush();
|
|
332
|
-
|
|
333
|
-
const first = win.document.querySelector(
|
|
334
|
-
'.trace-first',
|
|
335
|
-
) as HTMLButtonElement;
|
|
336
|
-
const prev = win.document.querySelector('.trace-prev') as HTMLButtonElement;
|
|
337
|
-
expect(first.disabled).toBe(false);
|
|
338
|
-
expect(prev.disabled).toBe(false);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
test('clicking first returns to step 0', async () => {
|
|
342
|
-
setupDefaultFetch();
|
|
343
|
-
const win = createWindow(widgetHtml());
|
|
344
|
-
mount(win);
|
|
345
|
-
await flush();
|
|
346
|
-
|
|
347
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
348
|
-
last.click();
|
|
349
|
-
await flush();
|
|
350
|
-
|
|
351
|
-
const first = win.document.querySelector(
|
|
352
|
-
'.trace-first',
|
|
353
|
-
) as HTMLButtonElement;
|
|
354
|
-
first.click();
|
|
355
|
-
await flush();
|
|
356
|
-
|
|
357
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
358
|
-
expect(counter!.textContent).toBe('1/3');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test('clicking prev goes back one step', async () => {
|
|
362
|
-
setupDefaultFetch();
|
|
363
|
-
const win = createWindow(widgetHtml());
|
|
364
|
-
mount(win);
|
|
365
|
-
await flush();
|
|
366
|
-
|
|
367
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
368
|
-
last.click();
|
|
369
|
-
await flush();
|
|
370
|
-
|
|
371
|
-
const prev = win.document.querySelector('.trace-prev') as HTMLButtonElement;
|
|
372
|
-
prev.click();
|
|
373
|
-
await flush();
|
|
374
|
-
|
|
375
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
376
|
-
expect(counter!.textContent).toBe('2/3');
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test('only one line is highlighted at a time', async () => {
|
|
380
|
-
setupDefaultFetch();
|
|
381
|
-
const win = createWindow(widgetHtml());
|
|
382
|
-
mount(win);
|
|
383
|
-
await flush();
|
|
384
|
-
|
|
385
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
386
|
-
next.click();
|
|
387
|
-
await flush();
|
|
388
|
-
|
|
389
|
-
const allActive = win.document.querySelectorAll('.trace-line-active');
|
|
390
|
-
expect(allActive.length).toBe(1);
|
|
391
|
-
expect(allActive[0].getAttribute('data-line')).toBe('2');
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test('loads additional chunks when stepping across boundaries', async () => {
|
|
395
|
-
const manifest = makeManifest({ totalSteps: 4, chunkSize: 2 });
|
|
396
|
-
const chunk0 = makeChunk([
|
|
397
|
-
{ line: 1, svg: '<svg>s0</svg>' },
|
|
398
|
-
{ line: 2, svg: '<svg>s1</svg>' },
|
|
399
|
-
]);
|
|
400
|
-
const chunk1 = makeChunk([
|
|
401
|
-
{ line: 3, stdout: 'out', svg: '<svg>s2</svg>' },
|
|
402
|
-
{ line: 1, svg: '<svg>s3</svg>' },
|
|
403
|
-
]);
|
|
404
|
-
setupFetch({
|
|
405
|
-
'/trace/manifest.json': manifest,
|
|
406
|
-
'/trace/chunk-0.json': chunk0,
|
|
407
|
-
'/trace/chunk-1.json': chunk1,
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const win = createWindow(widgetHtml());
|
|
411
|
-
mount(win);
|
|
412
|
-
await flush();
|
|
413
|
-
|
|
414
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
415
|
-
last.click();
|
|
416
|
-
await flush();
|
|
417
|
-
|
|
418
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
419
|
-
expect(counter!.textContent).toBe('4/4');
|
|
420
|
-
const diagram = win.document.querySelector('.trace-diagram') as HTMLElement;
|
|
421
|
-
expect(diagram.innerHTML).toBe('<svg>s3</svg>');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test('does not re-fetch already loaded chunks', async () => {
|
|
425
|
-
const fetched: string[] = [];
|
|
426
|
-
const manifest = makeManifest({ totalSteps: 4, chunkSize: 2 });
|
|
427
|
-
const chunk0 = makeChunk([
|
|
428
|
-
{ line: 1, svg: '<svg>s0</svg>' },
|
|
429
|
-
{ line: 2, svg: '<svg>s1</svg>' },
|
|
430
|
-
]);
|
|
431
|
-
const chunk1 = makeChunk([
|
|
432
|
-
{ line: 3, svg: '<svg>s2</svg>' },
|
|
433
|
-
{ line: 1, svg: '<svg>s3</svg>' },
|
|
434
|
-
]);
|
|
435
|
-
|
|
436
|
-
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
437
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
438
|
-
fetched.push(url);
|
|
439
|
-
if (url.includes('manifest.json')) {
|
|
440
|
-
return { ok: true, json: async () => manifest } as Response;
|
|
441
|
-
}
|
|
442
|
-
if (url.includes('chunk-0')) {
|
|
443
|
-
return { ok: true, json: async () => chunk0 } as Response;
|
|
444
|
-
}
|
|
445
|
-
return { ok: true, json: async () => chunk1 } as Response;
|
|
446
|
-
}) as typeof fetch;
|
|
447
|
-
|
|
448
|
-
const win = createWindow(widgetHtml());
|
|
449
|
-
mount(win);
|
|
450
|
-
await flush();
|
|
451
|
-
|
|
452
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
453
|
-
last.click();
|
|
454
|
-
await flush();
|
|
455
|
-
|
|
456
|
-
const first = win.document.querySelector(
|
|
457
|
-
'.trace-first',
|
|
458
|
-
) as HTMLButtonElement;
|
|
459
|
-
first.click();
|
|
460
|
-
await flush();
|
|
461
|
-
|
|
462
|
-
const chunk0Fetches = fetched.filter(u => u.includes('chunk-0'));
|
|
463
|
-
expect(chunk0Fetches.length).toBe(1);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test('initializes multiple widgets', async () => {
|
|
467
|
-
setupDefaultFetch();
|
|
468
|
-
const html = widgetHtml() + widgetHtml();
|
|
469
|
-
const win = createWindow(html);
|
|
470
|
-
mount(win);
|
|
471
|
-
await flush();
|
|
472
|
-
|
|
473
|
-
const counters = win.document.querySelectorAll('.trace-step-counter');
|
|
474
|
-
expect(counters.length).toBe(2);
|
|
475
|
-
expect(counters[0].textContent).toBe('1/3');
|
|
476
|
-
expect(counters[1].textContent).toBe('1/3');
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test('arrow keys move focus between enabled buttons', async () => {
|
|
480
|
-
setupDefaultFetch();
|
|
481
|
-
const win = createWindow(widgetHtml());
|
|
482
|
-
mount(win);
|
|
483
|
-
await flush();
|
|
484
|
-
|
|
485
|
-
// Advance to step 1 so all four buttons are enabled
|
|
486
|
-
const nextBtn = win.document.querySelector(
|
|
487
|
-
'.trace-next',
|
|
488
|
-
) as HTMLButtonElement;
|
|
489
|
-
nextBtn.click();
|
|
490
|
-
await flush();
|
|
491
|
-
|
|
492
|
-
const firstBtn = win.document.querySelector(
|
|
493
|
-
'.trace-first',
|
|
494
|
-
) as HTMLButtonElement;
|
|
495
|
-
firstBtn.focus();
|
|
496
|
-
|
|
497
|
-
firstBtn.dispatchEvent(
|
|
498
|
-
new win.KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }),
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
expect(win.document.activeElement).not.toBe(firstBtn);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test('disabled buttons get tabIndex -1', async () => {
|
|
505
|
-
setupDefaultFetch();
|
|
506
|
-
const win = createWindow(widgetHtml());
|
|
507
|
-
mount(win);
|
|
508
|
-
await flush();
|
|
509
|
-
|
|
510
|
-
const first = win.document.querySelector(
|
|
511
|
-
'.trace-first',
|
|
512
|
-
) as HTMLButtonElement;
|
|
513
|
-
const prev = win.document.querySelector('.trace-prev') as HTMLButtonElement;
|
|
514
|
-
expect(first.tabIndex).toBe(-1);
|
|
515
|
-
expect(prev.tabIndex).toBe(-1);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test('stays disabled when manifest fetch fails', async () => {
|
|
519
|
-
globalThis.fetch = (async () => {
|
|
520
|
-
return { ok: false, status: 404 } as Response;
|
|
521
|
-
}) as unknown as typeof fetch;
|
|
522
|
-
|
|
523
|
-
const win = createWindow(widgetHtml());
|
|
524
|
-
mount(win);
|
|
525
|
-
await flush();
|
|
526
|
-
|
|
527
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
528
|
-
expect(counter!.textContent).toBe('');
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test('stays disabled when manifest fetch throws', async () => {
|
|
532
|
-
globalThis.fetch = (async () => {
|
|
533
|
-
throw new TypeError('Failed to fetch');
|
|
534
|
-
}) as unknown as typeof fetch;
|
|
535
|
-
|
|
536
|
-
const win = createWindow(widgetHtml());
|
|
537
|
-
mount(win);
|
|
538
|
-
await flush();
|
|
539
|
-
|
|
540
|
-
const counter = win.document.querySelector('.trace-step-counter');
|
|
541
|
-
expect(counter!.textContent).toBe('');
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test('at least one enabled button has tabIndex 0', async () => {
|
|
545
|
-
setupDefaultFetch();
|
|
546
|
-
const win = createWindow(widgetHtml());
|
|
547
|
-
mount(win);
|
|
548
|
-
await flush();
|
|
549
|
-
|
|
550
|
-
const next = win.document.querySelector('.trace-next') as HTMLButtonElement;
|
|
551
|
-
const last = win.document.querySelector('.trace-last') as HTMLButtonElement;
|
|
552
|
-
expect(next.tabIndex === 0 || last.tabIndex === 0).toBe(true);
|
|
553
|
-
});
|
|
554
|
-
});
|
package/src/util.test.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { JSDOM } from 'jsdom';
|
|
3
|
-
import { formatDuration, removeClass, getElement, applyBasePath } from './util';
|
|
4
|
-
|
|
5
|
-
function create(html: string) {
|
|
6
|
-
const dom = new JSDOM(`<body>${html}</body>`);
|
|
7
|
-
return dom.window;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('formatDuration', () => {
|
|
11
|
-
test('formats sub-millisecond values', () => {
|
|
12
|
-
expect(formatDuration(0.5)).toBe('0.5000ms');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('formats small millisecond values', () => {
|
|
16
|
-
expect(formatDuration(3.14159)).toBe('3.1416ms');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('formats tens of milliseconds', () => {
|
|
20
|
-
expect(formatDuration(42.7)).toBe('42.700ms');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('formats hundreds of milliseconds', () => {
|
|
24
|
-
expect(formatDuration(456.12)).toBe('456.12ms');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('formats exactly 1 second', () => {
|
|
28
|
-
expect(formatDuration(1000)).toBe('1.00000s');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('formats seconds under 10', () => {
|
|
32
|
-
expect(formatDuration(5432)).toBe('5.43200s');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('formats seconds 10 and above', () => {
|
|
36
|
-
expect(formatDuration(12345)).toBe('12.3450s');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('formats exactly 1 minute', () => {
|
|
40
|
-
expect(formatDuration(60000)).toBe('1m0.000s');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('formats minutes with seconds', () => {
|
|
44
|
-
expect(formatDuration(90000)).toBe('1m30.00s');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('formats large durations', () => {
|
|
48
|
-
expect(formatDuration(600000)).toBe('10m0.00s');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('formats zero', () => {
|
|
52
|
-
expect(formatDuration(0)).toBe('0.0000ms');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('formats negative values with sign', () => {
|
|
56
|
-
expect(formatDuration(-500)).toBe('-500.00ms');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('formats negative seconds', () => {
|
|
60
|
-
expect(formatDuration(-5000)).toBe('-5.00000s');
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('removeClass', () => {
|
|
65
|
-
test('removes a class from an element', () => {
|
|
66
|
-
const win = create('<div class="foo bar">X</div>');
|
|
67
|
-
const el = win.document.querySelector('div') as HTMLElement;
|
|
68
|
-
removeClass(el, 'foo');
|
|
69
|
-
expect(el.classList.contains('foo')).toBe(false);
|
|
70
|
-
expect(el.classList.contains('bar')).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('removes the class attribute when no classes remain', () => {
|
|
74
|
-
const win = create('<div class="only">X</div>');
|
|
75
|
-
const el = win.document.querySelector('div') as HTMLElement;
|
|
76
|
-
removeClass(el, 'only');
|
|
77
|
-
expect(el.hasAttribute('class')).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('keeps class attribute when other classes remain', () => {
|
|
81
|
-
const win = create('<div class="a b">X</div>');
|
|
82
|
-
const el = win.document.querySelector('div') as HTMLElement;
|
|
83
|
-
removeClass(el, 'a');
|
|
84
|
-
expect(el.hasAttribute('class')).toBe(true);
|
|
85
|
-
expect(el.className).toBe('b');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('getElement', () => {
|
|
90
|
-
test('returns the matching element', () => {
|
|
91
|
-
const win = create('<div id="target">Hello</div>');
|
|
92
|
-
const el = getElement(win.document, '#target');
|
|
93
|
-
expect(el.textContent).toBe('Hello');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('throws when no element matches', () => {
|
|
97
|
-
const win = create('<div>No match</div>');
|
|
98
|
-
expect(() => getElement(win.document, '#missing')).toThrow(
|
|
99
|
-
'no element matching "#missing"',
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('works with a parent element scope', () => {
|
|
104
|
-
const win = create(
|
|
105
|
-
'<div id="a"><span class="inner">A</span></div><div id="b"><span class="inner">B</span></div>',
|
|
106
|
-
);
|
|
107
|
-
const parent = win.document.getElementById('b')!;
|
|
108
|
-
const el = getElement(parent, '.inner');
|
|
109
|
-
expect(el.textContent).toBe('B');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('applyBasePath', () => {
|
|
114
|
-
test('prepends the base path to a subpath', () => {
|
|
115
|
-
(globalThis as Record<string, unknown>).__SITE_BASE_PATH__ = '/docs/';
|
|
116
|
-
expect(applyBasePath('/page.html')).toBe('/docs/page.html');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('handles base path without trailing slash', () => {
|
|
120
|
-
(globalThis as Record<string, unknown>).__SITE_BASE_PATH__ = '/docs';
|
|
121
|
-
expect(applyBasePath('/page.html')).toBe('/docs/page.html');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('handles root base path', () => {
|
|
125
|
-
(globalThis as Record<string, unknown>).__SITE_BASE_PATH__ = '/';
|
|
126
|
-
expect(applyBasePath('/page.html')).toBe('/page.html');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('throws for subpath not starting with /', () => {
|
|
130
|
-
(globalThis as Record<string, unknown>).__SITE_BASE_PATH__ = '/docs';
|
|
131
|
-
expect(() => applyBasePath('page.html')).toThrow(
|
|
132
|
-
'invalid internal path, must start with "/"',
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
});
|