@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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 +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderRoute — a React-Testing-Library-style helper for unit-testing CLIENT
|
|
3
|
+
* components that read @rangojs/router client context (useParams, useReverse,
|
|
4
|
+
* Outlet, useNavigation, useLoader).
|
|
5
|
+
*
|
|
6
|
+
* Peer of React Router's createRoutesStub and Expo Router's renderRouter. It
|
|
7
|
+
* mounts the router's NavigationProvider plus a synthetic segment tree so that
|
|
8
|
+
* a component under test sees real router context, without spinning up a
|
|
9
|
+
* server, a Vite build, or a real Flight round-trip.
|
|
10
|
+
*
|
|
11
|
+
* FIDELITY CONTRACT — read before relying on this helper:
|
|
12
|
+
* This renders the CLIENT tree ONLY. The segment tree is built synthetically
|
|
13
|
+
* from the `routes` you pass; there is no server render and no Flight
|
|
14
|
+
* (de)serialization. Consequences:
|
|
15
|
+
* - It will NOT catch server/client boundary reference-identity remount bugs
|
|
16
|
+
* (a server-serialized component reference differing from the client
|
|
17
|
+
* reference). Use renderServerTree / e2e for those.
|
|
18
|
+
* - It will NOT catch real Flight serialization errors (non-serializable
|
|
19
|
+
* props crossing the RSC boundary), loader execution on the server,
|
|
20
|
+
* middleware, or handler ordering. Those are renderServerTree / renderHandler
|
|
21
|
+
* / e2e territory.
|
|
22
|
+
* - Loader data, location state, and handle output are SEEDED directly into
|
|
23
|
+
* client context (see the `loaders` / `locationState` / `handles` options) —
|
|
24
|
+
* nothing is executed on the server. This exercises the read path
|
|
25
|
+
* (useLoader / useLocationState / useHandle from context), not the run path.
|
|
26
|
+
* - navigate() commits synchronously, so it does NOT drive the navigation
|
|
27
|
+
* lifecycle: useNavigation().state, useLinkStatus().pending, and
|
|
28
|
+
* useAction().state stay "idle". Assert pending/loading/submitting transition
|
|
29
|
+
* states with renderServerTree / e2e instead (navigate() warns once if used).
|
|
30
|
+
* What it DOES cover: client hooks that read NavigationProvider /
|
|
31
|
+
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
32
|
+
* useRouter, usePathname, useSearchParams, Outlet nesting, useLoader /
|
|
33
|
+
* useFetchLoader (seeded data), useLocationState (seeded), and useHandle (seeded).
|
|
34
|
+
* Basename-mounted apps: pass the `basename` option so useRouter().basename,
|
|
35
|
+
* <Link> prefixing, and useMount/useHref resolve against the mount prefix
|
|
36
|
+
* (without it they resolve at the root "/"). For an include("/shop", ...)
|
|
37
|
+
* subtree, pass the `mount` option so useMount() returns the mounted prefix
|
|
38
|
+
* (the segment chain is wrapped in a MountContext exactly as in production).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type { ReactNode, ComponentType } from "react";
|
|
42
|
+
import type { RenderResult } from "@testing-library/react";
|
|
43
|
+
import { renderSegments } from "../segment-system.js";
|
|
44
|
+
import {
|
|
45
|
+
createNavigationStore,
|
|
46
|
+
generateHistoryKey,
|
|
47
|
+
} from "../browser/navigation-store.js";
|
|
48
|
+
import { createEventController } from "../browser/event-controller.js";
|
|
49
|
+
import type { NavigationStore, NavigationBridge } from "../browser/types.js";
|
|
50
|
+
import type { EventController } from "../browser/event-controller.js";
|
|
51
|
+
import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
|
|
52
|
+
import { NavigationProvider } from "../browser/react/NavigationProvider.js";
|
|
53
|
+
import { compilePattern } from "../router/pattern-matching.js";
|
|
54
|
+
import { normalizeBasename } from "../router/basename.js";
|
|
55
|
+
import type { LoaderDefinition } from "../types.js";
|
|
56
|
+
import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
|
|
57
|
+
import type { Handle } from "../handle.js";
|
|
58
|
+
import type { ThemeConfig } from "../theme/types.js";
|
|
59
|
+
import { resolveThemeConfig } from "../theme/constants.js";
|
|
60
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
61
|
+
|
|
62
|
+
const TEST_ORIGIN = "http://localhost";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Seed shape for `options.handle`, matching the handle wire format:
|
|
66
|
+
* `{ [handleName]: { [segmentId]: pushedValues[] } }` (each segment accumulates
|
|
67
|
+
* an array of values pushed for that handle).
|
|
68
|
+
*/
|
|
69
|
+
export type HandleDataSeed = Record<string, Record<string, unknown[]>>;
|
|
70
|
+
|
|
71
|
+
const syntheticIds = new WeakMap<object, string>();
|
|
72
|
+
let syntheticIdCounter = 0;
|
|
73
|
+
|
|
74
|
+
function ensureSyntheticId(
|
|
75
|
+
handle: object,
|
|
76
|
+
field: "$$id" | "__rsc_ls_key",
|
|
77
|
+
): string {
|
|
78
|
+
const existing = (handle as Record<string, string>)[field];
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
let id = syntheticIds.get(handle);
|
|
81
|
+
if (!id) {
|
|
82
|
+
id = `__rango_test_id_${syntheticIdCounter++}`;
|
|
83
|
+
syntheticIds.set(handle, id);
|
|
84
|
+
}
|
|
85
|
+
(handle as Record<string, string>)[field] = id;
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
|
|
90
|
+
const out: HandleDataSeed = {};
|
|
91
|
+
for (const [name, segMap] of Object.entries(seed ?? {})) {
|
|
92
|
+
out[name] = { ...segMap };
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RenderRouteSpec {
|
|
98
|
+
/**
|
|
99
|
+
* The route pattern this node matches, e.g. "/products/:productId". The LAST
|
|
100
|
+
* spec in the array is treated as the leaf route; earlier specs are layouts
|
|
101
|
+
* wrapping it. Only the leaf pattern is matched against the `request` URL to
|
|
102
|
+
* extract params; layout patterns are informational.
|
|
103
|
+
*/
|
|
104
|
+
path: string;
|
|
105
|
+
/** The component rendered for this node (the leaf route or a layout body). */
|
|
106
|
+
Component: ComponentType;
|
|
107
|
+
/**
|
|
108
|
+
* Optional layout component. When set on the LEAF spec it wraps the route in
|
|
109
|
+
* its own layout segment (useful for a route that owns a layout). Prefer
|
|
110
|
+
* expressing layouts as their own array entries instead.
|
|
111
|
+
*/
|
|
112
|
+
layout?: ComponentType;
|
|
113
|
+
/**
|
|
114
|
+
* Loader ids ($$id) whose seeded data (from `options.loaderData`) should be
|
|
115
|
+
* attached to THIS node's segment so useLoader/useFetchLoader resolve it from
|
|
116
|
+
* context. When omitted, every key in `options.loaderData` is attached to the
|
|
117
|
+
* leaf route segment.
|
|
118
|
+
*/
|
|
119
|
+
loaderIds?: string[];
|
|
120
|
+
/** Optional route name (informational; not used for matching). */
|
|
121
|
+
name?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Options for renderRoute.
|
|
126
|
+
*/
|
|
127
|
+
export interface RenderRouteOptions {
|
|
128
|
+
/**
|
|
129
|
+
* The initial location to render at: a `Request`, or a URL string (absolute or
|
|
130
|
+
* path). Only the URL is read (this is a client render — headers/method are
|
|
131
|
+
* ignored); named `request` for parity with the other primitives. Defaults to
|
|
132
|
+
* the leaf spec's static prefix or "/".
|
|
133
|
+
*/
|
|
134
|
+
request?: Request | string;
|
|
135
|
+
/**
|
|
136
|
+
* Loader data to seed into client context, keyed by loader id ($$id). A
|
|
137
|
+
* component calling useLoader(SomeLoader) reads `loaderData[SomeLoader.$$id]`.
|
|
138
|
+
* Seeded values are placed in the route segment's OutletProvider context, so
|
|
139
|
+
* the read path is exercised without executing any loader.
|
|
140
|
+
*/
|
|
141
|
+
loaderData?: Record<string, unknown>;
|
|
142
|
+
/**
|
|
143
|
+
* Loaders to seed by REFERENCE — the robust way to test a component that calls
|
|
144
|
+
* `useLoader(loader)`. A real `createLoader()` handle has an empty `$$id` in a
|
|
145
|
+
* bare test (the id is injected by the Vite plugin at build time), so keying
|
|
146
|
+
* `loaderData` by `$$id` collides under `""` and `useLoader` resolves nothing.
|
|
147
|
+
* Passing `[loader, data]` pairs lets renderRoute assign a synthetic stable id
|
|
148
|
+
* and wire `useLoader` to it. Prefer this over `loaderData` for real handles.
|
|
149
|
+
*
|
|
150
|
+
* NOTE: when a real handle has no `$$id`, renderRoute MUTATES it to assign a
|
|
151
|
+
* synthetic stable id (so repeat renders key consistently). This is a side
|
|
152
|
+
* effect on your input object; a handle reused across tests keeps that id.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* // useLoader returns an ENVELOPE — destructure `data`, it is not the bare value.
|
|
156
|
+
* function CartBadge() {
|
|
157
|
+
* const { data } = useLoader(CartLoader); // NOT `useLoader(CartLoader).itemCount`
|
|
158
|
+
* return <span>{data.itemCount}</span>;
|
|
159
|
+
* }
|
|
160
|
+
* renderRoute([{ path: "/cart", Component: CartBadge }], {
|
|
161
|
+
* loaders: [[CartLoader, { itemCount: 3, total: 89.97 }]],
|
|
162
|
+
* });
|
|
163
|
+
*/
|
|
164
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
165
|
+
/**
|
|
166
|
+
* Explicit params. Merged over (and overriding) params extracted from the
|
|
167
|
+
* `request` URL. Use this when the URL alone cannot express the params, or to
|
|
168
|
+
* avoid relying on URL parsing. Supplying params also OPTS OUT of the
|
|
169
|
+
* request/leaf match check: a `request` whose pathname does not resolve the
|
|
170
|
+
* leaf is normally rejected under the test runner, but passing params here
|
|
171
|
+
* tells renderRoute the request is intentionally not the param source.
|
|
172
|
+
*/
|
|
173
|
+
params?: Record<string, string>;
|
|
174
|
+
/**
|
|
175
|
+
* Location-state values to seed by REFERENCE, for components that call
|
|
176
|
+
* `useLocationState(StateDef)`. Like loaders, a real `createLocationState()`
|
|
177
|
+
* handle has an empty injected key in a bare test, so pass `[def, value]`
|
|
178
|
+
* pairs; renderRoute assigns a synthetic key and writes it to `history.state`.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* renderRoute([{ path: "/", Component: FlashBanner }], {
|
|
182
|
+
* locationState: [[FlashMessage, { text: "Saved" }]],
|
|
183
|
+
* });
|
|
184
|
+
*/
|
|
185
|
+
locationState?: ReadonlyArray<
|
|
186
|
+
readonly [LocationStateDefinition<any, any>, unknown]
|
|
187
|
+
>;
|
|
188
|
+
/**
|
|
189
|
+
* Handles to seed by REFERENCE, for components that read handle output via
|
|
190
|
+
* `useHandle(SomeHandle)` (e.g. a client `Breadcrumbs` trail). Each entry is
|
|
191
|
+
* `[handle, pushedValues[]]` — the values a route's handlers would have pushed;
|
|
192
|
+
* renderRoute attaches them to the leaf route segment under the handle's id.
|
|
193
|
+
* Built-in handles (Breadcrumbs/Meta) have stable ids and work directly.
|
|
194
|
+
*
|
|
195
|
+
* Handle data is accumulated GLOBALLY on the event controller, not scoped per
|
|
196
|
+
* segment like loaders — so ANY component in the chain reads the seeded values,
|
|
197
|
+
* a LAYOUT (e.g. a DetailLayout/ActionToolbar reading a handle) just as much as
|
|
198
|
+
* the leaf route. Most handle usage is server-side (`ctx.use(...)`) and is
|
|
199
|
+
* better covered by `renderToFlightString`/e2e; this seeds the client read path
|
|
200
|
+
* only.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* renderRoute([{ path: "/p", Component: BreadcrumbTrail }], {
|
|
204
|
+
* handles: [[Breadcrumbs, [{ label: "Home", href: "/" }, { label: "P", href: "/p" }]]],
|
|
205
|
+
* });
|
|
206
|
+
*/
|
|
207
|
+
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
|
|
208
|
+
/**
|
|
209
|
+
* Advanced: raw handle data in wire format
|
|
210
|
+
* `{ [handleId]: { [segmentId]: pushedValues[] } }`. Prefer `handles` (which
|
|
211
|
+
* computes the segment id for you). Merged with `handles`.
|
|
212
|
+
*/
|
|
213
|
+
handle?: HandleDataSeed;
|
|
214
|
+
/**
|
|
215
|
+
* Route name -> pattern map. Informational for parity with the server test
|
|
216
|
+
* context; client useReverse takes its map directly as an argument, so this
|
|
217
|
+
* is not consumed by the client hooks.
|
|
218
|
+
*/
|
|
219
|
+
routeMap?: Record<string, string>;
|
|
220
|
+
/**
|
|
221
|
+
* Router basename (the `createRouter({ basename })` value). Wired into
|
|
222
|
+
* NavigationProvider so `useRouter().basename`, `<Link>` href prefixing, and
|
|
223
|
+
* `useMount`/`useHref` resolve against the mounted prefix instead of the root.
|
|
224
|
+
* Normalized exactly like createRouter (leading slash forced, trailing
|
|
225
|
+
* stripped, bare "/" -> undefined). Defaults to undefined (root mount).
|
|
226
|
+
*/
|
|
227
|
+
basename?: string;
|
|
228
|
+
/**
|
|
229
|
+
* include() mount prefix, to model an `include("/shop", ...)` subtree so a
|
|
230
|
+
* component (route OR layout in the chain) calling `useMount()` returns the
|
|
231
|
+
* mounted prefix instead of "/". Wraps the segment chain in a MountContext
|
|
232
|
+
* exactly as `renderSegments` does in production (a segment whose `mountPath`
|
|
233
|
+
* is set is wrapped in a MountContextProvider). Normalized like a path prefix
|
|
234
|
+
* (leading slash forced, trailing stripped, bare "/" -> root). Defaults to "/".
|
|
235
|
+
* An explicitly-passed `request` must match the leaf `path` directly (paths are
|
|
236
|
+
* include-RELATIVE; the mount does NOT rewrite the request) — pass the relative
|
|
237
|
+
* path, not the mount-prefixed one, or renderRoute throws rather than silently
|
|
238
|
+
* rendering empty params.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* renderRoute([{ path: "/c/wine", Component: ProductPage }], { mount: "/shop" });
|
|
242
|
+
* // useMount() inside ProductPage returns "/shop"
|
|
243
|
+
*/
|
|
244
|
+
mount?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Theme config in the `createRouter({ theme })` shape (resolved internally) to
|
|
247
|
+
* wrap the tree in a ThemeProvider. Defaults to no provider. Note: a component
|
|
248
|
+
* that calls `useTheme()` REQUIRES a provider — it throws "used outside
|
|
249
|
+
* ThemeProvider" without one — so pass a config (e.g. `true`) to test such a
|
|
250
|
+
* component.
|
|
251
|
+
*/
|
|
252
|
+
theme?: ThemeConfig | true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Imperative handle returned alongside the RTL result.
|
|
257
|
+
*/
|
|
258
|
+
export interface TestRouterHandle {
|
|
259
|
+
/**
|
|
260
|
+
* Navigate to a new URL. Re-resolves the URL against the supplied `routes`,
|
|
261
|
+
* updates params + location, and re-renders the segment tree. This is a
|
|
262
|
+
* client-only navigation: no server fetch occurs, so only the components in
|
|
263
|
+
* `routes` can be reached.
|
|
264
|
+
*/
|
|
265
|
+
navigate(url: string): Promise<void>;
|
|
266
|
+
/** The current committed pathname. */
|
|
267
|
+
pathname(): string;
|
|
268
|
+
/** The current committed params. */
|
|
269
|
+
params(): Record<string, string>;
|
|
270
|
+
/** The underlying navigation store (advanced use). */
|
|
271
|
+
store: NavigationStore;
|
|
272
|
+
/** The underlying event controller (advanced use). */
|
|
273
|
+
eventController: EventController;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Result of renderRoute: RTL's render result plus the router handle. */
|
|
277
|
+
export type RenderRouteResult = RenderResult & { router: TestRouterHandle };
|
|
278
|
+
|
|
279
|
+
interface ResolvedMatch {
|
|
280
|
+
params: Record<string, string>;
|
|
281
|
+
pathname: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function matchLeaf(
|
|
285
|
+
pattern: string,
|
|
286
|
+
pathname: string,
|
|
287
|
+
): Record<string, string> | null {
|
|
288
|
+
const compiled = compilePattern(pattern);
|
|
289
|
+
const match = compiled.regex.exec(pathname);
|
|
290
|
+
if (!match) return null;
|
|
291
|
+
const params: Record<string, string> = {};
|
|
292
|
+
compiled.paramNames.forEach((name, index) => {
|
|
293
|
+
const value = match[index + 1];
|
|
294
|
+
if (value !== undefined) {
|
|
295
|
+
params[name] = decodeURIComponent(value);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return params;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function staticPrefix(pattern: string): string {
|
|
302
|
+
const out: string[] = [];
|
|
303
|
+
for (const part of pattern.split("/")) {
|
|
304
|
+
if (part === "") continue;
|
|
305
|
+
if (part.startsWith(":") || part === "*") break;
|
|
306
|
+
out.push(part);
|
|
307
|
+
}
|
|
308
|
+
return "/" + out.join("/");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildSegments(
|
|
312
|
+
routes: RenderRouteSpec[],
|
|
313
|
+
params: Record<string, string>,
|
|
314
|
+
loaderData: Record<string, unknown>,
|
|
315
|
+
mount?: string,
|
|
316
|
+
): ResolvedSegment[] {
|
|
317
|
+
const segments: ResolvedSegment[] = [];
|
|
318
|
+
const leafIndex = routes.length - 1;
|
|
319
|
+
let idPath = "";
|
|
320
|
+
|
|
321
|
+
const seededIds = Object.keys(loaderData);
|
|
322
|
+
const explicitlyOwned = new Set<string>();
|
|
323
|
+
for (const spec of routes) {
|
|
324
|
+
for (const id of spec.loaderIds ?? []) explicitlyOwned.add(id);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
routes.forEach((spec, i) => {
|
|
328
|
+
const isLeaf = i === leafIndex;
|
|
329
|
+
const tag = isLeaf ? `R${i}` : `L${i}`;
|
|
330
|
+
idPath = idPath + tag;
|
|
331
|
+
const segmentId = idPath;
|
|
332
|
+
|
|
333
|
+
const Component = spec.Component;
|
|
334
|
+
const node: ResolvedSegment = {
|
|
335
|
+
id: segmentId,
|
|
336
|
+
namespace: "",
|
|
337
|
+
type: isLeaf ? "route" : "layout",
|
|
338
|
+
index: i,
|
|
339
|
+
component: <Component />,
|
|
340
|
+
params,
|
|
341
|
+
belongsToRoute: true,
|
|
342
|
+
};
|
|
343
|
+
if (mount) node.mountPath = mount;
|
|
344
|
+
if (isLeaf && spec.layout) {
|
|
345
|
+
const Layout = spec.layout;
|
|
346
|
+
node.layout = <Layout />;
|
|
347
|
+
}
|
|
348
|
+
segments.push(node);
|
|
349
|
+
|
|
350
|
+
const ownedIds = spec.loaderIds
|
|
351
|
+
? spec.loaderIds.filter((id) => id in loaderData)
|
|
352
|
+
: isLeaf
|
|
353
|
+
? seededIds.filter((id) => !explicitlyOwned.has(id))
|
|
354
|
+
: [];
|
|
355
|
+
|
|
356
|
+
ownedIds.forEach((loaderId, li) => {
|
|
357
|
+
segments.push({
|
|
358
|
+
id: `${segmentId}D${li}.${loaderId}`,
|
|
359
|
+
namespace: "",
|
|
360
|
+
type: "loader",
|
|
361
|
+
index: li,
|
|
362
|
+
component: null,
|
|
363
|
+
loaderId,
|
|
364
|
+
loaderData: loaderData[loaderId],
|
|
365
|
+
params,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return segments;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function renderRoute(
|
|
374
|
+
routes: RenderRouteSpec[],
|
|
375
|
+
options: RenderRouteOptions = {},
|
|
376
|
+
): Promise<RenderRouteResult> {
|
|
377
|
+
if (routes.length === 0) {
|
|
378
|
+
throw new Error("renderRoute: `routes` must contain at least one entry");
|
|
379
|
+
}
|
|
380
|
+
if ("initialUrl" in options) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
"renderRoute: the `initialUrl` option was renamed to `request`. " +
|
|
383
|
+
"Pass { request: <Request | url> } instead.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { render, act } = await import("@testing-library/react");
|
|
388
|
+
|
|
389
|
+
const leaf = routes[routes.length - 1];
|
|
390
|
+
const requestUrl =
|
|
391
|
+
options.request instanceof Request ? options.request.url : options.request;
|
|
392
|
+
const initialUrl = requestUrl ?? staticPrefix(leaf.path) ?? "/";
|
|
393
|
+
const url = new URL(initialUrl, TEST_ORIGIN);
|
|
394
|
+
|
|
395
|
+
const loaderData: Record<string, unknown> = { ...(options.loaderData ?? {}) };
|
|
396
|
+
for (const [loader, data] of options.loaders ?? []) {
|
|
397
|
+
loaderData[ensureSyntheticId(loader as object, "$$id")] = data;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (typeof window !== "undefined") {
|
|
401
|
+
const stateObj: Record<string, unknown> = {};
|
|
402
|
+
for (const [def, value] of options.locationState ?? []) {
|
|
403
|
+
stateObj[ensureSyntheticId(def as object, "__rsc_ls_key")] = value;
|
|
404
|
+
}
|
|
405
|
+
window.history.replaceState(stateObj, "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const resolve = (pathname: string): ResolvedMatch => {
|
|
409
|
+
const matched = matchLeaf(leaf.path, pathname) ?? {};
|
|
410
|
+
return {
|
|
411
|
+
params: { ...matched, ...(options.params ?? {}) },
|
|
412
|
+
pathname,
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
const initialMatch = resolve(url.pathname);
|
|
416
|
+
|
|
417
|
+
const historyKey = generateHistoryKey(url.href);
|
|
418
|
+
const mount = normalizeBasename(options.mount);
|
|
419
|
+
// Fail loud on a request that cannot resolve the leaf route (a typo, or the
|
|
420
|
+
// mount-prefixed-vs-relative confusion) instead of silently rendering empty
|
|
421
|
+
// params (matchLeaf -> null -> {}). renderRoute paths are include-RELATIVE and
|
|
422
|
+
// resolve() matches the request against the leaf as-is, so the request must be
|
|
423
|
+
// the relative form — a mount does NOT rewrite it. Only checked when `request`
|
|
424
|
+
// was passed explicitly (a defaulted request is staticPrefix of the leaf and
|
|
425
|
+
// always matches). Skipped when explicit `params` are supplied: those are
|
|
426
|
+
// merged over the URL-extracted params in resolve(), so the request is
|
|
427
|
+
// intentionally not the param source and an empty matchLeaf is not the trap.
|
|
428
|
+
// Gated on the test runner so it can never affect production.
|
|
429
|
+
if (
|
|
430
|
+
options.request !== undefined &&
|
|
431
|
+
Object.keys(options.params ?? {}).length === 0 &&
|
|
432
|
+
isUnderTestRunner() &&
|
|
433
|
+
matchLeaf(leaf.path, url.pathname) === null
|
|
434
|
+
) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`renderRoute: request "${url.pathname}" does not match the leaf route ` +
|
|
437
|
+
`"${leaf.path}"${mount ? ` (mount "${mount}")` : ""}. renderRoute paths ` +
|
|
438
|
+
`are include-RELATIVE: pass a request that matches "${leaf.path}" ` +
|
|
439
|
+
`(e.g. "${staticPrefix(leaf.path)}"). A mount does NOT auto-rewrite the ` +
|
|
440
|
+
`request — pass the relative path, not the mount-prefixed one. If the ` +
|
|
441
|
+
`request URL intentionally does not carry the params, pass them ` +
|
|
442
|
+
`explicitly via the \`params\` option to bypass this check.`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const initialSegments = buildSegments(
|
|
446
|
+
routes,
|
|
447
|
+
initialMatch.params,
|
|
448
|
+
loaderData,
|
|
449
|
+
mount,
|
|
450
|
+
);
|
|
451
|
+
const store = createNavigationStore({
|
|
452
|
+
initialLocation: { href: url.href },
|
|
453
|
+
initialSegmentIds: initialSegments.map((s) => s.id),
|
|
454
|
+
initialHistoryKey: historyKey,
|
|
455
|
+
initialSegments,
|
|
456
|
+
crossTabSync: false,
|
|
457
|
+
});
|
|
458
|
+
const leafRouteSegmentId =
|
|
459
|
+
[...initialSegments].reverse().find((s) => s.type === "route")?.id ??
|
|
460
|
+
initialSegments[initialSegments.length - 1]?.id;
|
|
461
|
+
const handleSeed: HandleDataSeed = cloneHandleSeed(options.handle);
|
|
462
|
+
for (const [handle, values] of options.handles ?? []) {
|
|
463
|
+
if (leafRouteSegmentId === undefined) continue;
|
|
464
|
+
const id = (handle as unknown as { $$id: string }).$$id;
|
|
465
|
+
(handleSeed[id] ??= {})[leafRouteSegmentId] = values;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const eventController = createEventController({ initialLocation: url });
|
|
469
|
+
eventController.setParams(initialMatch.params);
|
|
470
|
+
eventController.setHandleData(
|
|
471
|
+
handleSeed,
|
|
472
|
+
initialSegments.map((s) => s.id),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
let warnedNavLifecycle = false;
|
|
476
|
+
const navigate = async (target: string): Promise<void> => {
|
|
477
|
+
// renderRoute commits navigations synchronously (no server fetch, no Flight
|
|
478
|
+
// stream), so it never drives the navigation lifecycle. The transition state
|
|
479
|
+
// useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting a
|
|
480
|
+
// pending/loading/submitting state here proves nothing. Warn once (per render)
|
|
481
|
+
// under the test runner so that false-confidence trap is loud, not silent.
|
|
482
|
+
if (isUnderTestRunner() && !warnedNavLifecycle) {
|
|
483
|
+
warnedNavLifecycle = true;
|
|
484
|
+
console.warn(
|
|
485
|
+
"renderRoute: navigate()/useRouter().push commit synchronously and do " +
|
|
486
|
+
"NOT drive the navigation lifecycle. useNavigation().state, " +
|
|
487
|
+
'useLinkStatus().pending, and useAction().state stay "idle" here. ' +
|
|
488
|
+
"Assert params/pathname/content after navigate(); use renderServerTree " +
|
|
489
|
+
"or e2e to assert pending/loading/submitting transition states.",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const nextUrl = new URL(target, TEST_ORIGIN);
|
|
493
|
+
const match = resolve(nextUrl.pathname);
|
|
494
|
+
const segments = buildSegments(routes, match.params, loaderData, mount);
|
|
495
|
+
const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
|
|
496
|
+
const root = await renderSegments(segments);
|
|
497
|
+
eventController.setLocation(nextUrl);
|
|
498
|
+
eventController.setParams(match.params);
|
|
499
|
+
store.setCurrentUrl(nextUrl.href);
|
|
500
|
+
store.setSegmentIds(segments.map((s) => s.id));
|
|
501
|
+
await act(async () => {
|
|
502
|
+
store.emitUpdate({ root, metadata });
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const bridge: NavigationBridge = {
|
|
507
|
+
navigate: (target) => navigate(target),
|
|
508
|
+
refresh: () => navigate(url.pathname + url.search),
|
|
509
|
+
handlePopstate: async () => {},
|
|
510
|
+
registerLinkInterception: () => () => {},
|
|
511
|
+
getVersion: () => undefined,
|
|
512
|
+
updateVersion: () => {},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const initialMetadata = makeMetadata(
|
|
516
|
+
url.pathname,
|
|
517
|
+
initialSegments,
|
|
518
|
+
initialMatch.params,
|
|
519
|
+
);
|
|
520
|
+
const initialTree = await renderSegments(initialSegments);
|
|
521
|
+
|
|
522
|
+
// Wrap render in an awaited async act so a tree that suspends (async loaders,
|
|
523
|
+
// loading states, deferred handle entries that arrive as a Promise) settles its
|
|
524
|
+
// Suspense within act — otherwise React orphans the resolution ("a component
|
|
525
|
+
// suspended inside an act scope, but the act call was not awaited") and the
|
|
526
|
+
// resolved content never reaches the asserted DOM.
|
|
527
|
+
let result!: Awaited<ReturnType<typeof render>>;
|
|
528
|
+
await act(async () => {
|
|
529
|
+
result = render(
|
|
530
|
+
<NavigationProvider
|
|
531
|
+
store={store}
|
|
532
|
+
eventController={eventController}
|
|
533
|
+
initialPayload={{ root: initialTree, metadata: initialMetadata }}
|
|
534
|
+
bridge={bridge}
|
|
535
|
+
basename={normalizeBasename(options.basename)}
|
|
536
|
+
themeConfig={
|
|
537
|
+
options.theme === undefined ? null : resolveThemeConfig(options.theme)
|
|
538
|
+
}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const router: TestRouterHandle = {
|
|
544
|
+
navigate,
|
|
545
|
+
pathname: () => new URL(eventController.getLocation().href).pathname,
|
|
546
|
+
params: () => eventController.getParams(),
|
|
547
|
+
store,
|
|
548
|
+
eventController,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return Object.assign(result, { router });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function makeMetadata(
|
|
555
|
+
pathname: string,
|
|
556
|
+
segments: ResolvedSegment[],
|
|
557
|
+
params: Record<string, string>,
|
|
558
|
+
): RscMetadata {
|
|
559
|
+
return {
|
|
560
|
+
pathname,
|
|
561
|
+
segments,
|
|
562
|
+
params,
|
|
563
|
+
matched: segments.map((s) => s.id),
|
|
564
|
+
isPartial: false,
|
|
565
|
+
};
|
|
566
|
+
}
|