(dir: string, target: Target)
| 161 | // acquireUseRelease so a vitest timeout (fiber interruption) still closes the |
| 162 | // browser and flushes video + trace — a bare promise would leak Chromium. |
| 163 | export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface => ({ |
| 164 | session: (identity, drive) => |
| 165 | Effect.acquireUseRelease( |
| 166 | Effect.promise(async () => { |
| 167 | const videoTmp = join(dir, ".video-tmp"); |
| 168 | mkdirSync(videoTmp, { recursive: true }); |
| 169 | |
| 170 | // Watchable mode (E2E_FILM / E2E_DESK): slow each Playwright action so |
| 171 | // the recording is readable instead of flickering through states at |
| 172 | // machine speed. Off by default — normal CI runs stay fast. |
| 173 | const watchable = process.env.E2E_FILM === "1" || process.env.E2E_DESK === "1"; |
| 174 | const slowMo = watchable ? 400 : undefined; |
| 175 | // On the desk (E2E_DESK), the browser is a real headed window on the |
| 176 | // virtual display — the desk's single screen recording films it next |
| 177 | // to the chat terminal, exactly like a developer tabbing over. |
| 178 | const browser = await chromium.launch( |
| 179 | process.env.E2E_DESK === "1" |
| 180 | ? { |
| 181 | headless: false, |
| 182 | args: ["--window-position=300,40", "--window-size=1100,830"], |
| 183 | slowMo, |
| 184 | } |
| 185 | : slowMo |
| 186 | ? { slowMo } |
| 187 | : {}, |
| 188 | ); |
| 189 | const context = await browser.newContext({ |
| 190 | colorScheme: "dark", |
| 191 | viewport: { width: 1280, height: 800 }, |
| 192 | recordVideo: { dir: videoTmp, size: { width: 1280, height: 800 } }, |
| 193 | baseURL: target.baseUrl, |
| 194 | }); |
| 195 | await context.tracing.start({ |
| 196 | screenshots: true, |
| 197 | snapshots: true, |
| 198 | sources: true, |
| 199 | }); |
| 200 | // Bake a synthetic URL bar into the recording so a shared session.mp4 |
| 201 | // (and the step screenshots) shows which page each moment was on. |
| 202 | await installRecordingUrlBar(context); |
| 203 | if (identity.cookies?.length) { |
| 204 | await context.addCookies( |
| 205 | identity.cookies.map((cookie) => ({ |
| 206 | ...cookie, |
| 207 | url: target.baseUrl, |
| 208 | })), |
| 209 | ); |
| 210 | } |
| 211 | const page = await context.newPage(); |
| 212 | // The session video's clock starts with the page; anchor it for the |
| 213 | // run's focus timeline (scripts/film.ts cuts on these). |
| 214 | markRecordingStart(dir, "browser"); |
| 215 | // Main-frame navigations feed the viewer's synthetic URL bar — the |
| 216 | // recording itself is chromeless, so this is the only place the |
| 217 | // address the developer "typed" survives. |
| 218 | page.on("framenavigated", (frame) => { |
| 219 | if (frame === page.mainFrame()) markNavigation(dir, frame.url()); |
| 220 | }); |
no test coverage detected