feat(testmu): hosted Chrome and Edge for the web engine with testmuBrowsers()

Merged
SA
21d ago
+689-8

What and why

The upstream maintainer closed tester-army/e2e#765 (TestMu AI desktop browsers) and asked to consolidate the TestMu AI integration in tester-army/e2e#772, which covers devices only. This PR adds the browser side to @e2e-dev/testmu so #772 carries both.

  • testmuBrowsers(), exported from @e2e-dev/testmu/web, is a BrowserProvider for @e2e-dev/web: TestMu AI hosted Chrome and Edge on Windows and macOS, one session per worker slot, or one per attempt with scope: 'attempt'.
  • A session starts when its CDP websocket opens and ends when the engine closes it, so the provider only builds the CDP URL. There's no API call and no SDK. Credentials come from the package's existing testmuCredentials() (LT_USERNAME/LT_ACCESS_KEY) and never appear in a log line.
  • Follows testmu()'s conventions:
    • build (default the run id) and sessionName (default e2e-<run id>-<target>-<slot or attempt id>) name sessions as the device provider does, so a run's device and browser sessions land in one build. project (default e2e) is sent as LT:Options.project, as testmu() sends its own.
    • geoLocation and timezone are top-level options, as on testmu().
    • Optional strings must be non-empty, defaults apply only to undefined (so null fails), and unknown keys are refused. These are the same rules cubic asked for on #772.
  • Other options: browserName (Chrome/MicrosoftEdge), browserVersion, platform, route, hub, scope, capabilities (further LT:Options). idleTimeout defaults to 600 s. capabilities cannot set what the provider sets (user, accessKey, build, name, project, platform, geoLocation, timezone, browserName, browserVersion, or a nested LT:Options).
  • route: /puppeteer (default) serves raw CDP on every machine. /playwright-cdp labels sessions as Playwright but serves raw CDP only on machines with TestMu AI's newest backend, which is still rolling out, and fails to connect elsewhere.
  • The package root stays device-only (unchanged index.ts). The browser provider lives only at @e2e-dev/testmu/web, so a web-only project never loads agent-device or @e2e-dev/mobile, and the root's types never reference @e2e-dev/web.
  • Optional peers: @e2e-dev/mobile, @e2e-dev/web and agent-device are now marked optional. Device users already install agent-device explicitly (no published release satisfies the range yet), so nothing changes for them. Without this, a web-only npm install @e2e-dev/testmu would try to resolve the unpublished agent-device range.
import type { E2EConfig } from 'e2e';
import { web } from '@e2e-dev/web';
import { testmuBrowsers } from '@e2e-dev/testmu/web';

export default {
  targets: [
    {
      name: 'testmu-web',
      engine: web({ browser: testmuBrowsers({ platform: 'Windows 11', capabilities: { video: true } }), viewport: null }),
      app: { url: 'https://staging.example.com' },
    },
  ],
} satisfies E2EConfig;
sequenceDiagram
  participant W as web engine
  participant P as testmuBrowsers()
  participant H as cdp.lambdatest.com
  W->>P: acquire(request)
  P-->>W: { id, cdpEndpoint } (caps + LT_* from request.env)
  W->>H: connectOverCDP(cdpEndpoint)
  Note over H: socket open = session starts
  W->>H: close
  Note over H: socket closed = session ends
  W->>P: release() (no-op)
  • Docs: new docs/integrations/testmu-browsers.mdx (options, scopes, routes, recordings, downloads, Limits), with its config sample checked as docs/examples/web/testmu-browsers.config.ts (registered in scripts/check-docs-examples.ts, like the mobile example), plus the nav, integrations card, environment table, security page, browser.mdx, package README, AGENTS.md and the skills/e2e setup row. Both TestMu AI pages now say where to get the credentials (Account Settings → Password & Security → Username and Access Key).

Verified

Ran it locally: yes

  • pnpm check passes (lint, dead code, typecheck, error codes, peer ranges, install scripts, docs). pnpm --filter @e2e-dev/testmu run test: 108/108, of which 35 are new for the browser provider. They cover the URL and capabilities, credentials from request.env only, every INVALID_CONFIG case (including null values and the reserved capabilities keys), the 600 s idleTimeout default, testmu()-style project/build/session naming, log lines without the key, the /web export with a device-only root, and 24 worker leases fitting the engine's 16 KB hand-off.
  • Packed @e2e-dev/testmu (pnpm pack) and installed it browser-only into a scratch project on e2e 0.15.1: npm pulled neither agent-device nor @e2e-dev/mobile, and the import came from @e2e-dev/testmu/web.
  • Real runs on TestMu AI production:
RoutePlatformResult
/puppeteer (default)Windows 11 + Chrome✅ 1/1, recorded with build <run id> and session e2e-<run id>-<target>-1
/puppeteer, scope: 'attempt'Windows 11 + Chrome✅ 2/2, one session per test
/playwright-cdpmacOS Sequoia + Chrome, pinned to a machine with the backend fix✅ 2/2, session labelled Playwright
timezone: 'UTC+05:30'Windows 11 + Chrome✅ browser reports Asia/Calcutta (+330 min)
  • No report, log or artifact file from any run contains the access key.
  • project is sent in LT:Options the way testmu() sends it. The desktop sessions API doesn't return a project field, so the docs only say it is sent, not that the dashboard groups by it.
  • Platform issues seen in the sanity run. Both reproduce with plain Puppeteer/Playwright, without this provider, and are being reported to TestMu AI:
    • Edge over CDP: on Windows 11 the hub answers 500 after about 4 minutes (Chrome connects in about 11 s), so Edge sessions hit e2e's 60 s connect limit.
    • Geo exits: geoLocation: 'DE' exits in Portugal and 'FR' in Spain. The option is applied (the default exit is Mumbai), but those exits sit in neighbouring countries.
  • Not included: linking TestMu AI's recording through record(), as testmu() does for devices. The web engine supports it, but it needs the desktop sessions API, so it's left for a follow-up.
  • In worker scope about 24–30 workers per target fit the engine's 16 KB lease hand-off (documented under Limits). scope: 'attempt' has no such ceiling.
  • A session that waits longer than 60 s for a machine (queued, or pinned to a busy one) fails with LAUNCH_TIMEOUT. The web engine's CDP connect is fixed at 60 s, as the Limits section documents.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KoNxaN1YAAP9PwmHtMm1WX