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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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.