(dir: string, target: Target)
| 39 | // acquireUseRelease so a vitest timeout (fiber interruption) still closes the |
| 40 | // browser and flushes video + trace — a bare promise would leak Chromium. |
| 41 | export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface => ({ |
| 42 | session: (identity, drive) => |
| 43 | Effect.acquireUseRelease( |
| 44 | Effect.promise(async () => { |
| 45 | const videoTmp = join(dir, ".video-tmp"); |
| 46 | mkdirSync(videoTmp, { recursive: true }); |
| 47 | |
| 48 | // Watchable mode (E2E_FILM / E2E_DESK): slow each Playwright action so |
| 49 | // the recording is readable instead of flickering through states at |
| 50 | // machine speed. Off by default — normal CI runs stay fast. |
| 51 | const watchable = process.env.E2E_FILM === "1" || process.env.E2E_DESK === "1"; |
| 52 | const slowMo = watchable ? 400 : undefined; |
| 53 | // On the desk (E2E_DESK), the browser is a real headed window on the |
| 54 | // virtual display — the desk's single screen recording films it next |
| 55 | // to the chat terminal, exactly like a developer tabbing over. |
| 56 | const browser = await chromium.launch( |
| 57 | process.env.E2E_DESK === "1" |
| 58 | ? { |
| 59 | headless: false, |
| 60 | args: ["--window-position=300,40", "--window-size=1100,830"], |
| 61 | slowMo, |
| 62 | } |
| 63 | : slowMo |
| 64 | ? { slowMo } |
| 65 | : {}, |
| 66 | ); |
| 67 | const context = await browser.newContext({ |
| 68 | colorScheme: "dark", |
| 69 | viewport: { width: 1280, height: 800 }, |
| 70 | recordVideo: { dir: videoTmp, size: { width: 1280, height: 800 } }, |
| 71 | baseURL: target.baseUrl, |
| 72 | }); |
| 73 | await context.tracing.start({ |
| 74 | screenshots: true, |
| 75 | snapshots: true, |
| 76 | sources: true, |
| 77 | }); |
| 78 | // Bake a synthetic URL bar into the recording so a shared session.mp4 |
| 79 | // (and the step screenshots) shows which page each moment was on. |
| 80 | await installRecordingUrlBar(context); |
| 81 | if (identity.cookies?.length) { |
| 82 | await context.addCookies( |
| 83 | identity.cookies.map((cookie) => ({ |
| 84 | ...cookie, |
| 85 | url: target.baseUrl, |
| 86 | })), |
| 87 | ); |
| 88 | } |
| 89 | const page = await context.newPage(); |
| 90 | // The session video's clock starts with the page; anchor it for the |
| 91 | // run's focus timeline (scripts/film.ts cuts on these). |
| 92 | markRecordingStart(dir, "browser"); |
| 93 | // Main-frame navigations feed the viewer's synthetic URL bar — the |
| 94 | // recording itself is chromeless, so this is the only place the |
| 95 | // address the developer "typed" survives. |
| 96 | page.on("framenavigated", (frame) => { |
| 97 | if (frame === page.mainFrame()) markNavigation(dir, frame.url()); |
| 98 | }); |
no test coverage detected