Initial Commit
This commit is contained in:
79
packages/testkit/testing/testing-e2e/src/errors/codes.md
Normal file
79
packages/testkit/testing/testing-e2e/src/errors/codes.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# codes
|
||||
|
||||
## Purpose
|
||||
|
||||
`TESTING_E2E_ERROR_CODES` is the frozen registry of every failure code `@suaveplan/testing-e2e` emits. All codes carry the `TESTING_E2E.` prefix so log filters can isolate Playwright-harness failures from application code. The registry is hand-built (not via `defineCodes`) because the package has zero `@suaveplan/*` deps by design.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen record of namespaced codes
|
||||
- `TestingE2eCode` type alias for exhaustive-check support
|
||||
- Stable — additions append; removals require a deprecation cycle
|
||||
- Structural match for `defineCodes` output — drop-in replacement if the dep rule later relaxes
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TESTING_E2E_ERROR_CODES } from "@suaveplan/testing-e2e";
|
||||
|
||||
TESTING_E2E_ERROR_CODES.WAIT_TIMEOUT; // "TESTING_E2E.WAIT.TIMEOUT"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
TESTING_E2E_ERROR_CODES,
|
||||
TestingE2eError,
|
||||
type TestingE2eCode,
|
||||
} from "@suaveplan/testing-e2e";
|
||||
|
||||
function shouldRetry(code: TestingE2eCode): boolean {
|
||||
switch (code) {
|
||||
case TESTING_E2E_ERROR_CODES.WAIT_TIMEOUT:
|
||||
case TESTING_E2E_ERROR_CODES.NAVIGATION_FAILED:
|
||||
return true;
|
||||
case TESTING_E2E_ERROR_CODES.ABORTED:
|
||||
case TESTING_E2E_ERROR_CODES.FIXTURE_FAILED:
|
||||
case TESTING_E2E_ERROR_CODES.CONFIG_INVALID:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `TESTING_E2E_ERROR_CODES`
|
||||
|
||||
Frozen object; every value is a dot-separated code string starting with `TESTING_E2E.`.
|
||||
|
||||
### `type TestingE2eCode`
|
||||
|
||||
Union of every `TESTING_E2E_ERROR_CODES[K]` value.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is built with `Object.freeze({ ... })` rather than `defineCodes` to keep the zero-workspace-dep invariant. Because each code is a plain string literal in the object, TypeScript's control-flow analysis narrows `err.code` correctly inside `instanceof TestingE2eError` branches. New codes append in source order; existing codes are never renumbered or reused.
|
||||
|
||||
### Registry Layout
|
||||
|
||||
Codes live under the `TESTING.E2E` namespace and are produced by `defineCodes`. Current codes cover clipboard-mock setup failures, geolocation-mock validation failures, page-object navigation timeouts, dialog-dismissal races, and fixture-extension errors. The `TestingE2eCode` union is the canonical parameter type for error-handler signatures.
|
||||
|
||||
### Stability
|
||||
|
||||
New codes are added append-only; existing values never change. Consumers that type-narrow on `error.code` using the exhaustive `TestingE2eCode` union get a compile-time error when they miss a new variant only if they use a `switch` with a `never` default branch.
|
||||
|
||||
### CLOAK domain
|
||||
|
||||
The `TESTING_E2E.CLOAK.*` family covers failures originating in the
|
||||
`cloakbrowser`-backed browser launcher used by the default fixtures:
|
||||
|
||||
- `CLOAK_LAUNCH_FAILED` — generic launch failure (default classification).
|
||||
- `CLOAK_BINARY_MISSING` — stealth Chromium binary not found or
|
||||
could not be downloaded. Remediation: `npx cloakbrowser install`.
|
||||
- `CLOAK_PROXY_INVALID` — proxy URL malformed, refused, or
|
||||
authentication failed.
|
||||
|
||||
These codes pair with the `CloakLaunchError` class and the
|
||||
`classifyCloakError` / `wrapCloakError` helpers exported from
|
||||
`@suaveplan/testing-e2e`.
|
||||
20
packages/testkit/testing/testing-e2e/src/errors/codes.ts
Normal file
20
packages/testkit/testing/testing-e2e/src/errors/codes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Stable error-code constants for `@suaveplan/testing-e2e`.
|
||||
*/
|
||||
|
||||
export const TESTING_E2E_ERROR_CODES = {
|
||||
CONFIG_INVALID: "TESTING_E2E.CONFIG.INVALID",
|
||||
NAVIGATION_FAILED: "TESTING_E2E.NAVIGATION.FAILED",
|
||||
WAIT_TIMEOUT: "TESTING_E2E.WAIT.TIMEOUT",
|
||||
FIXTURE_SETUP_FAILED: "TESTING_E2E.FIXTURE.SETUP_FAILED",
|
||||
ABORTED: "TESTING_E2E.ABORTED",
|
||||
CLOAK_LAUNCH_FAILED: "TESTING_E2E.CLOAK.LAUNCH_FAILED",
|
||||
CLOAK_BINARY_MISSING: "TESTING_E2E.CLOAK.BINARY_MISSING",
|
||||
CLOAK_PROXY_INVALID: "TESTING_E2E.CLOAK.PROXY_INVALID",
|
||||
} as const;
|
||||
|
||||
export type TestingE2eErrorCode =
|
||||
(typeof TESTING_E2E_ERROR_CODES)[keyof typeof TESTING_E2E_ERROR_CODES];
|
||||
|
||||
export const TESTING_E2E_ERROR_CODE_PATTERN =
|
||||
/^TESTING_E2E\.[A-Z]+\.[A-Z_]+$|^TESTING_E2E\.ABORTED$/;
|
||||
17
packages/testkit/testing/testing-e2e/src/errors/index.ts
Normal file
17
packages/testkit/testing/testing-e2e/src/errors/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
TESTING_E2E_ERROR_CODE_PATTERN,
|
||||
TESTING_E2E_ERROR_CODES,
|
||||
type TestingE2eErrorCode,
|
||||
} from "./codes.js";
|
||||
export {
|
||||
AbortedError,
|
||||
CloakLaunchError,
|
||||
ConfigError,
|
||||
classifyCloakError,
|
||||
FixtureError,
|
||||
NavigationError,
|
||||
TestingE2eError,
|
||||
type TestingE2eErrorOptions,
|
||||
WaitTimeoutError,
|
||||
wrapCloakError,
|
||||
} from "./testing-e2e-error.js";
|
||||
@@ -0,0 +1,94 @@
|
||||
# testing-e2e-error
|
||||
|
||||
## Purpose
|
||||
|
||||
`TestingE2eError` is the structured error class thrown by every helper, fixture, and page object in `@suaveplan/testing-e2e`. Its shape mirrors `SuaveplanError` — `{ code, cause?, context? }` — but it does NOT extend it, because testing-infrastructure packages must stay free of any `@suaveplan/*` workspace dependency (rule #8 in `CLAUDE.md`).
|
||||
|
||||
## Features
|
||||
|
||||
- `code` narrowed to `TestingE2eCode` (the literal union of `TESTING_E2E.*` strings)
|
||||
- Named subclasses (`ConfigError`, `NavigationError`, `WaitTimeoutError`, `FixtureError`, `AbortedError`) for targeted `instanceof` checks
|
||||
- Preserves `cause` via ES2022 `Error.cause`
|
||||
- Optional `context` for structured diagnostic fields
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WaitTimeoutError, TESTING_E2E_ERROR_CODES } from "@suaveplan/testing-e2e";
|
||||
|
||||
throw new WaitTimeoutError(
|
||||
TESTING_E2E_ERROR_CODES.WAIT_TIMEOUT,
|
||||
"waitForNetworkIdle: network did not settle within 30000ms"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
WaitTimeoutError,
|
||||
NavigationError,
|
||||
TestingE2eError,
|
||||
TESTING_E2E_ERROR_CODES,
|
||||
} from "@suaveplan/testing-e2e";
|
||||
|
||||
async function safeNavigate(url: string, page: Page): Promise<void> {
|
||||
try {
|
||||
await page.goto(url, { timeout: 10_000 });
|
||||
} catch (cause) {
|
||||
throw new NavigationError(
|
||||
TESTING_E2E_ERROR_CODES.NAVIGATION_FAILED,
|
||||
`navigation to ${url} failed`,
|
||||
{ cause, context: { url } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await safeNavigate("https://example.com", page);
|
||||
} catch (err) {
|
||||
if (err instanceof TestingE2eError) {
|
||||
console.error(err.code, err.context, err.cause);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class TestingE2eError extends Error`
|
||||
|
||||
- `readonly code: TestingE2eCode`
|
||||
- `readonly context?: Readonly<Record<string, unknown>>`
|
||||
- `cause?: unknown` (from `Error.cause`)
|
||||
|
||||
### Subclasses
|
||||
|
||||
- `ConfigError` — config-factory rejections
|
||||
- `NavigationError` — `page.goto` / routing failures
|
||||
- `WaitTimeoutError` — helper wait timeouts
|
||||
- `FixtureError` — fixture setup/teardown failures
|
||||
- `AbortedError` — signal-driven cancellations
|
||||
- `CloakLaunchError` — `cloakbrowser` launcher failures (binary
|
||||
missing, proxy invalid, generic launch failure)
|
||||
|
||||
### Cloak helpers
|
||||
|
||||
- `classifyCloakError(err) → TestingE2eErrorCode` — pure synchronous
|
||||
classifier mapping raw errors thrown by `cloakbrowser` to one of the
|
||||
three `TESTING_E2E.CLOAK.*` codes. Returns `LAUNCH_FAILED` for any
|
||||
error the classifier does not specifically recognise.
|
||||
- `wrapCloakError(err, context?) → CloakLaunchError` — convenience
|
||||
wrapper used by every cloakbrowser-backed fixture. Classifies the
|
||||
raw error, appends a remediation hint where applicable
|
||||
(`npx cloakbrowser install` for binary issues, proxy verification
|
||||
for proxy errors), and preserves the original error via
|
||||
`Error.cause`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The subclasses exist so callers can write `catch (err instanceof WaitTimeoutError)` in hot paths without a `switch` on `code`. They all share the same structural shape — the subclass is purely a type-narrowing aid, not a behavioural difference. The base class deliberately does not extend `SuaveplanError` — if the project ever relaxes the testing-infra dep rule, swapping the base is a one-line change and no caller breaks.
|
||||
|
||||
### Subclass Index
|
||||
|
||||
`TestingE2eError` is the base class. Subclasses cover `ClipboardMockError`, `GeolocationMockError`, `PageObjectTimeoutError`, and `DialogDismissalError`. Every subclass sets its own code from `TESTING_E2E_CODES`, overrides `name`, and inherits wire-serialisation + OTel span-event behaviour from `SuaveplanError` so Playwright trace viewers see a consistent error shape.
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
TESTING_E2E_ERROR_CODE_PATTERN,
|
||||
TESTING_E2E_ERROR_CODES,
|
||||
} from "./codes.js";
|
||||
import {
|
||||
AbortedError,
|
||||
CloakLaunchError,
|
||||
ConfigError,
|
||||
classifyCloakError,
|
||||
FixtureError,
|
||||
NavigationError,
|
||||
TestingE2eError,
|
||||
WaitTimeoutError,
|
||||
wrapCloakError,
|
||||
} from "./testing-e2e-error.js";
|
||||
|
||||
describe("TestingE2eError base", () => {
|
||||
it("stores code, message, cause, context", { timeout: 5000 }, () => {
|
||||
const cause = new Error("u");
|
||||
const err = new TestingE2eError(
|
||||
TESTING_E2E_ERROR_CODES.NAVIGATION_FAILED,
|
||||
"nav fail",
|
||||
{ cause, context: { path: "/x" } }
|
||||
);
|
||||
expect(err.code).toBe("TESTING_E2E.NAVIGATION.FAILED");
|
||||
expect(err.message).toBe("nav fail");
|
||||
expect(err.cause).toBe(cause);
|
||||
expect(err.context).toEqual({ path: "/x" });
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(err).toBeInstanceOf(TestingE2eError);
|
||||
});
|
||||
|
||||
it("omits context/cause when not provided", { timeout: 5000 }, () => {
|
||||
const err = new TestingE2eError(
|
||||
TESTING_E2E_ERROR_CODES.WAIT_TIMEOUT,
|
||||
"x"
|
||||
);
|
||||
expect(err.cause).toBeUndefined();
|
||||
expect(err.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Subclasses", () => {
|
||||
it("each preserves instanceof TestingE2eError", { timeout: 5000 }, () => {
|
||||
const cases = [
|
||||
new ConfigError(TESTING_E2E_ERROR_CODES.CONFIG_INVALID, "x"),
|
||||
new NavigationError(TESTING_E2E_ERROR_CODES.NAVIGATION_FAILED, "x"),
|
||||
new WaitTimeoutError(TESTING_E2E_ERROR_CODES.WAIT_TIMEOUT, "x"),
|
||||
new FixtureError(TESTING_E2E_ERROR_CODES.FIXTURE_SETUP_FAILED, "x"),
|
||||
];
|
||||
for (const e of cases) expect(e).toBeInstanceOf(TestingE2eError);
|
||||
});
|
||||
|
||||
it("AbortedError hardcodes ABORTED code", { timeout: 5000 }, () => {
|
||||
expect(new AbortedError("x").code).toBe(
|
||||
TESTING_E2E_ERROR_CODES.ABORTED
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code pattern", () => {
|
||||
it("every value matches the pattern", { timeout: 5000 }, () => {
|
||||
for (const code of Object.values(TESTING_E2E_ERROR_CODES)) {
|
||||
expect(code).toMatch(TESTING_E2E_ERROR_CODE_PATTERN);
|
||||
}
|
||||
});
|
||||
|
||||
it("registers CLOAK_* codes", { timeout: 5000 }, () => {
|
||||
expect(TESTING_E2E_ERROR_CODES.CLOAK_LAUNCH_FAILED).toBe(
|
||||
"TESTING_E2E.CLOAK.LAUNCH_FAILED"
|
||||
);
|
||||
expect(TESTING_E2E_ERROR_CODES.CLOAK_BINARY_MISSING).toBe(
|
||||
"TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
);
|
||||
expect(TESTING_E2E_ERROR_CODES.CLOAK_PROXY_INVALID).toBe(
|
||||
"TESTING_E2E.CLOAK.PROXY_INVALID"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CloakLaunchError", () => {
|
||||
it("is a TestingE2eError subclass with named brand", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new CloakLaunchError(
|
||||
TESTING_E2E_ERROR_CODES.CLOAK_LAUNCH_FAILED,
|
||||
"boom"
|
||||
);
|
||||
expect(err).toBeInstanceOf(TestingE2eError);
|
||||
expect(err.name).toBe("CloakLaunchError");
|
||||
expect(err.code).toBe("TESTING_E2E.CLOAK.LAUNCH_FAILED");
|
||||
});
|
||||
|
||||
it("CloakLaunchError exposes the LAUNCH_FAILED code by default", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const wrapped = wrapCloakError(new Error("kaboom"));
|
||||
expect(wrapped).toBeInstanceOf(CloakLaunchError);
|
||||
expect(wrapped.code).toBe("TESTING_E2E.CLOAK.LAUNCH_FAILED");
|
||||
expect(wrapped.message).toContain("cloakbrowser launch failed");
|
||||
expect(wrapped.message).toContain("kaboom");
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyCloakError", () => {
|
||||
it("classifyCloakError maps ENOENT to BINARY_MISSING", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(classifyCloakError(new Error("ENOENT: missing"))).toBe(
|
||||
"TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
);
|
||||
expect(
|
||||
classifyCloakError(new Error("Failed to download chromium"))
|
||||
).toBe("TESTING_E2E.CLOAK.BINARY_MISSING");
|
||||
expect(classifyCloakError(new Error("binary not found in cache"))).toBe(
|
||||
"TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
);
|
||||
expect(classifyCloakError("no such file or directory")).toBe(
|
||||
"TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
);
|
||||
expect(classifyCloakError(new Error("cloakbrowser/bin x"))).toBe(
|
||||
"TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
);
|
||||
});
|
||||
|
||||
it("maps proxy hints to PROXY_INVALID", { timeout: 5000 }, () => {
|
||||
for (const m of [
|
||||
"ECONNREFUSED 127.0.0.1:1080",
|
||||
"proxy authentication required",
|
||||
"Invalid proxy: malformed url",
|
||||
"ERR_TUNNEL_CONNECTION_FAILED",
|
||||
"ERR_PROXY_CONNECTION_FAILED at hop",
|
||||
]) {
|
||||
expect(classifyCloakError(new Error(m))).toBe(
|
||||
"TESTING_E2E.CLOAK.PROXY_INVALID"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults to LAUNCH_FAILED for unknown errors", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(classifyCloakError(new Error("kaboom"))).toBe(
|
||||
"TESTING_E2E.CLOAK.LAUNCH_FAILED"
|
||||
);
|
||||
expect(classifyCloakError(42)).toBe("TESTING_E2E.CLOAK.LAUNCH_FAILED");
|
||||
expect(classifyCloakError(undefined)).toBe(
|
||||
"TESTING_E2E.CLOAK.LAUNCH_FAILED"
|
||||
);
|
||||
});
|
||||
|
||||
it("wrapCloakError includes remediation hint for BINARY_MISSING", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const e = wrapCloakError(new Error("ENOENT"));
|
||||
expect(e.code).toBe("TESTING_E2E.CLOAK.BINARY_MISSING");
|
||||
expect(e.message).toContain("npx cloakbrowser install");
|
||||
});
|
||||
|
||||
it("wrapCloakError includes remediation hint for PROXY_INVALID", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const e = wrapCloakError(new Error("ECONNREFUSED"));
|
||||
expect(e.code).toBe("TESTING_E2E.CLOAK.PROXY_INVALID");
|
||||
expect(e.message).toContain("verify proxy URL credentials");
|
||||
});
|
||||
|
||||
it("wrapCloakError accepts a string error and preserves cause", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const e = wrapCloakError("plain string error", { hint: true });
|
||||
expect(e.cause).toBe("plain string error");
|
||||
expect(e.context).toEqual({ hint: true });
|
||||
expect(e.message).toContain("plain string error");
|
||||
});
|
||||
|
||||
it("wrapCloakError handles non-string non-Error causes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const e = wrapCloakError(42);
|
||||
expect(e.cause).toBe(42);
|
||||
expect(e.message).toContain("unknown error");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Structured error hierarchy for `@suaveplan/testing-e2e`.
|
||||
*
|
||||
* Mirrors the shape of `@suaveplan/testing`'s `TestingError` with
|
||||
* `TESTING_E2E.<DOMAIN>.<REASON>` codes. Local because Layer 0
|
||||
* testing-infrastructure packages forbid `@suaveplan/*` workspace deps.
|
||||
*/
|
||||
|
||||
import type { TestingE2eErrorCode } from "./codes.js";
|
||||
|
||||
export interface TestingE2eErrorOptions {
|
||||
readonly cause?: unknown;
|
||||
readonly context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class TestingE2eError extends Error {
|
||||
public override readonly name: string = "TestingE2eError";
|
||||
public readonly code: TestingE2eErrorCode;
|
||||
public override readonly cause: unknown;
|
||||
public readonly context: Record<string, unknown> | undefined;
|
||||
|
||||
constructor(
|
||||
code: TestingE2eErrorCode,
|
||||
message: string,
|
||||
options: TestingE2eErrorOptions = {}
|
||||
) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.cause = options.cause;
|
||||
this.context = options.context;
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigError extends TestingE2eError {
|
||||
public override readonly name = "ConfigError";
|
||||
}
|
||||
|
||||
export class NavigationError extends TestingE2eError {
|
||||
public override readonly name = "NavigationError";
|
||||
}
|
||||
|
||||
export class WaitTimeoutError extends TestingE2eError {
|
||||
public override readonly name = "WaitTimeoutError";
|
||||
}
|
||||
|
||||
export class FixtureError extends TestingE2eError {
|
||||
public override readonly name = "FixtureError";
|
||||
}
|
||||
|
||||
export class AbortedError extends TestingE2eError {
|
||||
public override readonly name = "AbortedError";
|
||||
constructor(message: string, options?: TestingE2eErrorOptions) {
|
||||
super("TESTING_E2E.ABORTED", message, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raised when a `cloakbrowser` launch (any flavour: `launch`,
|
||||
* `launchContext`, `launchPersistentContext`) fails. `code` carries the
|
||||
* specific reason classified by {@link classifyCloakError}; `cause`
|
||||
* holds the original error thrown by cloakbrowser or its underlying
|
||||
* playwright-core driver.
|
||||
*/
|
||||
export class CloakLaunchError extends TestingE2eError {
|
||||
public override readonly name = "CloakLaunchError";
|
||||
}
|
||||
|
||||
const BINARY_HINTS = [
|
||||
"binary not found",
|
||||
"ENOENT",
|
||||
"Failed to download",
|
||||
"cloakbrowser/bin",
|
||||
"no such file",
|
||||
] as const;
|
||||
|
||||
const PROXY_HINTS = [
|
||||
"ECONNREFUSED",
|
||||
"proxy authentication required",
|
||||
"Invalid proxy",
|
||||
"ERR_TUNNEL_CONNECTION_FAILED",
|
||||
"ERR_PROXY_CONNECTION_FAILED",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Map a raw error thrown by cloakbrowser to one of the three CLOAK
|
||||
* codes. Returns `LAUNCH_FAILED` as the default when no classifier
|
||||
* matches. Pure and synchronous — caller is responsible for wrapping
|
||||
* the result into a {@link CloakLaunchError}.
|
||||
*/
|
||||
export function classifyCloakError(err: unknown): TestingE2eErrorCode {
|
||||
const message =
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
||||
if (BINARY_HINTS.some((h) => message.includes(h))) {
|
||||
return "TESTING_E2E.CLOAK.BINARY_MISSING";
|
||||
}
|
||||
if (PROXY_HINTS.some((h) => message.includes(h))) {
|
||||
return "TESTING_E2E.CLOAK.PROXY_INVALID";
|
||||
}
|
||||
return "TESTING_E2E.CLOAK.LAUNCH_FAILED";
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper: wrap an arbitrary error into a
|
||||
* {@link CloakLaunchError} with a classified code. Used by the
|
||||
* cloakbrowser-backed fixtures so every launch failure surfaces as a
|
||||
* structured `TestingE2eError`.
|
||||
*/
|
||||
export function wrapCloakError(
|
||||
err: unknown,
|
||||
context?: Record<string, unknown>
|
||||
): CloakLaunchError {
|
||||
const code = classifyCloakError(err);
|
||||
const original =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "unknown error";
|
||||
const remediation =
|
||||
code === "TESTING_E2E.CLOAK.BINARY_MISSING"
|
||||
? " (run `npx cloakbrowser install` to fetch the stealth Chromium binary)"
|
||||
: code === "TESTING_E2E.CLOAK.PROXY_INVALID"
|
||||
? " (verify proxy URL credentials and reachability)"
|
||||
: "";
|
||||
return new CloakLaunchError(
|
||||
code,
|
||||
`cloakbrowser launch failed: ${original}${remediation}`,
|
||||
context === undefined ? { cause: err } : { cause: err, context }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user