| 120 | }; |
| 121 | |
| 122 | const launchApp = async (home, extraEnv = {}) => { |
| 123 | const child = spawn(appExe, ["--no-sandbox", "--remote-debugging-port=0"], { |
| 124 | env: { |
| 125 | ...process.env, |
| 126 | HOME: home, |
| 127 | EXECUTOR_TEST_SKIP_BACKGROUND_SERVICE: "1", |
| 128 | ...extraEnv, |
| 129 | }, |
| 130 | // Own process group so cleanup can SIGKILL the whole Electron tree (main, |
| 131 | // helpers, and the sidecar it spawns) via the negative pid. |
| 132 | detached: true, |
| 133 | stdio: ["ignore", "pipe", "pipe"], |
| 134 | }); |
| 135 | let out = ""; |
| 136 | const debugPort = await new Promise((resolve, reject) => { |
| 137 | const timer = setTimeout(() => reject(new Error(`no CDP URL\n${out}`)), 120_000); |
| 138 | const onData = (b) => { |
| 139 | out = (out + b.toString()).slice(-16_384); |
| 140 | const m = out.match(/DevTools listening on ws:\/\/[^/]+:(\d+)\//); |
| 141 | if (m) { |
| 142 | clearTimeout(timer); |
| 143 | resolve(Number(m[1])); |
| 144 | } |
| 145 | }; |
| 146 | child.stdout.on("data", onData); |
| 147 | child.stderr.on("data", onData); |
| 148 | child.once("exit", (c, s) => |
| 149 | reject(new Error(`app exited before CDP (code=${c} sig=${s})\n${out}`)), |
| 150 | ); |
| 151 | }); |
| 152 | const wsUrl = await waitFor("page CDP target", () => pageWsFor(debugPort), 60_000); |
| 153 | const cdp = await Cdp.connect(wsUrl); |
| 154 | await cdp.cmd("Runtime.enable"); |
| 155 | await cdp.cmd("Page.enable"); |
| 156 | return { child, cdp, debugPort }; |
| 157 | }; |
| 158 | |
| 159 | const bodyHas = (cdp, text) => |
| 160 | cdp.eval(`document.body && document.body.innerText.includes(${JSON.stringify(text)})`); |