// tests/uat/lib/extension.ts — Plan 01-11 harness extension/offscreen helpers. // // The offscreen-document attach uses a CDP-level target type that // Puppeteer 25 surfaces as `'background_page'` — NOT `'page'`. Per // Plan 01-11 RESEARCH §4 / Pitfall 1, finding the offscreen via // `t.type() === 'page'` returns no matches; `'background_page'` is // the right discriminator. After getting the target, `.asPage()` // returns a Page-like handle (NOT `.page()` — that returns undefined). // // References: // - Puppeteer Target types: // https://pptr.dev/api/puppeteer.targettype // - Chrome offscreen document: // https://developer.chrome.com/docs/extensions/reference/api/offscreen import type { Browser, Page, Target } from 'puppeteer'; /** How long to wait for the offscreen document target to appear. */ const OFFSCREEN_TARGET_TIMEOUT_MS = 5_000; /** * Poll the browser's target list for the offscreen document. The * offscreen is created lazily — only when the SW issues * `chrome.offscreen.createDocument(...)`. Caller MUST invoke a flow * that triggers offscreen creation (e.g. start a recording) BEFORE * calling this helper. * * @param browser - Puppeteer Browser handle. * @param extensionId - The extension's runtime id (for URL filtering). * @returns Resolved Target whose URL contains 'offscreen'. * @throws If no offscreen target appears within OFFSCREEN_TARGET_TIMEOUT_MS. */ export async function waitForOffscreenTarget( browser: Browser, extensionId: string, ): Promise { const predicate = (t: Target): boolean => { const url = t.url(); // Offscreen documents are loaded as chrome-extension:///... // with a path containing 'offscreen' (matches both 'src/offscreen/' // and the bundled equivalents). Target type 'background_page' per // RESEARCH §4 Pitfall 1. return ( t.type() === 'background_page' && url.startsWith(`chrome-extension://${extensionId}`) && url.includes('offscreen') ); }; return await browser.waitForTarget(predicate, { timeout: OFFSCREEN_TARGET_TIMEOUT_MS, }); } /** * Attach to the offscreen document as a Page-like handle. Uses * `.asPage()` (NOT `.page()` — Puppeteer 25 returns null for * `.page()` on background_page-type targets). * * @param target - The offscreen Target from waitForOffscreenTarget. * @returns Page handle for evaluate/expose/etc. */ export async function attachToOffscreen(target: Target): Promise { const page = await target.asPage(); return page; } /** * Count the offscreen targets currently in the browser. Used by * assertion 4 to verify that a toolbar click while recording does * NOT spawn a second offscreen document. * * @param browser - Puppeteer Browser handle. * @param extensionId - The extension's runtime id. * @returns Integer count of offscreen targets. */ export function countOffscreenTargets( browser: Browser, extensionId: string, ): number { const targets = browser.targets(); let count = 0; for (const t of targets) { if ( t.type() === 'background_page' && t.url().startsWith(`chrome-extension://${extensionId}`) && t.url().includes('offscreen') ) { count += 1; } } return count; }