Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# launch-with-extension
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`launchWithExtension` starts a stealth Chromium persistent browser
|
||||
context with an unpacked browser extension loaded, then resolves the
|
||||
extension's runtime ID by inspecting the background service worker
|
||||
page URL. It is the foundational fixture for browser-extension E2E
|
||||
tests: without the extension ID, tests cannot construct
|
||||
`chrome-extension://` URLs to open popup or options pages.
|
||||
|
||||
Under the hood the launcher is `cloakbrowser.launchPersistentContext`
|
||||
— the same stealth Chromium with 49 source-level fingerprint patches
|
||||
that powers every other fixture in this package. The launcher is
|
||||
injectable so unit tests can supply a spy and so adapters
|
||||
(profile-manager, remote browser pool) can replace it without forking
|
||||
this file.
|
||||
|
||||
## Features
|
||||
|
||||
- Defaults to `cloakbrowser.launchPersistentContext`; pass
|
||||
`launchPersistentContext` to override
|
||||
- Sets `--disable-extensions-except` and `--load-extension` flags
|
||||
- Forwards arbitrary `launchOptions` (proxy, humanize, geoip,
|
||||
timezone, locale, …) straight through to the launcher
|
||||
- Checks `context.backgroundPages()` first; falls back to
|
||||
`waitForEvent("backgroundpage")` for extensions with deferred
|
||||
registration
|
||||
- Extracts the extension ID from the `chrome-extension://<id>/...`
|
||||
URL via a strict regex
|
||||
- Classifies launcher failures via `wrapCloakError`, producing a
|
||||
`CloakLaunchError` with `TESTING_E2E.CLOAK.*` code
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { launchWithExtension } from "@suaveplan/testing-e2e/extension-fixture";
|
||||
import path from "node:path";
|
||||
|
||||
const extensionPath = path.resolve(__dirname, "../../dist/extension");
|
||||
const userDataDir = path.resolve(__dirname, "../../tmp/user-data");
|
||||
|
||||
const { context, extensionId } = await launchWithExtension({
|
||||
extensionPath,
|
||||
userDataDir,
|
||||
});
|
||||
|
||||
const popupPage = await context.newPage();
|
||||
await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
// ... test assertions ...
|
||||
await context.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { launchWithExtension } from "@suaveplan/testing-e2e/extension-fixture";
|
||||
import { test, expect } from "@suaveplan/testing-e2e";
|
||||
import path from "node:path";
|
||||
|
||||
const EXT_PATH = path.resolve(__dirname, "../../dist/extension");
|
||||
|
||||
test.describe("Browser extension popup behind anti-bot wall", () => {
|
||||
let extensionId: string;
|
||||
let context: import("@playwright/test").BrowserContext;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const fixture = await launchWithExtension({
|
||||
extensionPath: EXT_PATH,
|
||||
userDataDir: `/tmp/ext-test-${Date.now()}`,
|
||||
launchOptions: {
|
||||
proxy: "socks5://user:pass@residential-proxy:1080",
|
||||
geoip: true,
|
||||
humanize: true,
|
||||
locale: "en-US",
|
||||
},
|
||||
});
|
||||
context = fixture.context;
|
||||
extensionId = fixture.extensionId;
|
||||
});
|
||||
|
||||
test("popup renders inside the gated app", async () => {
|
||||
const page = await context.newPage();
|
||||
await page.goto("https://app.example.com");
|
||||
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Sign in" })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `launchWithExtension(options: ExtensionFixtureOptions): Promise<ExtensionFixtureResult>`
|
||||
|
||||
Launches a persistent Chromium context with the specified extension
|
||||
loaded and resolves the extension's runtime ID.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `options.extensionPath` — absolute path to the unpacked extension
|
||||
directory containing `manifest.json`
|
||||
- `options.userDataDir` — absolute path to the persistent browser
|
||||
profile directory; must be writable and unique per concurrent test
|
||||
run
|
||||
- `options.launchPersistentContext` — *optional* launcher override.
|
||||
Defaults to `cloakbrowser.launchPersistentContext` (loaded
|
||||
lazily via dynamic `import`). Tests inject a spy here; adapters
|
||||
inject a profile-manager-backed launcher
|
||||
- `options.launchOptions` — *optional* extra options forwarded
|
||||
unchanged to the launcher. Use this to set `proxy`, `humanize`,
|
||||
`geoip`, `timezone`, `locale`, or any other cloakbrowser
|
||||
`LaunchPersistentContextOptions` field
|
||||
|
||||
**Returns:** `ExtensionFixtureResult`
|
||||
|
||||
- `.context` — the `BrowserContext` instance; caller closes it
|
||||
- `.extensionId` — the lowercase alphanumeric extension ID extracted
|
||||
from the background page URL
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `CloakLaunchError` (`TESTING_E2E.CLOAK.*`) when the underlying
|
||||
launcher fails — code is one of `LAUNCH_FAILED`, `BINARY_MISSING`,
|
||||
or `PROXY_INVALID`. `context` carries `{ extensionPath, userDataDir }`
|
||||
- `Error("Could not extract extension ID …")` when the background
|
||||
page URL does not match the `chrome-extension://` pattern
|
||||
|
||||
### `ExtensionFixtureOptions` interface
|
||||
|
||||
```typescript
|
||||
interface ExtensionFixtureOptions {
|
||||
extensionPath: string;
|
||||
userDataDir: string;
|
||||
launchPersistentContext?: CloakLaunchPersistentContext;
|
||||
launchOptions?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### `ExtensionFixtureResult` interface
|
||||
|
||||
```typescript
|
||||
interface ExtensionFixtureResult {
|
||||
context: BrowserContext;
|
||||
extensionId: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `headless: false` option is hard-set: headless Chromium silently
|
||||
ignores `--load-extension`, so loading an extension always opens a
|
||||
visible window. CI environments must run under a virtual framebuffer
|
||||
(Xvfb) or a Linux headed runner.
|
||||
|
||||
The extension ID regex `/^chrome-extension:\/\/([a-z0-9]+)\//`
|
||||
matches only lowercase alphanumeric characters — Chromium generates
|
||||
the ID from the extension's public-key hash. IDs are 32 characters
|
||||
in practice but the regex stays length-flexible for forward
|
||||
compatibility.
|
||||
|
||||
The default `launchPersistentContext` is resolved via a one-time
|
||||
dynamic `import("cloakbrowser")`. The cache is module-private; tests
|
||||
reset it via the internal `_resetDefaultLauncherCache()` export so
|
||||
the `vi.doMock("cloakbrowser", …)` hook in the unit test can swap
|
||||
the implementation on a fresh load.
|
||||
Reference in New Issue
Block a user