@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17
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/AGENTS.md +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- 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 +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- 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 +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- 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 +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- 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 +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- 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 +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -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/taint.ts +55 -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 +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- 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 +70 -22
- 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 +52 -26
- package/src/index.ts +100 -38
- 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-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +63 -24
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +2 -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 +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +338 -126
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- 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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- 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 +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- 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,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,128 @@
|
|
|
1
|
+
import type { Expect, TestType } from "@playwright/test";
|
|
2
|
+
import {
|
|
3
|
+
createUseFixture,
|
|
4
|
+
type Fixture,
|
|
5
|
+
type FixtureOptions,
|
|
6
|
+
} from "./fixture.js";
|
|
7
|
+
import {
|
|
8
|
+
createPageHelpers,
|
|
9
|
+
createStopwatch,
|
|
10
|
+
getHistoryState,
|
|
11
|
+
getNumericContent,
|
|
12
|
+
goBack,
|
|
13
|
+
goForward,
|
|
14
|
+
isVisibleInViewport,
|
|
15
|
+
measureTime,
|
|
16
|
+
type PageHelpers,
|
|
17
|
+
parseNumber,
|
|
18
|
+
type Stopwatch,
|
|
19
|
+
testId,
|
|
20
|
+
waitForElement,
|
|
21
|
+
waitForHydration,
|
|
22
|
+
waitForNavigation,
|
|
23
|
+
} from "./page-helpers.js";
|
|
24
|
+
import {
|
|
25
|
+
createParity,
|
|
26
|
+
type ExpectParityOptions,
|
|
27
|
+
type Parity,
|
|
28
|
+
type ParityDescribeOptions,
|
|
29
|
+
type ParityIntent,
|
|
30
|
+
} from "./parity.js";
|
|
31
|
+
import { createRangoMatchers, type RangoMatchers } from "./matchers.js";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
assertCacheStatus,
|
|
35
|
+
assertCacheDecision,
|
|
36
|
+
parseCacheHeader,
|
|
37
|
+
createCacheSink,
|
|
38
|
+
filterCacheDecisions,
|
|
39
|
+
type CacheSink,
|
|
40
|
+
type ExpectedCacheStatus,
|
|
41
|
+
type CacheStatusTarget,
|
|
42
|
+
} from "../cache-status.js";
|
|
43
|
+
export {
|
|
44
|
+
testId,
|
|
45
|
+
waitForHydration,
|
|
46
|
+
waitForNavigation,
|
|
47
|
+
goBack,
|
|
48
|
+
goForward,
|
|
49
|
+
getHistoryState,
|
|
50
|
+
waitForElement,
|
|
51
|
+
isVisibleInViewport,
|
|
52
|
+
parseNumber,
|
|
53
|
+
getNumericContent,
|
|
54
|
+
createStopwatch,
|
|
55
|
+
measureTime,
|
|
56
|
+
createPageHelpers,
|
|
57
|
+
createUseFixture,
|
|
58
|
+
createParity,
|
|
59
|
+
createRangoMatchers,
|
|
60
|
+
};
|
|
61
|
+
export type {
|
|
62
|
+
Fixture,
|
|
63
|
+
FixtureOptions,
|
|
64
|
+
PageHelpers,
|
|
65
|
+
Stopwatch,
|
|
66
|
+
Parity,
|
|
67
|
+
ParityIntent,
|
|
68
|
+
ParityDescribeOptions,
|
|
69
|
+
ExpectParityOptions,
|
|
70
|
+
RangoMatchers,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export interface RangoE2E extends PageHelpers, Parity {
|
|
74
|
+
useFixture: (options: FixtureOptions) => Fixture;
|
|
75
|
+
testNoJs: TestType<any, any>;
|
|
76
|
+
rangoMatchers: RangoMatchers;
|
|
77
|
+
testId: typeof testId;
|
|
78
|
+
waitForHydration: typeof waitForHydration;
|
|
79
|
+
waitForNavigation: typeof waitForNavigation;
|
|
80
|
+
goBack: typeof goBack;
|
|
81
|
+
goForward: typeof goForward;
|
|
82
|
+
getHistoryState: typeof getHistoryState;
|
|
83
|
+
waitForElement: typeof waitForElement;
|
|
84
|
+
isVisibleInViewport: typeof isVisibleInViewport;
|
|
85
|
+
parseNumber: typeof parseNumber;
|
|
86
|
+
getNumericContent: typeof getNumericContent;
|
|
87
|
+
createStopwatch: typeof createStopwatch;
|
|
88
|
+
measureTime: typeof measureTime;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createRangoE2E({
|
|
92
|
+
test,
|
|
93
|
+
expect,
|
|
94
|
+
defaultRoot,
|
|
95
|
+
}: {
|
|
96
|
+
test: TestType<any, any>;
|
|
97
|
+
expect: Expect;
|
|
98
|
+
defaultRoot?: string;
|
|
99
|
+
}): RangoE2E {
|
|
100
|
+
const useFixture = createUseFixture(test);
|
|
101
|
+
const pageHelpers = createPageHelpers(expect);
|
|
102
|
+
const parity = createParity({ test, expect, useFixture, defaultRoot });
|
|
103
|
+
const rangoMatchers = createRangoMatchers(expect);
|
|
104
|
+
const testNoJs = test.extend({
|
|
105
|
+
javaScriptEnabled: ({}, use: (value: boolean) => Promise<void>) =>
|
|
106
|
+
use(false),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
useFixture,
|
|
111
|
+
testNoJs,
|
|
112
|
+
rangoMatchers,
|
|
113
|
+
...parity,
|
|
114
|
+
...pageHelpers,
|
|
115
|
+
testId,
|
|
116
|
+
waitForHydration,
|
|
117
|
+
waitForNavigation,
|
|
118
|
+
goBack,
|
|
119
|
+
goForward,
|
|
120
|
+
getHistoryState,
|
|
121
|
+
waitForElement,
|
|
122
|
+
isVisibleInViewport,
|
|
123
|
+
parseNumber,
|
|
124
|
+
getNumericContent,
|
|
125
|
+
createStopwatch,
|
|
126
|
+
measureTime,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Expect, Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
interface MatcherResult {
|
|
4
|
+
pass: boolean;
|
|
5
|
+
message: () => string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RangoMatchers {
|
|
9
|
+
toHaveRangoPathname: (page: Page, expected: string) => MatcherResult;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRangoMatchers(_expect: Expect): RangoMatchers {
|
|
13
|
+
return {
|
|
14
|
+
toHaveRangoPathname(page: Page, expected: string): MatcherResult {
|
|
15
|
+
const actual = new URL(page.url()).pathname;
|
|
16
|
+
const pass = actual === expected;
|
|
17
|
+
return {
|
|
18
|
+
pass,
|
|
19
|
+
message: () =>
|
|
20
|
+
pass
|
|
21
|
+
? `Expected pathname not to be "${expected}"`
|
|
22
|
+
: `Expected pathname "${expected}" but got "${actual}"`,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
30
|
+
namespace PlaywrightTest {
|
|
31
|
+
interface Matchers<R, T> {
|
|
32
|
+
toHaveRangoPathname(expected: string): R;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|