# testing-e2e — delta (swap-testing-e2e-cloakbrowser) ## ADDED Requirements ### Requirement: Cloakserve CDP connect helper The package MUST expose `@suaveplan/testing-e2e/cloakserve` (also re-exported from the package root) providing `connectCloakServe(options, chromium?)` and `buildCloakServeUrl(options)`. The URL builder MUST map camelCase option keys to cloakserve's kebab-case query-string keys (`hardwareConcurrency` → `hardware-concurrency`, etc.), stringify booleans, skip `undefined`/`null` values, and default the base URL to `http://localhost:9222`. `connectCloakServe` MUST inject `@playwright/test`'s `chromium` by default with a lazy dynamic-import cache, and wrap any `connectOverCDP` rejection in a `CloakLaunchError` whose `context` carries `{ cloakServeUrl }`. #### Scenario: buildCloakServeUrl maps keys and base URL - **WHEN** `buildCloakServeUrl({ fingerprint: 42, hardwareConcurrency: 8, geoip: true })` is called - **THEN** the result equals `http://localhost:9222/?fingerprint=42&hardware-concurrency=8&geoip=true` (param order is preserved) **Verified by:** `src/cloakserve/build-cloakserve-url.test.ts::"appends fingerprint seed"` AND `src/cloakserve/build-cloakserve-url.test.ts::"maps camelCase to kebab-case"` AND `src/cloakserve/build-cloakserve-url.test.ts::"stringifies booleans"` #### Scenario: connectCloakServe forwards options and returns the Browser - **WHEN** `connectCloakServe({ fingerprint: 11111, timezone: "America/New_York" }, injectedDriver)` is called - **THEN** `injectedDriver.connectOverCDP` is called with a URL containing `fingerprint=11111` and `timezone=America%2FNew_York`, and the returned `Browser` is the injected fake **Verified by:** `src/cloakserve/cloakserve-connect.test.ts::"uses injected chromium and builds the cloakserve URL"` #### Scenario: connectCloakServe wraps errors - **WHEN** the underlying `connectOverCDP` rejects with `ECONNREFUSED` - **THEN** `connectCloakServe` rethrows a `CloakLaunchError` with `code === TESTING_E2E.CLOAK.PROXY_INVALID` and `context.cloakServeUrl` set to the URL that was attempted **Verified by:** `src/cloakserve/cloakserve-connect.test.ts::"wraps connection errors in CloakLaunchError"` AND `src/cloakserve/cloakserve-connect.test.ts::"classifies generic connect errors as LAUNCH_FAILED"` ### Requirement: Stealth presets The package MUST expose `@suaveplan/testing-e2e/presets` (also re-exported from the package root) providing frozen `STEALTH_DEFAULT_CLOAK` (`{ headless: true }`) and `STEALTH_HEADED_CLOAK` (`{ headless: false, humanize: true }`) plus `stealthMax(proxy)` and `stealthMaxHeadless(proxy)` factories that return fresh objects on every call. `stealthMax(proxy)` MUST return `{ headless: false, humanize: true, geoip: true, proxy }` — exactly matching the CloakBrowser README's "Recommended config for anti-bot sites" block. #### Scenario: stealthMax matches the upstream recommendation - **WHEN** `stealthMax("http://proxy:8080")` is called - **THEN** the result is `{ headless: false, humanize: true, geoip: true, proxy: "http://proxy:8080" }` **Verified by:** `src/presets/stealth-presets.test.ts::"matches the upstream recommended config"` #### Scenario: static presets are frozen - **WHEN** `Object.isFrozen(STEALTH_DEFAULT_CLOAK)` is checked - **THEN** it is `true` (and same for `STEALTH_HEADED_CLOAK`) **Verified by:** `src/presets/stealth-presets.test.ts::"is headless and frozen"` AND `src/presets/stealth-presets.test.ts::"is headed + humanized and frozen"` ### Requirement: `waitForTimeout` lint guard The package's source-guard test MUST flag `waitForTimeout` and `wait_for_timeout` calls anywhere under `src/` (excluding `.test.ts` and `.bench.ts`). Both forms emit CDP protocol traffic that reCAPTCHA v3 / Enterprise behavioural scorers detect — the upstream CloakBrowser README documents this as a primary cause of low scores. #### Scenario: guard fires on a synthetic violation - **WHEN** the `no-disallowed source guard` test runs against the current `src/` tree - **THEN** the test passes (no violations) AND the `PATTERNS` array contains entries named `waitForTimeout` and `wait_for_timeout` **Verified by:** `tests/no-disallowed.test.ts::"src/ contains no disallowed constructs"` ### Requirement: Docker bundle The package MUST ship a `docker/` directory containing `Dockerfile.cloakserve` (extending `cloakhq/cloakbrowser`, installing the Kasada / Akamai font set, running `cloakserve` on `9222`), `Dockerfile.manager` (extending `cloakhq/cloakbrowser-manager`, exposing 8080 with a `cloakprofiles` named volume), `docker-compose.yml` (both services bound to `127.0.0.1`, manager behind the `manager` compose profile, named volume defined), `.env.example` (`CLOAKBROWSER_TAG`, `MANAGER_TAG`, `MANAGER_AUTH_TOKEN`), and a `docker/README.md` operator playbook. Both Dockerfiles MUST declare a healthcheck. #### Scenario: cloakserve Dockerfile builds on the upstream image and exposes 9222 - **WHEN** `docker build -f docker/Dockerfile.cloakserve` is run - **THEN** the resulting image is layered on `cloakhq/cloakbrowser`, exposes port `9222`, defaults its CMD to `cloakserve`, and runs the documented font-install step **Verified by:** manual operator verification (`docker compose up cloakserve` + smoke connect via `connectCloakServe`); source-level invariants are enforced by reading the Dockerfile in CI is out of scope for this change ### Requirement: CLOAK_* error codes The package MUST emit `CloakLaunchError extends TestingE2eError` with codes `TESTING_E2E.CLOAK.LAUNCH_FAILED`, `TESTING_E2E.CLOAK.BINARY_MISSING`, and `TESTING_E2E.CLOAK.PROXY_INVALID`. All three codes MUST appear in the exported `TESTING_E2E_ERROR_CODES` registry and conform to `TESTING_E2E_ERROR_CODE_PATTERN`. #### Scenario: codes registered - **WHEN** `TESTING_E2E_ERROR_CODES` is iterated - **THEN** the three CLOAK codes are present and each matches the registry pattern **Verified by:** `src/errors/testing-e2e-error.test.ts::"registers CLOAK_* codes"` AND `src/errors/testing-e2e-error.test.ts::"CloakLaunchError exposes the LAUNCH_FAILED code by default"` #### Scenario: BINARY_MISSING raised when cloakbrowser binary not present - **WHEN** the underlying cloakbrowser launcher throws an error whose message includes `binary not found`, `ENOENT`, or `Failed to download` - **THEN** the calling fixture rethrows a `CloakLaunchError` with code `TESTING_E2E.CLOAK.BINARY_MISSING`, the original error as `cause`, and a remediation hint pointing to `npx cloakbrowser install` **Verified by:** `src/errors/testing-e2e-error.test.ts::"classifyCloakError maps ENOENT to BINARY_MISSING"` ## MODIFIED Requirements ### Requirement: Layer 0 dep-graph isolation The `@suaveplan/testing-e2e` `dependencies` and `peerDependencies` MUST NOT contain any `@suaveplan/*` workspace package. `dependencies` MUST include `cloakbrowser` (any valid range — direct or `catalog:testing`) alongside the existing `@playwright/test`, `@opentelemetry/api`, `tslib`, and `zod` entries. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests. #### Scenario: runtime closure clean and contains cloakbrowser - **WHEN** `bunx turbo ls --filter=@suaveplan/testing-e2e...` is run - **THEN** no `@suaveplan/*` package appears in the runtime closure AND `cloakbrowser` is present as a direct dependency **Verified by:** `tests/dep-graph.test.ts::"no @suaveplan/* in dependencies or peerDependencies"` AND `tests/dep-graph.test.ts::"cloakbrowser is a direct dependency"` ### Requirement: `extendedTest` default browser is stealth `extendedTest.browser` MUST be overridden so the default browser used by every fixture downstream (`freshPage`, consumer-extended fixtures) launches via `cloakbrowser.launch()` (drop-in for stock chromium). The override MUST honour `testInfo.project.use.cloak` if defined, otherwise launch with cloakbrowser defaults (`headless: true`, stealth args on). The launcher MUST be injectable for unit testing — the fixture takes an internal default but exposes the launcher symbol on the `cloak` worker fixture so tests can supply a spy. #### Scenario: default browser is cloakbrowser - **WHEN** a test runs under `extendedTest` with no per-project override - **THEN** the `browser` fixture is produced by the cloakbrowser `launch` function (identity check via injected spy) **Verified by:** `src/fixtures/extended-test.test.ts::"browser fixture is launched via cloakbrowser launch"` AND `src/fixtures/extended-test.test.ts::"cloak options from testInfo.project.use.cloak flow into launch"` #### Scenario: errors map to CloakLaunchError - **WHEN** the underlying cloakbrowser launcher throws - **THEN** the fixture rethrows a `CloakLaunchError` with a classified code (`LAUNCH_FAILED`, `BINARY_MISSING`, or `PROXY_INVALID`) **Verified by:** `src/fixtures/extended-test.test.ts::"wraps launch errors in CloakLaunchError"` ### Requirement: Extension fixture uses cloakbrowser launchPersistentContext `launchWithExtension` MUST accept a `launchPersistentContext` function parameter (default: cloakbrowser's). The legacy `chromium: BrowserType` parameter is removed. Extension args (`--disable-extensions-except`, `--load-extension`) and userDataDir behaviour are unchanged. #### Scenario: default launcher is cloakbrowser - **WHEN** `launchWithExtension({ extensionPath, userDataDir })` is called with no explicit launcher - **THEN** `cloakbrowser.launchPersistentContext` is invoked with the expected `userDataDir` and Chromium args **Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"defaults to cloakbrowser launchPersistentContext"` #### Scenario: launcher is injectable - **WHEN** `launchWithExtension({ launchPersistentContext: spy, extensionPath, userDataDir })` is called - **THEN** the provided `spy` is invoked exactly once with the expected `userDataDir` and Chromium args **Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"uses injected launcher when provided"` #### Scenario: launchPersistentContext errors map to CloakLaunchError - **WHEN** the underlying launcher throws an error matching the `ENOENT`/binary classifier - **THEN** `launchWithExtension` rethrows a `CloakLaunchError` with `code === TESTING_E2E.CLOAK.BINARY_MISSING` **Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"wraps launcher errors with CloakLaunchError"`