@warlock.js/core 5.35.0 → 5.36.0
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/CHANGELOG.md +25 -0
- package/esm/dev-server/module-loader.mjs +1 -1
- package/esm/dev-server/parse-imports.mjs +1 -1
- package/esm/dev-server/type-generator.mjs +1 -1
- package/esm/e2e/cdp.d.mts.map +1 -1
- package/esm/e2e/cdp.mjs +9 -2
- package/esm/e2e/cdp.mjs.map +1 -1
- package/esm/e2e/page.d.mts +9 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +45 -7
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +18 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +24 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +24 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +7 -7
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +3 -3
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +3 -1
- package/esm/localization/index.mjs +3 -1
- package/esm/localization/translation-usage-graph.d.mts +49 -0
- package/esm/localization/translation-usage-graph.d.mts.map +1 -0
- package/esm/localization/translation-usage-graph.mjs +128 -0
- package/esm/localization/translation-usage-graph.mjs.map +1 -0
- package/esm/localization/translation-usage.d.mts +85 -0
- package/esm/localization/translation-usage.d.mts.map +1 -0
- package/esm/localization/translation-usage.mjs +307 -0
- package/esm/localization/translation-usage.mjs.map +1 -0
- package/esm/production/build-budgets.d.mts +18 -0
- package/esm/production/build-budgets.d.mts.map +1 -1
- package/esm/production/build-budgets.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +5 -3
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/router/router.d.mts +2 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +4 -1
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +19 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tooling/check/api-route-references.d.mts +16 -0
- package/esm/tooling/check/api-route-references.d.mts.map +1 -0
- package/esm/tooling/check/api-route-references.mjs +146 -0
- package/esm/tooling/check/api-route-references.mjs.map +1 -0
- package/esm/tooling/check/conventions-step.d.mts +6 -3
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +19 -10
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +4 -2
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/translation-conventions.mjs +32 -0
- package/esm/tooling/check/translation-conventions.mjs.map +1 -0
- package/esm/tooling/inspect/collect-files.mjs +1 -1
- package/esm/upgrade/codemods/index.mjs +2 -0
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
- package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +4 -0
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/warlock-config/index.d.mts +2 -2
- package/llms-full.txt +127 -16
- package/package.json +13 -13
- package/skills/register-route/SKILL.md +13 -0
- package/skills/test-service/SKILL.md +8 -8
- package/skills/upgrade-from-5-29/SKILL.md +91 -0
- package/skills/use-localization/SKILL.md +4 -0
- package/skills/warlock-check/SKILL.md +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.36.0 - 2026-10-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `clientVisible` route option. Set `clientVisible: false` on a named API route to keep it out of every page's browser route table and out of client code. The route snapshot carries `clientVisible: false` only on such routes.
|
|
14
|
+
- `warlock check` reports `WL9019` (warning) for a browser route call (`apiHref()`, `useSubmitForm({ route })`) whose name cannot be read statically, and `WL9020` (error) for a browser reference to a `clientVisible: false` route.
|
|
15
|
+
- `build.budgets.payload` (`PayloadBudget` type) sets the inline payload budget for each rendered document. Web reads it; `warlock build` does not measure it.
|
|
16
|
+
- `page.measurePayload({ limit })` in the browser end-to-end kit records a document's inline payload under the evidence `payloads`.
|
|
17
|
+
- Static translation usage analysis for the client module graph: `scanTranslationSource`, `analyzeTranslationGraph`, `analyzePageTranslationUsage`, `readTranslationDeclarations`, `translationDeclarationUsage`, `isTranslationKeyUsed` and `mergeTranslationUsage`. A usage lists the exact keys (each with its subtree), the static prefixes of template keys, the key-shaped string literals and template heads found anywhere in the walked modules (`literals`, `literalPrefixes`), and whether any lookup is dynamic, with the source position of each dynamic lookup. Literal keys, module-level constants, literal choices, `useTrans()` bindings under any local name, and `config.translations` declarations are resolved; a translator passed on as a value, or a key with no static prefix, is dynamic. `isTranslationKeyUsed` also treats a name as the base of its count variants: `"cart.items"` reaches `cart.items_one` and `cart.items_other`, but not `cart.itemsTotal` or `cart.items_one.note`.
|
|
18
|
+
- `WL9104`: `warlock check` warns when a translation key in `src/web` is computed at runtime, because the page that renders it then ships every translation group. The warning points at the `config.translations` declaration that resolves it.
|
|
19
|
+
- `warlock upgrade` codemod `vite-8-vitest-5` (release 5.36.0): raises `vite`, `vitest`, `@vitest/*` and `@vitejs/plugin-react` in `package.json` (dependencies, devDependencies, `overrides`, `resolutions`), rewrites a test runner that imports `VitestTestRunner` from `vitest/runners` to `TestRunner` from `vitest`, and lists removed `vitest/*` entry points, a Vite config `esbuild` option and object `manualChunks` for review. A 5.36.0 note lists the Vitest 5 default changes.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `warlock add test` generates a runner that extends `TestRunner` from `vitest` (Vitest 5 has no `vitest/runners`) and declares `vitest` and `@vitest/coverage-v8` `^5.0.3`. `warlock add web` declares `vite` `^8.3.3` and `@vitejs/plugin-react` `^6.1.2`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The browser end-to-end kit keeps the reason a target was last observed as blocking when `page.click()` (and the `fill()` and `type()` calls that click first) or a wait runs out of time. A CDP timeout caused by the shrinking deadline no longer replaces that reason, so the error still reads, for example, "target is covered by DIALOG#modal" instead of a CDP timeout.
|
|
28
|
+
|
|
29
|
+
### Upgrading
|
|
30
|
+
|
|
31
|
+
- **BREAKING** The test kit needs Vitest 5. A runner that imports `VitestTestRunner` from `vitest/runners` fails on Vitest 5; run `warlock upgrade --from 5.35 --to 5.36.0` (codemod `vite-8-vitest-5`) or import `TestRunner` from `vitest` and call `this.onCleanupWorkerContext?.(cleanup)`. See the upgrade skill section "5.35 to 5.36 Vite 8 and Vitest 5".
|
|
32
|
+
- Vitest 5 sets `clearMocks` to true, writes `json` and `junit` reports to `.vitest/` and makes `VITEST_POOL_ID` 1-based. Review specs that rely on mock state surviving between tests.
|
|
33
|
+
|
|
9
34
|
## 5.35.0 - 2026-10-21
|
|
10
35
|
|
|
11
36
|
### Added
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { router } from "../router/router.mjs";
|
|
2
2
|
import { registerGroupedTranslationKeys, registerLocaleDictionary } from "../localization/register-locale-dictionary.mjs";
|
|
3
|
+
import { parseSourceFile } from "../tooling/syntax/ast.mjs";
|
|
3
4
|
import "../localization/index.mjs";
|
|
4
5
|
import { devLogError, formatModuleNotFoundError } from "./dev-logger.mjs";
|
|
5
|
-
import { parseSourceFile } from "../tooling/syntax/ast.mjs";
|
|
6
6
|
import { importAppModule } from "../loader/app-module-importer.mjs";
|
|
7
7
|
import { extractTranslationKeys } from "./extract-translation-keys.mjs";
|
|
8
8
|
import { warnForLegacyLocales } from "./legacy-locales-warning.mjs";
|
|
@@ -2,8 +2,8 @@ import { Path } from "../utils/normalized-path.mjs";
|
|
|
2
2
|
import { tsconfigManager } from "./tsconfig-manager.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { directoryExistsAsync, fileExistsAsync } from "@warlock.js/fs";
|
|
5
|
-
import { parse } from "es-module-lexer";
|
|
6
5
|
import { parseSync } from "oxc-parser";
|
|
6
|
+
import { parse } from "es-module-lexer";
|
|
7
7
|
|
|
8
8
|
//#region ../../packages/core/src/dev-server/parse-imports.ts
|
|
9
9
|
/**
|
|
@@ -2,11 +2,11 @@ import { Path } from "../utils/normalized-path.mjs";
|
|
|
2
2
|
import { warlockPath } from "../utils/paths.mjs";
|
|
3
3
|
import "../utils/index.mjs";
|
|
4
4
|
import { parseLocaleDictionary } from "../localization/parse-locale-dictionary.mjs";
|
|
5
|
+
import { forEachChild, hasModifier, isImportDeclaration, isInterfaceDeclaration, isObjectLiteralExpression, isPropertyAssignment, isShorthandPropertyAssignment, isTypeAliasDeclaration, isVariableDeclaration, keyName, textOf } from "../tooling/syntax/ast.mjs";
|
|
5
6
|
import "../localization/index.mjs";
|
|
6
7
|
import { devLogDim, devLogError, devLogInfo, devLogSuccess, devServeLog } from "./dev-logger.mjs";
|
|
7
8
|
import { parseImports } from "./parse-imports.mjs";
|
|
8
9
|
import { getFilesFromDirectory } from "./utils.mjs";
|
|
9
|
-
import { forEachChild, hasModifier, isImportDeclaration, isInterfaceDeclaration, isObjectLiteralExpression, isPropertyAssignment, isShorthandPropertyAssignment, isTypeAliasDeclaration, isVariableDeclaration, keyName, textOf } from "../tooling/syntax/ast.mjs";
|
|
10
10
|
import { extractTranslationKeys } from "./extract-translation-keys.mjs";
|
|
11
11
|
import { filesOrchestrator } from "./files-orchestrator.mjs";
|
|
12
12
|
import { readConfigAst } from "./read-config-ast.mjs";
|
package/esm/e2e/cdp.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdp.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"mappings":";;;;;;KAKK;EAAiB;EAAiB;;KAClC;EAAqB;EAAc,YAAY;;YACxC;EAEV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IACE;MAAU;MAAoB;MAAwB;MAAuB;;IAC7E;MAAU,QAAQ;MAAc,mBAAmB;;;EAGrD;IAAiB,QAAQ;IAAuB,QAAQ;;EAExD;IACE;MAAU;;IACV;MAAU;MAAiB;MAAmB;;;EAGhD;IAA4B;MAAU;;IAAiB;MAAU;;;EAEjE;IACE;MAAU;MAAe;MAAgB;MAA2B;;IACpE,QAAQ;;EAGV;IACE;MACE;MACA;MACA;MACA;MACA;MACA;;IAEF,QAAQ;;EAGV;IAAsB;MAAU;;IAAgB,QAAQ;;EAExD;IACE;MAAU;MAA2B;MAAa;MAAc;;IAChE,QAAQ;;EAGV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IAAgB,QAAQ;IAAuB,QAAQ;;;YAE7C;EAEV;IAAyB;;EAEzB;IAA8B;IAAc,MAAM;;EAElD;IAA6B,kBAAkB;;EAE/C;IAAoB;MAAS;MAAe;;;EAE5C;IAA+B;IAAmB;MAAW;;;EAE7D;IAA8B;IAAmB;MAAY;MAAa;;;EAE1E;IAA2B;IAAmB;IAAmB;;EAEjE;IAA6B;;;
|
|
1
|
+
{"version":3,"file":"cdp.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"mappings":";;;;;;KAKK;EAAiB;EAAiB;;KAClC;EAAqB;EAAc,YAAY;;YACxC;EAEV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IACE;MAAU;MAAoB;MAAwB;MAAuB;;IAC7E;MAAU,QAAQ;MAAc,mBAAmB;;;EAGrD;IAAiB,QAAQ;IAAuB,QAAQ;;EAExD;IACE;MAAU;;IACV;MAAU;MAAiB;MAAmB;;;EAGhD;IAA4B;MAAU;;IAAiB;MAAU;;;EAEjE;IACE;MAAU;MAAe;MAAgB;MAA2B;;IACpE,QAAQ;;EAGV;IACE;MACE;MACA;MACA;MACA;MACA;MACA;;IAEF,QAAQ;;EAGV;IAAsB;MAAU;;IAAgB,QAAQ;;EAExD;IACE;MAAU;MAA2B;MAAa;MAAc;;IAChE,QAAQ;;EAGV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IAAgB,QAAQ;IAAuB,QAAQ;;;YAE7C;EAEV;IAAyB;;EAEzB;IAA8B;IAAc,MAAM;;EAElD;IAA6B,kBAAkB;;EAE/C;IAAoB;MAAS;MAAe;;;EAE5C;IAA+B;IAAmB;MAAW;;;EAE7D;IAA8B;IAAmB;MAAY;MAAa;;;EAE1E;IAA2B;IAAmB;IAAmB;;EAEjE;IAA6B;;;qBAgBlB;UAMiB;UALpB;UACA;UACA;UACA;UAED;SA4BM,QAAQ,aAAa,qBAAqB,QAAQ;EAiC/D,QAAQ,gBAAgB,YACtB,QAAQ,GACR,QAAQ,WAAW,cACnB,qBACC,QAAQ,WAAW;EA0BtB,GAAG,gBAAgB,WAAW,QAAQ,GAAG,WAAW,QAAQ,UAAU;UAU9D;EASR"}
|
package/esm/e2e/cdp.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region ../../packages/core/src/e2e/cdp.ts
|
|
2
|
+
/** A command got no reply within its budget. Retry loops that shrink their budget use this type to tell a timeout apart from a reply that failed. */
|
|
3
|
+
var CdpTimeoutError = class extends Error {
|
|
4
|
+
constructor(timeoutMs, method) {
|
|
5
|
+
super(`CDP timeout after ${timeoutMs}ms: ${method}`);
|
|
6
|
+
this.name = "CdpTimeoutError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
2
9
|
var CdpConnection = class CdpConnection {
|
|
3
10
|
constructor(socket) {
|
|
4
11
|
this.socket = socket;
|
|
@@ -57,7 +64,7 @@ var CdpConnection = class CdpConnection {
|
|
|
57
64
|
const id = ++this.sequence;
|
|
58
65
|
const timer = setTimeout(() => {
|
|
59
66
|
this.pending.delete(id);
|
|
60
|
-
reject(
|
|
67
|
+
reject(new CdpTimeoutError(timeoutMs, method));
|
|
61
68
|
}, Math.max(1, timeoutMs));
|
|
62
69
|
this.pending.set(id, {
|
|
63
70
|
resolve: (value) => resolve(value),
|
|
@@ -102,5 +109,5 @@ var CdpConnection = class CdpConnection {
|
|
|
102
109
|
};
|
|
103
110
|
|
|
104
111
|
//#endregion
|
|
105
|
-
export { CdpConnection };
|
|
112
|
+
export { CdpConnection, CdpTimeoutError };
|
|
106
113
|
//# sourceMappingURL=cdp.mjs.map
|
package/esm/e2e/cdp.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdp.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"sourcesContent":["/** Wire shapes verified against the official protocol viewer and its source JSON:\n * https://chromedevtools.github.io/devtools-protocol/\n * https://github.com/ChromeDevTools/devtools-protocol/tree/master/json\n * Only the fields consumed by this harness are modeled here.\n */\ntype RemoteObject = { value?: unknown; description?: string };\ntype ExceptionDetails = { text: string; exception?: RemoteObject };\nexport type CdpMethods = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-enable\n \"Runtime.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate\n \"Runtime.evaluate\": {\n params: { expression: string; returnByValue: boolean; awaitPromise: boolean; silent: boolean };\n result: { result: RemoteObject; exceptionDetails?: ExceptionDetails };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-enable\n \"Page.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate\n \"Page.navigate\": {\n params: { url: string };\n result: { frameId: string; loaderId?: string; errorText?: string };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot\n \"Page.captureScreenshot\": { params: { format: \"png\" }; result: { data: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride\n \"Emulation.setDeviceMetricsOverride\": {\n params: { width: number; height: number; deviceScaleFactor: number; mobile: boolean };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent\n \"Input.dispatchMouseEvent\": {\n params: {\n type: \"mouseMoved\" | \"mousePressed\" | \"mouseReleased\";\n x: number;\n y: number;\n button?: \"left\";\n buttons?: number;\n clickCount?: number;\n };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText\n \"Input.insertText\": { params: { text: string }; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent\n \"Input.dispatchKeyEvent\": {\n params: { type: \"keyDown\" | \"keyUp\"; key: string; code: string; windowsVirtualKeyCode: number };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-enable\n \"Network.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#method-enable\n \"Log.enable\": { params: Record<string, never>; result: Record<string, never> };\n};\nexport type CdpEvents = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-loadEventFired\n \"Page.loadEventFired\": { timestamp: number };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-consoleAPICalled\n \"Runtime.consoleAPICalled\": { type: string; args: RemoteObject[] };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-exceptionThrown\n \"Runtime.exceptionThrown\": { exceptionDetails: ExceptionDetails };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#event-entryAdded\n \"Log.entryAdded\": { entry: { level: string; text: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSent\n \"Network.requestWillBeSent\": { requestId: string; request: { url: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived\n \"Network.responseReceived\": { requestId: string; response: { url: string; status: number } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFailed\n \"Network.loadingFailed\": { requestId: string; errorText: string; canceled?: boolean };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFinished\n \"Network.loadingFinished\": { requestId: string };\n};\ntype Pending = {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport class CdpConnection {\n private sequence = 0;\n private pending = new Map<number, Pending>();\n private listeners = new Map<string, Set<(params: unknown) => void>>();\n private closed = false;\n\n private constructor(private socket: WebSocket) {\n socket.addEventListener(\"message\", ({ data }) => {\n try {\n const message = JSON.parse(String(data)) as {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n };\n if (typeof message.id === \"number\") {\n const pending = this.pending.get(message.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this.pending.delete(message.id);\n if (message.error) pending.reject(new Error(`CDP: ${message.error.message}`));\n else pending.resolve(message.result);\n } else if (message.method) {\n for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);\n }\n } catch (error) {\n this.fail(new Error(`Invalid CDP message: ${String(error)}`));\n }\n });\n socket.addEventListener(\"close\", () => this.fail(new Error(\"CDP connection closed\")));\n socket.addEventListener(\"error\", () => this.fail(new Error(\"CDP WebSocket error\")));\n }\n\n static async connect(url: string, timeoutMs = 10_000): Promise<CdpConnection> {\n // Stable since Node 22.4, present in the minimum supported Node 22.15.\n // https://nodejs.org/download/release/v22.15.0/docs/api/globals.html#websocket\n if (typeof WebSocket !== \"function\")\n throw new Error(\n \"Browser e2e requires Node's global WebSocket (Node >=22.15, without --no-experimental-websocket)\",\n );\n const socket = new WebSocket(url);\n const connection = new CdpConnection(socket);\n await new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timer);\n socket.removeEventListener(\"open\", opened);\n socket.removeEventListener(\"error\", failed);\n socket.removeEventListener(\"close\", failed);\n };\n const opened = () => {\n cleanup();\n resolve();\n };\n const failed = () => {\n cleanup();\n socket.close();\n reject(new Error(`Cannot connect to owned browser CDP endpoint ${url}`));\n };\n const timer = setTimeout(failed, timeoutMs);\n socket.addEventListener(\"open\", opened, { once: true });\n socket.addEventListener(\"error\", failed, { once: true });\n socket.addEventListener(\"close\", failed, { once: true });\n });\n return connection;\n }\n\n command<K extends keyof CdpMethods>(\n method: K,\n params: CdpMethods[K][\"params\"],\n timeoutMs = 10_000,\n ): Promise<CdpMethods[K][\"result\"]> {\n if (this.closed) return Promise.reject(new Error(`CDP closed before ${method}`));\n return new Promise<CdpMethods[K][\"result\"]>((resolve, reject) => {\n const id = ++this.sequence;\n const timer = setTimeout(\n () => {\n this.pending.delete(id);\n reject(new Error(`CDP timeout after ${timeoutMs}ms: ${method}`));\n },\n Math.max(1, timeoutMs),\n );\n this.pending.set(id, {\n resolve: (value) => resolve(value as CdpMethods[K][\"result\"]),\n reject,\n timer,\n });\n try {\n this.socket.send(JSON.stringify({ id, method, params }));\n } catch (error) {\n clearTimeout(timer);\n this.pending.delete(id);\n reject(error);\n }\n });\n }\n\n on<K extends keyof CdpEvents>(method: K, listener: (params: CdpEvents[K]) => void): () => void {\n const wrapped = (params: unknown) => listener(params as CdpEvents[K]);\n const listeners = this.listeners.get(method) ?? new Set();\n listeners.add(wrapped);\n this.listeners.set(method, listeners);\n return () => {\n listeners.delete(wrapped);\n };\n }\n\n private fail(error: Error): void {\n this.closed = true;\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n close(): void {\n this.fail(new Error(\"CDP connection closed by harness\"));\n this.listeners.clear();\n this.socket.close();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cdp.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"sourcesContent":["/** Wire shapes verified against the official protocol viewer and its source JSON:\n * https://chromedevtools.github.io/devtools-protocol/\n * https://github.com/ChromeDevTools/devtools-protocol/tree/master/json\n * Only the fields consumed by this harness are modeled here.\n */\ntype RemoteObject = { value?: unknown; description?: string };\ntype ExceptionDetails = { text: string; exception?: RemoteObject };\nexport type CdpMethods = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-enable\n \"Runtime.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate\n \"Runtime.evaluate\": {\n params: { expression: string; returnByValue: boolean; awaitPromise: boolean; silent: boolean };\n result: { result: RemoteObject; exceptionDetails?: ExceptionDetails };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-enable\n \"Page.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate\n \"Page.navigate\": {\n params: { url: string };\n result: { frameId: string; loaderId?: string; errorText?: string };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot\n \"Page.captureScreenshot\": { params: { format: \"png\" }; result: { data: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride\n \"Emulation.setDeviceMetricsOverride\": {\n params: { width: number; height: number; deviceScaleFactor: number; mobile: boolean };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent\n \"Input.dispatchMouseEvent\": {\n params: {\n type: \"mouseMoved\" | \"mousePressed\" | \"mouseReleased\";\n x: number;\n y: number;\n button?: \"left\";\n buttons?: number;\n clickCount?: number;\n };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText\n \"Input.insertText\": { params: { text: string }; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent\n \"Input.dispatchKeyEvent\": {\n params: { type: \"keyDown\" | \"keyUp\"; key: string; code: string; windowsVirtualKeyCode: number };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-enable\n \"Network.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#method-enable\n \"Log.enable\": { params: Record<string, never>; result: Record<string, never> };\n};\nexport type CdpEvents = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-loadEventFired\n \"Page.loadEventFired\": { timestamp: number };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-consoleAPICalled\n \"Runtime.consoleAPICalled\": { type: string; args: RemoteObject[] };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-exceptionThrown\n \"Runtime.exceptionThrown\": { exceptionDetails: ExceptionDetails };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#event-entryAdded\n \"Log.entryAdded\": { entry: { level: string; text: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSent\n \"Network.requestWillBeSent\": { requestId: string; request: { url: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived\n \"Network.responseReceived\": { requestId: string; response: { url: string; status: number } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFailed\n \"Network.loadingFailed\": { requestId: string; errorText: string; canceled?: boolean };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFinished\n \"Network.loadingFinished\": { requestId: string };\n};\ntype Pending = {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/** A command got no reply within its budget. Retry loops that shrink their budget use this type to tell a timeout apart from a reply that failed. */\nexport class CdpTimeoutError extends Error {\n constructor(timeoutMs: number, method: string) {\n super(`CDP timeout after ${timeoutMs}ms: ${method}`);\n this.name = \"CdpTimeoutError\";\n }\n}\n\nexport class CdpConnection {\n private sequence = 0;\n private pending = new Map<number, Pending>();\n private listeners = new Map<string, Set<(params: unknown) => void>>();\n private closed = false;\n\n private constructor(private socket: WebSocket) {\n socket.addEventListener(\"message\", ({ data }) => {\n try {\n const message = JSON.parse(String(data)) as {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n };\n if (typeof message.id === \"number\") {\n const pending = this.pending.get(message.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this.pending.delete(message.id);\n if (message.error) pending.reject(new Error(`CDP: ${message.error.message}`));\n else pending.resolve(message.result);\n } else if (message.method) {\n for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);\n }\n } catch (error) {\n this.fail(new Error(`Invalid CDP message: ${String(error)}`));\n }\n });\n socket.addEventListener(\"close\", () => this.fail(new Error(\"CDP connection closed\")));\n socket.addEventListener(\"error\", () => this.fail(new Error(\"CDP WebSocket error\")));\n }\n\n static async connect(url: string, timeoutMs = 10_000): Promise<CdpConnection> {\n // Stable since Node 22.4, present in the minimum supported Node 22.15.\n // https://nodejs.org/download/release/v22.15.0/docs/api/globals.html#websocket\n if (typeof WebSocket !== \"function\")\n throw new Error(\n \"Browser e2e requires Node's global WebSocket (Node >=22.15, without --no-experimental-websocket)\",\n );\n const socket = new WebSocket(url);\n const connection = new CdpConnection(socket);\n await new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timer);\n socket.removeEventListener(\"open\", opened);\n socket.removeEventListener(\"error\", failed);\n socket.removeEventListener(\"close\", failed);\n };\n const opened = () => {\n cleanup();\n resolve();\n };\n const failed = () => {\n cleanup();\n socket.close();\n reject(new Error(`Cannot connect to owned browser CDP endpoint ${url}`));\n };\n const timer = setTimeout(failed, timeoutMs);\n socket.addEventListener(\"open\", opened, { once: true });\n socket.addEventListener(\"error\", failed, { once: true });\n socket.addEventListener(\"close\", failed, { once: true });\n });\n return connection;\n }\n\n command<K extends keyof CdpMethods>(\n method: K,\n params: CdpMethods[K][\"params\"],\n timeoutMs = 10_000,\n ): Promise<CdpMethods[K][\"result\"]> {\n if (this.closed) return Promise.reject(new Error(`CDP closed before ${method}`));\n return new Promise<CdpMethods[K][\"result\"]>((resolve, reject) => {\n const id = ++this.sequence;\n const timer = setTimeout(\n () => {\n this.pending.delete(id);\n reject(new CdpTimeoutError(timeoutMs, method));\n },\n Math.max(1, timeoutMs),\n );\n this.pending.set(id, {\n resolve: (value) => resolve(value as CdpMethods[K][\"result\"]),\n reject,\n timer,\n });\n try {\n this.socket.send(JSON.stringify({ id, method, params }));\n } catch (error) {\n clearTimeout(timer);\n this.pending.delete(id);\n reject(error);\n }\n });\n }\n\n on<K extends keyof CdpEvents>(method: K, listener: (params: CdpEvents[K]) => void): () => void {\n const wrapped = (params: unknown) => listener(params as CdpEvents[K]);\n const listeners = this.listeners.get(method) ?? new Set();\n listeners.add(wrapped);\n this.listeners.set(method, listeners);\n return () => {\n listeners.delete(wrapped);\n };\n }\n\n private fail(error: Error): void {\n this.closed = true;\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n close(): void {\n this.fail(new Error(\"CDP connection closed by harness\"));\n this.listeners.clear();\n this.socket.close();\n }\n}\n"],"mappings":"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"}
|
package/esm/e2e/page.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CdpConnection } from "./cdp.mjs";
|
|
2
|
-
import { AccessibilityEvidence, AccessibilityOptions, E2EEvidence, E2ESpec, Viewport } from "./types.mjs";
|
|
2
|
+
import { AccessibilityEvidence, AccessibilityOptions, E2EEvidence, E2ESpec, PayloadEvidence, Viewport } from "./types.mjs";
|
|
3
3
|
//#region ../../packages/core/src/e2e/page.d.ts
|
|
4
4
|
type WaitOptions = {
|
|
5
5
|
selector?: string;
|
|
@@ -57,6 +57,14 @@ export declare class BrowserPage {
|
|
|
57
57
|
* API: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axerun
|
|
58
58
|
* No CDN, DOM script element, or application dependency is required.
|
|
59
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Measures the inline payload of the document the page currently shows and
|
|
62
|
+
* records it under `evidence.payloads`. Measure after a full load: a client
|
|
63
|
+
* navigation replaces the data it reads. Pass `limit` to mark an overage.
|
|
64
|
+
*/
|
|
65
|
+
measurePayload(options?: {
|
|
66
|
+
limit?: number;
|
|
67
|
+
}): Promise<PayloadEvidence>;
|
|
60
68
|
assertAccessible(options?: AccessibilityOptions): Promise<AccessibilityEvidence>;
|
|
61
69
|
assertViewport(): Promise<ViewportReport>;
|
|
62
70
|
screenshot(name: string): Promise<string>;
|
package/esm/e2e/page.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"page.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"mappings":";;;KAiBK;EAAgB;EAAmB;EAAe;EAAc;;KAEhE;EACH;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;;qBAgES;UAQD;UACA;UACA;UACA;UACA;UAXF;UACA;UACA;UACA;UACA;EAER,YACU,KAAK,eACL,gBACA,yBACA,UAAU,aACV,MAAM;UAgDR;EAMF,cAAc;EAQd,KAAK,GAAG,cAAc,cAAc,QAAQ,KAAK,QAAQ;EAqBzD,SAAS,aAAa,oBAAoB,qBAAqB,QAAQ;UAa/D;EAoBR,KAAK,cAAc,qBAAqB;EAkBxC,QAAQ,SAAS,cAAc;;;;EA0B/B,kBACJ,cAAc,kBACd;IAAW;IAAc;MACxB;EAkBG,MAAM,kBAAkB,qBAAqB;EA6E7C,KAAK,kBAAkB,gBAAgB;;EA4BvC,KAAK,kBAAkB,eAAe;EA0BtC,YAAY,UAAU,WAAW;;;;;;;;;;EAgCjC,eAAe;IAAW;MAAwB,QAAQ;EA2B1D,iBAAiB,UAAS,uBAA4B,QAAQ;EA8C9D,kBAAkB,QAAQ;EAoB1B,WAAW,eAAe;EAY1B,iBACJ,cACA,qBAAS,aACR;EAuBG,mBAAmB,qBAAqB;EAgB9C"}
|
package/esm/e2e/page.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CdpTimeoutError } from "./cdp.mjs";
|
|
1
2
|
import { defaultViewports, errorMessage, matches } from "./types.mjs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -55,6 +56,14 @@ function boxesStable(first, second) {
|
|
|
55
56
|
"height"
|
|
56
57
|
].every((key) => Math.abs(first[key] - second[key]) < .25);
|
|
57
58
|
}
|
|
59
|
+
/** Reason to report after a failed probe in a retry loop. A CDP timeout comes from
|
|
60
|
+
* the deadline shrinking under the probe, not from the target, so it must not
|
|
61
|
+
* replace a reason already observed on the target. Any other error is reported.
|
|
62
|
+
*/
|
|
63
|
+
function probeReason(error, observed) {
|
|
64
|
+
if (error instanceof CdpTimeoutError && observed !== void 0) return observed;
|
|
65
|
+
return errorMessage(error);
|
|
66
|
+
}
|
|
58
67
|
var BrowserPage = class {
|
|
59
68
|
constructor(cdp, origin, outputDirectory, evidence, spec) {
|
|
60
69
|
this.cdp = cdp;
|
|
@@ -153,16 +162,16 @@ var BrowserPage = class {
|
|
|
153
162
|
}
|
|
154
163
|
async poll(description, check, timeoutMs) {
|
|
155
164
|
const deadline = Date.now() + timeoutMs;
|
|
156
|
-
let reason
|
|
165
|
+
let reason;
|
|
157
166
|
while (Date.now() < deadline) {
|
|
158
167
|
try {
|
|
159
168
|
if (await check(Math.max(1, deadline - Date.now()))) return;
|
|
160
169
|
} catch (error) {
|
|
161
|
-
reason =
|
|
170
|
+
reason = probeReason(error, reason);
|
|
162
171
|
}
|
|
163
172
|
await setTimeout(Math.min(50, Math.max(0, deadline - Date.now())));
|
|
164
173
|
}
|
|
165
|
-
throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);
|
|
174
|
+
throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason ?? "condition stayed false"}`);
|
|
166
175
|
}
|
|
167
176
|
async goto(path, timeoutMs = 1e4) {
|
|
168
177
|
await this.step(`goto ${path}`, async () => {
|
|
@@ -198,14 +207,14 @@ var BrowserPage = class {
|
|
|
198
207
|
await this.step(`click ${selector}`, async () => {
|
|
199
208
|
const deadline = Date.now() + timeoutMs;
|
|
200
209
|
let previous;
|
|
201
|
-
let reason
|
|
210
|
+
let reason;
|
|
202
211
|
while (Date.now() < deadline) {
|
|
203
212
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
204
213
|
let box;
|
|
205
214
|
try {
|
|
206
215
|
box = await this.evaluate(`(${clickProbe.toString()})(${JSON.stringify(selector)})`, remaining());
|
|
207
216
|
} catch (error) {
|
|
208
|
-
reason =
|
|
217
|
+
reason = probeReason(error, reason);
|
|
209
218
|
previous = void 0;
|
|
210
219
|
await setTimeout(25);
|
|
211
220
|
continue;
|
|
@@ -221,7 +230,7 @@ var BrowserPage = class {
|
|
|
221
230
|
}, remaining());
|
|
222
231
|
hovered = await this.evaluate(`(${clickProbe.toString()})(${JSON.stringify(selector)})`, remaining());
|
|
223
232
|
} catch (error) {
|
|
224
|
-
reason =
|
|
233
|
+
reason = probeReason(error, reason);
|
|
225
234
|
previous = void 0;
|
|
226
235
|
await setTimeout(Math.min(25, remaining()));
|
|
227
236
|
continue;
|
|
@@ -250,7 +259,7 @@ var BrowserPage = class {
|
|
|
250
259
|
} else previous = box;
|
|
251
260
|
await setTimeout(Math.min(50, remaining()));
|
|
252
261
|
}
|
|
253
|
-
throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);
|
|
262
|
+
throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason ?? "layout has not settled"}`);
|
|
254
263
|
});
|
|
255
264
|
}
|
|
256
265
|
async fill(selector, value) {
|
|
@@ -311,6 +320,35 @@ var BrowserPage = class {
|
|
|
311
320
|
* API: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axerun
|
|
312
321
|
* No CDN, DOM script element, or application dependency is required.
|
|
313
322
|
*/
|
|
323
|
+
/**
|
|
324
|
+
* Measures the inline payload of the document the page currently shows and
|
|
325
|
+
* records it under `evidence.payloads`. Measure after a full load: a client
|
|
326
|
+
* navigation replaces the data it reads. Pass `limit` to mark an overage.
|
|
327
|
+
*/
|
|
328
|
+
async measurePayload(options = {}) {
|
|
329
|
+
return this.step(`measurePayload ${this.viewport.name}`, async () => {
|
|
330
|
+
const measured = await this.evaluate(`(() => {
|
|
331
|
+
const bytes = (id) => {
|
|
332
|
+
const node = document.getElementById(id);
|
|
333
|
+
return node === null ? 0 : new TextEncoder().encode(node.textContent || "").length;
|
|
334
|
+
};
|
|
335
|
+
return { url: location.href, data: bytes("__WARLOCK_DATA__"), routes: bytes("__WARLOCK_NAMED_API_ROUTES__") };
|
|
336
|
+
})()`, 1e4);
|
|
337
|
+
const total = measured.data + measured.routes;
|
|
338
|
+
const report = {
|
|
339
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
340
|
+
viewport: { ...this.viewport },
|
|
341
|
+
...measured,
|
|
342
|
+
total,
|
|
343
|
+
...options.limit === void 0 ? {} : {
|
|
344
|
+
limit: options.limit,
|
|
345
|
+
overBudget: total > options.limit
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
(this.evidence.payloads ??= []).push(report);
|
|
349
|
+
return report;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
314
352
|
async assertAccessible(options = {}) {
|
|
315
353
|
return this.step(`assertAccessible ${this.viewport.name}`, async () => {
|
|
316
354
|
const source = await readFile(createRequire(import.meta.url).resolve("axe-core/axe.min.js"), "utf8");
|
package/esm/e2e/page.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { CdpConnection } from \"./cdp\";\nimport {\n type AccessibilityOptions,\n type AccessibilityEvidence,\n defaultViewports,\n errorMessage,\n matches,\n type E2EEvidence,\n type E2ESpec,\n type Viewport,\n} from \"./types\";\n\ntype WaitOptions = { selector?: string; text?: string; url?: string; timeoutMs?: number };\ntype ClickBox = { x: number; y: number; width: number; height: number; reason: string };\ntype ViewportReport = {\n requestedWidth: number;\n innerWidth: number;\n documentWidth: number;\n bodyWidth: number;\n offenders: {\n tag: string;\n id: string;\n className: string;\n width: number;\n scrollWidth: number;\n right: number;\n left: number;\n }[];\n};\n\n/** Runs inside the browser. Two paints plus cross-probe box comparison detect\n * scroll/animation/layout races. The hit test also accepts target descendants.\n */\nfunction clickProbe(selector: string): Promise<ClickBox> {\n return (async () => {\n const empty = (reason: string): ClickBox => ({ x: 0, y: 0, width: 0, height: 0, reason });\n const element = document.querySelector(selector);\n if (!(element instanceof HTMLElement || element instanceof SVGElement))\n return empty(\"target is missing or is not an HTML/SVG element\");\n element.scrollIntoView({ block: \"center\", inline: \"center\", behavior: \"instant\" });\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n if (!element.isConnected || document.querySelector(selector) !== element)\n return empty(\"target was replaced during layout\");\n const box = element.getBoundingClientRect();\n const x = box.x + box.width / 2;\n const y = box.y + box.height / 2;\n const style = getComputedStyle(element);\n let reason = \"\";\n if (\n box.width <= 0 ||\n box.height <= 0 ||\n style.visibility !== \"visible\" ||\n style.display === \"none\" ||\n Number(style.opacity) === 0\n )\n reason = \"target is hidden or has an empty box\";\n else if (element.matches(\":disabled\") || element.closest('[inert], [aria-disabled=\"true\"]'))\n reason = \"target is disabled or inert\";\n else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight)\n reason = \"target center is outside the viewport\";\n else {\n const top = document.elementFromPoint(x, y);\n if (!top || !element.contains(top))\n reason = `target is covered by ${top?.tagName ?? \"no element\"}${top?.id ? \"#\" + top.id : \"\"}`;\n }\n return { x, y, width: box.width, height: box.height, reason };\n })();\n}\n\nexport function boxesStable(first: ClickBox, second: ClickBox): boolean {\n return (\n !first.reason &&\n !second.reason &&\n ([\"x\", \"y\", \"width\", \"height\"] as const).every(\n (key) => Math.abs(first[key] - second[key]) < 0.25,\n )\n );\n}\n\nexport class BrowserPage {\n private viewport: Viewport = { ...defaultViewports[0] };\n private loads = 0;\n private pendingRequests = new Map<string, string>();\n private requestUrls = new Map<string, string>();\n private lastNetworkActivity = Date.now();\n\n constructor(\n private cdp: CdpConnection,\n private origin: string,\n private outputDirectory: string,\n private evidence: E2EEvidence,\n private spec: E2ESpec,\n ) {\n cdp.on(\"Page.loadEventFired\", () => {\n this.loads++;\n });\n cdp.on(\"Runtime.consoleAPICalled\", ({ type, args }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: type === \"assert\" ? \"error\" : type,\n text: args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \"),\n }),\n );\n cdp.on(\"Runtime.exceptionThrown\", ({ exceptionDetails }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: \"error\",\n text: exceptionDetails.exception?.description ?? exceptionDetails.text,\n }),\n );\n cdp.on(\"Log.entryAdded\", ({ entry }) =>\n evidence.console.push({ at: new Date().toISOString(), level: entry.level, text: entry.text }),\n );\n cdp.on(\"Network.requestWillBeSent\", ({ requestId, request }) => {\n this.pendingRequests.set(requestId, request.url);\n this.requestUrls.set(requestId, request.url);\n this.lastNetworkActivity = Date.now();\n });\n cdp.on(\"Network.responseReceived\", ({ requestId, response }) => {\n this.requestUrls.set(requestId, response.url);\n if (response.status >= 400)\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: response.url,\n status: response.status,\n });\n });\n cdp.on(\"Network.loadingFailed\", ({ requestId, errorText, canceled }) => {\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,\n errorText,\n canceled,\n });\n this.completeRequest(requestId);\n });\n cdp.on(\"Network.loadingFinished\", ({ requestId }) => this.completeRequest(requestId));\n }\n\n private completeRequest(requestId: string): void {\n this.pendingRequests.delete(requestId);\n this.requestUrls.delete(requestId);\n this.lastNetworkActivity = Date.now();\n }\n\n async initialize(): Promise<void> {\n await this.cdp.command(\"Page.enable\", {});\n await this.cdp.command(\"Runtime.enable\", {});\n await this.cdp.command(\"Network.enable\", {});\n await this.cdp.command(\"Log.enable\", {});\n await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);\n }\n\n async step<T>(name: string, action: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const entry = {\n name,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"passed\" as \"passed\" | \"failed\",\n error: undefined as string | undefined,\n };\n this.evidence.steps.push(entry);\n try {\n return await action();\n } catch (error) {\n entry.status = \"failed\";\n entry.error = errorMessage(error);\n throw error;\n } finally {\n entry.durationMs = Math.round(performance.now() - start);\n }\n }\n\n async evaluate<T = unknown>(expression: string, timeoutMs = 10_000): Promise<T> {\n const response = await this.cdp.command(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true, silent: true },\n timeoutMs,\n );\n if (response.exceptionDetails)\n throw new Error(\n `Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`,\n );\n return response.result.value as T;\n }\n\n private async poll(\n description: string,\n check: (remainingMs: number) => Promise<boolean>,\n timeoutMs: number,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let reason = \"condition stayed false\";\n while (Date.now() < deadline) {\n try {\n if (await check(Math.max(1, deadline - Date.now()))) return;\n } catch (error) {\n reason = errorMessage(error);\n }\n await delay(Math.min(50, Math.max(0, deadline - Date.now())));\n }\n throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);\n }\n\n async goto(path: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`goto ${path}`, async () => {\n const url = new URL(path, this.origin).href;\n const before = this.loads;\n const navigation = await this.cdp.command(\"Page.navigate\", { url }, timeoutMs);\n if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);\n await this.poll(\n `Navigation to ${url}`,\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : \"true\"}`,\n remaining,\n ),\n timeoutMs,\n );\n });\n }\n\n async waitFor(options: WaitOptions): Promise<void> {\n if (options.selector === undefined && options.text === undefined && options.url === undefined)\n throw new Error(\"waitFor requires a selector, text, or URL\");\n await this.step(`waitFor ${JSON.stringify(options)}`, () =>\n this.poll(\n \"waitFor \" + JSON.stringify(options),\n (remaining) =>\n this.evaluate<boolean>(\n `(() => {\n const options = ${JSON.stringify(options)};\n const element = options.selector ? document.querySelector(options.selector) : document.body;\n if (!element) return false;\n if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }\n return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&\n (options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);\n })()`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n ),\n );\n }\n\n /** Arms the document/URL comparison BEFORE invoking the navigation action,\n * covering full loads, reloads, hash changes and client-side routing.\n */\n async waitForNavigation(\n action: () => Promise<unknown>,\n options: { url?: string; timeoutMs?: number } = {},\n ): Promise<void> {\n await this.step(\"waitForNavigation\", async () => {\n const before = await this.evaluate<{ href: string; timeOrigin: number }>(\n \"({href:location.href,timeOrigin:performance.timeOrigin})\",\n );\n await action();\n await this.poll(\n \"Navigation\",\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : \"true\"}`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n );\n });\n }\n\n async click(selector: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`click ${selector}`, async () => {\n const deadline = Date.now() + timeoutMs;\n let previous: ClickBox | undefined;\n let reason = \"layout has not settled\";\n while (Date.now() < deadline) {\n const remaining = () => Math.max(1, deadline - Date.now());\n let box: ClickBox;\n try {\n box = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(25);\n continue;\n }\n reason = box.reason || \"target box is moving\";\n if (previous && boxesStable(previous, box)) {\n // Hover can move menus and overlays. Re-measure and hit-test after it.\n let hovered: ClickBox;\n try {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n { type: \"mouseMoved\", x: box.x, y: box.y },\n remaining(),\n );\n hovered = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(Math.min(25, remaining()));\n continue;\n }\n if (Date.now() < deadline && boxesStable(box, hovered)) {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mousePressed\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 1,\n clickCount: 1,\n },\n remaining(),\n );\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mouseReleased\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 0,\n clickCount: 1,\n },\n remaining(),\n );\n return;\n }\n reason = hovered.reason || \"hover changed the target box\";\n previous = undefined;\n } else previous = box;\n await delay(Math.min(50, remaining()));\n }\n throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);\n });\n }\n\n async fill(selector: string, value: string): Promise<void> {\n await this.step(`fill ${selector}`, async () => {\n await this.click(selector);\n // The native value setter works with controlled inputs; input/change are\n // emitted after clearing. CDP insertText then performs actual text input.\n await this.evaluate(`(() => {\n const element=document.querySelector(${JSON.stringify(selector)});\n if (element !== document.activeElement) throw new Error('Fill target lost focus');\n if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {\n const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;\n const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;\n if (!setter) throw new Error('Fill target has no native value setter');\n setter.call(element,'');\n } else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';\n else throw new Error('Fill target must be an editable input, textarea or contenteditable');\n element.dispatchEvent(new Event('input',{bubbles:true}));\n })()`);\n await this.evaluate(\n `document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error(\"Fill target lost focus after input\"); })()`,\n );\n if (value) await this.cdp.command(\"Input.insertText\", { text: value });\n await this.evaluate(\n \"document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))\",\n );\n });\n }\n\n /** Appends Unicode text through the browser's text-input path. */\n async type(selector: string, text: string): Promise<void> {\n await this.step(`type ${selector}`, async () => {\n await this.click(selector);\n const needsEndKey = await this.evaluate<boolean>(\n `(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== \"number\") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`,\n );\n // Email/number inputs do not expose selectionRange; use their native\n // keyboard editing path instead of throwing on setSelectionRange.\n if (needsEndKey) {\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyDown\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyUp\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n }\n await this.cdp.command(\"Input.insertText\", { text });\n });\n }\n\n async setViewport(viewport: Viewport): Promise<void> {\n if (\n !/^[a-z0-9_-]+$/i.test(viewport.name) ||\n !Number.isInteger(viewport.width) ||\n !Number.isInteger(viewport.height) ||\n viewport.width <= 0 ||\n viewport.height <= 0\n )\n throw new Error(\"Viewport requires a safe name and positive integer width/height\");\n await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {\n await this.cdp.command(\"Emulation.setDeviceMetricsOverride\", {\n width: viewport.width,\n height: viewport.height,\n deviceScaleFactor: 1,\n mobile: viewport.mobile ?? false,\n });\n this.viewport = { ...viewport };\n await this.evaluate(\n \"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))\",\n );\n });\n }\n\n /** Inject the installed axe distribution through Runtime.evaluate on every scan.\n * API: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axerun\n * No CDN, DOM script element, or application dependency is required.\n */\n async assertAccessible(options: AccessibilityOptions = {}): Promise<AccessibilityEvidence> {\n return this.step(`assertAccessible ${this.viewport.name}`, async () => {\n const source = await readFile(\n createRequire(import.meta.url).resolve(\"axe-core/axe.min.js\"),\n \"utf8\",\n );\n const timeout = options.timeoutMs ?? 30_000;\n await this.evaluate(source, timeout);\n const result = await this.evaluate<\n Pick<AccessibilityEvidence, \"url\" | \"violations\" | \"incomplete\">\n >(\n `(async () => {\n const selector = ${JSON.stringify(options.selector ?? null)};\n const context = selector === null ? document : document.querySelector(selector);\n if (!context) throw new Error('Accessibility scan selector did not match: ' + selector);\n const results = await axe.run(context, ${JSON.stringify(options.tags ? { runOnly: { type: \"tag\", values: options.tags } } : {})});\n const summarize = entries => entries.map(({id, impact, help, helpUrl, nodes}) => ({\n id, impact, help, helpUrl, nodes: nodes.map(({target, failureSummary}) => ({target, failureSummary}))\n }));\n return {url: location.href, violations: summarize(results.violations), incomplete: summarize(results.incomplete)};\n })()`,\n timeout,\n );\n for (const violation of result.violations) {\n for (const node of violation.nodes) {\n node.allowed =\n options.allow?.some(\n (rule) =>\n rule.id === violation.id &&\n (rule.selector === undefined ||\n (node.target.length === 1 && node.target[0] === rule.selector)),\n ) ?? false;\n }\n }\n const report = { at: new Date().toISOString(), viewport: { ...this.viewport }, ...result };\n (this.evidence.accessibility ??= []).push(report);\n const failures = report.violations.flatMap((violation) =>\n violation.nodes\n .filter((node) => !node.allowed)\n .map((node) => `${violation.id}: ${JSON.stringify(node.target)} — ${violation.help}`),\n );\n if (failures.length) throw new Error(`Accessibility violations:\\n${failures.join(\"\\n\")}`);\n return report;\n });\n }\n\n async assertViewport(): Promise<ViewportReport> {\n return this.step(`assertViewport ${this.viewport.name}`, async () => {\n const report = await this.evaluate<ViewportReport>(`({\n requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,\n offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})\n .filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})\n .sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)\n })`);\n if (\n report.innerWidth !== report.requestedWidth ||\n report.documentWidth > report.requestedWidth ||\n report.bodyWidth > report.requestedWidth\n )\n throw new Error(\n `Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`,\n );\n return report;\n });\n }\n\n async screenshot(name: string): Promise<string> {\n if (!/^[a-z0-9_-]+$/i.test(name))\n throw new Error(\"Screenshot names must contain only letters, digits, '_' or '-'\");\n return this.step(`screenshot ${name}`, async () => {\n const file = `${String(this.evidence.screenshots.length + 1).padStart(3, \"0\")}-${name}-${this.viewport.name}.png`;\n const { data } = await this.cdp.command(\"Page.captureScreenshot\", { format: \"png\" });\n await writeFile(join(this.outputDirectory, file), Buffer.from(data, \"base64\"));\n this.evidence.screenshots.push({ name, file, viewport: { ...this.viewport } });\n return join(this.outputDirectory, file);\n });\n }\n\n async captureViewports(\n name: string,\n viewports = this.spec.viewports ?? defaultViewports,\n ): Promise<void> {\n const failures: unknown[] = [];\n const original = this.viewport;\n for (const viewport of viewports) {\n try {\n await this.setViewport(viewport);\n try {\n await this.assertViewport();\n } finally {\n await this.screenshot(name);\n }\n } catch (error) {\n failures.push(error);\n }\n }\n try {\n await this.setViewport(original);\n } catch (error) {\n failures.push(error);\n }\n if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join(\"; \"));\n }\n\n async waitForNetworkIdle(timeoutMs = 10_000): Promise<void> {\n await this.step(\"waitForNetworkIdle\", () =>\n this.poll(\n \"Network idle\",\n async () => {\n const pending = [...this.pendingRequests.values()].filter(\n (url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)),\n );\n if (pending.length) throw new Error(`Pending requests: ${pending.join(\", \")}`);\n return Date.now() - this.lastNetworkActivity >= 300;\n },\n timeoutMs,\n ),\n );\n }\n\n assertHealthy(): void {\n for (const entry of this.evidence.console)\n if (entry.level === \"error\")\n entry.allowed =\n this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;\n for (const entry of this.evidence.networkFailures)\n entry.allowed =\n this.spec.allowNetworkFailures?.some(\n (rule) =>\n matches(rule.url, entry.url) &&\n (rule.status === undefined || rule.status === entry.status) &&\n (rule.errorText === undefined || matches(rule.errorText, entry.errorText ?? \"\")),\n ) ?? false;\n const consoleErrors = this.evidence.console.filter(\n (entry) => entry.level === \"error\" && !entry.allowed,\n );\n const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);\n if (consoleErrors.length || networkErrors.length)\n throw new Error(\n `Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\\n${JSON.stringify({ console: consoleErrors, network: networkErrors })}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"page.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { CdpConnection, CdpTimeoutError } from \"./cdp\";\nimport {\n type AccessibilityOptions,\n type AccessibilityEvidence,\n defaultViewports,\n errorMessage,\n matches,\n type E2EEvidence,\n type E2ESpec,\n type PayloadEvidence,\n type Viewport,\n} from \"./types\";\n\ntype WaitOptions = { selector?: string; text?: string; url?: string; timeoutMs?: number };\ntype ClickBox = { x: number; y: number; width: number; height: number; reason: string };\ntype ViewportReport = {\n requestedWidth: number;\n innerWidth: number;\n documentWidth: number;\n bodyWidth: number;\n offenders: {\n tag: string;\n id: string;\n className: string;\n width: number;\n scrollWidth: number;\n right: number;\n left: number;\n }[];\n};\n\n/** Runs inside the browser. Two paints plus cross-probe box comparison detect\n * scroll/animation/layout races. The hit test also accepts target descendants.\n */\nfunction clickProbe(selector: string): Promise<ClickBox> {\n return (async () => {\n const empty = (reason: string): ClickBox => ({ x: 0, y: 0, width: 0, height: 0, reason });\n const element = document.querySelector(selector);\n if (!(element instanceof HTMLElement || element instanceof SVGElement))\n return empty(\"target is missing or is not an HTML/SVG element\");\n element.scrollIntoView({ block: \"center\", inline: \"center\", behavior: \"instant\" });\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n if (!element.isConnected || document.querySelector(selector) !== element)\n return empty(\"target was replaced during layout\");\n const box = element.getBoundingClientRect();\n const x = box.x + box.width / 2;\n const y = box.y + box.height / 2;\n const style = getComputedStyle(element);\n let reason = \"\";\n if (\n box.width <= 0 ||\n box.height <= 0 ||\n style.visibility !== \"visible\" ||\n style.display === \"none\" ||\n Number(style.opacity) === 0\n )\n reason = \"target is hidden or has an empty box\";\n else if (element.matches(\":disabled\") || element.closest('[inert], [aria-disabled=\"true\"]'))\n reason = \"target is disabled or inert\";\n else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight)\n reason = \"target center is outside the viewport\";\n else {\n const top = document.elementFromPoint(x, y);\n if (!top || !element.contains(top))\n reason = `target is covered by ${top?.tagName ?? \"no element\"}${top?.id ? \"#\" + top.id : \"\"}`;\n }\n return { x, y, width: box.width, height: box.height, reason };\n })();\n}\n\nexport function boxesStable(first: ClickBox, second: ClickBox): boolean {\n return (\n !first.reason &&\n !second.reason &&\n ([\"x\", \"y\", \"width\", \"height\"] as const).every(\n (key) => Math.abs(first[key] - second[key]) < 0.25,\n )\n );\n}\n\n/** Reason to report after a failed probe in a retry loop. A CDP timeout comes from\n * the deadline shrinking under the probe, not from the target, so it must not\n * replace a reason already observed on the target. Any other error is reported.\n */\nfunction probeReason(error: unknown, observed: string | undefined): string {\n if (error instanceof CdpTimeoutError && observed !== undefined) return observed;\n return errorMessage(error);\n}\n\nexport class BrowserPage {\n private viewport: Viewport = { ...defaultViewports[0] };\n private loads = 0;\n private pendingRequests = new Map<string, string>();\n private requestUrls = new Map<string, string>();\n private lastNetworkActivity = Date.now();\n\n constructor(\n private cdp: CdpConnection,\n private origin: string,\n private outputDirectory: string,\n private evidence: E2EEvidence,\n private spec: E2ESpec,\n ) {\n cdp.on(\"Page.loadEventFired\", () => {\n this.loads++;\n });\n cdp.on(\"Runtime.consoleAPICalled\", ({ type, args }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: type === \"assert\" ? \"error\" : type,\n text: args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \"),\n }),\n );\n cdp.on(\"Runtime.exceptionThrown\", ({ exceptionDetails }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: \"error\",\n text: exceptionDetails.exception?.description ?? exceptionDetails.text,\n }),\n );\n cdp.on(\"Log.entryAdded\", ({ entry }) =>\n evidence.console.push({ at: new Date().toISOString(), level: entry.level, text: entry.text }),\n );\n cdp.on(\"Network.requestWillBeSent\", ({ requestId, request }) => {\n this.pendingRequests.set(requestId, request.url);\n this.requestUrls.set(requestId, request.url);\n this.lastNetworkActivity = Date.now();\n });\n cdp.on(\"Network.responseReceived\", ({ requestId, response }) => {\n this.requestUrls.set(requestId, response.url);\n if (response.status >= 400)\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: response.url,\n status: response.status,\n });\n });\n cdp.on(\"Network.loadingFailed\", ({ requestId, errorText, canceled }) => {\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,\n errorText,\n canceled,\n });\n this.completeRequest(requestId);\n });\n cdp.on(\"Network.loadingFinished\", ({ requestId }) => this.completeRequest(requestId));\n }\n\n private completeRequest(requestId: string): void {\n this.pendingRequests.delete(requestId);\n this.requestUrls.delete(requestId);\n this.lastNetworkActivity = Date.now();\n }\n\n async initialize(): Promise<void> {\n await this.cdp.command(\"Page.enable\", {});\n await this.cdp.command(\"Runtime.enable\", {});\n await this.cdp.command(\"Network.enable\", {});\n await this.cdp.command(\"Log.enable\", {});\n await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);\n }\n\n async step<T>(name: string, action: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const entry = {\n name,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"passed\" as \"passed\" | \"failed\",\n error: undefined as string | undefined,\n };\n this.evidence.steps.push(entry);\n try {\n return await action();\n } catch (error) {\n entry.status = \"failed\";\n entry.error = errorMessage(error);\n throw error;\n } finally {\n entry.durationMs = Math.round(performance.now() - start);\n }\n }\n\n async evaluate<T = unknown>(expression: string, timeoutMs = 10_000): Promise<T> {\n const response = await this.cdp.command(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true, silent: true },\n timeoutMs,\n );\n if (response.exceptionDetails)\n throw new Error(\n `Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`,\n );\n return response.result.value as T;\n }\n\n private async poll(\n description: string,\n check: (remainingMs: number) => Promise<boolean>,\n timeoutMs: number,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let reason: string | undefined;\n while (Date.now() < deadline) {\n try {\n if (await check(Math.max(1, deadline - Date.now()))) return;\n } catch (error) {\n reason = probeReason(error, reason);\n }\n await delay(Math.min(50, Math.max(0, deadline - Date.now())));\n }\n throw new Error(\n `${description} timed out after ${timeoutMs}ms: ${reason ?? \"condition stayed false\"}`,\n );\n }\n\n async goto(path: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`goto ${path}`, async () => {\n const url = new URL(path, this.origin).href;\n const before = this.loads;\n const navigation = await this.cdp.command(\"Page.navigate\", { url }, timeoutMs);\n if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);\n await this.poll(\n `Navigation to ${url}`,\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : \"true\"}`,\n remaining,\n ),\n timeoutMs,\n );\n });\n }\n\n async waitFor(options: WaitOptions): Promise<void> {\n if (options.selector === undefined && options.text === undefined && options.url === undefined)\n throw new Error(\"waitFor requires a selector, text, or URL\");\n await this.step(`waitFor ${JSON.stringify(options)}`, () =>\n this.poll(\n \"waitFor \" + JSON.stringify(options),\n (remaining) =>\n this.evaluate<boolean>(\n `(() => {\n const options = ${JSON.stringify(options)};\n const element = options.selector ? document.querySelector(options.selector) : document.body;\n if (!element) return false;\n if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }\n return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&\n (options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);\n })()`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n ),\n );\n }\n\n /** Arms the document/URL comparison BEFORE invoking the navigation action,\n * covering full loads, reloads, hash changes and client-side routing.\n */\n async waitForNavigation(\n action: () => Promise<unknown>,\n options: { url?: string; timeoutMs?: number } = {},\n ): Promise<void> {\n await this.step(\"waitForNavigation\", async () => {\n const before = await this.evaluate<{ href: string; timeOrigin: number }>(\n \"({href:location.href,timeOrigin:performance.timeOrigin})\",\n );\n await action();\n await this.poll(\n \"Navigation\",\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : \"true\"}`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n );\n });\n }\n\n async click(selector: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`click ${selector}`, async () => {\n const deadline = Date.now() + timeoutMs;\n let previous: ClickBox | undefined;\n let reason: string | undefined;\n while (Date.now() < deadline) {\n const remaining = () => Math.max(1, deadline - Date.now());\n let box: ClickBox;\n try {\n box = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = probeReason(error, reason);\n previous = undefined;\n await delay(25);\n continue;\n }\n reason = box.reason || \"target box is moving\";\n if (previous && boxesStable(previous, box)) {\n // Hover can move menus and overlays. Re-measure and hit-test after it.\n let hovered: ClickBox;\n try {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n { type: \"mouseMoved\", x: box.x, y: box.y },\n remaining(),\n );\n hovered = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = probeReason(error, reason);\n previous = undefined;\n await delay(Math.min(25, remaining()));\n continue;\n }\n if (Date.now() < deadline && boxesStable(box, hovered)) {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mousePressed\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 1,\n clickCount: 1,\n },\n remaining(),\n );\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mouseReleased\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 0,\n clickCount: 1,\n },\n remaining(),\n );\n return;\n }\n reason = hovered.reason || \"hover changed the target box\";\n previous = undefined;\n } else previous = box;\n await delay(Math.min(50, remaining()));\n }\n throw new Error(\n `Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason ?? \"layout has not settled\"}`,\n );\n });\n }\n\n async fill(selector: string, value: string): Promise<void> {\n await this.step(`fill ${selector}`, async () => {\n await this.click(selector);\n // The native value setter works with controlled inputs; input/change are\n // emitted after clearing. CDP insertText then performs actual text input.\n await this.evaluate(`(() => {\n const element=document.querySelector(${JSON.stringify(selector)});\n if (element !== document.activeElement) throw new Error('Fill target lost focus');\n if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {\n const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;\n const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;\n if (!setter) throw new Error('Fill target has no native value setter');\n setter.call(element,'');\n } else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';\n else throw new Error('Fill target must be an editable input, textarea or contenteditable');\n element.dispatchEvent(new Event('input',{bubbles:true}));\n })()`);\n await this.evaluate(\n `document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error(\"Fill target lost focus after input\"); })()`,\n );\n if (value) await this.cdp.command(\"Input.insertText\", { text: value });\n await this.evaluate(\n \"document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))\",\n );\n });\n }\n\n /** Appends Unicode text through the browser's text-input path. */\n async type(selector: string, text: string): Promise<void> {\n await this.step(`type ${selector}`, async () => {\n await this.click(selector);\n const needsEndKey = await this.evaluate<boolean>(\n `(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== \"number\") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`,\n );\n // Email/number inputs do not expose selectionRange; use their native\n // keyboard editing path instead of throwing on setSelectionRange.\n if (needsEndKey) {\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyDown\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyUp\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n }\n await this.cdp.command(\"Input.insertText\", { text });\n });\n }\n\n async setViewport(viewport: Viewport): Promise<void> {\n if (\n !/^[a-z0-9_-]+$/i.test(viewport.name) ||\n !Number.isInteger(viewport.width) ||\n !Number.isInteger(viewport.height) ||\n viewport.width <= 0 ||\n viewport.height <= 0\n )\n throw new Error(\"Viewport requires a safe name and positive integer width/height\");\n await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {\n await this.cdp.command(\"Emulation.setDeviceMetricsOverride\", {\n width: viewport.width,\n height: viewport.height,\n deviceScaleFactor: 1,\n mobile: viewport.mobile ?? false,\n });\n this.viewport = { ...viewport };\n await this.evaluate(\n \"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))\",\n );\n });\n }\n\n /** Inject the installed axe distribution through Runtime.evaluate on every scan.\n * API: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axerun\n * No CDN, DOM script element, or application dependency is required.\n */\n /**\n * Measures the inline payload of the document the page currently shows and\n * records it under `evidence.payloads`. Measure after a full load: a client\n * navigation replaces the data it reads. Pass `limit` to mark an overage.\n */\n async measurePayload(options: { limit?: number } = {}): Promise<PayloadEvidence> {\n return this.step(`measurePayload ${this.viewport.name}`, async () => {\n const measured = await this.evaluate<Pick<PayloadEvidence, \"url\" | \"data\" | \"routes\">>(\n `(() => {\n const bytes = (id) => {\n const node = document.getElementById(id);\n return node === null ? 0 : new TextEncoder().encode(node.textContent || \"\").length;\n };\n return { url: location.href, data: bytes(\"__WARLOCK_DATA__\"), routes: bytes(\"__WARLOCK_NAMED_API_ROUTES__\") };\n })()`,\n 10_000,\n );\n const total = measured.data + measured.routes;\n const report: PayloadEvidence = {\n at: new Date().toISOString(),\n viewport: { ...this.viewport },\n ...measured,\n total,\n ...(options.limit === undefined\n ? {}\n : { limit: options.limit, overBudget: total > options.limit }),\n };\n (this.evidence.payloads ??= []).push(report);\n return report;\n });\n }\n\n async assertAccessible(options: AccessibilityOptions = {}): Promise<AccessibilityEvidence> {\n return this.step(`assertAccessible ${this.viewport.name}`, async () => {\n const source = await readFile(\n createRequire(import.meta.url).resolve(\"axe-core/axe.min.js\"),\n \"utf8\",\n );\n const timeout = options.timeoutMs ?? 30_000;\n await this.evaluate(source, timeout);\n const result = await this.evaluate<\n Pick<AccessibilityEvidence, \"url\" | \"violations\" | \"incomplete\">\n >(\n `(async () => {\n const selector = ${JSON.stringify(options.selector ?? null)};\n const context = selector === null ? document : document.querySelector(selector);\n if (!context) throw new Error('Accessibility scan selector did not match: ' + selector);\n const results = await axe.run(context, ${JSON.stringify(options.tags ? { runOnly: { type: \"tag\", values: options.tags } } : {})});\n const summarize = entries => entries.map(({id, impact, help, helpUrl, nodes}) => ({\n id, impact, help, helpUrl, nodes: nodes.map(({target, failureSummary}) => ({target, failureSummary}))\n }));\n return {url: location.href, violations: summarize(results.violations), incomplete: summarize(results.incomplete)};\n })()`,\n timeout,\n );\n for (const violation of result.violations) {\n for (const node of violation.nodes) {\n node.allowed =\n options.allow?.some(\n (rule) =>\n rule.id === violation.id &&\n (rule.selector === undefined ||\n (node.target.length === 1 && node.target[0] === rule.selector)),\n ) ?? false;\n }\n }\n const report = { at: new Date().toISOString(), viewport: { ...this.viewport }, ...result };\n (this.evidence.accessibility ??= []).push(report);\n const failures = report.violations.flatMap((violation) =>\n violation.nodes\n .filter((node) => !node.allowed)\n .map((node) => `${violation.id}: ${JSON.stringify(node.target)} — ${violation.help}`),\n );\n if (failures.length) throw new Error(`Accessibility violations:\\n${failures.join(\"\\n\")}`);\n return report;\n });\n }\n\n async assertViewport(): Promise<ViewportReport> {\n return this.step(`assertViewport ${this.viewport.name}`, async () => {\n const report = await this.evaluate<ViewportReport>(`({\n requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,\n offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})\n .filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})\n .sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)\n })`);\n if (\n report.innerWidth !== report.requestedWidth ||\n report.documentWidth > report.requestedWidth ||\n report.bodyWidth > report.requestedWidth\n )\n throw new Error(\n `Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`,\n );\n return report;\n });\n }\n\n async screenshot(name: string): Promise<string> {\n if (!/^[a-z0-9_-]+$/i.test(name))\n throw new Error(\"Screenshot names must contain only letters, digits, '_' or '-'\");\n return this.step(`screenshot ${name}`, async () => {\n const file = `${String(this.evidence.screenshots.length + 1).padStart(3, \"0\")}-${name}-${this.viewport.name}.png`;\n const { data } = await this.cdp.command(\"Page.captureScreenshot\", { format: \"png\" });\n await writeFile(join(this.outputDirectory, file), Buffer.from(data, \"base64\"));\n this.evidence.screenshots.push({ name, file, viewport: { ...this.viewport } });\n return join(this.outputDirectory, file);\n });\n }\n\n async captureViewports(\n name: string,\n viewports = this.spec.viewports ?? defaultViewports,\n ): Promise<void> {\n const failures: unknown[] = [];\n const original = this.viewport;\n for (const viewport of viewports) {\n try {\n await this.setViewport(viewport);\n try {\n await this.assertViewport();\n } finally {\n await this.screenshot(name);\n }\n } catch (error) {\n failures.push(error);\n }\n }\n try {\n await this.setViewport(original);\n } catch (error) {\n failures.push(error);\n }\n if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join(\"; \"));\n }\n\n async waitForNetworkIdle(timeoutMs = 10_000): Promise<void> {\n await this.step(\"waitForNetworkIdle\", () =>\n this.poll(\n \"Network idle\",\n async () => {\n const pending = [...this.pendingRequests.values()].filter(\n (url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)),\n );\n if (pending.length) throw new Error(`Pending requests: ${pending.join(\", \")}`);\n return Date.now() - this.lastNetworkActivity >= 300;\n },\n timeoutMs,\n ),\n );\n }\n\n assertHealthy(): void {\n for (const entry of this.evidence.console)\n if (entry.level === \"error\")\n entry.allowed =\n this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;\n for (const entry of this.evidence.networkFailures)\n entry.allowed =\n this.spec.allowNetworkFailures?.some(\n (rule) =>\n matches(rule.url, entry.url) &&\n (rule.status === undefined || rule.status === entry.status) &&\n (rule.errorText === undefined || matches(rule.errorText, entry.errorText ?? \"\")),\n ) ?? false;\n const consoleErrors = this.evidence.console.filter(\n (entry) => entry.level === \"error\" && !entry.allowed,\n );\n const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);\n if (consoleErrors.length || networkErrors.length)\n throw new Error(\n `Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\\n${JSON.stringify({ console: consoleErrors, network: networkErrors })}`,\n );\n }\n}\n"],"mappings":"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"}
|
package/esm/e2e/types.d.mts
CHANGED
|
@@ -108,6 +108,23 @@ export type AccessibilityEvidence = {
|
|
|
108
108
|
violations: AccessibilityViolation[];
|
|
109
109
|
incomplete: AccessibilityViolation[];
|
|
110
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* The inline payload of one loaded document, in bytes as the HTML carries it:
|
|
113
|
+
* the hydration data script and the browser route table. `limit` and
|
|
114
|
+
* `overBudget` are present when the caller passed a limit to `measurePayload`.
|
|
115
|
+
*/
|
|
116
|
+
export type PayloadEvidence = {
|
|
117
|
+
at: string;
|
|
118
|
+
url: string;
|
|
119
|
+
viewport: Viewport;
|
|
120
|
+
/** The `__WARLOCK_DATA__` script: page, layout and app data, translations, session. */
|
|
121
|
+
data: number;
|
|
122
|
+
/** The `__WARLOCK_NAMED_API_ROUTES__` script: the page's browser route table. */
|
|
123
|
+
routes: number;
|
|
124
|
+
total: number;
|
|
125
|
+
limit?: number;
|
|
126
|
+
overBudget?: boolean;
|
|
127
|
+
};
|
|
111
128
|
export type E2EEvidence = {
|
|
112
129
|
schemaVersion: 1;
|
|
113
130
|
startedAt: string;
|
|
@@ -123,6 +140,7 @@ export type E2EEvidence = {
|
|
|
123
140
|
viewport: Viewport;
|
|
124
141
|
}[];
|
|
125
142
|
accessibility?: AccessibilityEvidence[];
|
|
143
|
+
payloads?: PayloadEvidence[];
|
|
126
144
|
processes: ProcessEvidence[];
|
|
127
145
|
errors: string[];
|
|
128
146
|
};
|
package/esm/e2e/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"mappings":";;YAEY,oBAAoB,uBAAuB;YAC3C,mBAAmB;YACnB;EAAa;EAAc;EAAe;EAAgB;;qBACzD,2BAA2B;YAK5B;;EAEV;EACA;;EAEA;EACA;;EAEA;;YAEU;EACV;;EAEA,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,MAAM,2BAA2B,SAAS,mBAAmB;;EAE7D;;EAEA,WAAW,SAAS,0BAA0B;;EAE9C;EACA;;EAEA;EACA;;YAEU;EACV;;EAEA;;;;;;EAMA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EAAiB;EAAY;EAAe;EAAc;;YAC1D;EACV;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;;EAEV;;EAEA;;EAEA;IAAU;IAAY;;EACtB;;YAEU;EACV;EACA;EACA;EACA;EACA;IAAS;IAA+B;IAAyB;;;YAEvD;EACV;EACA;EACA,UAAU;EACV,YAAY;EACZ,YAAY;;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"mappings":";;YAEY,oBAAoB,uBAAuB;YAC3C,mBAAmB;YACnB;EAAa;EAAc;EAAe;EAAgB;;qBACzD,2BAA2B;YAK5B;;EAEV;EACA;;EAEA;EACA;;EAEA;;YAEU;EACV;;EAEA,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,QAAQ,YAAY,SAAS,mBAAmB;;EAEhD,MAAM,2BAA2B,SAAS,mBAAmB;;EAE7D;;EAEA,WAAW,SAAS,0BAA0B;;EAE9C;EACA;;EAEA;EACA;;YAEU;EACV;;EAEA;;;;;;EAMA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;EAAiB;EAAY;EAAe;EAAc;;YAC1D;EACV;EACA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA;;YAEU;;EAEV;;EAEA;;EAEA;IAAU;IAAY;;EACtB;;YAEU;EACV;EACA;EACA;EACA;EACA;IAAS;IAA+B;IAAyB;;;YAEvD;EACV;EACA;EACA,UAAU;EACV,YAAY;EACZ,YAAY;;;;;;;YAOF;EACV;EACA;EACA,UAAU;;EAEV;;EAEA;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA,OAAO;EACP,SAAS;EACT,iBAAiB;EACjB;IAAe;IAAc;IAAc,UAAU;;EACrD,gBAAgB;EAChB,WAAW;EACX,WAAW;EACX;;YAEU;EAAqB,KAAK;EAAS;EAAiB,YAAY;;YAChE,aAAa;EACvB,MAAM;EACN,UAAU;EACV;EACA,QAAQ;EACR,OAAO,GAAG,cAAc,cAAc,QAAQ,OAAO,QAAQ;;YAEnD;EACV,KAAK;EACL,UAAU;EACV,qBAAqB;EACrB,qBAAqB;EACrB,uBAAuB;;EAEvB,uBAAuB;EACvB;EACA,MAAM,SAAS,eAAe;;YAEpB;EACV;EACA;EACA;EACA,SAAS;;wBAGK,UAAU,MAAM,UAAU"}
|
package/esm/e2e/types.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"sourcesContent":["import type { BrowserPage } from \"./page\";\n\nexport type Command = readonly [executable: string, ...args: string[]];\nexport type Pattern = string | RegExp;\nexport type Viewport = { name: string; width: number; height: number; mobile?: boolean };\nexport const defaultViewports: readonly Viewport[] = [\n { name: \"desktop\", width: 1280, height: 800 },\n { name: \"phone\", width: 390, height: 844, mobile: true },\n];\n\nexport type FixtureContext = {\n /** Disposable copy of the app. All commands run here. */\n directory: string;\n dataRoot: string;\n /** OS-selected ephemeral port, reserved until immediately before app startup. */\n port: number;\n url: string;\n /** Write an actual port here when the app binds port 0 itself. */\n portFile: string;\n};\nexport type AppFixtureOptions = {\n directory?: string;\n /** Optional build, executed inside the disposable copy before start. */\n build?: Command | ((fixture: FixtureContext) => Command);\n /** Defaults to [process.execPath, \"dist/app.js\"]. Must stay in the foreground. */\n start?: Command | ((fixture: FixtureContext) => Command);\n /** Only these app variables are added to the isolated environment. */\n env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);\n /** Directories prepended to PATH, for fixture-owned fake engines/binaries. */\n path?: string[];\n /** Prepare data/fake binaries in the disposable fixture before build/start. */\n prepare?: (fixture: FixtureContext) => void | Promise<void>;\n /** Additional relative directories to omit when copying app files. */\n exclude?: string[];\n readyPath?: string;\n /** If true, await portFile instead of using the preallocated port. */\n portFromFile?: boolean;\n startupTimeoutMs?: number;\n};\nexport type BrowserOptions = {\n binary?: string;\n /** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */\n args?: string[];\n /**\n * How long to wait for the browser to report its DevTools endpoint.\n * A first launch with a fresh profile on a busy machine can be slow.\n * @default 30_000\n */\n startupTimeoutMs?: number;\n};\nexport type NetworkFailure = {\n at: string;\n url: string;\n status?: number;\n errorText?: string;\n canceled?: boolean;\n allowed?: boolean;\n};\nexport type ConsoleEntry = { at: string; level: string; text: string; allowed?: boolean };\nexport type StepEvidence = {\n name: string;\n startedAt: string;\n durationMs: number;\n status: \"passed\" | \"failed\";\n error?: string;\n};\nexport type ProcessEvidence = {\n role: string;\n pid: number;\n childPid?: number;\n command: string[];\n ownerFile: string;\n stopped?: boolean;\n};\nexport type AccessibilityOptions = {\n /** CSS selector limiting the scan; defaults to the document. */\n selector?: string;\n /** axe rule tags, for example wcag2a and wcag2aa. Defaults to all enabled rules. */\n tags?: string[];\n /** Exact rule ID, optionally limited to an exact reported selector. */\n allow?: { id: string; selector?: string }[];\n timeoutMs?: number;\n};\nexport type AccessibilityViolation = {\n id: string;\n impact: string | null;\n help: string;\n helpUrl: string;\n nodes: { target: (string | string[])[]; failureSummary?: string; allowed?: boolean }[];\n};\nexport type AccessibilityEvidence = {\n at: string;\n url: string;\n viewport: Viewport;\n violations: AccessibilityViolation[];\n incomplete: AccessibilityViolation[];\n};\nexport type E2EEvidence = {\n schemaVersion: 1;\n startedAt: string;\n durationMs: number;\n status: \"running\" | \"passed\" | \"failed\";\n url?: string;\n steps: StepEvidence[];\n console: ConsoleEntry[];\n networkFailures: NetworkFailure[];\n screenshots: { name: string; file: string; viewport: Viewport }[];\n accessibility?: AccessibilityEvidence[];\n processes: ProcessEvidence[];\n errors: string[];\n};\nexport type NetworkAllowance = { url: Pattern; status?: number; errorText?: Pattern };\nexport type E2EContext = FixtureContext & {\n page: BrowserPage;\n evidence: E2EEvidence;\n outputDirectory: string;\n signal: AbortSignal;\n step: <T>(name: string, action: () => Promise<T>) => Promise<T>;\n};\nexport type E2ESpec = {\n app: AppFixtureOptions;\n browser?: BrowserOptions;\n viewports?: readonly Viewport[];\n allowConsoleErrors?: Pattern[];\n allowNetworkFailures?: NetworkAllowance[];\n /** Long-lived HTTP requests excluded from the final network-idle barrier. */\n allowPendingRequests?: Pattern[];\n timeoutMs?: number;\n run: (context: E2EContext) => Promise<void>;\n};\nexport type RunE2EOptions = {\n browser?: string;\n path?: string[];\n outputDirectory?: string;\n signal?: AbortSignal;\n};\n\nexport function defineE2E(spec: E2ESpec): E2ESpec {\n return spec;\n}\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\nexport function matches(pattern: Pattern, value: string): boolean {\n // A fresh regexp avoids stateful /g and /y allowlists changing their verdict.\n return typeof pattern === \"string\"\n ? value.includes(pattern)\n : new RegExp(pattern.source, pattern.flags).test(value);\n}\n"],"mappings":";AAKA,MAAa,mBAAwC,CACnD;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,GAC5C;CAAE,MAAM;CAAS,OAAO;CAAK,QAAQ;CAAK,QAAQ;AAAK,CACzD;
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/types.ts"],"sourcesContent":["import type { BrowserPage } from \"./page\";\n\nexport type Command = readonly [executable: string, ...args: string[]];\nexport type Pattern = string | RegExp;\nexport type Viewport = { name: string; width: number; height: number; mobile?: boolean };\nexport const defaultViewports: readonly Viewport[] = [\n { name: \"desktop\", width: 1280, height: 800 },\n { name: \"phone\", width: 390, height: 844, mobile: true },\n];\n\nexport type FixtureContext = {\n /** Disposable copy of the app. All commands run here. */\n directory: string;\n dataRoot: string;\n /** OS-selected ephemeral port, reserved until immediately before app startup. */\n port: number;\n url: string;\n /** Write an actual port here when the app binds port 0 itself. */\n portFile: string;\n};\nexport type AppFixtureOptions = {\n directory?: string;\n /** Optional build, executed inside the disposable copy before start. */\n build?: Command | ((fixture: FixtureContext) => Command);\n /** Defaults to [process.execPath, \"dist/app.js\"]. Must stay in the foreground. */\n start?: Command | ((fixture: FixtureContext) => Command);\n /** Only these app variables are added to the isolated environment. */\n env?: Record<string, string> | ((fixture: FixtureContext) => Record<string, string>);\n /** Directories prepended to PATH, for fixture-owned fake engines/binaries. */\n path?: string[];\n /** Prepare data/fake binaries in the disposable fixture before build/start. */\n prepare?: (fixture: FixtureContext) => void | Promise<void>;\n /** Additional relative directories to omit when copying app files. */\n exclude?: string[];\n readyPath?: string;\n /** If true, await portFile instead of using the preallocated port. */\n portFromFile?: boolean;\n startupTimeoutMs?: number;\n};\nexport type BrowserOptions = {\n binary?: string;\n /** Additional Chrome flags, e.g. --no-sandbox in a controlled root container. */\n args?: string[];\n /**\n * How long to wait for the browser to report its DevTools endpoint.\n * A first launch with a fresh profile on a busy machine can be slow.\n * @default 30_000\n */\n startupTimeoutMs?: number;\n};\nexport type NetworkFailure = {\n at: string;\n url: string;\n status?: number;\n errorText?: string;\n canceled?: boolean;\n allowed?: boolean;\n};\nexport type ConsoleEntry = { at: string; level: string; text: string; allowed?: boolean };\nexport type StepEvidence = {\n name: string;\n startedAt: string;\n durationMs: number;\n status: \"passed\" | \"failed\";\n error?: string;\n};\nexport type ProcessEvidence = {\n role: string;\n pid: number;\n childPid?: number;\n command: string[];\n ownerFile: string;\n stopped?: boolean;\n};\nexport type AccessibilityOptions = {\n /** CSS selector limiting the scan; defaults to the document. */\n selector?: string;\n /** axe rule tags, for example wcag2a and wcag2aa. Defaults to all enabled rules. */\n tags?: string[];\n /** Exact rule ID, optionally limited to an exact reported selector. */\n allow?: { id: string; selector?: string }[];\n timeoutMs?: number;\n};\nexport type AccessibilityViolation = {\n id: string;\n impact: string | null;\n help: string;\n helpUrl: string;\n nodes: { target: (string | string[])[]; failureSummary?: string; allowed?: boolean }[];\n};\nexport type AccessibilityEvidence = {\n at: string;\n url: string;\n viewport: Viewport;\n violations: AccessibilityViolation[];\n incomplete: AccessibilityViolation[];\n};\n/**\n * The inline payload of one loaded document, in bytes as the HTML carries it:\n * the hydration data script and the browser route table. `limit` and\n * `overBudget` are present when the caller passed a limit to `measurePayload`.\n */\nexport type PayloadEvidence = {\n at: string;\n url: string;\n viewport: Viewport;\n /** The `__WARLOCK_DATA__` script: page, layout and app data, translations, session. */\n data: number;\n /** The `__WARLOCK_NAMED_API_ROUTES__` script: the page's browser route table. */\n routes: number;\n total: number;\n limit?: number;\n overBudget?: boolean;\n};\nexport type E2EEvidence = {\n schemaVersion: 1;\n startedAt: string;\n durationMs: number;\n status: \"running\" | \"passed\" | \"failed\";\n url?: string;\n steps: StepEvidence[];\n console: ConsoleEntry[];\n networkFailures: NetworkFailure[];\n screenshots: { name: string; file: string; viewport: Viewport }[];\n accessibility?: AccessibilityEvidence[];\n payloads?: PayloadEvidence[];\n processes: ProcessEvidence[];\n errors: string[];\n};\nexport type NetworkAllowance = { url: Pattern; status?: number; errorText?: Pattern };\nexport type E2EContext = FixtureContext & {\n page: BrowserPage;\n evidence: E2EEvidence;\n outputDirectory: string;\n signal: AbortSignal;\n step: <T>(name: string, action: () => Promise<T>) => Promise<T>;\n};\nexport type E2ESpec = {\n app: AppFixtureOptions;\n browser?: BrowserOptions;\n viewports?: readonly Viewport[];\n allowConsoleErrors?: Pattern[];\n allowNetworkFailures?: NetworkAllowance[];\n /** Long-lived HTTP requests excluded from the final network-idle barrier. */\n allowPendingRequests?: Pattern[];\n timeoutMs?: number;\n run: (context: E2EContext) => Promise<void>;\n};\nexport type RunE2EOptions = {\n browser?: string;\n path?: string[];\n outputDirectory?: string;\n signal?: AbortSignal;\n};\n\nexport function defineE2E(spec: E2ESpec): E2ESpec {\n return spec;\n}\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\nexport function matches(pattern: Pattern, value: string): boolean {\n // A fresh regexp avoids stateful /g and /y allowlists changing their verdict.\n return typeof pattern === \"string\"\n ? value.includes(pattern)\n : new RegExp(pattern.source, pattern.flags).test(value);\n}\n"],"mappings":";AAKA,MAAa,mBAAwC,CACnD;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,GAC5C;CAAE,MAAM;CAAS,OAAO;CAAK,QAAQ;CAAK,QAAQ;AAAK,CACzD;AAmJA,SAAgB,UAAU,MAAwB;CAChD,OAAO;AACT;AACA,SAAgB,aAAa,OAAwB;CACnD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AACA,SAAgB,QAAQ,SAAkB,OAAwB;CAEhE,OAAO,OAAO,YAAY,WACtB,MAAM,SAAS,OAAO,IACtB,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,KAAK,KAAK;AAC1D"}
|
|
@@ -657,6 +657,22 @@ export declare const ERROR_CODES: {
|
|
|
657
657
|
readonly fix: "Assign controller.responseSchema with a contract for each response status.";
|
|
658
658
|
readonly skill: "warlock-check";
|
|
659
659
|
};
|
|
660
|
+
readonly WL9019: {
|
|
661
|
+
readonly code: "WL9019";
|
|
662
|
+
readonly area: "convention";
|
|
663
|
+
readonly category: "code";
|
|
664
|
+
readonly title: "A browser route call takes a name that cannot be read statically";
|
|
665
|
+
readonly fix: 'Pass a string literal (or a template with a static prefix), or declare the names the page uses with `config.apiRoutes` in its setup file (`"orders.*"` covers a prefix).';
|
|
666
|
+
readonly skill: "warlock-check";
|
|
667
|
+
};
|
|
668
|
+
readonly WL9020: {
|
|
669
|
+
readonly code: "WL9020";
|
|
670
|
+
readonly area: "convention";
|
|
671
|
+
readonly category: "code";
|
|
672
|
+
readonly title: "Browser code references a named API route that is clientVisible: false";
|
|
673
|
+
readonly fix: "Call the endpoint from the server instead, or remove `clientVisible: false` from the route if the browser must reach it.";
|
|
674
|
+
readonly skill: "warlock-check";
|
|
675
|
+
};
|
|
660
676
|
readonly WL9101: {
|
|
661
677
|
readonly code: "WL9101";
|
|
662
678
|
readonly area: "convention";
|
|
@@ -681,6 +697,14 @@ export declare const ERROR_CODES: {
|
|
|
681
697
|
readonly fix: "Add a CHANGELOG.md with a `## x.y.z` heading for the current version.";
|
|
682
698
|
readonly skill: "warlock-doctor";
|
|
683
699
|
};
|
|
700
|
+
readonly WL9104: {
|
|
701
|
+
readonly code: "WL9104";
|
|
702
|
+
readonly area: "convention";
|
|
703
|
+
readonly category: "code";
|
|
704
|
+
readonly title: "Translation key is computed at runtime, so the page cannot ship only the keys it uses";
|
|
705
|
+
readonly fix: 'Declare the keys or prefixes the page renders in its setup file, for example `export const config = { translations: ["statuses.*", "members.title"] }`. Until then the page ships every translation group.';
|
|
706
|
+
readonly skill: "use-localization";
|
|
707
|
+
};
|
|
684
708
|
};
|
|
685
709
|
/** Every registered code, as a union. */
|
|
686
710
|
export type WarlockErrorCode = keyof typeof ERROR_CODES;
|