@empiricalrun/playwright-utils 0.58.5 → 0.59.0

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.
@@ -20,6 +20,25 @@ const useCache = (data) => {
20
20
  }
21
21
  fs_1.default.writeFileSync(cache_1.CACHE_FILE, JSON.stringify(cacheValue, null, 2), "utf8");
22
22
  };
23
+ // Unsets env vars for the duration of a test and puts them back exactly as they
24
+ // were — assigning `undefined` to process.env would store the string
25
+ // "undefined", which reads as a present (and truthy) credential.
26
+ function clearEnv(names) {
27
+ const saved = names.map((name) => [name, process.env[name]]);
28
+ for (const name of names) {
29
+ delete process.env[name];
30
+ }
31
+ return () => {
32
+ for (const [name, value] of saved) {
33
+ if (value === undefined) {
34
+ delete process.env[name];
35
+ }
36
+ else {
37
+ process.env[name] = value;
38
+ }
39
+ }
40
+ };
41
+ }
23
42
  fixtures_1.test.beforeEach(async ({ page }) => {
24
43
  (0, test_1.injectLocatorHighlightScripts)(page, fixtures_1.test);
25
44
  });
@@ -39,9 +58,12 @@ fixtures_1.test.afterEach(() => {
39
58
  ]);
40
59
  await page.goto(`${server.baseURL}/glass-pane.html`);
41
60
  let error;
42
- // Overwriting API key to ensure we don't call OpenAI
43
- const originalApiKey = process.env.OPENAI_API_KEY;
44
- process.env.OPENAI_API_KEY = undefined;
61
+ // The only two routes the browser agent has to a model: the project API key
62
+ // (api-worker's proxy) and a sandbox's OPENAI_BASE_URL. Clearing both makes
63
+ // llmProxyOpenAIConfig throw before any request — which matters here more
64
+ // than elsewhere, because this is the recursive case and every extra
65
+ // dismissal would spawn another agent and another set of screenshots.
66
+ const restoreEnv = clearEnv(["EMPIRICALRUN_API_KEY", "OPENAI_BASE_URL"]);
45
67
  try {
46
68
  // Target element click will trigger the cache - which will invoke another
47
69
  // click to the same element (recursive overlay dismissal)
@@ -50,7 +72,9 @@ fixtures_1.test.afterEach(() => {
50
72
  catch (e) {
51
73
  error = e;
52
74
  }
75
+ finally {
76
+ restoreEnv();
77
+ }
53
78
  (0, fixtures_1.expect)(error).toBeDefined();
54
- process.env.OPENAI_API_KEY = originalApiKey;
55
79
  (0, fixtures_1.expect)(error.message).toContain('<div class="glass-pane"></div> intercepts pointer events');
56
80
  });
@@ -1,28 +1,5 @@
1
+ import type { TestCaseSyncManifest } from "@empiricalrun/shared-types/playwright-utils";
1
2
  import type { FullConfig, Suite } from "@playwright/test/reporter";
2
- export interface TestCaseSyncTestCase {
3
- id: string;
4
- name: string;
5
- file_path: string;
6
- suites: string[];
7
- playwright_project: string;
8
- tags: string[];
9
- }
10
- export interface TestCaseDependencyGroupMember {
11
- test_case_id: string;
12
- dependency_order: number;
13
- }
14
- export interface TestCaseDependencyGroup {
15
- id: string;
16
- kind: "playwright_serial";
17
- file_path: string;
18
- title_path: string[];
19
- playwright_project: string;
20
- members: TestCaseDependencyGroupMember[];
21
- }
22
- export interface TestCaseSyncManifest {
23
- version: 1;
24
- test_cases: TestCaseSyncTestCase[];
25
- dependency_groups: TestCaseDependencyGroup[];
26
- }
3
+ export type { TestCaseDependencyGroup, TestCaseDependencyGroupMember, TestCaseSyncManifest, TestCaseSyncTestCase, } from "@empiricalrun/shared-types/playwright-utils";
27
4
  export declare function collectTestCaseSyncManifest(config: FullConfig, rootSuite: Suite): TestCaseSyncManifest;
28
5
  //# sourceMappingURL=test-case-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAMnC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,6BAA6B,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;CAC9C;AA+JD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAuBtB"}
1
+ {"version":3,"file":"test-case-sync.d.ts","sourceRoot":"","sources":["../../src/reporter/test-case-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,UAAU,EAEV,KAAK,EAEN,MAAM,2BAA2B,CAAC;AAUnC,YAAY,EACV,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,6CAA6C,CAAC;AAwRrD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,KAAK,GACf,oBAAoB,CAyBtB"}
@@ -41,7 +41,7 @@ function stableGroupId(input) {
41
41
  input.column,
42
42
  ]))
43
43
  .digest("hex");
44
- return `playwright_serial:${hash}`;
44
+ return `${input.kind}:${hash}`;
45
45
  }
46
46
  function collectDirectTestCases(rootDir, suite, state) {
47
47
  return suite.tests.map((test) => ({
@@ -53,11 +53,7 @@ function collectDirectTestCases(rootDir, suite, state) {
53
53
  tags: normalizeTags(test.tags),
54
54
  }));
55
55
  }
56
- function collectOutermostSerialGroup(rootDir, suite, state) {
57
- if (state.serialAncestor || getSuiteParallelMode(suite) !== "serial") {
58
- return null;
59
- }
60
- const tests = suite.allTests();
56
+ function buildDependencyGroup(rootDir, suite, state, kind, tests) {
61
57
  if (tests.length < 2) {
62
58
  return null;
63
59
  }
@@ -71,13 +67,14 @@ function collectOutermostSerialGroup(rootDir, suite, state) {
71
67
  const location = suite.location ?? firstTest.location;
72
68
  return {
73
69
  id: stableGroupId({
70
+ kind,
74
71
  projectName,
75
72
  filePath,
76
73
  titlePath,
77
74
  line: location.line,
78
75
  column: location.column,
79
76
  }),
80
- kind: "playwright_serial",
77
+ kind,
81
78
  file_path: filePath,
82
79
  title_path: titlePath,
83
80
  playwright_project: projectName,
@@ -87,14 +84,81 @@ function collectOutermostSerialGroup(rootDir, suite, state) {
87
84
  })),
88
85
  };
89
86
  }
87
+ function collectOutermostSerialGroup(rootDir, suite, state) {
88
+ if (state.serialAncestor || getSuiteParallelMode(suite) !== "serial") {
89
+ return null;
90
+ }
91
+ return buildDependencyGroup(rootDir, suite, state, "playwright_serial", suite.allTests());
92
+ }
93
+ /**
94
+ * Tests under this suite that Playwright would place in its sequential
95
+ * ("general") group: everything except subtrees that opt back into
96
+ * parallelism. Order is declaration order, which is the order the group
97
+ * executes in.
98
+ */
99
+ function sequentialTests(suite) {
100
+ const tests = [...suite.tests];
101
+ for (const child of suite.suites) {
102
+ if (getSuiteParallelMode(child) !== "parallel") {
103
+ tests.push(...sequentialTests(child));
104
+ }
105
+ }
106
+ return tests;
107
+ }
108
+ /**
109
+ * The `playwright_default` counterpart of the serial collector: the outermost
110
+ * scope whose tests Playwright schedules as one ordered worker group without
111
+ * serial semantics.
112
+ *
113
+ * Two shapes qualify (mirroring `createTestGroups` in Playwright's
114
+ * `runner/testGroups.ts`):
115
+ *
116
+ * - a `test.describe.configure({ mode: "default" })` suite — grouped whole,
117
+ * including any nested parallel subtree, because Playwright keys the group
118
+ * on the outermost sequential suite;
119
+ * - a file with no explicit mode in a non-fullyParallel project — Playwright's
120
+ * per-file "general" group, which excludes subtrees that opt back into
121
+ * `parallel`.
122
+ */
123
+ function collectOutermostDefaultGroup(rootDir, suite, state) {
124
+ if (state.sequentialAncestor) {
125
+ return null;
126
+ }
127
+ const mode = getSuiteParallelMode(suite);
128
+ if (mode === "default") {
129
+ return buildDependencyGroup(rootDir, suite, state, "playwright_default", suite.allTests());
130
+ }
131
+ if (suite.type === "file" && !state.insideParallel && mode === "none") {
132
+ return buildDependencyGroup(rootDir, suite, state, "playwright_default", sequentialTests(suite));
133
+ }
134
+ return null;
135
+ }
90
136
  function walkSuite(rootDir, suite, state, testCases, dependencyGroups) {
91
137
  const isDescribe = suite.type === "describe";
92
138
  const suites = isDescribe ? [...state.suites, suite.title] : state.suites;
93
139
  const nextState = { ...state, suites };
140
+ const mode = getSuiteParallelMode(suite);
141
+ // A serial group is emitted even inside a default scope: the outer default
142
+ // group wins for scheduling, but serial membership carries extra semantics
143
+ // (failure cascade, re-expansion) its consumers still need.
94
144
  const serialGroup = collectOutermostSerialGroup(rootDir, suite, nextState);
95
145
  if (serialGroup) {
96
146
  dependencyGroups.push(serialGroup);
97
147
  nextState.serialAncestor = suite;
148
+ if (!nextState.sequentialAncestor) {
149
+ nextState.sequentialAncestor = suite;
150
+ }
151
+ }
152
+ const defaultGroup = collectOutermostDefaultGroup(rootDir, suite, nextState);
153
+ if (defaultGroup) {
154
+ dependencyGroups.push(defaultGroup);
155
+ nextState.sequentialAncestor = suite;
156
+ }
157
+ // fullyParallel (project suite) or an explicit `mode: "parallel"` — every
158
+ // descendant is independently schedulable unless a sequential suite below
159
+ // says otherwise.
160
+ if (mode === "parallel") {
161
+ nextState.insideParallel = true;
98
162
  }
99
163
  testCases.push(...collectDirectTestCases(rootDir, suite, nextState));
100
164
  for (const child of suite.suites) {
@@ -112,6 +176,15 @@ function walkSuite(rootDir, suite, state, testCases, dependencyGroups) {
112
176
  : "";
113
177
  childState.suites = [];
114
178
  childState.serialAncestor = null;
179
+ childState.sequentialAncestor = null;
180
+ }
181
+ else if (childState.sequentialAncestor &&
182
+ getSuiteParallelMode(childState.sequentialAncestor) === "none" &&
183
+ getSuiteParallelMode(child) === "parallel") {
184
+ // The scope was a plain file's "general" group, which excludes subtrees
185
+ // that opt back into parallel — so below this child, sequential suites
186
+ // start their own groups again.
187
+ childState.sequentialAncestor = null;
115
188
  }
116
189
  walkSuite(rootDir, child, childState, testCases, dependencyGroups);
117
190
  }
@@ -125,6 +198,8 @@ function collectTestCaseSyncManifest(config, rootSuite) {
125
198
  filePath: "",
126
199
  suites: [],
127
200
  serialAncestor: null,
201
+ sequentialAncestor: null,
202
+ insideParallel: false,
128
203
  }, testCases, dependencyGroups);
129
204
  return {
130
205
  version: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAShE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA0E7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CAyFxB"}
1
+ {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAUhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA0E7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CA2FxB"}
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const openai_1 = __importDefault(require("openai"));
11
11
  const os_1 = __importDefault(require("os"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const api_worker_client_1 = require("../../api-worker-client");
13
14
  const mouse_pointer_1 = require("../scripts/mouse-pointer");
14
15
  function getCallerLine() {
15
16
  const stack = new Error().stack;
@@ -85,7 +86,9 @@ async function toLookRight(target, pageDescription) {
85
86
  const base64Image = screenshot.toString("base64");
86
87
  await (0, mouse_pointer_1.addHighlighterScriptsToPage)(page);
87
88
  const model = "gpt-4o";
88
- const openai = new openai_1.default({ maxRetries: 5 });
89
+ // Same api-worker proxy the overlay-dismissal agent uses, so a test run needs
90
+ // no OpenAI key of its own; undefined falls back to the SDK's env defaults.
91
+ const openai = new openai_1.default({ maxRetries: 5, ...(0, api_worker_client_1.llmProxyOpenAIConfig)() });
89
92
  const completion = await openai.chat.completions.create({
90
93
  model,
91
94
  messages: [
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,WAKhE;AAED,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,iBAAiB,CAIhB;IAET,YAA6B,IAAI,EAAE,IAAI,EAAI;IAErC,WAAW,kBAIhB;IAEK,GAAG,CACP,aAAa,EAAE,GAAG,EAClB,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuDf;YAEa,QAAQ;CAiCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,WAKhE;AAED,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,iBAAiB,CAIhB;IAET,YAA6B,IAAI,EAAE,IAAI,EAAI;IAErC,WAAW,kBAIhB;IAEK,GAAG,CACP,aAAa,EAAE,GAAG,EAClB,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuDf;YAEa,QAAQ;CAqCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
@@ -4,6 +4,7 @@ exports.OverlayDismissAgent = void 0;
4
4
  exports.isErrorSupported = isErrorSupported;
5
5
  exports.extractInterceptingElement = extractInterceptingElement;
6
6
  const cua_1 = require("@empiricalrun/cua");
7
+ const api_worker_client_1 = require("../../../../api-worker-client");
7
8
  const telemetry_1 = require("../../../../telemetry");
8
9
  const cache_1 = require("./cache");
9
10
  const prompt_1 = require("./prompt");
@@ -99,6 +100,10 @@ class OverlayDismissAgent {
99
100
  // already on the current page. Including page_goto causes the model
100
101
  // to waste turns trying to navigate instead of clicking dismiss.
101
102
  includePageGotoTool: false,
103
+ // Route the model call through api-worker (Cloudflare AI Gateway) on the
104
+ // project API key, so test-run containers carry no OpenAI key. Undefined
105
+ // on a dev machine or in a pi sandbox — see the helper.
106
+ openAIClientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
102
107
  });
103
108
  this.cachingCandidates.push({
104
109
  dom: element?.interceptor,
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/vision/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAOzE"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/vision/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QASzE"}
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.patchQuery = patchQuery;
4
4
  const vision_1 = require("@empiricalrun/llm/vision");
5
+ const api_worker_client_1 = require("../../../../api-worker-client");
5
6
  function patchQuery(LocatorClass) {
6
7
  LocatorClass.prototype.query = async function (instruction) {
7
8
  await this.waitFor({ state: "visible" });
8
9
  const screenshot = await this.screenshot();
9
- const answer = await (0, vision_1.query)(screenshot.toString("base64"), instruction);
10
+ const answer = await (0, vision_1.query)(screenshot.toString("base64"), instruction, {
11
+ clientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
12
+ });
10
13
  return answer;
11
14
  };
12
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.58.5",
3
+ "version": "0.59.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -41,7 +41,7 @@
41
41
  "openapi-typescript": "7.6.1",
42
42
  "playwright-core": "1.62.1",
43
43
  "serve-handler": "^6.1.6",
44
- "@empiricalrun/shared-types": "0.15.4"
44
+ "@empiricalrun/shared-types": "0.16.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "@babel/code-frame": "^7.24.7",
@@ -54,10 +54,10 @@
54
54
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
55
55
  "rimraf": "^6.0.1",
56
56
  "ts-morph": "^23.0.0",
57
- "@empiricalrun/cua": "^0.4.1",
58
- "@empiricalrun/llm": "^0.27.0",
57
+ "@empiricalrun/cua": "^0.4.2",
58
+ "@empiricalrun/llm": "^0.27.1",
59
59
  "@empiricalrun/r2-uploader": "^0.10.2",
60
- "@empiricalrun/reporter": "^0.29.1"
60
+ "@empiricalrun/reporter": "^0.30.0"
61
61
  },
62
62
  "scripts": {
63
63
  "dev": "pnpm gen:api-worker-openapi && tsgo --build --watch",