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