@abreen/tada 1.5.0 → 1.5.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.
Files changed (45) hide show
  1. package/build/generate-content-assets.ts +2 -2
  2. package/build/types.d.ts +1 -0
  3. package/build/utils/render.ts +5 -0
  4. package/build/utils/trace.ts +70 -22
  5. package/package.json +4 -3
  6. package/src/trace/index.ts +9 -1
  7. package/bin/validators.test.ts +0 -205
  8. package/build/build-manifest.test.ts +0 -369
  9. package/build/code.test.ts +0 -272
  10. package/build/colors.test.ts +0 -66
  11. package/build/globals.test.ts +0 -100
  12. package/build/json-schema.test.ts +0 -57
  13. package/build/log.test.ts +0 -117
  14. package/build/markdown-plugins.test.ts +0 -456
  15. package/build/pagefind.test.ts +0 -192
  16. package/build/pdf-text.test.ts +0 -52
  17. package/build/reachability.test.ts +0 -80
  18. package/build/serve.test.ts +0 -59
  19. package/build/toc-plugin.test.ts +0 -120
  20. package/build/utils/content-files.test.ts +0 -56
  21. package/build/utils/derive-theme.test.ts +0 -111
  22. package/build/utils/file-types.test.ts +0 -79
  23. package/build/utils/front-matter.test.ts +0 -77
  24. package/build/utils/include.test.ts +0 -117
  25. package/build/utils/paths.test.ts +0 -91
  26. package/build/utils/trace-layout.test.ts +0 -570
  27. package/build/utils/trace-svg.test.ts +0 -425
  28. package/build/utils/trace.test.ts +0 -169
  29. package/build/validate-config-links.test.ts +0 -239
  30. package/build/validate-internal-links-plugin.test.ts +0 -134
  31. package/build/watch-reachability-state.test.ts +0 -198
  32. package/src/anchor/index.test.ts +0 -64
  33. package/src/code/index.test.ts +0 -71
  34. package/src/header/index.test.ts +0 -89
  35. package/src/navigate/eligible.test.ts +0 -101
  36. package/src/navigate/index.test.ts +0 -82
  37. package/src/navigate/lifecycle.test.ts +0 -26
  38. package/src/print/index.test.ts +0 -71
  39. package/src/question/index.test.ts +0 -82
  40. package/src/search/index.test.ts +0 -203
  41. package/src/timezone/index.test.ts +0 -400
  42. package/src/toc/index.test.ts +0 -237
  43. package/src/top/index.test.ts +0 -46
  44. package/src/trace/index.test.ts +0 -528
  45. package/src/util.test.ts +0 -135
@@ -1,71 +0,0 @@
1
- import { beforeAll, describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
- import mount from './index';
4
-
5
- beforeAll(() => {
6
- // jsdom does not provide ResizeObserver
7
- if (typeof globalThis.ResizeObserver === 'undefined') {
8
- (globalThis as Record<string, unknown>).ResizeObserver = class {
9
- observe() {}
10
- unobserve() {}
11
- disconnect() {}
12
- };
13
- }
14
- });
15
-
16
- function create(html: string) {
17
- const dom = new JSDOM(`<body class="code">${html}</body>`);
18
- return dom.window;
19
- }
20
-
21
- describe('code', () => {
22
- test('returns early when body does not have code class', async () => {
23
- const dom = new JSDOM('<body><div>No code</div></body>');
24
- await mount(dom.window);
25
- });
26
-
27
- test('returns early when no code-body or scrollbar', async () => {
28
- const win = create('<div>Code page with no code body</div>');
29
- await mount(win);
30
- });
31
-
32
- test('registers a copy event listener on code page', async () => {
33
- const win = create(
34
- '<div class="code-body"><div class="code-row"><code>hello</code></div></div>' +
35
- '<div class="code-scrollbar"><div></div></div>',
36
- );
37
- await mount(win);
38
-
39
- // Dispatch a copy event to exercise the listener (jsdom has limited
40
- // ClipboardEvent support, so the handler exits early at getSelection)
41
- const event = new win.Event('copy', { bubbles: true });
42
- win.document.dispatchEvent(event);
43
- });
44
-
45
- test('sets up scrollbar syncing between code-body and scrollbar', async () => {
46
- const win = create(
47
- '<div class="code-body"><div class="code-row"><code>x</code></div></div>' +
48
- '<div class="code-scrollbar"><div></div></div>',
49
- );
50
- await mount(win);
51
-
52
- const codeBody = win.document.querySelector('.code-body') as HTMLElement;
53
- const scrollbar = win.document.querySelector(
54
- '.code-scrollbar',
55
- ) as HTMLElement;
56
- expect(codeBody).not.toBeNull();
57
- expect(scrollbar).not.toBeNull();
58
- });
59
-
60
- test('handles download link when present', async () => {
61
- const win = create(
62
- '<div class="file-header"><a download="test.py" href="/test.py">Download</a></div>' +
63
- '<div class="code-body"><div class="code-row"><code>x</code></div></div>' +
64
- '<div class="code-scrollbar"><div></div></div>',
65
- );
66
- await mount(win);
67
-
68
- const link = win.document.querySelector('a[download]');
69
- expect(link).not.toBeNull();
70
- });
71
- });
@@ -1,89 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
- import mount from './index';
4
-
5
- function create(open = false) {
6
- const dom = new JSDOM(
7
- `<body><header><details${open ? ' open' : ''}><summary>Menu</summary><nav>Links</nav></details></header></body>`,
8
- );
9
- return dom.window;
10
- }
11
-
12
- describe('header', () => {
13
- test('closes open details on outside click', () => {
14
- const win = create(true);
15
- mount(win);
16
-
17
- const details = win.document.querySelector('details') as HTMLDetailsElement;
18
- expect(details.open).toBe(true);
19
-
20
- win.document.body.dispatchEvent(
21
- new win.MouseEvent('click', { bubbles: true }),
22
- );
23
- expect(details.open).toBe(false);
24
- });
25
-
26
- test('does not close details when clicking inside', () => {
27
- const win = create(true);
28
- mount(win);
29
-
30
- const details = win.document.querySelector('details') as HTMLDetailsElement;
31
- details.dispatchEvent(new win.MouseEvent('click', { bubbles: false }));
32
- expect(details.open).toBe(true);
33
- });
34
-
35
- test('closes on Escape key when details is open', () => {
36
- const win = create(true);
37
- mount(win);
38
-
39
- const details = win.document.querySelector('details') as HTMLDetailsElement;
40
- const summary = win.document.querySelector('summary')!;
41
-
42
- // Focus the summary so activeElement is inside details
43
- summary.focus();
44
-
45
- win.dispatchEvent(
46
- new win.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
47
- );
48
- expect(details.open).toBe(false);
49
- });
50
-
51
- test('ignores Escape when details is closed', () => {
52
- const win = create(false);
53
- mount(win);
54
-
55
- const details = win.document.querySelector('details') as HTMLDetailsElement;
56
- win.dispatchEvent(
57
- new win.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
58
- );
59
- expect(details.open).toBe(false);
60
- });
61
-
62
- test('ignores other keys', () => {
63
- const win = create(true);
64
- mount(win);
65
-
66
- const details = win.document.querySelector('details') as HTMLDetailsElement;
67
- const summary = win.document.querySelector('summary')!;
68
- summary.focus();
69
-
70
- win.dispatchEvent(
71
- new win.KeyboardEvent('keydown', { key: 'Tab', bubbles: true }),
72
- );
73
- expect(details.open).toBe(true);
74
- });
75
-
76
- test('cleanup removes event listeners', () => {
77
- const win = create(true);
78
- const cleanup = mount(win);
79
-
80
- cleanup!();
81
-
82
- win.document.body.dispatchEvent(
83
- new win.MouseEvent('click', { bubbles: true }),
84
- );
85
-
86
- const details = win.document.querySelector('details') as HTMLDetailsElement;
87
- expect(details.open).toBe(true);
88
- });
89
- });
@@ -1,101 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { isEligibleLink } from './eligible';
3
-
4
- describe('isEligibleLink', () => {
5
- const origin = 'http://localhost';
6
- const basePath = '/';
7
-
8
- test('eligible: same-origin absolute path', () => {
9
- expect(
10
- isEligibleLink('http://localhost/about.html', origin, basePath),
11
- ).toBe(true);
12
- });
13
-
14
- test('eligible: path without extension', () => {
15
- expect(isEligibleLink('http://localhost/about', origin, basePath)).toBe(
16
- true,
17
- );
18
- });
19
-
20
- test('eligible: path ending in .html', () => {
21
- expect(isEligibleLink('http://localhost/page.html', origin, basePath)).toBe(
22
- true,
23
- );
24
- });
25
-
26
- test('eligible: root path', () => {
27
- expect(isEligibleLink('http://localhost/', origin, basePath)).toBe(true);
28
- });
29
-
30
- test('eligible: path with hash', () => {
31
- expect(
32
- isEligibleLink('http://localhost/page.html#section', origin, basePath),
33
- ).toBe(true);
34
- });
35
-
36
- test('ineligible: different origin', () => {
37
- expect(
38
- isEligibleLink('https://example.com/page.html', origin, basePath),
39
- ).toBe(false);
40
- });
41
-
42
- test('ineligible: PDF file', () => {
43
- expect(isEligibleLink('http://localhost/doc.pdf', origin, basePath)).toBe(
44
- false,
45
- );
46
- });
47
-
48
- test('ineligible: Java file', () => {
49
- expect(isEligibleLink('http://localhost/Main.java', origin, basePath)).toBe(
50
- false,
51
- );
52
- });
53
-
54
- test('ineligible: Python file', () => {
55
- expect(isEligibleLink('http://localhost/script.py', origin, basePath)).toBe(
56
- false,
57
- );
58
- });
59
-
60
- test('ineligible: PNG image', () => {
61
- expect(isEligibleLink('http://localhost/image.png', origin, basePath)).toBe(
62
- false,
63
- );
64
- });
65
-
66
- test('ineligible: ZIP file', () => {
67
- expect(
68
- isEligibleLink('http://localhost/archive.zip', origin, basePath),
69
- ).toBe(false);
70
- });
71
-
72
- test('ineligible: JPG image', () => {
73
- expect(isEligibleLink('http://localhost/photo.jpg', origin, basePath)).toBe(
74
- false,
75
- );
76
- });
77
-
78
- test('ineligible: JSON file', () => {
79
- expect(isEligibleLink('http://localhost/data.json', origin, basePath)).toBe(
80
- false,
81
- );
82
- });
83
-
84
- test('eligible: link under non-root base path', () => {
85
- expect(
86
- isEligibleLink('http://localhost/course1/page.html', origin, '/course1/'),
87
- ).toBe(true);
88
- });
89
-
90
- test('ineligible: link outside base path', () => {
91
- expect(
92
- isEligibleLink('http://localhost/course2/page.html', origin, '/course1/'),
93
- ).toBe(false);
94
- });
95
-
96
- test('ineligible: link to root when base path is subdir', () => {
97
- expect(isEligibleLink('http://localhost/', origin, '/course1/')).toBe(
98
- false,
99
- );
100
- });
101
- });
@@ -1,82 +0,0 @@
1
- import { describe, expect, test, beforeEach, mock } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
-
4
- // Mock the lifecycle module so we can track calls
5
- const mockTeardown = mock(() => {});
6
- const mockMount = mock(async () => mockTeardown);
7
- mock.module('./lifecycle', () => ({
8
- mountPerPageComponents: mockMount,
9
- teardownPerPageComponents: mockTeardown,
10
- }));
11
-
12
- import mount from './index';
13
-
14
- function createDOM(bodyContent = '') {
15
- const html = `<html><head><title>Page One</title><meta name="description" content="desc one"></head><body class="default toc-is-active"><header><details><summary>Menu</summary><nav></nav></details></header><div class="container">${bodyContent}</div></body></html>`;
16
- const dom = new JSDOM(html, {
17
- url: 'http://localhost/',
18
- pretendToBeVisual: true,
19
- });
20
- return dom.window;
21
- }
22
-
23
- describe('navigate', () => {
24
- beforeEach(() => {
25
- mockTeardown.mockClear();
26
- mockMount.mockClear();
27
- });
28
-
29
- test('mount returns a cleanup function', () => {
30
- const win = createDOM();
31
- const cleanup = mount(win);
32
- expect(typeof cleanup).toBe('function');
33
- });
34
-
35
- test('cleanup removes event listeners without throwing', () => {
36
- const win = createDOM();
37
- const cleanup = mount(win);
38
- cleanup!();
39
- });
40
-
41
- test('ignores clicks with modifier keys', () => {
42
- const win = createDOM('<a href="http://localhost/other.html">Link</a>');
43
- mount(win);
44
-
45
- const link = win.document.querySelector('a')!;
46
- const event = new win.MouseEvent('click', { bubbles: true, ctrlKey: true });
47
- const prevented = !link.dispatchEvent(event);
48
- expect(prevented).toBe(false);
49
- });
50
-
51
- test('ignores links with target attribute', () => {
52
- const win = createDOM(
53
- '<a href="http://localhost/other.html" target="_blank">Link</a>',
54
- );
55
- mount(win);
56
-
57
- const link = win.document.querySelector('a')!;
58
- const event = new win.MouseEvent('click', { bubbles: true });
59
- const prevented = !link.dispatchEvent(event);
60
- expect(prevented).toBe(false);
61
- });
62
-
63
- test('ignores external links', () => {
64
- const win = createDOM('<a href="https://example.com/page">Link</a>');
65
- mount(win);
66
-
67
- const link = win.document.querySelector('a')!;
68
- const event = new win.MouseEvent('click', { bubbles: true });
69
- const prevented = !link.dispatchEvent(event);
70
- expect(prevented).toBe(false);
71
- });
72
-
73
- test('ignores non-HTML resource links', () => {
74
- const win = createDOM('<a href="http://localhost/file.pdf">PDF</a>');
75
- mount(win);
76
-
77
- const link = win.document.querySelector('a')!;
78
- const event = new win.MouseEvent('click', { bubbles: true });
79
- const prevented = !link.dispatchEvent(event);
80
- expect(prevented).toBe(false);
81
- });
82
- });
@@ -1,26 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
-
4
- describe('component lifecycle', () => {
5
- test('mountPerPageComponents returns a teardown function', async () => {
6
- const dom = new JSDOM(
7
- `<body><header><details><summary>Menu</summary><nav></nav></details></header><div class="container"></div></body>`,
8
- { url: 'http://localhost/' },
9
- );
10
-
11
- const { mountPerPageComponents } = await import('./lifecycle');
12
- const teardown = await mountPerPageComponents(dom.window);
13
- expect(typeof teardown).toBe('function');
14
- });
15
-
16
- test('teardown calls all per-page cleanup functions', async () => {
17
- const dom = new JSDOM(
18
- `<body><header><details><summary>Menu</summary><nav></nav></details></header><div class="container"></div></body>`,
19
- { url: 'http://localhost/' },
20
- );
21
-
22
- const { mountPerPageComponents } = await import('./lifecycle');
23
- const teardown = await mountPerPageComponents(dom.window);
24
- teardown();
25
- });
26
- });
@@ -1,71 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
- import mount from './index';
4
-
5
- function create(html: string) {
6
- const dom = new JSDOM(`<body>${html}</body>`);
7
- return dom.window;
8
- }
9
-
10
- describe('print', () => {
11
- test('opens closed details on beforeprint', () => {
12
- const win = create('<details><summary>S</summary>Content</details>');
13
- mount(win);
14
-
15
- const details = win.document.querySelector('details')!;
16
- expect(details.open).toBe(false);
17
-
18
- win.dispatchEvent(new win.Event('beforeprint'));
19
- expect(details.open).toBe(true);
20
- expect(details.getAttribute('data-was-closed')).toBe('true');
21
- });
22
-
23
- test('restores closed details on afterprint', () => {
24
- const win = create('<details><summary>S</summary>Content</details>');
25
- mount(win);
26
-
27
- win.dispatchEvent(new win.Event('beforeprint'));
28
- win.dispatchEvent(new win.Event('afterprint'));
29
-
30
- const details = win.document.querySelector('details')!;
31
- expect(details.open).toBe(false);
32
- expect(details.hasAttribute('data-was-closed')).toBe(false);
33
- });
34
-
35
- test('leaves already-open details open after print', () => {
36
- const win = create('<details open><summary>S</summary>Content</details>');
37
- mount(win);
38
-
39
- win.dispatchEvent(new win.Event('beforeprint'));
40
- win.dispatchEvent(new win.Event('afterprint'));
41
-
42
- const details = win.document.querySelector('details')!;
43
- expect(details.open).toBe(true);
44
- });
45
-
46
- test('skips details inside header', () => {
47
- const win = create(
48
- '<header><details><summary>Nav</summary>Menu</details></header>' +
49
- '<details><summary>S</summary>Content</details>',
50
- );
51
- mount(win);
52
-
53
- win.dispatchEvent(new win.Event('beforeprint'));
54
-
55
- const headerDetails = win.document.querySelector('header details')!;
56
- const bodyDetails = win.document.querySelectorAll('details')[1];
57
- expect(headerDetails.hasAttribute('data-was-closed')).toBe(false);
58
- expect(bodyDetails.open).toBe(true);
59
- });
60
-
61
- test('cleanup removes event listeners', () => {
62
- const win = create('<details><summary>S</summary>Content</details>');
63
- const cleanup = mount(win);
64
-
65
- cleanup!();
66
-
67
- win.dispatchEvent(new win.Event('beforeprint'));
68
- const details = win.document.querySelector('details')!;
69
- expect(details.open).toBe(false);
70
- });
71
- });
@@ -1,82 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
- import mount from './index';
4
-
5
- function create(html: string) {
6
- const dom = new JSDOM(`<body>${html}</body>`);
7
- return dom.window;
8
- }
9
-
10
- describe('question', () => {
11
- test('sets ARIA attributes on answer body', () => {
12
- const win = create('<div class="question-a-body">Answer</div>');
13
- mount(win);
14
-
15
- const el = win.document.querySelector('.question-a-body')!;
16
- expect(el.getAttribute('role')).toBe('button');
17
- expect(el.getAttribute('tabindex')).toBe('0');
18
- expect(el.getAttribute('aria-label')).toBe('Click to reveal answer');
19
- });
20
-
21
- test('reveals answer on click', () => {
22
- const win = create('<div class="question-a-body">Answer</div>');
23
- mount(win);
24
-
25
- const el = win.document.querySelector('.question-a-body')!;
26
- el.dispatchEvent(new win.MouseEvent('click', { bubbles: true }));
27
-
28
- expect(el.hasAttribute('data-revealed')).toBe(true);
29
- expect(el.hasAttribute('role')).toBe(false);
30
- expect(el.hasAttribute('tabindex')).toBe(false);
31
- expect(el.hasAttribute('aria-label')).toBe(false);
32
- });
33
-
34
- test('reveals answer on Enter key', () => {
35
- const win = create('<div class="question-a-body">Answer</div>');
36
- mount(win);
37
-
38
- const el = win.document.querySelector('.question-a-body')!;
39
- el.dispatchEvent(
40
- new win.KeyboardEvent('keydown', { key: 'Enter', bubbles: true }),
41
- );
42
-
43
- expect(el.hasAttribute('data-revealed')).toBe(true);
44
- });
45
-
46
- test('reveals answer on Space key', () => {
47
- const win = create('<div class="question-a-body">Answer</div>');
48
- mount(win);
49
-
50
- const el = win.document.querySelector('.question-a-body')!;
51
- el.dispatchEvent(
52
- new win.KeyboardEvent('keydown', { key: ' ', bubbles: true }),
53
- );
54
-
55
- expect(el.hasAttribute('data-revealed')).toBe(true);
56
- });
57
-
58
- test('does not reveal on other keys', () => {
59
- const win = create('<div class="question-a-body">Answer</div>');
60
- mount(win);
61
-
62
- const el = win.document.querySelector('.question-a-body')!;
63
- el.dispatchEvent(
64
- new win.KeyboardEvent('keydown', { key: 'Tab', bubbles: true }),
65
- );
66
-
67
- expect(el.hasAttribute('data-revealed')).toBe(false);
68
- });
69
-
70
- test('handles multiple question bodies', () => {
71
- const win = create(
72
- '<div class="question-a-body">A1</div><div class="question-a-body">A2</div>',
73
- );
74
- mount(win);
75
-
76
- const els = win.document.querySelectorAll('.question-a-body');
77
- expect(els.length).toBe(2);
78
- els.forEach(el => {
79
- expect(el.getAttribute('role')).toBe('button');
80
- });
81
- });
82
- });