toiljs 0.0.15 → 0.0.16
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/.babelrc +13 -13
- package/.gitattributes +2 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
- package/.github/changelog-config.json +45 -45
- package/.github/dependabot.yml +27 -27
- package/.github/workflows/ci.yml +191 -191
- package/.prettierrc.json +11 -11
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +5 -5
- package/LICENSE +187 -187
- package/README.md +339 -315
- package/as-pect.asconfig.json +34 -34
- package/as-pect.config.js +65 -65
- package/assets/logo.svg +36 -36
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +0 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +442 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- package/build/client/navigation/prefetch.d.ts +1 -0
- package/build/client/navigation/prefetch.js +35 -0
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +6 -2
- package/build/client/routing/loader.d.ts +23 -0
- package/build/client/routing/loader.js +53 -7
- package/build/client/routing/mount.js +4 -3
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +7 -0
- package/build/compiler/docs.js +16 -16
- package/build/compiler/index.d.ts +2 -2
- package/build/compiler/index.js +1 -1
- package/build/compiler/plugin.js +156 -0
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +1 -1
- package/build/compiler/seo.d.ts +1 -1
- package/build/compiler/seo.js +5 -4
- package/build/compiler/ssg.js +32 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/eslint.config.js +48 -48
- package/examples/basic/client/404.tsx +11 -11
- package/examples/basic/client/components/.gitkeep +1 -1
- package/examples/basic/client/global-error.tsx +13 -13
- package/examples/basic/client/layout.tsx +25 -25
- package/examples/basic/client/public/images/.gitkeep +1 -1
- package/examples/basic/client/public/images/logo.svg +36 -36
- package/examples/basic/client/public/robots.txt +2 -2
- package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
- package/examples/basic/client/routes/features/error/error.tsx +16 -16
- package/examples/basic/client/routes/features/template/b.tsx +14 -14
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
- package/examples/basic/client/routes/gallery/layout.tsx +13 -13
- package/examples/basic/client/routes/io.tsx +24 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +155 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier.json +18 -18
- package/presets/tsconfig.json +37 -37
- package/src/backend/index.ts +160 -160
- package/src/cli/proc.ts +50 -50
- package/src/cli/updates.ts +69 -69
- package/src/cli/validate.ts +31 -31
- package/src/client/channel/channel.ts +146 -146
- package/src/client/components/Form.tsx +65 -65
- package/src/client/components/Script.tsx +113 -113
- package/src/client/components/Slot.tsx +21 -21
- package/src/client/dev/devtools.tsx +973 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +89 -89
- package/src/client/navigation/NavLink.tsx +86 -86
- package/src/client/navigation/navigation.ts +235 -235
- package/src/client/navigation/prefetch.ts +169 -130
- package/src/client/navigation/scroll.ts +53 -53
- package/src/client/routing/Router.tsx +8 -2
- package/src/client/routing/action.ts +122 -122
- package/src/client/routing/error-boundary.tsx +43 -43
- package/src/client/routing/hooks.ts +21 -6
- package/src/client/routing/loader.ts +325 -235
- package/src/client/routing/match.ts +47 -47
- package/src/client/routing/mount.tsx +54 -52
- package/src/client/routing/params-context.ts +10 -10
- package/src/client/routing/slot-context.ts +7 -7
- package/src/client/search/search.ts +189 -189
- package/src/client/search/use-page-search.ts +73 -73
- package/src/client/types.ts +73 -73
- package/src/compiler/config.ts +219 -182
- package/src/compiler/docs.ts +228 -228
- package/src/compiler/generate.ts +394 -394
- package/src/compiler/index.ts +64 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +170 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +397 -390
- package/src/compiler/ssg.ts +162 -126
- package/src/io/BinaryReader.ts +340 -340
- package/src/io/BinaryWriter.ts +385 -385
- package/src/io/FastMap.ts +127 -127
- package/src/io/index.ts +11 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +18 -18
- package/src/logger/index.ts +22 -22
- package/src/server/index.ts +10 -10
- package/src/server/main.ts +13 -13
- package/src/server/tsconfig.json +4 -4
- package/src/shared/index.ts +10 -10
- package/std/client/index.d.ts +15 -15
- package/std/client/package.json +3 -3
- package/test/assembly/example.spec.ts +7 -7
- package/test/channel.test.ts +21 -21
- package/test/dom/Link.test.tsx +47 -47
- package/test/dom/NavLink.test.tsx +37 -37
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +121 -121
- package/test/dom/navigation.test.ts +59 -59
- package/test/dom/revalidate.test.tsx +38 -38
- package/test/dom/route-head.test.tsx +78 -78
- package/test/dom/router-loading.test.tsx +44 -44
- package/test/dom/scroll.test.ts +56 -56
- package/test/dom/use-metadata.test.tsx +58 -58
- package/test/io.test.ts +93 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/routes.test.ts +76 -76
- package/test/seo.test.ts +175 -164
- package/test/slot-layouts.test.ts +69 -69
- package/test/ssg.test.ts +36 -36
- package/test/update.test.ts +44 -44
- package/test/validate.test.ts +42 -42
- package/toil-routes.d.ts +7 -0
- package/toilconfig.json +30 -30
- package/tsconfig.backend.json +13 -13
- package/tsconfig.base.json +35 -35
- package/tsconfig.cli.json +13 -13
- package/tsconfig.client.json +14 -14
- package/tsconfig.compiler.json +13 -13
- package/tsconfig.io.json +12 -12
- package/tsconfig.json +22 -22
- package/tsconfig.logger.json +12 -12
- package/tsconfig.server.json +10 -10
- package/tsconfig.shared.json +12 -12
- package/vitest.config.ts +26 -26
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/toiljs.iml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.toil/entry.tsx +0 -9
- package/.toil/index.html +0 -12
- package/.toil/routes.ts +0 -9
- package/build/cli/configure.d.ts +0 -16
- package/build/cli/configure.js +0 -272
- package/build/cli/create.d.ts +0 -16
- package/build/cli/create.js +0 -420
- package/build/cli/diagnostics.d.ts +0 -55
- package/build/cli/diagnostics.js +0 -333
- package/build/cli/doctor.d.ts +0 -6
- package/build/cli/doctor.js +0 -249
- package/build/cli/features.d.ts +0 -25
- package/build/cli/features.js +0 -107
- package/build/cli/index.d.ts +0 -2
- package/build/cli/proc.d.ts +0 -6
- package/build/cli/proc.js +0 -31
- package/build/cli/ui.d.ts +0 -9
- package/build/cli/ui.js +0 -75
- package/build/cli/update.d.ts +0 -7
- package/build/cli/update.js +0 -117
- package/build/cli/updates.d.ts +0 -10
- package/build/cli/updates.js +0 -45
- package/build/cli/validate.d.ts +0 -4
- package/build/cli/validate.js +0 -19
- package/build/client/Link.d.ts +0 -8
- package/build/client/Link.js +0 -44
- package/build/client/NavLink.d.ts +0 -14
- package/build/client/NavLink.js +0 -37
- package/build/client/Router.d.ts +0 -7
- package/build/client/Router.js +0 -55
- package/build/client/channel.d.ts +0 -23
- package/build/client/channel.js +0 -94
- package/build/client/error-boundary.d.ts +0 -16
- package/build/client/error-boundary.js +0 -19
- package/build/client/head.d.ts +0 -26
- package/build/client/head.js +0 -87
- package/build/client/hooks.d.ts +0 -17
- package/build/client/hooks.js +0 -48
- package/build/client/lazy.d.ts +0 -16
- package/build/client/lazy.js +0 -53
- package/build/client/match.d.ts +0 -2
- package/build/client/match.js +0 -32
- package/build/client/mount.d.ts +0 -2
- package/build/client/mount.js +0 -13
- package/build/client/navigation.d.ts +0 -13
- package/build/client/navigation.js +0 -97
- package/build/client/params-context.d.ts +0 -2
- package/build/client/params-context.js +0 -2
- package/build/client/prefetch.d.ts +0 -11
- package/build/client/prefetch.js +0 -100
- package/build/client/runtime.d.ts +0 -31
- package/build/client/runtime.js +0 -112
- package/build/client/scroll.d.ts +0 -8
- package/build/client/scroll.js +0 -36
- package/toil-env.d.ts +0 -16
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { setRouteHead, useHead } from '../../src/client/head/head';
|
|
5
|
-
import { resolveMetadata } from '../../src/client/head/metadata';
|
|
6
|
-
import { cleanup, render } from '@testing-library/react';
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
cleanup();
|
|
10
|
-
setRouteHead(null);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const desc = (): string | null =>
|
|
14
|
-
document.head.querySelector('meta[name="description"]')?.getAttribute('content') ?? null;
|
|
15
|
-
|
|
16
|
-
describe('route head (metadata baseline)', () => {
|
|
17
|
-
it('applies a resolved metadata head to the document', () => {
|
|
18
|
-
setRouteHead(resolveMetadata({ title: 'About', description: 'about page' }));
|
|
19
|
-
expect(document.title).toBe('About');
|
|
20
|
-
expect(desc()).toBe('about page');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("wins over a layout's useHead/<Head> defaults for the keys it sets", () => {
|
|
24
|
-
// A layout default title + a route's metadata title: the route metadata should win.
|
|
25
|
-
function LayoutDefaults() {
|
|
26
|
-
useHead({ title: 'Site Default', meta: [{ name: 'description', content: 'site' }] });
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
render(<LayoutDefaults />);
|
|
30
|
-
setRouteHead(resolveMetadata({ title: 'useReducer', description: 'route desc' }));
|
|
31
|
-
expect(document.title).toBe('useReducer');
|
|
32
|
-
expect(desc()).toBe('route desc');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("applies a layout's titleTemplate to the route's title", () => {
|
|
36
|
-
function LayoutDefaults() {
|
|
37
|
-
useHead({ titleTemplate: '%s · toiljs' });
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
render(<LayoutDefaults />);
|
|
41
|
-
setRouteHead(resolveMetadata({ title: 'About' }));
|
|
42
|
-
expect(document.title).toBe('About · toiljs');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Regression for the "metadata title doesn't update" report: a real layout (title + template)
|
|
46
|
-
// plus a route's full `metadata` (the exact shape users write) must land on the route's title,
|
|
47
|
-
// wrapped by the layout template, with the route's og:title applied too.
|
|
48
|
-
it('applies a full route metadata over a layout title + template', () => {
|
|
49
|
-
function LayoutDefaults() {
|
|
50
|
-
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
render(<LayoutDefaults />);
|
|
54
|
-
setRouteHead(
|
|
55
|
-
resolveMetadata({
|
|
56
|
-
title: 'useReducer | React Hooks',
|
|
57
|
-
description: 'Manage complex state with a reducer.',
|
|
58
|
-
openGraph: { title: 'useReducer | React Hooks', type: 'website' },
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
61
|
-
expect(document.title).toBe('useReducer | React Hooks | ToilJS');
|
|
62
|
-
expect(
|
|
63
|
-
document.head.querySelector('meta[property="og:title"]')?.getAttribute('content'),
|
|
64
|
-
).toBe('useReducer | React Hooks');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// A route can opt out of the layout's template by setting its own `titleTemplate: '%s'`, so the
|
|
68
|
-
// tab reads exactly the route title with no site suffix.
|
|
69
|
-
it("lets a route override the layout template with its own '%s'", () => {
|
|
70
|
-
function LayoutDefaults() {
|
|
71
|
-
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
render(<LayoutDefaults />);
|
|
75
|
-
setRouteHead(resolveMetadata({ title: 'useReducer | React Hooks', titleTemplate: '%s' }));
|
|
76
|
-
expect(document.title).toBe('useReducer | React Hooks');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { setRouteHead, useHead } from '../../src/client/head/head';
|
|
5
|
+
import { resolveMetadata } from '../../src/client/head/metadata';
|
|
6
|
+
import { cleanup, render } from '@testing-library/react';
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup();
|
|
10
|
+
setRouteHead(null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const desc = (): string | null =>
|
|
14
|
+
document.head.querySelector('meta[name="description"]')?.getAttribute('content') ?? null;
|
|
15
|
+
|
|
16
|
+
describe('route head (metadata baseline)', () => {
|
|
17
|
+
it('applies a resolved metadata head to the document', () => {
|
|
18
|
+
setRouteHead(resolveMetadata({ title: 'About', description: 'about page' }));
|
|
19
|
+
expect(document.title).toBe('About');
|
|
20
|
+
expect(desc()).toBe('about page');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("wins over a layout's useHead/<Head> defaults for the keys it sets", () => {
|
|
24
|
+
// A layout default title + a route's metadata title: the route metadata should win.
|
|
25
|
+
function LayoutDefaults() {
|
|
26
|
+
useHead({ title: 'Site Default', meta: [{ name: 'description', content: 'site' }] });
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
render(<LayoutDefaults />);
|
|
30
|
+
setRouteHead(resolveMetadata({ title: 'useReducer', description: 'route desc' }));
|
|
31
|
+
expect(document.title).toBe('useReducer');
|
|
32
|
+
expect(desc()).toBe('route desc');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies a layout's titleTemplate to the route's title", () => {
|
|
36
|
+
function LayoutDefaults() {
|
|
37
|
+
useHead({ titleTemplate: '%s · toiljs' });
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
render(<LayoutDefaults />);
|
|
41
|
+
setRouteHead(resolveMetadata({ title: 'About' }));
|
|
42
|
+
expect(document.title).toBe('About · toiljs');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Regression for the "metadata title doesn't update" report: a real layout (title + template)
|
|
46
|
+
// plus a route's full `metadata` (the exact shape users write) must land on the route's title,
|
|
47
|
+
// wrapped by the layout template, with the route's og:title applied too.
|
|
48
|
+
it('applies a full route metadata over a layout title + template', () => {
|
|
49
|
+
function LayoutDefaults() {
|
|
50
|
+
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
render(<LayoutDefaults />);
|
|
54
|
+
setRouteHead(
|
|
55
|
+
resolveMetadata({
|
|
56
|
+
title: 'useReducer | React Hooks',
|
|
57
|
+
description: 'Manage complex state with a reducer.',
|
|
58
|
+
openGraph: { title: 'useReducer | React Hooks', type: 'website' },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
expect(document.title).toBe('useReducer | React Hooks | ToilJS');
|
|
62
|
+
expect(
|
|
63
|
+
document.head.querySelector('meta[property="og:title"]')?.getAttribute('content'),
|
|
64
|
+
).toBe('useReducer | React Hooks');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// A route can opt out of the layout's template by setting its own `titleTemplate: '%s'`, so the
|
|
68
|
+
// tab reads exactly the route title with no site suffix.
|
|
69
|
+
it("lets a route override the layout template with its own '%s'", () => {
|
|
70
|
+
function LayoutDefaults() {
|
|
71
|
+
useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
render(<LayoutDefaults />);
|
|
75
|
+
setRouteHead(resolveMetadata({ title: 'useReducer | React Hooks', titleTemplate: '%s' }));
|
|
76
|
+
expect(document.title).toBe('useReducer | React Hooks');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { act, cleanup, render, waitFor } from '@testing-library/react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { navigate } from '../../src/client/navigation/navigation';
|
|
6
|
-
import { Router } from '../../src/client/routing/Router';
|
|
7
|
-
import type { RouteDef } from '../../src/client/types';
|
|
8
|
-
|
|
9
|
-
afterEach(cleanup);
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
window.history.replaceState({}, '', '/');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const routes: RouteDef[] = [
|
|
15
|
-
{
|
|
16
|
-
pattern: '/',
|
|
17
|
-
load: () => Promise.resolve({ default: () => <div>HOME</div> }),
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
// Page chunk never resolves, so the route stays suspended, exercising the fallback path.
|
|
21
|
-
pattern: '/slow',
|
|
22
|
-
load: () => new Promise<{ default: () => null }>(() => undefined),
|
|
23
|
-
loading: () => Promise.resolve({ default: () => <div>LOADING</div> }),
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
describe('Router loading fallback', () => {
|
|
28
|
-
it("shows the route's loading.tsx immediately when navigating to a suspending route", async () => {
|
|
29
|
-
const { findByText, queryByText } = render(<Router routes={routes} />);
|
|
30
|
-
await findByText('HOME');
|
|
31
|
-
|
|
32
|
-
// A route with a `loading.tsx` keys its Suspense boundary per URL, so even though navigation
|
|
33
|
-
// runs in a transition the fallback appears immediately (it isn't suppressed / frozen).
|
|
34
|
-
act(() => {
|
|
35
|
-
navigate('/slow');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
await waitFor(() => {
|
|
39
|
-
expect(queryByText('LOADING')).not.toBeNull();
|
|
40
|
-
});
|
|
41
|
-
// The keyed boundary remounts for the new route, so the previous page is gone (not frozen).
|
|
42
|
-
expect(queryByText('HOME')).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, cleanup, render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { navigate } from '../../src/client/navigation/navigation';
|
|
6
|
+
import { Router } from '../../src/client/routing/Router';
|
|
7
|
+
import type { RouteDef } from '../../src/client/types';
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
window.history.replaceState({}, '', '/');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const routes: RouteDef[] = [
|
|
15
|
+
{
|
|
16
|
+
pattern: '/',
|
|
17
|
+
load: () => Promise.resolve({ default: () => <div>HOME</div> }),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// Page chunk never resolves, so the route stays suspended, exercising the fallback path.
|
|
21
|
+
pattern: '/slow',
|
|
22
|
+
load: () => new Promise<{ default: () => null }>(() => undefined),
|
|
23
|
+
loading: () => Promise.resolve({ default: () => <div>LOADING</div> }),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
describe('Router loading fallback', () => {
|
|
28
|
+
it("shows the route's loading.tsx immediately when navigating to a suspending route", async () => {
|
|
29
|
+
const { findByText, queryByText } = render(<Router routes={routes} />);
|
|
30
|
+
await findByText('HOME');
|
|
31
|
+
|
|
32
|
+
// A route with a `loading.tsx` keys its Suspense boundary per URL, so even though navigation
|
|
33
|
+
// runs in a transition the fallback appears immediately (it isn't suppressed / frozen).
|
|
34
|
+
act(() => {
|
|
35
|
+
navigate('/slow');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(queryByText('LOADING')).not.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
// The keyed boundary remounts for the new route, so the previous page is gone (not frozen).
|
|
42
|
+
expect(queryByText('HOME')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
package/test/dom/scroll.test.ts
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
applyScroll,
|
|
6
|
-
enableManualScrollRestoration,
|
|
7
|
-
planScroll,
|
|
8
|
-
rememberScroll,
|
|
9
|
-
} from '../../src/client/navigation/scroll';
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
window.scrollTo = vi.fn();
|
|
13
|
-
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('scroll management', () => {
|
|
17
|
-
it('enables manual scroll restoration when the browser supports it', () => {
|
|
18
|
-
// jsdom doesn't implement scrollRestoration; define it so the guarded branch runs.
|
|
19
|
-
(window.history as History & { scrollRestoration: ScrollRestoration }).scrollRestoration =
|
|
20
|
-
'auto';
|
|
21
|
-
enableManualScrollRestoration();
|
|
22
|
-
expect(window.history.scrollRestoration).toBe('manual');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('scrolls to top', () => {
|
|
26
|
-
planScroll({ hash: '', toTop: true });
|
|
27
|
-
applyScroll();
|
|
28
|
-
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('restores a saved position', () => {
|
|
32
|
-
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
|
|
33
|
-
rememberScroll('k1');
|
|
34
|
-
planScroll({ restoreKey: 'k1', hash: '', toTop: false });
|
|
35
|
-
applyScroll();
|
|
36
|
-
expect(window.scrollTo).toHaveBeenCalledWith(0, 250);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('scrolls to a #hash element', () => {
|
|
40
|
-
const el = document.createElement('div');
|
|
41
|
-
el.id = 'sec';
|
|
42
|
-
el.scrollIntoView = vi.fn();
|
|
43
|
-
document.body.appendChild(el);
|
|
44
|
-
planScroll({ hash: '#sec', toTop: false });
|
|
45
|
-
applyScroll();
|
|
46
|
-
expect(el.scrollIntoView).toHaveBeenCalled();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('consumes the plan (second applyScroll is a no-op)', () => {
|
|
50
|
-
planScroll({ hash: '', toTop: true });
|
|
51
|
-
applyScroll();
|
|
52
|
-
(window.scrollTo as ReturnType<typeof vi.fn>).mockClear();
|
|
53
|
-
applyScroll();
|
|
54
|
-
expect(window.scrollTo).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyScroll,
|
|
6
|
+
enableManualScrollRestoration,
|
|
7
|
+
planScroll,
|
|
8
|
+
rememberScroll,
|
|
9
|
+
} from '../../src/client/navigation/scroll';
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
window.scrollTo = vi.fn();
|
|
13
|
+
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('scroll management', () => {
|
|
17
|
+
it('enables manual scroll restoration when the browser supports it', () => {
|
|
18
|
+
// jsdom doesn't implement scrollRestoration; define it so the guarded branch runs.
|
|
19
|
+
(window.history as History & { scrollRestoration: ScrollRestoration }).scrollRestoration =
|
|
20
|
+
'auto';
|
|
21
|
+
enableManualScrollRestoration();
|
|
22
|
+
expect(window.history.scrollRestoration).toBe('manual');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('scrolls to top', () => {
|
|
26
|
+
planScroll({ hash: '', toTop: true });
|
|
27
|
+
applyScroll();
|
|
28
|
+
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('restores a saved position', () => {
|
|
32
|
+
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
|
|
33
|
+
rememberScroll('k1');
|
|
34
|
+
planScroll({ restoreKey: 'k1', hash: '', toTop: false });
|
|
35
|
+
applyScroll();
|
|
36
|
+
expect(window.scrollTo).toHaveBeenCalledWith(0, 250);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('scrolls to a #hash element', () => {
|
|
40
|
+
const el = document.createElement('div');
|
|
41
|
+
el.id = 'sec';
|
|
42
|
+
el.scrollIntoView = vi.fn();
|
|
43
|
+
document.body.appendChild(el);
|
|
44
|
+
planScroll({ hash: '#sec', toTop: false });
|
|
45
|
+
applyScroll();
|
|
46
|
+
expect(el.scrollIntoView).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('consumes the plan (second applyScroll is a no-op)', () => {
|
|
50
|
+
planScroll({ hash: '', toTop: true });
|
|
51
|
+
applyScroll();
|
|
52
|
+
(window.scrollTo as ReturnType<typeof vi.fn>).mockClear();
|
|
53
|
+
applyScroll();
|
|
54
|
+
expect(window.scrollTo).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { cleanup, render } from '@testing-library/react';
|
|
4
|
-
|
|
5
|
-
import { Metadata, useMetadata } from '../../src/client/head/metadata';
|
|
6
|
-
import { setRouteHead } from '../../src/client/head/head';
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
cleanup();
|
|
10
|
-
setRouteHead(null);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const meta = (key: string): string | null =>
|
|
14
|
-
document.head.querySelector(`meta[${key}]`)?.getAttribute('content') ?? null;
|
|
15
|
-
|
|
16
|
-
describe('useMetadata / <Metadata>', () => {
|
|
17
|
-
it('applies a full Metadata object to the document head from a component', () => {
|
|
18
|
-
function Article() {
|
|
19
|
-
useMetadata({
|
|
20
|
-
title: 'Article',
|
|
21
|
-
description: 'an article',
|
|
22
|
-
openGraph: { title: 'og title', type: 'website' },
|
|
23
|
-
});
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
render(<Article />);
|
|
27
|
-
expect(document.title).toBe('Article');
|
|
28
|
-
expect(meta('name="description"')).toBe('an article');
|
|
29
|
-
expect(meta('property="og:title"')).toBe('og title');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('reverts the head when the component unmounts', () => {
|
|
33
|
-
function Article() {
|
|
34
|
-
useMetadata({ title: 'Temp' });
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
const { unmount } = render(<Article />);
|
|
38
|
-
expect(document.title).toBe('Temp');
|
|
39
|
-
unmount();
|
|
40
|
-
expect(document.title).not.toBe('Temp');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('the declarative <Metadata> form applies too', () => {
|
|
44
|
-
render(<Metadata title="Declarative" />);
|
|
45
|
-
expect(document.title).toBe('Declarative');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("a route's metadata still wins over a component's useMetadata", () => {
|
|
49
|
-
function Article() {
|
|
50
|
-
useMetadata({ title: 'Component' });
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
render(<Article />);
|
|
54
|
-
// The route baseline (applied last) takes precedence for keys it sets.
|
|
55
|
-
setRouteHead({ title: 'Route' });
|
|
56
|
-
expect(document.title).toBe('Route');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render } from '@testing-library/react';
|
|
4
|
+
|
|
5
|
+
import { Metadata, useMetadata } from '../../src/client/head/metadata';
|
|
6
|
+
import { setRouteHead } from '../../src/client/head/head';
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup();
|
|
10
|
+
setRouteHead(null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const meta = (key: string): string | null =>
|
|
14
|
+
document.head.querySelector(`meta[${key}]`)?.getAttribute('content') ?? null;
|
|
15
|
+
|
|
16
|
+
describe('useMetadata / <Metadata>', () => {
|
|
17
|
+
it('applies a full Metadata object to the document head from a component', () => {
|
|
18
|
+
function Article() {
|
|
19
|
+
useMetadata({
|
|
20
|
+
title: 'Article',
|
|
21
|
+
description: 'an article',
|
|
22
|
+
openGraph: { title: 'og title', type: 'website' },
|
|
23
|
+
});
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
render(<Article />);
|
|
27
|
+
expect(document.title).toBe('Article');
|
|
28
|
+
expect(meta('name="description"')).toBe('an article');
|
|
29
|
+
expect(meta('property="og:title"')).toBe('og title');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reverts the head when the component unmounts', () => {
|
|
33
|
+
function Article() {
|
|
34
|
+
useMetadata({ title: 'Temp' });
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const { unmount } = render(<Article />);
|
|
38
|
+
expect(document.title).toBe('Temp');
|
|
39
|
+
unmount();
|
|
40
|
+
expect(document.title).not.toBe('Temp');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('the declarative <Metadata> form applies too', () => {
|
|
44
|
+
render(<Metadata title="Declarative" />);
|
|
45
|
+
expect(document.title).toBe('Declarative');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("a route's metadata still wins over a component's useMetadata", () => {
|
|
49
|
+
function Article() {
|
|
50
|
+
useMetadata({ title: 'Component' });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
render(<Article />);
|
|
54
|
+
// The route baseline (applied last) takes precedence for keys it sets.
|
|
55
|
+
setRouteHead({ title: 'Route' });
|
|
56
|
+
expect(document.title).toBe('Route');
|
|
57
|
+
});
|
|
58
|
+
});
|