toiljs 0.0.10 → 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 (128) hide show
  1. package/README.md +315 -1
  2. package/assets/logo.svg +37 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/configure.js +10 -4
  5. package/build/cli/create.js +60 -32
  6. package/build/cli/diagnostics.d.ts +55 -0
  7. package/build/cli/diagnostics.js +333 -0
  8. package/build/cli/doctor.d.ts +6 -0
  9. package/build/cli/doctor.js +249 -0
  10. package/build/cli/index.js +26 -0
  11. package/build/cli/proc.d.ts +5 -0
  12. package/build/cli/proc.js +20 -0
  13. package/build/cli/ui.d.ts +1 -0
  14. package/build/cli/ui.js +1 -0
  15. package/build/cli/update.d.ts +7 -0
  16. package/build/cli/update.js +117 -0
  17. package/build/cli/updates.d.ts +10 -0
  18. package/build/cli/updates.js +45 -0
  19. package/build/client/.tsbuildinfo +1 -1
  20. package/build/client/dev/error-overlay.js +1 -1
  21. package/build/client/head/metadata.js +3 -1
  22. package/build/client/index.d.ts +5 -1
  23. package/build/client/index.js +2 -0
  24. package/build/client/navigation/navigation.js +1 -1
  25. package/build/client/routing/Router.js +2 -2
  26. package/build/client/search/search.d.ts +26 -0
  27. package/build/client/search/search.js +101 -0
  28. package/build/client/search/use-page-search.d.ts +8 -0
  29. package/build/client/search/use-page-search.js +21 -0
  30. package/build/compiler/.tsbuildinfo +1 -1
  31. package/build/compiler/generate.js +35 -26
  32. package/build/compiler/index.d.ts +2 -0
  33. package/build/compiler/index.js +1 -0
  34. package/build/compiler/pages.d.ts +8 -0
  35. package/build/compiler/pages.js +37 -0
  36. package/build/compiler/plugin.js +3 -1
  37. package/build/compiler/prerender.d.ts +1 -0
  38. package/build/compiler/prerender.js +11 -5
  39. package/build/compiler/seo.js +10 -3
  40. package/build/compiler/vite.js +7 -0
  41. package/build/io/.tsbuildinfo +1 -1
  42. package/examples/basic/client/components/Header.tsx +43 -38
  43. package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
  44. package/examples/basic/client/layout.tsx +4 -1
  45. package/examples/basic/client/public/index.html +18 -16
  46. package/examples/basic/client/routes/(legal)/privacy.tsx +18 -0
  47. package/examples/basic/client/routes/(legal)/terms.tsx +15 -0
  48. package/examples/basic/client/routes/about.tsx +21 -19
  49. package/examples/basic/client/routes/blog/[id].tsx +26 -12
  50. package/examples/basic/client/routes/features/actions.tsx +67 -0
  51. package/examples/basic/client/routes/features/error/error.tsx +16 -0
  52. package/examples/basic/client/routes/features/error/index.tsx +27 -0
  53. package/examples/basic/client/routes/features/head.tsx +38 -0
  54. package/examples/basic/client/routes/features/index.tsx +83 -0
  55. package/examples/basic/client/routes/features/realtime.tsx +34 -0
  56. package/examples/basic/client/routes/features/script.tsx +31 -0
  57. package/examples/basic/client/routes/features/seo.tsx +39 -0
  58. package/examples/basic/client/routes/features/template/b.tsx +14 -0
  59. package/examples/basic/client/routes/features/template/index.tsx +20 -0
  60. package/examples/basic/client/routes/features/template/template.tsx +16 -0
  61. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
  62. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
  63. package/examples/basic/client/routes/gallery/index.tsx +42 -0
  64. package/examples/basic/client/routes/gallery/layout.tsx +13 -0
  65. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
  66. package/examples/basic/client/routes/get-started.tsx +157 -84
  67. package/examples/basic/client/routes/index.tsx +137 -87
  68. package/examples/basic/client/routes/loader-demo/index.tsx +59 -50
  69. package/examples/basic/client/routes/search.tsx +61 -0
  70. package/examples/basic/client/routes/test.tsx +7 -8
  71. package/examples/basic/client/styles/main.css +624 -552
  72. package/examples/basic/client/toil.tsx +2 -4
  73. package/package.json +3 -2
  74. package/presets/eslint.js +10 -3
  75. package/src/cli/configure.ts +363 -353
  76. package/src/cli/create.ts +563 -530
  77. package/src/cli/diagnostics.ts +421 -0
  78. package/src/cli/doctor.ts +318 -0
  79. package/src/cli/features.ts +166 -160
  80. package/src/cli/index.ts +242 -211
  81. package/src/cli/proc.ts +30 -0
  82. package/src/cli/ui.ts +111 -103
  83. package/src/cli/update.ts +150 -0
  84. package/src/cli/updates.ts +69 -0
  85. package/src/client/components/Image.tsx +91 -89
  86. package/src/client/dev/error-overlay.tsx +193 -197
  87. package/src/client/head/metadata.ts +94 -92
  88. package/src/client/index.ts +79 -64
  89. package/src/client/navigation/Link.tsx +94 -100
  90. package/src/client/navigation/navigation.ts +215 -218
  91. package/src/client/routing/Router.tsx +210 -193
  92. package/src/client/routing/hooks.ts +110 -114
  93. package/src/client/routing/lazy.ts +77 -81
  94. package/src/client/search/search.ts +189 -0
  95. package/src/client/search/use-page-search.ts +73 -0
  96. package/src/compiler/config.ts +173 -171
  97. package/src/compiler/fonts.ts +89 -87
  98. package/src/compiler/generate.ts +378 -364
  99. package/src/compiler/image-report.ts +88 -85
  100. package/src/compiler/index.ts +2 -0
  101. package/src/compiler/pages.ts +70 -0
  102. package/src/compiler/plugin.ts +51 -47
  103. package/src/compiler/prerender.ts +152 -130
  104. package/src/compiler/routes.ts +132 -131
  105. package/src/compiler/seo.ts +381 -356
  106. package/src/compiler/vite.ts +155 -130
  107. package/src/io/FastSet.ts +99 -96
  108. package/test/configure.test.ts +94 -90
  109. package/test/doctor.test.ts +140 -0
  110. package/test/dom/Image.test.tsx +73 -46
  111. package/test/dom/Script.test.tsx +48 -45
  112. package/test/dom/action.test.tsx +146 -129
  113. package/test/dom/error-overlay.test.tsx +44 -44
  114. package/test/dom/loader.test.tsx +2 -2
  115. package/test/dom/revalidate.test.tsx +1 -1
  116. package/test/dom/route-head.test.tsx +35 -2
  117. package/test/dom/slot.test.tsx +131 -109
  118. package/test/dom/view-transitions.test.tsx +53 -51
  119. package/test/features.test.ts +149 -142
  120. package/test/fonts.test.ts +28 -26
  121. package/test/head.test.ts +45 -35
  122. package/test/metadata.test.ts +42 -41
  123. package/test/pages.test.ts +105 -0
  124. package/test/prerender.test.ts +54 -46
  125. package/test/search.test.ts +114 -0
  126. package/test/seo.test.ts +164 -142
  127. package/test/slot-layouts.test.ts +69 -0
  128. 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
+ });