toiljs 0.0.11 → 0.0.12

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 (119) hide show
  1. package/README.md +2 -0
  2. package/build/cli/.tsbuildinfo +1 -1
  3. package/build/cli/configure.js +10 -4
  4. package/build/cli/create.js +58 -30
  5. package/build/cli/diagnostics.d.ts +55 -0
  6. package/build/cli/diagnostics.js +333 -0
  7. package/build/cli/doctor.d.ts +6 -0
  8. package/build/cli/doctor.js +249 -0
  9. package/build/cli/index.js +26 -0
  10. package/build/cli/proc.d.ts +5 -0
  11. package/build/cli/proc.js +20 -0
  12. package/build/cli/ui.d.ts +1 -0
  13. package/build/cli/ui.js +1 -0
  14. package/build/cli/update.d.ts +7 -0
  15. package/build/cli/update.js +117 -0
  16. package/build/cli/updates.d.ts +10 -0
  17. package/build/cli/updates.js +45 -0
  18. package/build/client/.tsbuildinfo +1 -1
  19. package/build/client/dev/error-overlay.js +1 -1
  20. package/build/client/head/metadata.js +3 -1
  21. package/build/client/index.d.ts +5 -1
  22. package/build/client/index.js +2 -0
  23. package/build/client/navigation/navigation.js +1 -1
  24. package/build/client/routing/Router.js +2 -2
  25. package/build/client/search/search.d.ts +26 -0
  26. package/build/client/search/search.js +101 -0
  27. package/build/client/search/use-page-search.d.ts +8 -0
  28. package/build/client/search/use-page-search.js +21 -0
  29. package/build/compiler/.tsbuildinfo +1 -1
  30. package/build/compiler/generate.js +26 -23
  31. package/build/compiler/index.d.ts +2 -0
  32. package/build/compiler/index.js +1 -0
  33. package/build/compiler/pages.d.ts +8 -0
  34. package/build/compiler/pages.js +37 -0
  35. package/build/compiler/plugin.js +3 -1
  36. package/build/compiler/prerender.d.ts +1 -0
  37. package/build/compiler/prerender.js +11 -5
  38. package/build/compiler/seo.js +10 -3
  39. package/build/io/.tsbuildinfo +1 -1
  40. package/examples/basic/client/components/Header.tsx +43 -41
  41. package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
  42. package/examples/basic/client/public/index.html +18 -16
  43. package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
  44. package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
  45. package/examples/basic/client/routes/about.tsx +21 -22
  46. package/examples/basic/client/routes/blog/[id].tsx +26 -18
  47. package/examples/basic/client/routes/features/actions.tsx +67 -67
  48. package/examples/basic/client/routes/features/error/index.tsx +27 -27
  49. package/examples/basic/client/routes/features/head.tsx +38 -38
  50. package/examples/basic/client/routes/features/index.tsx +83 -75
  51. package/examples/basic/client/routes/features/realtime.tsx +34 -32
  52. package/examples/basic/client/routes/features/script.tsx +31 -31
  53. package/examples/basic/client/routes/features/seo.tsx +39 -39
  54. package/examples/basic/client/routes/features/template/index.tsx +20 -20
  55. package/examples/basic/client/routes/features/template/template.tsx +16 -18
  56. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
  57. package/examples/basic/client/routes/gallery/index.tsx +42 -42
  58. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
  59. package/examples/basic/client/routes/get-started.tsx +157 -84
  60. package/examples/basic/client/routes/index.tsx +137 -96
  61. package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
  62. package/examples/basic/client/routes/search.tsx +61 -0
  63. package/examples/basic/client/routes/test.tsx +7 -8
  64. package/examples/basic/client/styles/main.css +624 -552
  65. package/package.json +2 -2
  66. package/presets/eslint.js +10 -3
  67. package/src/cli/configure.ts +363 -353
  68. package/src/cli/create.ts +563 -530
  69. package/src/cli/diagnostics.ts +421 -0
  70. package/src/cli/doctor.ts +318 -0
  71. package/src/cli/features.ts +166 -160
  72. package/src/cli/index.ts +242 -211
  73. package/src/cli/proc.ts +30 -0
  74. package/src/cli/ui.ts +111 -103
  75. package/src/cli/update.ts +150 -0
  76. package/src/cli/updates.ts +69 -0
  77. package/src/client/components/Image.tsx +91 -89
  78. package/src/client/dev/error-overlay.tsx +193 -197
  79. package/src/client/head/metadata.ts +94 -92
  80. package/src/client/index.ts +79 -64
  81. package/src/client/navigation/Link.tsx +94 -100
  82. package/src/client/navigation/navigation.ts +215 -218
  83. package/src/client/routing/Router.tsx +210 -193
  84. package/src/client/routing/hooks.ts +110 -114
  85. package/src/client/routing/lazy.ts +77 -81
  86. package/src/client/search/search.ts +189 -0
  87. package/src/client/search/use-page-search.ts +73 -0
  88. package/src/compiler/config.ts +173 -171
  89. package/src/compiler/fonts.ts +89 -87
  90. package/src/compiler/generate.ts +378 -373
  91. package/src/compiler/image-report.ts +88 -85
  92. package/src/compiler/index.ts +2 -0
  93. package/src/compiler/pages.ts +70 -0
  94. package/src/compiler/plugin.ts +51 -47
  95. package/src/compiler/prerender.ts +152 -130
  96. package/src/compiler/routes.ts +132 -131
  97. package/src/compiler/seo.ts +381 -356
  98. package/src/compiler/vite.ts +155 -145
  99. package/src/io/FastSet.ts +99 -96
  100. package/test/configure.test.ts +94 -90
  101. package/test/doctor.test.ts +140 -0
  102. package/test/dom/Image.test.tsx +73 -46
  103. package/test/dom/Script.test.tsx +48 -45
  104. package/test/dom/action.test.tsx +146 -129
  105. package/test/dom/error-overlay.test.tsx +44 -44
  106. package/test/dom/loader.test.tsx +2 -2
  107. package/test/dom/revalidate.test.tsx +1 -1
  108. package/test/dom/route-head.test.tsx +1 -2
  109. package/test/dom/slot.test.tsx +131 -109
  110. package/test/dom/view-transitions.test.tsx +53 -51
  111. package/test/features.test.ts +149 -142
  112. package/test/fonts.test.ts +28 -26
  113. package/test/head.test.ts +45 -35
  114. package/test/metadata.test.ts +42 -41
  115. package/test/pages.test.ts +105 -0
  116. package/test/prerender.test.ts +54 -46
  117. package/test/search.test.ts +114 -0
  118. package/test/seo.test.ts +164 -142
  119. package/test/update.test.ts +44 -0
@@ -1,90 +1,94 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
-
7
- import { applyConfigure } from '../src/cli/configure';
8
- import type { StyleFeatures } from '../src/cli/features';
9
-
10
- const CSS: StyleFeatures = { preprocessor: 'css', tailwind: false };
11
- const SASS_TW: StyleFeatures = { preprocessor: 'sass', tailwind: true };
12
-
13
- const ENTRY = [
14
- "import { routes, layout, notFound } from 'toiljs/routes';",
15
- '',
16
- "import './styles/main.css';",
17
- '',
18
- 'Toil.mount(routes, layout, notFound);',
19
- '',
20
- ].join('\n');
21
-
22
- let dir: string;
23
- let clientDir: string;
24
- let pkgPath: string;
25
-
26
- async function readJson(p: string): Promise<{ devDependencies?: Record<string, string> }> {
27
- return JSON.parse(await fs.readFile(p, 'utf8')) as { devDependencies?: Record<string, string> };
28
- }
29
-
30
- async function exists(p: string): Promise<boolean> {
31
- try {
32
- await fs.access(p);
33
- return true;
34
- } catch {
35
- return false;
36
- }
37
- }
38
-
39
- beforeEach(async () => {
40
- dir = await fs.mkdtemp(path.join(os.tmpdir(), 'toil-cfg-'));
41
- clientDir = path.join(dir, 'client');
42
- pkgPath = path.join(dir, 'package.json');
43
- await fs.mkdir(path.join(clientDir, 'styles'), { recursive: true });
44
- await fs.writeFile(path.join(clientDir, 'toil.tsx'), ENTRY, 'utf8');
45
- await fs.writeFile(path.join(clientDir, 'styles/main.css'), 'body { margin: 0; }\n', 'utf8');
46
- await fs.writeFile(pkgPath, JSON.stringify({ devDependencies: { typescript: '^6' } }, null, 4), 'utf8');
47
- });
48
-
49
- afterEach(async () => {
50
- await fs.rm(dir, { recursive: true, force: true });
51
- });
52
-
53
- describe('applyConfigure', () => {
54
- it('adds Sass + Tailwind: renames stylesheet, adds entry + imports + deps', async () => {
55
- const pkg = await readJson(pkgPath);
56
- await applyConfigure(clientDir, pkgPath, pkg, CSS, SASS_TW);
57
-
58
- expect(await exists(path.join(clientDir, 'styles/main.scss'))).toBe(true);
59
- expect(await exists(path.join(clientDir, 'styles/main.css'))).toBe(false);
60
- expect(await exists(path.join(clientDir, 'styles/tailwind.css'))).toBe(true);
61
-
62
- const entry = await fs.readFile(path.join(clientDir, 'toil.tsx'), 'utf8');
63
- expect(entry).toContain("import './styles/tailwind.css';");
64
- expect(entry).toContain("import './styles/main.scss';");
65
- expect(entry).not.toContain('main.css');
66
- expect(entry).toContain('Toil.mount(routes, layout, notFound);');
67
-
68
- const deps = (await readJson(pkgPath)).devDependencies ?? {};
69
- expect(deps).toHaveProperty('sass');
70
- expect(deps).toHaveProperty('tailwindcss');
71
- expect(deps).toHaveProperty('@tailwindcss/vite');
72
- });
73
-
74
- it('removes everything cleanly when switching back to plain CSS', async () => {
75
- const pkg = await readJson(pkgPath);
76
- await applyConfigure(clientDir, pkgPath, pkg, CSS, SASS_TW);
77
- const mid = await readJson(pkgPath);
78
- await applyConfigure(clientDir, pkgPath, mid, SASS_TW, CSS);
79
-
80
- expect(await exists(path.join(clientDir, 'styles/main.css'))).toBe(true);
81
- expect(await exists(path.join(clientDir, 'styles/main.scss'))).toBe(false);
82
- expect(await exists(path.join(clientDir, 'styles/tailwind.css'))).toBe(false);
83
-
84
- const deps = (await readJson(pkgPath)).devDependencies ?? {};
85
- expect(deps).not.toHaveProperty('sass');
86
- expect(deps).not.toHaveProperty('tailwindcss');
87
- expect(deps).not.toHaveProperty('@tailwindcss/vite');
88
- expect(deps).toHaveProperty('typescript');
89
- });
90
- });
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { applyConfigure } from '../src/cli/configure';
8
+ import type { StyleFeatures } from '../src/cli/features';
9
+
10
+ const CSS: StyleFeatures = { preprocessor: 'css', tailwind: false };
11
+ const SASS_TW: StyleFeatures = { preprocessor: 'sass', tailwind: true };
12
+
13
+ const ENTRY = [
14
+ "import { routes, layout, notFound } from 'toiljs/routes';",
15
+ '',
16
+ "import './styles/main.css';",
17
+ '',
18
+ 'Toil.mount(routes, layout, notFound);',
19
+ '',
20
+ ].join('\n');
21
+
22
+ let dir: string;
23
+ let clientDir: string;
24
+ let pkgPath: string;
25
+
26
+ async function readJson(p: string): Promise<{ devDependencies?: Record<string, string> }> {
27
+ return JSON.parse(await fs.readFile(p, 'utf8')) as { devDependencies?: Record<string, string> };
28
+ }
29
+
30
+ async function exists(p: string): Promise<boolean> {
31
+ try {
32
+ await fs.access(p);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ beforeEach(async () => {
40
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), 'toil-cfg-'));
41
+ clientDir = path.join(dir, 'client');
42
+ pkgPath = path.join(dir, 'package.json');
43
+ await fs.mkdir(path.join(clientDir, 'styles'), { recursive: true });
44
+ await fs.writeFile(path.join(clientDir, 'toil.tsx'), ENTRY, 'utf8');
45
+ await fs.writeFile(path.join(clientDir, 'styles/main.css'), 'body { margin: 0; }\n', 'utf8');
46
+ await fs.writeFile(
47
+ pkgPath,
48
+ JSON.stringify({ devDependencies: { typescript: '^6' } }, null, 4),
49
+ 'utf8',
50
+ );
51
+ });
52
+
53
+ afterEach(async () => {
54
+ await fs.rm(dir, { recursive: true, force: true });
55
+ });
56
+
57
+ describe('applyConfigure', () => {
58
+ it('adds Sass + Tailwind: renames stylesheet, adds entry + imports + deps', async () => {
59
+ const pkg = await readJson(pkgPath);
60
+ await applyConfigure(clientDir, pkgPath, pkg, CSS, SASS_TW);
61
+
62
+ expect(await exists(path.join(clientDir, 'styles/main.scss'))).toBe(true);
63
+ expect(await exists(path.join(clientDir, 'styles/main.css'))).toBe(false);
64
+ expect(await exists(path.join(clientDir, 'styles/tailwind.css'))).toBe(true);
65
+
66
+ const entry = await fs.readFile(path.join(clientDir, 'toil.tsx'), 'utf8');
67
+ expect(entry).toContain("import './styles/tailwind.css';");
68
+ expect(entry).toContain("import './styles/main.scss';");
69
+ expect(entry).not.toContain('main.css');
70
+ expect(entry).toContain('Toil.mount(routes, layout, notFound);');
71
+
72
+ const deps = (await readJson(pkgPath)).devDependencies ?? {};
73
+ expect(deps).toHaveProperty('sass');
74
+ expect(deps).toHaveProperty('tailwindcss');
75
+ expect(deps).toHaveProperty('@tailwindcss/vite');
76
+ });
77
+
78
+ it('removes everything cleanly when switching back to plain CSS', async () => {
79
+ const pkg = await readJson(pkgPath);
80
+ await applyConfigure(clientDir, pkgPath, pkg, CSS, SASS_TW);
81
+ const mid = await readJson(pkgPath);
82
+ await applyConfigure(clientDir, pkgPath, mid, SASS_TW, CSS);
83
+
84
+ expect(await exists(path.join(clientDir, 'styles/main.css'))).toBe(true);
85
+ expect(await exists(path.join(clientDir, 'styles/main.scss'))).toBe(false);
86
+ expect(await exists(path.join(clientDir, 'styles/tailwind.css'))).toBe(false);
87
+
88
+ const deps = (await readJson(pkgPath)).devDependencies ?? {};
89
+ expect(deps).not.toHaveProperty('sass');
90
+ expect(deps).not.toHaveProperty('tailwindcss');
91
+ expect(deps).not.toHaveProperty('@tailwindcss/vite');
92
+ expect(deps).toHaveProperty('typescript');
93
+ });
94
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ checkBasePath,
5
+ checkDuplicatePatterns,
6
+ type CheckGroup,
7
+ checkMountSlots,
8
+ checkNode,
9
+ checkPeer,
10
+ checkRelativeAssets,
11
+ checkRootElement,
12
+ checkSeoUrl,
13
+ checkStyling,
14
+ findRelativeAssets,
15
+ satisfiesMin,
16
+ summarize,
17
+ } from '../src/cli/diagnostics';
18
+
19
+ describe('satisfiesMin', () => {
20
+ it('compares against a >= minimum, ignoring range prefixes', () => {
21
+ expect(satisfiesMin('25.8.0', '>=24.0.0')).toBe(true);
22
+ expect(satisfiesMin('20.0.0', '>=24.0.0')).toBe(false);
23
+ expect(satisfiesMin('^19.2.6', '>=18.0.0')).toBe(true); // declared caret range, compared by floor
24
+ expect(satisfiesMin('6.0.0', '>=6.0.0')).toBe(true);
25
+ expect(satisfiesMin('5.9.9', '>=6.0.0')).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe('checkMountSlots', () => {
30
+ it('warns when mount() omits slots, passes when present', () => {
31
+ expect(checkMountSlots('Toil.mount(routes, layout, notFound, globalError);').status).toBe(
32
+ 'warn',
33
+ );
34
+ expect(
35
+ checkMountSlots('Toil.mount(routes, layout, notFound, globalError, slots);').status,
36
+ ).toBe('pass');
37
+ });
38
+
39
+ it('warns when there is no entry or no mount() call', () => {
40
+ expect(checkMountSlots(null).status).toBe('warn');
41
+ expect(checkMountSlots('export const x = 1;').status).toBe('warn');
42
+ });
43
+ });
44
+
45
+ describe('findRelativeAssets / checkRelativeAssets', () => {
46
+ it('flags root-relative asset paths but not absolute, url, or expression refs', () => {
47
+ const issues = findRelativeAssets([
48
+ {
49
+ path: 'client/components/Header.tsx',
50
+ source: [
51
+ '<img src="images/logo.svg" />', // broken: relative asset
52
+ '<img src="/images/logo.svg" />', // ok: root-absolute
53
+ '<img src="https://cdn/x.png" />', // ok: url
54
+ '<img src={logo} />', // ok: expression (not a string literal)
55
+ '<a href="/about">about</a>', // ok: no extension, route
56
+ ].join('\n'),
57
+ },
58
+ ]);
59
+ expect(issues).toHaveLength(1);
60
+ expect(issues[0]).toMatchObject({ line: 1, value: 'images/logo.svg' });
61
+ expect(checkRelativeAssets(issues).status).toBe('warn');
62
+ expect(checkRelativeAssets([]).status).toBe('pass');
63
+ });
64
+ });
65
+
66
+ describe('config + environment checks', () => {
67
+ it('checkBasePath: root or wrapped in slashes passes, otherwise warns', () => {
68
+ expect(checkBasePath('/').status).toBe('pass');
69
+ expect(checkBasePath('/app/').status).toBe('pass');
70
+ expect(checkBasePath('/app').status).toBe('warn');
71
+ });
72
+
73
+ it('checkSeoUrl: warns only when seo is configured without a url', () => {
74
+ expect(checkSeoUrl(false, false).status).toBe('pass');
75
+ expect(checkSeoUrl(true, true).status).toBe('pass');
76
+ expect(checkSeoUrl(true, false).status).toBe('warn');
77
+ });
78
+
79
+ it('checkNode / checkPeer reflect version satisfaction', () => {
80
+ expect(checkNode('25.0.0', '>=24.0.0').status).toBe('pass');
81
+ expect(checkNode('18.0.0', '>=24.0.0').status).toBe('fail');
82
+ expect(checkPeer('react', null, '>=18.0.0').status).toBe('fail');
83
+ expect(checkPeer('react', '^17.0.0', '>=18.0.0').status).toBe('warn');
84
+ expect(checkPeer('react', '^19.0.0', '>=18.0.0').status).toBe('pass');
85
+ });
86
+
87
+ it('checkDuplicatePatterns flags repeated route URLs', () => {
88
+ expect(checkDuplicatePatterns(['/', '/about', '/blog/:id']).status).toBe('pass');
89
+ expect(checkDuplicatePatterns(['/a', '/a']).status).toBe('warn');
90
+ });
91
+
92
+ it('checkRootElement requires an id="root" mount target', () => {
93
+ expect(checkRootElement('<div id="root"></div>').status).toBe('pass');
94
+ expect(checkRootElement('<div id="app"></div>').status).toBe('fail');
95
+ expect(checkRootElement(null).status).toBe('fail');
96
+ });
97
+
98
+ it('checkStyling fails when an imported preprocessor/Tailwind is not installed', () => {
99
+ expect(
100
+ checkStyling({
101
+ preprocessorImported: 'sass',
102
+ preprocessorInstalled: false,
103
+ tailwindImported: false,
104
+ tailwindInstalled: false,
105
+ }).status,
106
+ ).toBe('fail');
107
+ expect(
108
+ checkStyling({
109
+ preprocessorImported: 'sass',
110
+ preprocessorInstalled: true,
111
+ tailwindImported: true,
112
+ tailwindInstalled: false,
113
+ }).status,
114
+ ).toBe('fail');
115
+ expect(
116
+ checkStyling({
117
+ preprocessorImported: 'css',
118
+ preprocessorInstalled: true,
119
+ tailwindImported: false,
120
+ tailwindInstalled: false,
121
+ }).status,
122
+ ).toBe('pass');
123
+ });
124
+ });
125
+
126
+ describe('summarize', () => {
127
+ it('tallies pass/warn/fail across groups', () => {
128
+ const groups: CheckGroup[] = [
129
+ {
130
+ title: 'A',
131
+ checks: [
132
+ { id: '1', label: 'x', status: 'pass' },
133
+ { id: '2', label: 'y', status: 'warn' },
134
+ ],
135
+ },
136
+ { title: 'B', checks: [{ id: '3', label: 'z', status: 'fail' }] },
137
+ ];
138
+ expect(summarize(groups)).toEqual({ pass: 1, warn: 1, fail: 1 });
139
+ });
140
+ });
@@ -1,46 +1,73 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, fireEvent, render } from '@testing-library/react';
3
- import { afterEach, describe, expect, it } from 'vitest';
4
-
5
- import { Image } from '../../src/client/components/Image';
6
-
7
- afterEach(cleanup);
8
-
9
- describe('Image', () => {
10
- it('lazy-loads and decodes async by default, with the given dimensions', () => {
11
- const { getByAltText } = render(<Image src="/a.png" alt="a" width={200} height={100} />);
12
- const img = getByAltText('a') as HTMLImageElement;
13
- expect(img.getAttribute('src')).toBe('/a.png');
14
- expect(img.getAttribute('loading')).toBe('lazy');
15
- expect(img.getAttribute('decoding')).toBe('async');
16
- expect(img.getAttribute('width')).toBe('200');
17
- expect(img.getAttribute('height')).toBe('100');
18
- expect(img.getAttribute('fetchpriority')).toBe('auto');
19
- });
20
-
21
- it('priority images load eagerly with high fetch priority', () => {
22
- const { getByAltText } = render(<Image src="/hero.png" alt="hero" priority />);
23
- const img = getByAltText('hero') as HTMLImageElement;
24
- expect(img.getAttribute('loading')).toBe('eager');
25
- expect(img.getAttribute('fetchpriority')).toBe('high');
26
- });
27
-
28
- it('fill drops width/height and absolutely positions the image', () => {
29
- const { getByAltText } = render(<Image src="/bg.png" alt="bg" fill objectFit="cover" />);
30
- const img = getByAltText('bg') as HTMLImageElement;
31
- expect(img.hasAttribute('width')).toBe(false);
32
- expect(img.hasAttribute('height')).toBe(false);
33
- expect(img.style.position).toBe('absolute');
34
- expect(img.style.objectFit).toBe('cover');
35
- });
36
-
37
- it('shows a blur placeholder until the image loads', () => {
38
- const { getByAltText } = render(
39
- <Image src="/p.png" alt="p" width={10} height={10} placeholder="blur" blurDataURL="data:image/x" />,
40
- );
41
- const img = getByAltText('p') as HTMLImageElement;
42
- expect(img.style.backgroundImage).toContain('data:image/x');
43
- fireEvent.load(img);
44
- expect(img.style.backgroundImage).toBe('');
45
- });
46
- });
1
+ // @vitest-environment jsdom
2
+ import { cleanup, fireEvent, render } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+
5
+ import { Image } from '../../src/client/components/Image';
6
+
7
+ afterEach(cleanup);
8
+
9
+ describe('Image', () => {
10
+ it('lazy-loads and decodes async by default, with the given dimensions', () => {
11
+ const { getByAltText } = render(
12
+ <Image
13
+ src="/a.png"
14
+ alt="a"
15
+ width={200}
16
+ height={100}
17
+ />,
18
+ );
19
+ const img = getByAltText('a') as HTMLImageElement;
20
+ expect(img.getAttribute('src')).toBe('/a.png');
21
+ expect(img.getAttribute('loading')).toBe('lazy');
22
+ expect(img.getAttribute('decoding')).toBe('async');
23
+ expect(img.getAttribute('width')).toBe('200');
24
+ expect(img.getAttribute('height')).toBe('100');
25
+ expect(img.getAttribute('fetchpriority')).toBe('auto');
26
+ });
27
+
28
+ it('priority images load eagerly with high fetch priority', () => {
29
+ const { getByAltText } = render(
30
+ <Image
31
+ src="/hero.png"
32
+ alt="hero"
33
+ priority
34
+ />,
35
+ );
36
+ const img = getByAltText('hero') as HTMLImageElement;
37
+ expect(img.getAttribute('loading')).toBe('eager');
38
+ expect(img.getAttribute('fetchpriority')).toBe('high');
39
+ });
40
+
41
+ it('fill drops width/height and absolutely positions the image', () => {
42
+ const { getByAltText } = render(
43
+ <Image
44
+ src="/bg.png"
45
+ alt="bg"
46
+ fill
47
+ objectFit="cover"
48
+ />,
49
+ );
50
+ const img = getByAltText('bg') as HTMLImageElement;
51
+ expect(img.hasAttribute('width')).toBe(false);
52
+ expect(img.hasAttribute('height')).toBe(false);
53
+ expect(img.style.position).toBe('absolute');
54
+ expect(img.style.objectFit).toBe('cover');
55
+ });
56
+
57
+ it('shows a blur placeholder until the image loads', () => {
58
+ const { getByAltText } = render(
59
+ <Image
60
+ src="/p.png"
61
+ alt="p"
62
+ width={10}
63
+ height={10}
64
+ placeholder="blur"
65
+ blurDataURL="data:image/x"
66
+ />,
67
+ );
68
+ const img = getByAltText('p') as HTMLImageElement;
69
+ expect(img.style.backgroundImage).toContain('data:image/x');
70
+ fireEvent.load(img);
71
+ expect(img.style.backgroundImage).toBe('');
72
+ });
73
+ });
@@ -1,45 +1,48 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, render } from '@testing-library/react';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import { Script } from '../../src/client/components/Script';
6
-
7
- afterEach(cleanup);
8
-
9
- const scriptsFor = (key: string): HTMLScriptElement[] =>
10
- Array.from(document.querySelectorAll<HTMLScriptElement>(`script[data-toil-script="${key}"]`));
11
-
12
- describe('Script', () => {
13
- it('injects an async external script on mount (afterInteractive)', () => {
14
- render(<Script src="https://cdn.example.com/a.js" />);
15
- const els = scriptsFor('https://cdn.example.com/a.js');
16
- expect(els).toHaveLength(1);
17
- expect(els[0].async).toBe(true);
18
- });
19
-
20
- it('dedups: the same src is only injected once across instances', () => {
21
- const src = 'https://cdn.example.com/dedup.js';
22
- render(
23
- <>
24
- <Script src={src} />
25
- <Script src={src} />
26
- </>,
27
- );
28
- expect(scriptsFor(src)).toHaveLength(1);
29
- });
30
-
31
- it('injects an inline script body and fires onLoad + onReady', () => {
32
- const onLoad = vi.fn();
33
- const onReady = vi.fn();
34
- render(
35
- <Script id="inline-1" onLoad={onLoad} onReady={onReady}>
36
- {'window.__toilTest = 1;'}
37
- </Script>,
38
- );
39
- const els = scriptsFor('inline-1');
40
- expect(els).toHaveLength(1);
41
- expect(els[0].textContent).toBe('window.__toilTest = 1;');
42
- expect(onLoad).toHaveBeenCalledOnce();
43
- expect(onReady).toHaveBeenCalledOnce();
44
- });
45
- });
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { Script } from '../../src/client/components/Script';
6
+
7
+ afterEach(cleanup);
8
+
9
+ const scriptsFor = (key: string): HTMLScriptElement[] =>
10
+ Array.from(document.querySelectorAll<HTMLScriptElement>(`script[data-toil-script="${key}"]`));
11
+
12
+ describe('Script', () => {
13
+ it('injects an async external script on mount (afterInteractive)', () => {
14
+ render(<Script src="https://cdn.example.com/a.js" />);
15
+ const els = scriptsFor('https://cdn.example.com/a.js');
16
+ expect(els).toHaveLength(1);
17
+ expect(els[0].async).toBe(true);
18
+ });
19
+
20
+ it('dedups: the same src is only injected once across instances', () => {
21
+ const src = 'https://cdn.example.com/dedup.js';
22
+ render(
23
+ <>
24
+ <Script src={src} />
25
+ <Script src={src} />
26
+ </>,
27
+ );
28
+ expect(scriptsFor(src)).toHaveLength(1);
29
+ });
30
+
31
+ it('injects an inline script body and fires onLoad + onReady', () => {
32
+ const onLoad = vi.fn();
33
+ const onReady = vi.fn();
34
+ render(
35
+ <Script
36
+ id="inline-1"
37
+ onLoad={onLoad}
38
+ onReady={onReady}>
39
+ {'window.__toilTest = 1;'}
40
+ </Script>,
41
+ );
42
+ const els = scriptsFor('inline-1');
43
+ expect(els).toHaveLength(1);
44
+ expect(els[0].textContent).toBe('window.__toilTest = 1;');
45
+ expect(onLoad).toHaveBeenCalledOnce();
46
+ expect(onReady).toHaveBeenCalledOnce();
47
+ });
48
+ });