@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,188 @@
1
+ // Consumer-facing server-lifecycle fixture. Parameterized lift of the internal
2
+ // e2e/fixture.ts: the test-app-specific shared-server reuse and default-root
3
+ // build skipping are removed; a consumer points `root` at their own app.
4
+
5
+ import { rm } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import type { TestType } from "@playwright/test";
8
+ import {
9
+ createIsolatedViteCacheDir,
10
+ runCli,
11
+ type RunCliHandle,
12
+ type SpawnOptions,
13
+ waitForReady,
14
+ warmupDevServer,
15
+ } from "./server.js";
16
+
17
+ export interface FixtureOptions {
18
+ /** Absolute or cwd-relative path to the consumer app under test. */
19
+ root: string;
20
+ /**
21
+ * Server mode. Required: omitting it would spawn no server in beforeAll and
22
+ * surface only as a bare "Invalid URL" from `url()` at first use, so we fail
23
+ * eagerly at useFixture time instead.
24
+ */
25
+ mode: "dev" | "build";
26
+ /** Override the server command (default: `pnpm dev` for dev, `pnpm preview` for build). */
27
+ command?: string;
28
+ /** Override the build command (default: `pnpm build`). */
29
+ buildCommand?: string;
30
+ cliOptions?: SpawnOptions;
31
+ /** Spawn a per-suite server with an isolated Vite cache dir (dev only warmup). */
32
+ isolatedServer?: boolean;
33
+ /** Path to poll for readiness (default: "/"). Use when a basename moves routes off "/". */
34
+ readyPath?: string;
35
+ /** Skip the production build step (assumes an existing build). */
36
+ skipBuild?: boolean;
37
+ }
38
+
39
+ export interface Fixture {
40
+ mode: "dev" | "build";
41
+ root: string;
42
+ /** Resolve a path against the running server's base URL. */
43
+ url: (url?: string) => string;
44
+ /** The underlying spawned process handle (undefined before beforeAll). */
45
+ proc: () => RunCliHandle | undefined;
46
+ }
47
+
48
+ /**
49
+ * Build a `useFixture` bound to a consumer-provided Playwright `test` object.
50
+ * The returned function registers `beforeAll`/`afterAll` hooks that spawn and
51
+ * tear down a dev or preview server for the app at `options.root`.
52
+ */
53
+ export function createUseFixture(
54
+ test: TestType<any, any>,
55
+ ): (options: FixtureOptions) => Fixture {
56
+ return function useFixture(options: FixtureOptions): Fixture {
57
+ if (options.mode !== "dev" && options.mode !== "build") {
58
+ throw new Error(
59
+ `useFixture: mode is required: 'dev' | 'build' (got ${JSON.stringify(options.mode)}).`,
60
+ );
61
+ }
62
+ let cleanup: (() => Promise<void>) | undefined;
63
+ let baseURL!: string;
64
+
65
+ const cwd = path.resolve(options.root);
66
+ let proc: RunCliHandle | undefined;
67
+ let isolatedViteCacheDir: string | undefined;
68
+
69
+ test.beforeAll(async ({}, testInfo) => {
70
+ if (options.isolatedServer) {
71
+ isolatedViteCacheDir = createIsolatedViteCacheDir(
72
+ cwd,
73
+ testInfo.project.name,
74
+ options.mode,
75
+ );
76
+ }
77
+ const cliEnv = {
78
+ ...options.cliOptions?.env,
79
+ ...(isolatedViteCacheDir
80
+ ? { RANGO_E2E_VITE_CACHE_DIR: isolatedViteCacheDir }
81
+ : {}),
82
+ };
83
+
84
+ if (options.mode === "dev") {
85
+ proc = runCli({
86
+ command: options.command ?? `pnpm dev`,
87
+ label: `${options.root}:dev`,
88
+ cwd,
89
+ ...options.cliOptions,
90
+ env: cliEnv,
91
+ });
92
+ // Assign cleanup immediately after spawn, before any await that can
93
+ // throw (findPort/waitForReady). Otherwise a beforeAll failure leaves
94
+ // afterAll's cleanup a no-op and orphans the dev server (plus workerd
95
+ // children) for the rest of the run.
96
+ cleanup = async () => {
97
+ proc!.kill();
98
+ await proc!.done;
99
+ };
100
+ const port = await proc.findPort();
101
+ baseURL = `http://localhost:${port}`;
102
+ const readyUrl = options.readyPath
103
+ ? `${baseURL}${options.readyPath}`
104
+ : baseURL;
105
+ await waitForReady(readyUrl, () => ({
106
+ stdout: proc!.stdout(),
107
+ stderr: proc!.stderr(),
108
+ }));
109
+ // Isolated dev servers need a warmup SSR request to trigger Vite's
110
+ // dep optimizer before real tests run. The first full SSR request
111
+ // discovers deps -> `ERR_OUTDATED_OPTIMIZED_DEP` -> module
112
+ // re-evaluation -> loss of in-memory cache. Without this, cache
113
+ // tests see different values on the second request.
114
+ if (options.isolatedServer) {
115
+ await warmupDevServer(readyUrl);
116
+ }
117
+ }
118
+
119
+ if (options.mode === "build") {
120
+ const skipBuild = options.skipBuild || !!process.env.TEST_SKIP_BUILD;
121
+ if (!skipBuild) {
122
+ const buildProc = runCli({
123
+ command: options.buildCommand ?? `pnpm build`,
124
+ label: `${options.root}:build`,
125
+ cwd,
126
+ ...options.cliOptions,
127
+ env: cliEnv,
128
+ });
129
+ // The build is a finite process, but assign cleanup before the await
130
+ // so a hung build is still killed if beforeAll is torn down.
131
+ cleanup = async () => {
132
+ buildProc.kill();
133
+ await buildProc.done;
134
+ };
135
+ // Fail loudly on a nonzero build exit. Without this a failed build is
136
+ // swallowed and preview serves the previous stale dist/ (green
137
+ // production tests against old code), or times out with "Server not
138
+ // ready" that never mentions the build failure. The captured build
139
+ // output (stdout/stderr above) precedes this throw.
140
+ const code = await buildProc.exitCode;
141
+ if (code !== 0) {
142
+ throw new Error(
143
+ `Build failed with exit code ${code} for "${options.root}" ` +
144
+ `(command: ${options.buildCommand ?? "pnpm build"}). ` +
145
+ `See the captured build output above for the cause.`,
146
+ );
147
+ }
148
+ }
149
+ proc = runCli({
150
+ command: options.command ?? `pnpm preview`,
151
+ label: `${options.root}:preview`,
152
+ cwd,
153
+ ...options.cliOptions,
154
+ env: cliEnv,
155
+ });
156
+ // Switch cleanup to the preview process immediately after spawn, before
157
+ // findPort/waitForReady can throw and orphan it.
158
+ cleanup = async () => {
159
+ proc!.kill();
160
+ await proc!.done;
161
+ };
162
+ const port = await proc.findPort();
163
+ baseURL = `http://localhost:${port}`;
164
+ const buildReadyUrl = options.readyPath
165
+ ? `${baseURL}${options.readyPath}`
166
+ : baseURL;
167
+ await waitForReady(buildReadyUrl, () => ({
168
+ stdout: proc!.stdout(),
169
+ stderr: proc!.stderr(),
170
+ }));
171
+ }
172
+ });
173
+
174
+ test.afterAll(async () => {
175
+ await cleanup?.();
176
+ if (isolatedViteCacheDir) {
177
+ await rm(isolatedViteCacheDir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ return {
182
+ mode: options.mode,
183
+ root: cwd,
184
+ url: (url: string = "./") => new URL(url, baseURL).href,
185
+ proc: () => proc,
186
+ };
187
+ };
188
+ }
@@ -0,0 +1,149 @@
1
+ // Public entry for the consumer e2e harness. `createRangoE2E({ test, expect })`
2
+ // wires the server fixture, page helpers, parity helpers, and matchers around
3
+ // the consumer's Playwright `test`/`expect` objects so this module never
4
+ // imports `@playwright/test` at runtime (type-only imports are erased).
5
+
6
+ import type { Expect, TestType } from "@playwright/test";
7
+ import {
8
+ createUseFixture,
9
+ type Fixture,
10
+ type FixtureOptions,
11
+ } from "./fixture.js";
12
+ import {
13
+ createPageHelpers,
14
+ createStopwatch,
15
+ getHistoryState,
16
+ getNumericContent,
17
+ goBack,
18
+ goForward,
19
+ isVisibleInViewport,
20
+ measureTime,
21
+ type PageHelpers,
22
+ parseNumber,
23
+ type Stopwatch,
24
+ testId,
25
+ waitForElement,
26
+ waitForHydration,
27
+ waitForNavigation,
28
+ } from "./page-helpers.js";
29
+ import {
30
+ createParity,
31
+ type ExpectParityOptions,
32
+ type Parity,
33
+ type ParityDescribeOptions,
34
+ type ParityIntent,
35
+ } from "./parity.js";
36
+ import { createRangoMatchers, type RangoMatchers } from "./matchers.js";
37
+
38
+ // Cache-status helpers are pure (cache-status.ts imports only TYPES), so they
39
+ // are safe to surface from this Playwright-runnable entry. Importing them from
40
+ // the `@rangojs/router/testing` barrel does NOT work in a plain Playwright
41
+ // runner — the barrel transitively pulls the build-only `@rangojs/router:version`
42
+ // virtual via the route-manifest path. Asserting cache status on a real
43
+ // response is an e2e activity, so this is their Playwright-safe home.
44
+ export {
45
+ assertCacheStatus,
46
+ parseCacheHeader,
47
+ createCacheSink,
48
+ filterCacheDecisions,
49
+ type CacheSink,
50
+ type ExpectedCacheStatus,
51
+ type CacheStatusTarget,
52
+ } from "../cache-status.js";
53
+
54
+ // Re-export standalone helpers and all public types so the barrel can re-export
55
+ // them from a single module.
56
+ export {
57
+ testId,
58
+ waitForHydration,
59
+ waitForNavigation,
60
+ goBack,
61
+ goForward,
62
+ getHistoryState,
63
+ waitForElement,
64
+ isVisibleInViewport,
65
+ parseNumber,
66
+ getNumericContent,
67
+ createStopwatch,
68
+ measureTime,
69
+ createPageHelpers,
70
+ createUseFixture,
71
+ createParity,
72
+ createRangoMatchers,
73
+ };
74
+ export type {
75
+ Fixture,
76
+ FixtureOptions,
77
+ PageHelpers,
78
+ Stopwatch,
79
+ Parity,
80
+ ParityIntent,
81
+ ParityDescribeOptions,
82
+ ExpectParityOptions,
83
+ RangoMatchers,
84
+ };
85
+
86
+ export interface RangoE2E extends PageHelpers, Parity {
87
+ useFixture: (options: FixtureOptions) => Fixture;
88
+ testNoJs: TestType<any, any>;
89
+ rangoMatchers: RangoMatchers;
90
+ // Standalone helpers, re-surfaced for convenience.
91
+ testId: typeof testId;
92
+ waitForHydration: typeof waitForHydration;
93
+ waitForNavigation: typeof waitForNavigation;
94
+ goBack: typeof goBack;
95
+ goForward: typeof goForward;
96
+ getHistoryState: typeof getHistoryState;
97
+ waitForElement: typeof waitForElement;
98
+ isVisibleInViewport: typeof isVisibleInViewport;
99
+ parseNumber: typeof parseNumber;
100
+ getNumericContent: typeof getNumericContent;
101
+ createStopwatch: typeof createStopwatch;
102
+ measureTime: typeof measureTime;
103
+ }
104
+
105
+ /**
106
+ * Wire the full e2e harness around a consumer's Playwright `test`/`expect`.
107
+ *
108
+ * @param defaultRoot - fallback app root for `parityDescribe` when a call omits
109
+ * `options.root`.
110
+ */
111
+ export function createRangoE2E({
112
+ test,
113
+ expect,
114
+ defaultRoot,
115
+ }: {
116
+ test: TestType<any, any>;
117
+ expect: Expect;
118
+ defaultRoot?: string;
119
+ }): RangoE2E {
120
+ const useFixture = createUseFixture(test);
121
+ const pageHelpers = createPageHelpers(expect);
122
+ const parity = createParity({ test, expect, useFixture, defaultRoot });
123
+ const rangoMatchers = createRangoMatchers(expect);
124
+ const testNoJs = test.extend({
125
+ javaScriptEnabled: ({}, use: (value: boolean) => Promise<void>) =>
126
+ use(false),
127
+ });
128
+
129
+ return {
130
+ useFixture,
131
+ testNoJs,
132
+ rangoMatchers,
133
+ ...parity,
134
+ ...pageHelpers,
135
+ // Standalone helpers.
136
+ testId,
137
+ waitForHydration,
138
+ waitForNavigation,
139
+ goBack,
140
+ goForward,
141
+ getHistoryState,
142
+ waitForElement,
143
+ isVisibleInViewport,
144
+ parseNumber,
145
+ getNumericContent,
146
+ createStopwatch,
147
+ measureTime,
148
+ };
149
+ }
@@ -0,0 +1,51 @@
1
+ // Custom Playwright matchers for Rango assertions. Returned as an object
2
+ // suitable for `expect.extend(...)`. v1 ships only `toHaveRangoPathname`.
3
+
4
+ import type { Expect, Page } from "@playwright/test";
5
+
6
+ interface MatcherResult {
7
+ pass: boolean;
8
+ message: () => string;
9
+ }
10
+
11
+ export interface RangoMatchers {
12
+ toHaveRangoPathname: (page: Page, expected: string) => MatcherResult;
13
+ }
14
+
15
+ /**
16
+ * Build the matcher object for `expect.extend(createRangoMatchers(expect))`.
17
+ *
18
+ * `toHaveRangoPathname(page, expected)` asserts that the pathname of the page's
19
+ * current URL equals `expected`.
20
+ *
21
+ * TODO: `toHaveSegments` / `toHaveParams` are intentionally not implemented.
22
+ * They require a client-emitted signal (the active segment chain / resolved
23
+ * params exposed on the page) that does not exist yet; implementing them by
24
+ * scraping the DOM would be a guess. Add them once the router emits that signal.
25
+ */
26
+ export function createRangoMatchers(_expect: Expect): RangoMatchers {
27
+ return {
28
+ toHaveRangoPathname(page: Page, expected: string): MatcherResult {
29
+ const actual = new URL(page.url()).pathname;
30
+ const pass = actual === expected;
31
+ return {
32
+ pass,
33
+ message: () =>
34
+ pass
35
+ ? `Expected pathname not to be "${expected}"`
36
+ : `Expected pathname "${expected}" but got "${actual}"`,
37
+ };
38
+ },
39
+ };
40
+ }
41
+
42
+ // Type augmentation so consumers can call `await expect(page).toHaveRangoPathname("/x")`
43
+ // after `expect.extend(rangoMatchers)`, without re-declaring the matcher.
44
+ declare global {
45
+ // eslint-disable-next-line @typescript-eslint/no-namespace
46
+ namespace PlaywrightTest {
47
+ interface Matchers<R, T> {
48
+ toHaveRangoPathname(expected: string): R;
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,272 @@
1
+ // Page helpers for the consumer e2e harness. Type-only Playwright imports keep
2
+ // this module loadable in a plain-node process. Helpers needing assertions are
3
+ // returned from `createPageHelpers(expect)`; the rest are standalone.
4
+
5
+ import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
6
+
7
+ // ============================================================================
8
+ // Standalone helpers (need only a page/locator)
9
+ // ============================================================================
10
+
11
+ /** Get a locator by data-testid attribute. */
12
+ export function testId(page: Page, id: string): Locator {
13
+ return page.locator(`[data-testid="${id}"]`);
14
+ }
15
+
16
+ /**
17
+ * Wait for React hydration to complete and verify no hydration errors.
18
+ * Rango sets `data-hydrated` on `<html>` after hydration via useEffect; this
19
+ * is a stable readiness signal that does not rely on React internals.
20
+ */
21
+ export async function waitForHydration(page: Page): Promise<void> {
22
+ const hydrationErrors: string[] = [];
23
+
24
+ const isHydrationError = (text: string) =>
25
+ text.includes("Hydration failed") ||
26
+ text.includes("hydration mismatch") ||
27
+ text.includes("Text content does not match") ||
28
+ text.includes("did not match") ||
29
+ text.includes("server rendered HTML") ||
30
+ text.includes("Hydration error");
31
+
32
+ const consoleHandler = (msg: ConsoleMessage) => {
33
+ const text = msg.text();
34
+ if (isHydrationError(text)) hydrationErrors.push(text);
35
+ };
36
+
37
+ // React throws hydration errors as page errors.
38
+ const pageErrorHandler = (error: Error) => {
39
+ if (isHydrationError(error.message)) hydrationErrors.push(error.message);
40
+ };
41
+
42
+ page.on("console", consoleHandler);
43
+ page.on("pageerror", pageErrorHandler);
44
+
45
+ try {
46
+ await page.waitForLoadState("domcontentloaded");
47
+ await page.waitForFunction(
48
+ () => document.documentElement.hasAttribute("data-hydrated"),
49
+ { timeout: 20000 },
50
+ );
51
+ // Small delay to catch any async hydration errors.
52
+ await page.waitForTimeout(100);
53
+ if (hydrationErrors.length > 0) {
54
+ throw new Error(
55
+ `Hydration errors detected:\n${hydrationErrors.join("\n")}`,
56
+ );
57
+ }
58
+ } finally {
59
+ page.off("console", consoleHandler);
60
+ page.off("pageerror", pageErrorHandler);
61
+ }
62
+ }
63
+
64
+ /** Wait for navigation to complete (URL change + network idle). */
65
+ export async function waitForNavigation(
66
+ page: Page,
67
+ expectedUrl: string | RegExp,
68
+ ): Promise<void> {
69
+ await page.waitForURL(expectedUrl, { waitUntil: "networkidle" });
70
+ }
71
+
72
+ /** Navigate back and wait for navigation to complete. */
73
+ export async function goBack(page: Page): Promise<void> {
74
+ // Wait for the URL to actually CHANGE. A `/.*/ ` pattern matches the current
75
+ // URL immediately, so it would resolve before the history navigation happened.
76
+ const from = page.url();
77
+ await Promise.all([
78
+ page.waitForURL((url) => url.toString() !== from, {
79
+ waitUntil: "networkidle",
80
+ }),
81
+ page.goBack(),
82
+ ]);
83
+ }
84
+
85
+ /** Navigate forward and wait for navigation to complete. */
86
+ export async function goForward(page: Page): Promise<void> {
87
+ const from = page.url();
88
+ await Promise.all([
89
+ page.waitForURL((url) => url.toString() !== from, {
90
+ waitUntil: "networkidle",
91
+ }),
92
+ page.goForward(),
93
+ ]);
94
+ }
95
+
96
+ /** Get the current history state. */
97
+ export async function getHistoryState(page: Page): Promise<unknown> {
98
+ return page.evaluate(() => window.history.state);
99
+ }
100
+
101
+ /** Wait for an element to appear and be visible. */
102
+ export async function waitForElement(
103
+ page: Page,
104
+ selector: string,
105
+ timeout = 10000,
106
+ ): Promise<void> {
107
+ await page.locator(selector).waitFor({ state: "visible", timeout });
108
+ }
109
+
110
+ /** Check if an element is visible. */
111
+ export async function isVisibleInViewport(
112
+ page: Page,
113
+ selector: string,
114
+ ): Promise<boolean> {
115
+ return page.locator(selector).isVisible();
116
+ }
117
+
118
+ /**
119
+ * Parse a number from text content (finds first number in string).
120
+ *
121
+ * @example
122
+ * parseNumber("Load count: 42") // 42
123
+ * parseNumber(null) // 0
124
+ */
125
+ export function parseNumber(text: string | null | undefined): number {
126
+ return parseInt(text?.match(/\d+/)?.[0] || "0", 10);
127
+ }
128
+
129
+ /** Get the numeric value from an element's text content. */
130
+ export async function getNumericContent(locator: Locator): Promise<number> {
131
+ const text = await locator.textContent();
132
+ return parseNumber(text);
133
+ }
134
+
135
+ export interface Stopwatch {
136
+ elapsed: () => number;
137
+ checkpoint: () => number;
138
+ }
139
+
140
+ /** Create a stopwatch for timing multiple checkpoints. */
141
+ export function createStopwatch(): Stopwatch {
142
+ const startTime = Date.now();
143
+ return {
144
+ elapsed: () => Date.now() - startTime,
145
+ checkpoint: () => Date.now() - startTime,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Measure elapsed time for an async operation. Returns `{ elapsed, result }`
151
+ * where `elapsed` is in milliseconds.
152
+ */
153
+ export async function measureTime<T>(
154
+ fn: () => Promise<T>,
155
+ ): Promise<{ elapsed: number; result: T }> {
156
+ const startTime = Date.now();
157
+ const result = await fn();
158
+ const elapsed = Date.now() - startTime;
159
+ return { elapsed, result };
160
+ }
161
+
162
+ // ============================================================================
163
+ // Factory-bound helpers (need the injected `expect`)
164
+ // ============================================================================
165
+
166
+ export interface PageHelpers {
167
+ /** Assert that no full page reload occurred between scope entry and exit. */
168
+ expectNoReload: (page: Page) => AsyncDisposable;
169
+ /** Collect page errors and assert none occurred on scope exit. */
170
+ expectNoPageError: (page: Page) => Disposable;
171
+ /** Wait for an element's text to change from its initial value. */
172
+ waitForTextChange: (
173
+ locator: Locator,
174
+ initialText: string,
175
+ timeout?: number,
176
+ ) => Promise<void>;
177
+ /** Wait for a numeric value in an element to change. */
178
+ waitForNumericChange: (
179
+ locator: Locator,
180
+ initialValue: number,
181
+ timeout?: number,
182
+ ) => Promise<void>;
183
+ /** Assert timing is within `expected +/- tolerance`. */
184
+ expectTiming: (elapsed: number, expected: number, tolerance: number) => void;
185
+ /** Assert timing is at least `minimum`. */
186
+ expectMinTiming: (elapsed: number, minimum: number) => void;
187
+ /** Assert timing is less than `maximum`. */
188
+ expectMaxTiming: (elapsed: number, maximum: number) => void;
189
+ }
190
+
191
+ export function createPageHelpers(expect: Expect): PageHelpers {
192
+ function expectNoReload(page: Page): AsyncDisposable {
193
+ const setup = page.evaluate(() => {
194
+ const el = document.createElement("meta");
195
+ el.setAttribute("name", "x-reload-check");
196
+ document.head.append(el);
197
+ });
198
+ return {
199
+ [Symbol.asyncDispose]: async () => {
200
+ await setup;
201
+ // Keep the timeout small so a real reload is reported quickly, but not
202
+ // so small that a busy page produces a spurious failure (which would
203
+ // mask the real test error as a SuppressedError under `await using`).
204
+ await expect(page.locator(`meta[name="x-reload-check"]`)).toBeAttached({
205
+ timeout: 100,
206
+ });
207
+ await page.evaluate(() => {
208
+ document.querySelector(`meta[name="x-reload-check"]`)!.remove();
209
+ });
210
+ },
211
+ };
212
+ }
213
+
214
+ function expectNoPageError(page: Page): Disposable {
215
+ const errors: Error[] = [];
216
+ page.on("pageerror", (error) => {
217
+ errors.push(error);
218
+ });
219
+ return {
220
+ [Symbol.dispose]: () => {
221
+ expect(errors).toEqual([]);
222
+ },
223
+ };
224
+ }
225
+
226
+ async function waitForTextChange(
227
+ locator: Locator,
228
+ initialText: string,
229
+ timeout = 10000,
230
+ ): Promise<void> {
231
+ await expect
232
+ .poll(async () => await locator.textContent(), { timeout })
233
+ .not.toBe(initialText);
234
+ }
235
+
236
+ async function waitForNumericChange(
237
+ locator: Locator,
238
+ initialValue: number,
239
+ timeout = 10000,
240
+ ): Promise<void> {
241
+ await expect
242
+ .poll(async () => parseNumber(await locator.textContent()), { timeout })
243
+ .not.toBe(initialValue);
244
+ }
245
+
246
+ function expectTiming(
247
+ elapsed: number,
248
+ expected: number,
249
+ tolerance: number,
250
+ ): void {
251
+ expect(elapsed).toBeGreaterThan(expected - tolerance);
252
+ expect(elapsed).toBeLessThan(expected + tolerance);
253
+ }
254
+
255
+ function expectMinTiming(elapsed: number, minimum: number): void {
256
+ expect(elapsed).toBeGreaterThan(minimum);
257
+ }
258
+
259
+ function expectMaxTiming(elapsed: number, maximum: number): void {
260
+ expect(elapsed).toBeLessThan(maximum);
261
+ }
262
+
263
+ return {
264
+ expectNoReload,
265
+ expectNoPageError,
266
+ waitForTextChange,
267
+ waitForNumericChange,
268
+ expectTiming,
269
+ expectMinTiming,
270
+ expectMaxTiming,
271
+ };
272
+ }