@opice/harness 0.2.1 → 0.2.2

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/dist/context.d.ts CHANGED
@@ -3,8 +3,18 @@ import { type BrowserContext, type Page } from 'playwright';
3
3
  export declare function getPage(): Page;
4
4
  /** The active browser context (for cookies/storage, new tabs, etc.). */
5
5
  export declare function getContext(): BrowserContext;
6
- /** Launch a fresh isolated browser + context + page. Called from `beforeAll`. */
6
+ /**
7
+ * Open a fresh isolated context + page for a scenario, reusing the shared
8
+ * browser. Called from `beforeAll`. Any context left over from a previous
9
+ * scenario whose teardown didn't complete is closed first so state never
10
+ * bleeds across scenarios.
11
+ */
7
12
  export declare function launchPage(): Promise<Page>;
8
- /** Close the page, context, and browser. Called from `afterAll`. */
13
+ /**
14
+ * Close the scenario's context (and page); keep the shared browser alive for
15
+ * the next scenario. Called from `afterAll`. The browser itself is launched
16
+ * once and reaped by Playwright's own process-exit handler when `bun test`
17
+ * exits — see the `beforeExit` hook below for the graceful path.
18
+ */
9
19
  export declare function closePage(): Promise<void>;
10
20
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAA;AAoBnF,4EAA4E;AAC5E,wBAAgB,OAAO,IAAI,IAAI,CAK9B;AAED,wEAAwE;AACxE,wBAAgB,UAAU,IAAI,cAAc,CAK3C;AAED,iFAAiF;AACjF,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAKhD;AAED,oEAAoE;AACpE,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAS/C"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAA;AA2BnF,4EAA4E;AAC5E,wBAAgB,OAAO,IAAI,IAAI,CAK9B;AAED,wEAAwE;AACxE,wBAAgB,UAAU,IAAI,cAAc,CAK3C;AAUD;;;;;GAKG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAUhD;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAO/C"}
package/dist/context.js CHANGED
@@ -1,11 +1,18 @@
1
1
  import { chromium } from 'playwright';
2
2
  /**
3
- * The live Playwright page for the running scenario. `browserTest` launches a
4
- * fresh browser + context + page per scenario (`beforeAll`) and tears it down
5
- * (`afterAll`); the DSL `el`, `byRole`, navigation reads the current page
6
- * from here. This module replaces the old agent-browser CLI session handling:
7
- * there is no shell-out and no daemon, the browser runs in-process under
8
- * `bun test`.
3
+ * The live Playwright page for the running scenario.
4
+ *
5
+ * The browser process is launched **once** and reused across every scenario;
6
+ * each `browserTest` only opens a fresh isolated `context` + `page` in
7
+ * `beforeAll` and closes that context in `afterAll`. Launching (and tearing
8
+ * down) a whole chromium per scenario is expensive — on a constrained CI runner
9
+ * that per-scenario launch competes with the app/server for CPU and, when a
10
+ * teardown stalls, leaks a zombie browser that drags the rest of the suite
11
+ * down. A fresh context per scenario keeps the same isolation (separate
12
+ * storage/cookies) at a fraction of the cost.
13
+ *
14
+ * The DSL — `el`, `byRole`, navigation — reads the current page from here. The
15
+ * browser runs in-process under `bun test`; there is no shell-out and no daemon.
9
16
  */
10
17
  let browser = null;
11
18
  let context = null;
@@ -28,23 +35,51 @@ export function getContext() {
28
35
  }
29
36
  return context;
30
37
  }
31
- /** Launch a fresh isolated browser + context + page. Called from `beforeAll`. */
38
+ /** Launch the shared browser once; reuse it on subsequent scenarios. */
39
+ async function getBrowser() {
40
+ if (!browser || !browser.isConnected()) {
41
+ browser = await chromium.launch({ headless: !headed() });
42
+ }
43
+ return browser;
44
+ }
45
+ /**
46
+ * Open a fresh isolated context + page for a scenario, reusing the shared
47
+ * browser. Called from `beforeAll`. Any context left over from a previous
48
+ * scenario whose teardown didn't complete is closed first so state never
49
+ * bleeds across scenarios.
50
+ */
32
51
  export async function launchPage() {
33
- browser = await chromium.launch({ headless: !headed() });
34
- context = await browser.newContext();
52
+ if (context) {
53
+ await context.close().catch(() => { });
54
+ context = null;
55
+ page = null;
56
+ }
57
+ const b = await getBrowser();
58
+ context = await b.newContext();
35
59
  page = await context.newPage();
36
60
  return page;
37
61
  }
38
- /** Close the page, context, and browser. Called from `afterAll`. */
62
+ /**
63
+ * Close the scenario's context (and page); keep the shared browser alive for
64
+ * the next scenario. Called from `afterAll`. The browser itself is launched
65
+ * once and reaped by Playwright's own process-exit handler when `bun test`
66
+ * exits — see the `beforeExit` hook below for the graceful path.
67
+ */
39
68
  export async function closePage() {
40
69
  try {
41
70
  await context?.close();
42
71
  }
43
72
  finally {
44
- await browser?.close();
45
73
  page = null;
46
74
  context = null;
47
- browser = null;
48
75
  }
49
76
  }
77
+ // Graceful shutdown of the shared browser when the test process winds down. If
78
+ // this doesn't fire (hard exit/signal), Playwright's own exit handler still
79
+ // kills the chromium child, so the process never outlives the run.
80
+ process.once('beforeExit', () => {
81
+ const b = browser;
82
+ browser = null;
83
+ void b?.close().catch(() => { });
84
+ });
50
85
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAA;AAEnF;;;;;;;GAOG;AAEH,IAAI,OAAO,GAAmB,IAAI,CAAA;AAClC,IAAI,OAAO,GAA0B,IAAI,CAAA;AACzC,IAAI,IAAI,GAAgB,IAAI,CAAA;AAE5B,oFAAoF;AACpF,SAAS,MAAM;IACd,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAA;IAC3F,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAA;IAC1F,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,UAAU;IAC/B,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACxD,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IACpC,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IAC9B,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,SAAS;IAC9B,IAAI,CAAC;QACJ,MAAM,OAAO,EAAE,KAAK,EAAE,CAAA;IACvB,CAAC;YAAS,CAAC;QACV,MAAM,OAAO,EAAE,KAAK,EAAE,CAAA;QACtB,IAAI,GAAG,IAAI,CAAA;QACX,OAAO,GAAG,IAAI,CAAA;QACd,OAAO,GAAG,IAAI,CAAA;IACf,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAA;AAEnF;;;;;;;;;;;;;;GAcG;AAEH,IAAI,OAAO,GAAmB,IAAI,CAAA;AAClC,IAAI,OAAO,GAA0B,IAAI,CAAA;AACzC,IAAI,IAAI,GAAgB,IAAI,CAAA;AAE5B,oFAAoF;AACpF,SAAS,MAAM;IACd,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAA;IAC3F,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAA;IAC1F,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC;AAED,wEAAwE;AACxE,KAAK,UAAU,UAAU;IACxB,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QACxC,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC/B,IAAI,OAAO,EAAE,CAAC;QACb,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACrC,OAAO,GAAG,IAAI,CAAA;QACd,IAAI,GAAG,IAAI,CAAA;IACZ,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,UAAU,EAAE,CAAA;IAC5B,OAAO,GAAG,MAAM,CAAC,CAAC,UAAU,EAAE,CAAA;IAC9B,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IAC9B,OAAO,IAAI,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC9B,IAAI,CAAC;QACJ,MAAM,OAAO,EAAE,KAAK,EAAE,CAAA;IACvB,CAAC;YAAS,CAAC;QACV,IAAI,GAAG,IAAI,CAAA;QACX,OAAO,GAAG,IAAI,CAAA;IACf,CAAC;AACF,CAAC;AAED,+EAA+E;AAC/E,4EAA4E;AAC5E,mEAAmE;AACnE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,EAAE;IAC/B,MAAM,CAAC,GAAG,OAAO,CAAA;IACjB,OAAO,GAAG,IAAI,CAAA;IACd,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;AAChC,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opice/harness",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Runtime primitives for opice — AI-driven E2E browser tests on top of Playwright",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/context.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import { chromium, type Browser, type BrowserContext, type Page } from 'playwright'
2
2
 
3
3
  /**
4
- * The live Playwright page for the running scenario. `browserTest` launches a
5
- * fresh browser + context + page per scenario (`beforeAll`) and tears it down
6
- * (`afterAll`); the DSL `el`, `byRole`, navigation reads the current page
7
- * from here. This module replaces the old agent-browser CLI session handling:
8
- * there is no shell-out and no daemon, the browser runs in-process under
9
- * `bun test`.
4
+ * The live Playwright page for the running scenario.
5
+ *
6
+ * The browser process is launched **once** and reused across every scenario;
7
+ * each `browserTest` only opens a fresh isolated `context` + `page` in
8
+ * `beforeAll` and closes that context in `afterAll`. Launching (and tearing
9
+ * down) a whole chromium per scenario is expensive — on a constrained CI runner
10
+ * that per-scenario launch competes with the app/server for CPU and, when a
11
+ * teardown stalls, leaks a zombie browser that drags the rest of the suite
12
+ * down. A fresh context per scenario keeps the same isolation (separate
13
+ * storage/cookies) at a fraction of the cost.
14
+ *
15
+ * The DSL — `el`, `byRole`, navigation — reads the current page from here. The
16
+ * browser runs in-process under `bun test`; there is no shell-out and no daemon.
10
17
  */
11
18
 
12
19
  let browser: Browser | null = null
@@ -34,22 +41,52 @@ export function getContext(): BrowserContext {
34
41
  return context
35
42
  }
36
43
 
37
- /** Launch a fresh isolated browser + context + page. Called from `beforeAll`. */
44
+ /** Launch the shared browser once; reuse it on subsequent scenarios. */
45
+ async function getBrowser(): Promise<Browser> {
46
+ if (!browser || !browser.isConnected()) {
47
+ browser = await chromium.launch({ headless: !headed() })
48
+ }
49
+ return browser
50
+ }
51
+
52
+ /**
53
+ * Open a fresh isolated context + page for a scenario, reusing the shared
54
+ * browser. Called from `beforeAll`. Any context left over from a previous
55
+ * scenario whose teardown didn't complete is closed first so state never
56
+ * bleeds across scenarios.
57
+ */
38
58
  export async function launchPage(): Promise<Page> {
39
- browser = await chromium.launch({ headless: !headed() })
40
- context = await browser.newContext()
59
+ if (context) {
60
+ await context.close().catch(() => {})
61
+ context = null
62
+ page = null
63
+ }
64
+ const b = await getBrowser()
65
+ context = await b.newContext()
41
66
  page = await context.newPage()
42
67
  return page
43
68
  }
44
69
 
45
- /** Close the page, context, and browser. Called from `afterAll`. */
70
+ /**
71
+ * Close the scenario's context (and page); keep the shared browser alive for
72
+ * the next scenario. Called from `afterAll`. The browser itself is launched
73
+ * once and reaped by Playwright's own process-exit handler when `bun test`
74
+ * exits — see the `beforeExit` hook below for the graceful path.
75
+ */
46
76
  export async function closePage(): Promise<void> {
47
77
  try {
48
78
  await context?.close()
49
79
  } finally {
50
- await browser?.close()
51
80
  page = null
52
81
  context = null
53
- browser = null
54
82
  }
55
83
  }
84
+
85
+ // Graceful shutdown of the shared browser when the test process winds down. If
86
+ // this doesn't fire (hard exit/signal), Playwright's own exit handler still
87
+ // kills the chromium child, so the process never outlives the run.
88
+ process.once('beforeExit', () => {
89
+ const b = browser
90
+ browser = null
91
+ void b?.close().catch(() => {})
92
+ })