@sudobility/testomniac_runner 0.0.149 → 0.0.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/testomniac_runner",
3
- "version": "0.0.149",
3
+ "version": "0.0.152",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import type { Browser } from "puppeteer-core";
4
+ import { PuppeteerAdapter } from "./PuppeteerAdapter";
5
+ import { ChromiumManager } from "../browser/chromium";
6
+ import { loadConfig } from "../config";
7
+
8
+ // Marker lives ONLY in the XHR response body, never in the page HTML source,
9
+ // so html.includes(MARKER) is true iff the delayed XHR completed and injected.
10
+ const MARKER = "XHR_INJECTED_MARKER_7Q";
11
+
12
+ let server: Server;
13
+ let baseUrl: string;
14
+ let browser: Browser;
15
+
16
+ beforeAll(async () => {
17
+ // Fixture page: 1500ms after load, an XHR resolves and injects MARKER.
18
+ // The delay is deliberately long so that, WITHOUT a network-idle wait, the
19
+ // HTML read definitively beats the XHR; WITH it, the read waits.
20
+ server = createServer((req, res) => {
21
+ if (req.url === "/late.json") {
22
+ setTimeout(() => {
23
+ res.writeHead(200, { "content-type": "application/json" });
24
+ res.end(JSON.stringify({ marker: MARKER }));
25
+ }, 1500);
26
+ return;
27
+ }
28
+ res.writeHead(200, { "content-type": "text/html" });
29
+ res.end(`<!doctype html><html><body><div id="late"></div>
30
+ <script>
31
+ fetch('/late.json').then(r => r.json()).then((d) => {
32
+ document.getElementById('late').textContent = d.marker;
33
+ });
34
+ </script></body></html>`);
35
+ });
36
+ await new Promise<void>(r => server.listen(0, r));
37
+ const addr = server.address();
38
+ const port = typeof addr === "object" && addr ? addr.port : 0;
39
+ baseUrl = `http://127.0.0.1:${port}`;
40
+
41
+ const config = loadConfig();
42
+ browser = await new ChromiumManager(config).launch();
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await browser?.close();
47
+ await new Promise<void>(r => server.close(() => r()));
48
+ });
49
+
50
+ // Prefixed "scanner service:" so the CI `test:unit` job (which has no browser)
51
+ // excludes it via the `^(?!scanner service)` name filter. Runs under `bun test`.
52
+ describe("scanner service: PuppeteerAdapter.waitForNetworkIdle", () => {
53
+ it("waits for the late XHR so the injected content is present", async () => {
54
+ const page = await browser.newPage();
55
+ const adapter = new PuppeteerAdapter(page);
56
+ await adapter.goto(`${baseUrl}/`, { waitUntil: "load" });
57
+ await adapter.waitForNetworkIdle?.();
58
+ const html = await adapter.content();
59
+ expect(html).toContain(MARKER);
60
+ await page.close();
61
+ });
62
+ });
@@ -1,5 +1,9 @@
1
1
  import type { Page } from "puppeteer-core";
2
2
  import type { BrowserAdapter } from "@sudobility/testomniac_runner_service";
3
+ import {
4
+ NetworkIdleTracker,
5
+ waitForNetworkIdle,
6
+ } from "@sudobility/testomniac_runner_service";
3
7
  import pino from "pino";
4
8
 
5
9
  const logger = pino({ name: "puppeteer-adapter" });
@@ -17,8 +21,37 @@ export class PuppeteerAdapter implements BrowserAdapter {
17
21
  contentType: string;
18
22
  }> = [];
19
23
 
24
+ private readonly idleTracker = new NetworkIdleTracker();
25
+
20
26
  constructor(page: Page) {
21
27
  this.page = page;
28
+ this.bindNetworkIdleTracking(page);
29
+ }
30
+
31
+ private bindNetworkIdleTracking(page: Page): void {
32
+ // Puppeteer request objects have no stable id across events, so key on
33
+ // URL + resourceType. Collisions only cause a marginally early `end`,
34
+ // which is harmless for idle detection.
35
+ page.on("request", req =>
36
+ this.idleTracker.start(
37
+ req.url() + "\0" + req.resourceType(),
38
+ req.resourceType()
39
+ )
40
+ );
41
+ const done = (req: { url: () => string; resourceType: () => string }) =>
42
+ this.idleTracker.end(req.url() + "\0" + req.resourceType());
43
+ page.on("requestfinished", done);
44
+ page.on("requestfailed", done);
45
+ }
46
+
47
+ async waitForNetworkIdle(opts?: {
48
+ idleMs?: number;
49
+ floorMs?: number;
50
+ staleMs?: number;
51
+ timeout?: number;
52
+ pollMs?: number;
53
+ }): Promise<void> {
54
+ await waitForNetworkIdle(this.idleTracker, opts);
22
55
  }
23
56
 
24
57
  private async materializeSelector(selector: string): Promise<string> {
@@ -152,7 +185,7 @@ export class PuppeteerAdapter implements BrowserAdapter {
152
185
  options?: { waitUntil?: string; timeout?: number }
153
186
  ): Promise<void> {
154
187
  await this.page.goto(url, {
155
- waitUntil: (options?.waitUntil as any) || "networkidle0",
188
+ waitUntil: (options?.waitUntil as any) || "load",
156
189
  timeout: options?.timeout || 30000,
157
190
  });
158
191
  this.currentUrl = this.page.url();
@@ -210,7 +243,7 @@ export class PuppeteerAdapter implements BrowserAdapter {
210
243
  }): Promise<void> {
211
244
  try {
212
245
  await this.page.waitForNavigation({
213
- waitUntil: (options?.waitUntil as any) || "networkidle0",
246
+ waitUntil: (options?.waitUntil as any) || "load",
214
247
  timeout: options?.timeout || 5000,
215
248
  });
216
249
  } catch (err) {