@empiricalrun/playwright-utils 0.63.0 → 0.63.1

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/generated/api-worker-openapi.d.ts +596 -24
  3. package/dist/generated/api-worker-openapi.d.ts.map +1 -1
  4. package/dist/overlay-tests/cache.spec.js +31 -7
  5. package/dist/overlay-tests/click.spec.js +2 -7
  6. package/dist/overlay-tests/dismiss-mechanics.spec.d.ts +2 -0
  7. package/dist/overlay-tests/dismiss-mechanics.spec.d.ts.map +1 -0
  8. package/dist/overlay-tests/dismiss-mechanics.spec.js +59 -0
  9. package/dist/overlay-tests/dropdown.spec.d.ts +2 -0
  10. package/dist/overlay-tests/dropdown.spec.d.ts.map +1 -0
  11. package/dist/overlay-tests/dropdown.spec.js +53 -0
  12. package/dist/overlay-tests/fixtures.d.ts +3 -1
  13. package/dist/overlay-tests/fixtures.d.ts.map +1 -1
  14. package/dist/overlay-tests/fixtures.js +15 -0
  15. package/dist/overlay-tests/typesafe-hit-check.spec.d.ts +2 -0
  16. package/dist/overlay-tests/typesafe-hit-check.spec.d.ts.map +1 -0
  17. package/dist/overlay-tests/typesafe-hit-check.spec.js +57 -0
  18. package/dist/overlay-tests/typesafe.spec.d.ts +2 -0
  19. package/dist/overlay-tests/typesafe.spec.d.ts.map +1 -0
  20. package/dist/overlay-tests/typesafe.spec.js +86 -0
  21. package/dist/test/scripts/pw-locator-patch/actions/click.d.ts.map +1 -1
  22. package/dist/test/scripts/pw-locator-patch/actions/click.js +8 -0
  23. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.d.ts +1 -1
  24. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.d.ts.map +1 -1
  25. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.js +9 -6
  26. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts +3 -0
  27. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts.map +1 -1
  28. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.js +13 -3
  29. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/typesafe.d.ts +38 -0
  30. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/typesafe.d.ts.map +1 -0
  31. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/typesafe.js +159 -0
  32. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts +3 -0
  33. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts.map +1 -1
  34. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.js +11 -0
  35. package/package.json +3 -3
  36. package/tsconfig.tsbuildinfo +1 -1
@@ -13,11 +13,11 @@ const useCache = (data) => {
13
13
  version: "2025-03-06",
14
14
  data,
15
15
  };
16
- const dir = path_1.default.dirname(cache_1.CACHE_FILE);
16
+ const dir = path_1.default.dirname((0, cache_1.cacheFile)());
17
17
  if (!fs_1.default.existsSync(dir)) {
18
18
  fs_1.default.mkdirSync(dir, { recursive: true });
19
19
  }
20
- fs_1.default.writeFileSync(cache_1.CACHE_FILE, JSON.stringify(cacheValue, null, 2), "utf8");
20
+ fs_1.default.writeFileSync((0, cache_1.cacheFile)(), JSON.stringify(cacheValue, null, 2), "utf8");
21
21
  };
22
22
  // Unsets env vars for the duration of a test and puts them back exactly as they
23
23
  // were — assigning `undefined` to process.env would store the string
@@ -41,11 +41,6 @@ function clearEnv(names) {
41
41
  fixtures_1.test.beforeEach(async ({ page }) => {
42
42
  (0, test_1.injectLocatorHighlightScripts)(page, fixtures_1.test);
43
43
  });
44
- fixtures_1.test.afterEach(() => {
45
- if (fs_1.default.existsSync(cache_1.CACHE_FILE)) {
46
- fs_1.default.unlinkSync(cache_1.CACHE_FILE);
47
- }
48
- });
49
44
  (0, fixtures_1.test)("overlay dismiss should not trigger recursively when executing from cache", async ({ page, server, }) => {
50
45
  useCache([
51
46
  {
@@ -68,3 +63,32 @@ fixtures_1.test.afterEach(() => {
68
63
  (0, fixtures_1.expect)(error).toBeDefined();
69
64
  (0, fixtures_1.expect)(error.message).toContain('<div class="glass-pane"></div> intercepts pointer events');
70
65
  });
66
+ for (const clickMode of ["mouse", "dispatch"]) {
67
+ (0, fixtures_1.test)(`cached dismissal in ${clickMode} mode keeps the test's listbox open`, async ({ page, server, }) => {
68
+ fixtures_1.test.fail(clickMode === "mouse", "A mouse click replayed on the toast is a pointerdown outside the listbox, which closes it");
69
+ await page.goto(`${server.baseURL}/dropdown-toast.html`);
70
+ await page.getByRole("combobox", { name: "Usage month" }).click();
71
+ const toast = page.getByRole("status", { name: "Notifications" });
72
+ await (0, fixtures_1.expect)(toast).toBeVisible();
73
+ const toastText = await toast.evaluate((el) => el.textContent?.trim().replace(/\s+/g, " ") ?? "");
74
+ useCache([
75
+ {
76
+ element: {
77
+ dom: '<section id="toast" role="status" aria-label="Notifications">',
78
+ textContent: toastText,
79
+ },
80
+ code: `await page.getByRole('button', { name: 'Later' }).click();\n`,
81
+ },
82
+ ]);
83
+ const restoreEnv = clearEnv(["EMPIRICALRUN_API_KEY", "OPENAI_BASE_URL"]);
84
+ process.env.EMPIRICALRUN_OVERLAY_CLICK_MODE = clickMode;
85
+ try {
86
+ await page.getByRole("option", { name: "September" }).click();
87
+ }
88
+ finally {
89
+ delete process.env.EMPIRICALRUN_OVERLAY_CLICK_MODE;
90
+ restoreEnv();
91
+ }
92
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText("Showing usage for September (1280 credits)");
93
+ });
94
+ }
@@ -10,13 +10,8 @@ const fixtures_1 = require("./fixtures");
10
10
  fixtures_1.test.beforeEach(async ({ page }) => {
11
11
  (0, test_1.injectLocatorHighlightScripts)(page, fixtures_1.test);
12
12
  });
13
- fixtures_1.test.afterAll(async () => {
14
- if (fs_1.default.existsSync(cache_1.CACHE_FILE)) {
15
- fs_1.default.unlinkSync(cache_1.CACHE_FILE);
16
- }
17
- });
18
13
  function getCacheDataWithDom({ dom }) {
19
- const cacheData = JSON.parse(fs_1.default.readFileSync(cache_1.CACHE_FILE, "utf8"));
14
+ const cacheData = JSON.parse(fs_1.default.readFileSync((0, cache_1.cacheFile)(), "utf8"));
20
15
  return cacheData.data.find(({ element }) => {
21
16
  if (dom) {
22
17
  return element.dom.includes(dom);
@@ -25,7 +20,7 @@ function getCacheDataWithDom({ dom }) {
25
20
  });
26
21
  }
27
22
  function getCacheDataWithTextContent({ textContent }) {
28
- const cacheData = JSON.parse(fs_1.default.readFileSync(cache_1.CACHE_FILE, "utf8"));
23
+ const cacheData = JSON.parse(fs_1.default.readFileSync((0, cache_1.cacheFile)(), "utf8"));
29
24
  return cacheData.data.find(({ element }) => {
30
25
  if (textContent) {
31
26
  return element.textContent.includes(textContent);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dismiss-mechanics.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismiss-mechanics.spec.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/dismiss-mechanics.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fixtures_1 = require("./fixtures");
4
+ const dismissals = {
5
+ "native click": (page) => page.getByRole("button", { name: "Later" }).click(),
6
+ "dispatched click": (page) => page.getByRole("button", { name: "Later" }).dispatchEvent("click"),
7
+ "Escape key": (page) => page.keyboard.press("Escape"),
8
+ };
9
+ const keepsListboxOpen = {
10
+ pointerdown: {
11
+ "native click": false,
12
+ "dispatched click": true,
13
+ "Escape key": false,
14
+ },
15
+ focusout: {
16
+ "native click": false,
17
+ "dispatched click": true,
18
+ "Escape key": false,
19
+ },
20
+ };
21
+ for (const [closeOn, expectations] of Object.entries(keepsListboxOpen)) {
22
+ for (const [name, keepsOpen] of Object.entries(expectations)) {
23
+ (0, fixtures_1.test)(`${name} on a toast ${keepsOpen ? "keeps" : "closes"} a listbox that closes on ${closeOn}`, async ({ page, server, }) => {
24
+ await page.goto(`${server.baseURL}/dropdown-toast.html?close=${closeOn}`);
25
+ await page.getByRole("combobox", { name: "Usage month" }).click();
26
+ const listbox = page.getByRole("listbox", { name: "Months" });
27
+ await (0, fixtures_1.expect)(listbox).toBeVisible();
28
+ await (0, fixtures_1.expect)(page.getByText("Install our CLI for your coding agents")).toBeVisible();
29
+ await dismissals[name](page);
30
+ await (0, fixtures_1.expect)(page.getByText("Install our CLI for your coding agents")).toBeVisible({ visible: name === "Escape key" });
31
+ await (0, fixtures_1.expect)(listbox).toBeVisible({ visible: keepsOpen });
32
+ });
33
+ }
34
+ }
35
+ (0, fixtures_1.test)("dispatched click lets the test finish selecting a month", async ({ page, server, }) => {
36
+ await page.goto(`${server.baseURL}/dropdown-toast.html?close=pointerdown`);
37
+ await page.getByRole("combobox", { name: "Usage month" }).click();
38
+ await (0, fixtures_1.expect)(page.getByText("Install our CLI for your coding agents")).toBeVisible();
39
+ await page.getByRole("button", { name: "Later" }).dispatchEvent("click");
40
+ const response = page.waitForResponse(/usage\.json/);
41
+ await page.getByRole("option", { name: "September" }).click();
42
+ await response;
43
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText("Showing usage for September (1280 credits)");
44
+ });
45
+ (0, fixtures_1.test)("Escape on an open date picker throws away the picked date", async ({ page, server, }) => {
46
+ await page.goto(`${server.baseURL}/datepicker.html`);
47
+ await page.getByPlaceholder("Pick a date").click();
48
+ await page.getByRole("grid").getByRole("button", { name: "15" }).click();
49
+ await page.keyboard.press("Escape");
50
+ await page.getByRole("button", { name: "Save" }).click();
51
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText("Saved without a date");
52
+ });
53
+ (0, fixtures_1.test)("Escape on open search results clears the query", async ({ page, server, }) => {
54
+ await page.goto(`${server.baseURL}/search-results.html`);
55
+ await page.getByRole("searchbox", { name: "Search assets" }).fill("orders");
56
+ await (0, fixtures_1.expect)(page.getByText("Showing 74 results")).toBeVisible();
57
+ await page.keyboard.press("Escape");
58
+ await (0, fixtures_1.expect)(page.getByRole("searchbox", { name: "Search assets" })).toHaveValue("");
59
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dropdown.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropdown.spec.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/dropdown.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const test_1 = require("../test");
4
+ const fixtures_1 = require("./fixtures");
5
+ fixtures_1.test.beforeEach(async ({ page }) => {
6
+ (0, test_1.injectLocatorHighlightScripts)(page, fixtures_1.test);
7
+ });
8
+ async function openMonthsUnderToast(page) {
9
+ await page.getByRole("combobox", { name: "Usage month" }).click();
10
+ await (0, fixtures_1.expect)(page.getByText("Install our CLI for your coding agents")).toBeVisible();
11
+ }
12
+ for (const clickMode of ["mouse", "dispatch"]) {
13
+ fixtures_1.test.describe(`overlay click mode: ${clickMode}`, () => {
14
+ fixtures_1.test.beforeEach(() => {
15
+ process.env.EMPIRICALRUN_OVERLAY_CLICK_MODE = clickMode;
16
+ });
17
+ fixtures_1.test.afterEach(() => {
18
+ delete process.env.EMPIRICALRUN_OVERLAY_CLICK_MODE;
19
+ });
20
+ (0, fixtures_1.test)("selects a month through a toast covering the open listbox", async ({ page, server, }) => {
21
+ fixtures_1.test.fail(clickMode === "mouse", "A mouse click on the toast is a pointerdown outside the listbox, which closes it before the retried click");
22
+ await page.goto(`${server.baseURL}/dropdown-toast.html`);
23
+ await openMonthsUnderToast(page);
24
+ await page.getByRole("option", { name: "September" }).click();
25
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText("Showing usage for September (1280 credits)");
26
+ });
27
+ });
28
+ }
29
+ (0, fixtures_1.test)("usage response wait survives a toast covering the listbox", async ({ page, server, }) => {
30
+ fixtures_1.test.fail(true, "waitForResponse times out while the click waits out its actionability timeout, before any dismissal runs");
31
+ await page.goto(`${server.baseURL}/dropdown-toast.html`);
32
+ await openMonthsUnderToast(page);
33
+ await Promise.all([
34
+ page.waitForResponse(/usage\.json/),
35
+ page.getByRole("option", { name: "September" }).click(),
36
+ ]);
37
+ });
38
+ (0, fixtures_1.test)("saves with the picked date when the open date picker covers Save", async ({ page, server, }) => {
39
+ fixtures_1.test.skip(true, "Not deterministic: the agent keeps the picked date in some runs and loses it in others");
40
+ await page.goto(`${server.baseURL}/datepicker.html`);
41
+ await page.getByPlaceholder("Pick a date").click();
42
+ await page.getByRole("grid").getByRole("button", { name: "15" }).click();
43
+ await page.getByRole("button", { name: "Save" }).click();
44
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText("Saved for 2026-09-15");
45
+ });
46
+ (0, fixtures_1.test)("keeps the search query when open results cover a filter", async ({ page, server, }) => {
47
+ fixtures_1.test.skip(true, "Not deterministic: the agent keeps the query in some runs and clears it in others");
48
+ await page.goto(`${server.baseURL}/search-results.html`);
49
+ await page.getByRole("searchbox", { name: "Search assets" }).fill("orders");
50
+ await (0, fixtures_1.expect)(page.getByText("Showing 74 results")).toBeVisible();
51
+ await page.getByRole("button", { name: "Verified" }).click();
52
+ await (0, fixtures_1.expect)(page.locator("#status")).toHaveText('Query "orders", verified only: true');
53
+ });
@@ -3,7 +3,9 @@ type ServerFixture = {
3
3
  baseURL: string;
4
4
  };
5
5
  export declare const expect: import("@playwright/test").Expect<{}>;
6
- export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & {
6
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
7
+ overlayCacheFile: string;
8
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & {
7
9
  server: ServerFixture;
8
10
  }>;
9
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/fixtures.ts"],"names":[],"mappings":"AAKA,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,MAAM,uCAAc,CAAC;AAClC,eAAO,MAAM,IAAI;YAA6B,aAAa;EAuBzD,CAAC"}
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/fixtures.ts"],"names":[],"mappings":"AAKA,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,MAAM,uCAAc,CAAC;AAClC,eAAO,MAAM,IAAI;sBACK,MAAM;;YAChB,aAAa;EAsCvB,CAAC"}
@@ -10,6 +10,21 @@ const path_1 = __importDefault(require("path"));
10
10
  const serve_handler_1 = __importDefault(require("serve-handler"));
11
11
  exports.expect = test_1.test.expect;
12
12
  exports.test = test_1.test.extend({
13
+ overlayCacheFile: [
14
+ async ({}, use, testInfo) => {
15
+ const previous = process.env.EMPIRICALRUN_OVERLAY_CACHE_FILE;
16
+ const file = testInfo.outputPath("overlay-cache.json");
17
+ process.env.EMPIRICALRUN_OVERLAY_CACHE_FILE = file;
18
+ await use(file);
19
+ if (previous === undefined) {
20
+ delete process.env.EMPIRICALRUN_OVERLAY_CACHE_FILE;
21
+ }
22
+ else {
23
+ process.env.EMPIRICALRUN_OVERLAY_CACHE_FILE = previous;
24
+ }
25
+ },
26
+ { auto: true },
27
+ ],
13
28
  server: [
14
29
  async ({}, use, workerInfo) => {
15
30
  const port = 1234 + workerInfo.workerIndex;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=typesafe-hit-check.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typesafe-hit-check.spec.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/typesafe-hit-check.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const typesafe_1 = require("../test/scripts/pw-locator-patch/dismiss-overlays/typesafe");
4
+ const fixtures_1 = require("./fixtures");
5
+ async function centerOf(locator) {
6
+ const box = await locator.boundingBox();
7
+ if (!box) {
8
+ throw new Error("Element has no bounding box");
9
+ }
10
+ return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
11
+ }
12
+ (0, fixtures_1.test)("allows the toast close button", async ({ page, server }) => {
13
+ await page.goto(`${server.baseURL}/choose-an-overlay.html`);
14
+ const target = await page
15
+ .getByRole("button", { name: "Target" })
16
+ .elementHandle();
17
+ const point = await centerOf(page.locator("#toastCloseBtn"));
18
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, point)).toBe("ok");
19
+ });
20
+ (0, fixtures_1.test)("refuses the dialog behind a toast that blocks the target", async ({ page, server, }) => {
21
+ await page.goto(`${server.baseURL}/choose-an-overlay.html`);
22
+ const target = await page
23
+ .getByRole("button", { name: "Target" })
24
+ .elementHandle();
25
+ const point = await centerOf(page.getByRole("button", { name: "Noted" }));
26
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, point)).toBe("outside_overlay");
27
+ });
28
+ (0, fixtures_1.test)("refuses a page control outside the blocking toast", async ({ page, server, }) => {
29
+ await page.goto(`${server.baseURL}/toast-without-close.html`);
30
+ const target = await page
31
+ .getByRole("button", { name: "Target" })
32
+ .elementHandle();
33
+ const point = await centerOf(page.getByRole("button", { name: "Close project" }));
34
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, point)).toBe("outside_overlay");
35
+ });
36
+ (0, fixtures_1.test)("refuses text inside the blocking toast", async ({ page, server }) => {
37
+ await page.goto(`${server.baseURL}/toast-without-close.html`);
38
+ const target = await page
39
+ .getByRole("button", { name: "Target" })
40
+ .elementHandle();
41
+ const point = await centerOf(page.locator("#toast"));
42
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, point)).toBe("not_clickable");
43
+ });
44
+ (0, fixtures_1.test)("refuses the target under a glass pane", async ({ page, server }) => {
45
+ await page.goto(`${server.baseURL}/glass-pane.html`);
46
+ const target = await page
47
+ .getByRole("button", { name: "Target" })
48
+ .elementHandle();
49
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, await centerOf(page.getByRole("button", { name: "Target" })))).toBe("not_clickable");
50
+ });
51
+ (0, fixtures_1.test)("reports no blocker when the target is uncovered", async ({ page, server, }) => {
52
+ await page.goto(`${server.baseURL}/toast-without-close.html`);
53
+ const target = await page
54
+ .getByRole("button", { name: "Close project" })
55
+ .elementHandle();
56
+ (0, fixtures_1.expect)(await (0, typesafe_1.checkHit)(target, await centerOf(page.getByRole("button", { name: "Close project" })))).toBe("no_blocker");
57
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=typesafe.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typesafe.spec.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/typesafe.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_fs_1 = __importDefault(require("node:fs"));
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const dismiss_overlays_1 = require("../test/scripts/pw-locator-patch/dismiss-overlays");
9
+ const typesafe_1 = require("../test/scripts/pw-locator-patch/dismiss-overlays/typesafe");
10
+ const utils_1 = require("../test/scripts/pw-locator-patch/dismiss-overlays/utils");
11
+ const fixtures_1 = require("./fixtures");
12
+ fixtures_1.test.skip(!process.env.TYPESAFE_API_KEY, "TYPESAFE_API_KEY is not set");
13
+ const RESULTS_FILE = node_path_1.default.join(process.cwd(), "test-results", "typesafe-overlay-results.jsonl");
14
+ const DISMISS_ATTEMPTS = 3;
15
+ async function actWithTypeSafeDismissal(page, target, action) {
16
+ const results = [];
17
+ for (let attempt = 0;; attempt++) {
18
+ try {
19
+ await target[action]({ timeout: 3_000 });
20
+ return results;
21
+ }
22
+ catch (error) {
23
+ if (attempt === DISMISS_ATTEMPTS || !(0, dismiss_overlays_1.isErrorSupported)(error.message)) {
24
+ throw error;
25
+ }
26
+ const element = (0, dismiss_overlays_1.extractInterceptingElement)(error.message);
27
+ const text = await (0, utils_1.textContent)(page, element);
28
+ const result = await (0, typesafe_1.dismissWithTypeSafe)(page, {
29
+ text,
30
+ target: await target.elementHandle(),
31
+ });
32
+ results.push(result);
33
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(RESULTS_FILE), { recursive: true });
34
+ node_fs_1.default.appendFileSync(RESULTS_FILE, JSON.stringify({
35
+ test: fixtures_1.test.info().title,
36
+ attempt,
37
+ overlayText: text,
38
+ ...result,
39
+ }) + "\n");
40
+ if (!result.dismissed) {
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+ }
46
+ (0, fixtures_1.test)("dismisses the toast but not the dialog", async ({ page, server }) => {
47
+ await page.goto(`${server.baseURL}/choose-an-overlay.html`);
48
+ await actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click");
49
+ await (0, fixtures_1.expect)(page.getByText("This is a toast message")).not.toBeVisible();
50
+ await (0, fixtures_1.expect)(page.getByRole("heading", { name: "Center Overlay" })).toBeVisible();
51
+ });
52
+ (0, fixtures_1.test)("ticks the checkbox and accepts terms", async ({ page, server }) => {
53
+ await page.goto(`${server.baseURL}/tos.html`);
54
+ page.on("dialog", (dialog) => dialog.dismiss());
55
+ await actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click");
56
+ await (0, fixtures_1.expect)(page.getByRole("heading", { name: "Terms of Service" })).not.toBeVisible();
57
+ });
58
+ (0, fixtures_1.test)("dismisses a product tour and terms in sequence", async ({ page, server, }) => {
59
+ await page.goto(`${server.baseURL}/loop-to-dismiss.html`);
60
+ await actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click");
61
+ await (0, fixtures_1.expect)(page.getByText("Clicked!")).toBeVisible();
62
+ });
63
+ (0, fixtures_1.test)("dismisses a Survicate survey for hover", async ({ page, server }) => {
64
+ await page.goto(`${server.baseURL}/survey.html`);
65
+ await (0, fixtures_1.expect)(page.getByText("Start survey")).toBeVisible({ timeout: 15_000 });
66
+ await actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Click target" }), "hover");
67
+ });
68
+ (0, fixtures_1.test)("cannot fill a form overlay", async ({ page, server }) => {
69
+ await page.goto(`${server.baseURL}/overlay-form.html`);
70
+ await (0, fixtures_1.expect)(actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click")).rejects.toThrow();
71
+ });
72
+ (0, fixtures_1.test)("does not click a page control when the toast has no close button", async ({ page, server, }) => {
73
+ await page.goto(`${server.baseURL}/toast-without-close.html`);
74
+ await (0, fixtures_1.expect)(actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click")).rejects.toThrow();
75
+ await (0, fixtures_1.expect)(page.getByText("Project closed")).not.toBeVisible();
76
+ });
77
+ (0, fixtures_1.test)("does not click anything for an invisible glass pane", async ({ page, server, }) => {
78
+ await page.goto(`${server.baseURL}/glass-pane.html`);
79
+ let dialogOpened = false;
80
+ page.on("dialog", (dialog) => {
81
+ dialogOpened = true;
82
+ return dialog.dismiss();
83
+ });
84
+ await (0, fixtures_1.expect)(actWithTypeSafeDismissal(page, page.getByRole("button", { name: "Target" }), "click")).rejects.toThrow();
85
+ (0, fixtures_1.expect)(dialogOpened).toBe(false);
86
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/actions/click.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC,wBAAgB,UAAU,CACxB,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,EAC/C,MAAM,EAAE,MAAM,QAsDf"}
1
+ {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/actions/click.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAWrC,wBAAgB,UAAU,CACxB,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,EAC/C,MAAM,EAAE,MAAM,QA+Df"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.patchClick = patchClick;
4
+ const cua_1 = require("@empiricalrun/cua");
4
5
  const dismiss_overlays_1 = require("../dismiss-overlays");
5
6
  const utils_1 = require("../dismiss-overlays/utils");
6
7
  const utils_2 = require("../utils");
@@ -15,6 +16,13 @@ function patchClick(LocatorClass, testFn) {
15
16
  LocatorClass.prototype.click = async function (options) {
16
17
  const stepName = `locator.click(${(0, utils_2.description)(this)})`;
17
18
  return await testFn.step(stepName, async () => {
19
+ if (options?.isExecutingForOverlayDismiss &&
20
+ (0, dismiss_overlays_1.overlayClickMode)() === "dispatch" &&
21
+ !(await this.evaluate(cua_1.needsFocus, undefined, {
22
+ timeout: options.timeout,
23
+ }))) {
24
+ return await this.dispatchEvent("click");
25
+ }
18
26
  let overlayAttemptsRemaining = 2;
19
27
  let originalError;
20
28
  const overlayDismiss = new dismiss_overlays_1.OverlayDismissAgent(this._frame._page);
@@ -1,5 +1,5 @@
1
1
  import type { Page } from "@playwright/test";
2
- export declare const CACHE_FILE: string;
2
+ export declare function cacheFile(): string;
3
3
  export declare function setCodeToCache({ dom, text, code, }: {
4
4
  dom: string | undefined;
5
5
  text: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAM7C,eAAO,MAAM,UAAU,QAItB,CAAC;AAMF,wBAAsB,cAAc,CAAC,EACnC,GAAG,EACH,IAAI,EACJ,IAAI,GACL,EAAE;IACD,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,iBAgCA;AAED,wBAAsB,gBAAgB,CAAC,EACrC,IAAI,EACJ,GAAG,EACH,IAAI,GACL,EAAE;IACD,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAiChC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,UAY5C"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAM7C,wBAAgB,SAAS,WAKxB;AAMD,wBAAsB,cAAc,CAAC,EACnC,GAAG,EACH,IAAI,EACJ,IAAI,GACL,EAAE;IACD,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,iBAgCA;AAED,wBAAsB,gBAAgB,CAAC,EACrC,IAAI,EACJ,GAAG,EACH,IAAI,GACL,EAAE;IACD,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAiChC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,UAY5C"}
@@ -3,13 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CACHE_FILE = void 0;
6
+ exports.cacheFile = cacheFile;
7
7
  exports.setCodeToCache = setCodeToCache;
8
8
  exports.executeFromCache = executeFromCache;
9
9
  exports.addOverlayOption = addOverlayOption;
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
- exports.CACHE_FILE = path_1.default.join(process.cwd(), ".empiricalrun", `overlay-cache.json`);
12
+ function cacheFile() {
13
+ return (process.env.EMPIRICALRUN_OVERLAY_CACHE_FILE ??
14
+ path_1.default.join(process.cwd(), ".empiricalrun", "overlay-cache.json"));
15
+ }
13
16
  function normalizeString(str) {
14
17
  return str.replace(/\s+/g, " ").trim();
15
18
  }
@@ -32,7 +35,7 @@ async function setCodeToCache({ dom, text, code, }) {
32
35
  data: [],
33
36
  };
34
37
  try {
35
- cache = JSON.parse(fs_1.default.readFileSync(exports.CACHE_FILE, "utf8"));
38
+ cache = JSON.parse(fs_1.default.readFileSync(cacheFile(), "utf8"));
36
39
  cache.data.push(obj);
37
40
  }
38
41
  catch {
@@ -41,18 +44,18 @@ async function setCodeToCache({ dom, text, code, }) {
41
44
  data: [obj],
42
45
  };
43
46
  }
44
- const dir = path_1.default.dirname(exports.CACHE_FILE);
47
+ const dir = path_1.default.dirname(cacheFile());
45
48
  if (!fs_1.default.existsSync(dir)) {
46
49
  fs_1.default.mkdirSync(dir, { recursive: true });
47
50
  }
48
- fs_1.default.writeFileSync(exports.CACHE_FILE, JSON.stringify(cache, null, 2));
51
+ fs_1.default.writeFileSync(cacheFile(), JSON.stringify(cache, null, 2));
49
52
  }
50
53
  async function executeFromCache({ page, dom, text, }) {
51
54
  if (!dom && !text) {
52
55
  return { success: false };
53
56
  }
54
57
  try {
55
- const cache = JSON.parse(fs_1.default.readFileSync(exports.CACHE_FILE, "utf8"));
58
+ const cache = JSON.parse(fs_1.default.readFileSync(cacheFile(), "utf8"));
56
59
  const match = cache.data.find((c) => {
57
60
  return (c.element.dom === (dom || "") &&
58
61
  c.element.textContent === normalizeString(text || ""));
@@ -1,6 +1,8 @@
1
+ import { type ClickMode } from "@empiricalrun/cua";
1
2
  import { Page } from "@playwright/test";
2
3
  import type { TestFn } from "../../types";
3
4
  import type { OverlayElement } from "./types";
5
+ export declare function overlayClickMode(): ClickMode;
4
6
  export declare function isErrorSupported(errorMessage: string | undefined): boolean;
5
7
  export declare class OverlayDismissAgent {
6
8
  private readonly page;
@@ -8,6 +10,7 @@ export declare class OverlayDismissAgent {
8
10
  constructor(page: Page);
9
11
  saveToCache(): Promise<void>;
10
12
  run(originalError: any, reporter: (description: string) => void, testFn: TestFn): Promise<void>;
13
+ private dismiss;
11
14
  private runAgent;
12
15
  }
13
16
  export declare function extractInterceptingElement(errorMessage: string): OverlayElement | undefined;
@@ -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;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,CAqDf;YAEa,QAAQ;CA+BvB;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":"AAAA,OAAO,EACL,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAC3B,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;AAW9C,wBAAgB,gBAAgB,IAAI,SAAS,CAI5C;AAED,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,CAUf;YAEa,OAAO;YAwDP,QAAQ;CAkCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OverlayDismissAgent = void 0;
4
+ exports.overlayClickMode = overlayClickMode;
4
5
  exports.isErrorSupported = isErrorSupported;
5
6
  exports.extractInterceptingElement = extractInterceptingElement;
6
7
  const cua_1 = require("@empiricalrun/cua");
@@ -10,6 +11,11 @@ const cache_1 = require("./cache");
10
11
  const prompt_1 = require("./prompt");
11
12
  const utils_1 = require("./utils");
12
13
  const ERROR_SUBSTRING_INTERCEPTION = "intercepts pointer events";
14
+ function overlayClickMode() {
15
+ return process.env.EMPIRICALRUN_OVERLAY_CLICK_MODE === "dispatch"
16
+ ? "dispatch"
17
+ : "mouse";
18
+ }
13
19
  function isErrorSupported(errorMessage) {
14
20
  if (!errorMessage) {
15
21
  return false;
@@ -28,12 +34,15 @@ class OverlayDismissAgent {
28
34
  }
29
35
  }
30
36
  async run(originalError, reporter, testFn) {
31
- const startTime = Date.now();
32
37
  let element;
33
38
  try {
34
39
  element = extractInterceptingElement(originalError.message);
35
40
  }
36
41
  catch { }
42
+ await testFn.step((0, utils_1.dismissOverlayStepTitle)(element), () => this.dismiss(element, originalError, reporter, testFn), { box: true });
43
+ }
44
+ async dismiss(element, originalError, reporter, testFn) {
45
+ const startTime = Date.now();
37
46
  const description = (0, utils_1.overlayDescription)(element) ?? "unknown";
38
47
  const testName = testFn.info().title;
39
48
  reporter(`Attempting to auto-dismiss overlay: ${description}`);
@@ -83,7 +92,7 @@ class OverlayDismissAgent {
83
92
  async runAgent(element, text, testFn) {
84
93
  const task = await (0, prompt_1.getTask)(this.page, element, text);
85
94
  const trace = (0, utils_1.traceThis)(task, testFn);
86
- const result = await (0, cua_1.createTestUsingComputerUseAgent)({
95
+ const result = await testFn.step(utils_1.DISMISSAL_AGENT_STEP, () => (0, cua_1.createTestUsingComputerUseAgent)({
87
96
  page: this.page,
88
97
  task,
89
98
  trace,
@@ -96,7 +105,8 @@ class OverlayDismissAgent {
96
105
  prefersElementFromPointCodegen: true,
97
106
  includePageGotoTool: false,
98
107
  openAIClientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
99
- });
108
+ clickMode: overlayClickMode(),
109
+ }));
100
110
  this.cachingCandidates.push({
101
111
  dom: element?.interceptor,
102
112
  text,
@@ -0,0 +1,38 @@
1
+ import type { ElementHandle, Page } from "playwright-core";
2
+ type Box = {
3
+ top: number;
4
+ bottom: number;
5
+ left: number;
6
+ right: number;
7
+ };
8
+ export type TypeSafeDismissStep = {
9
+ popupProbability: number;
10
+ box: Box;
11
+ boxConfidence: number;
12
+ clicked: {
13
+ x: number;
14
+ y: number;
15
+ } | undefined;
16
+ hitCheck: HitCheck | undefined;
17
+ latencyMs: number;
18
+ inputTokens: number;
19
+ outputTokens: number;
20
+ };
21
+ type Point = {
22
+ x: number;
23
+ y: number;
24
+ };
25
+ type HitCheck = "ok" | "no_blocker" | "outside_overlay" | "not_clickable";
26
+ export type TypeSafeStopReason = "popup_gone" | "low_box_confidence" | Exclude<HitCheck, "ok"> | "no_visual_change" | "max_steps";
27
+ export type TypeSafeDismissResult = {
28
+ dismissed: boolean;
29
+ stopReason: TypeSafeStopReason;
30
+ steps: TypeSafeDismissStep[];
31
+ };
32
+ export declare function dismissWithTypeSafe(page: Page, overlay: {
33
+ text: string | undefined;
34
+ target: ElementHandle<Element>;
35
+ }): Promise<TypeSafeDismissResult>;
36
+ export declare function checkHit(target: ElementHandle<Element>, point: Point): Promise<"no_blocker" | "not_clickable" | "ok" | "outside_overlay">;
37
+ export {};
38
+ //# sourceMappingURL=typesafe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typesafe.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/typesafe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAQ3D,KAAK,GAAG,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAWxE,MAAM,MAAM,mBAAmB,GAAG;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,GAAG,CAAC;IACT,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC9C,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,KAAK,QAAQ,GAAG,IAAI,GAAG,YAAY,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAE1E,MAAM,MAAM,kBAAkB,GAC1B,YAAY,GACZ,oBAAoB,GACpB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,GACvB,kBAAkB,GAClB,WAAW,CAAC;AAEhB,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,CAAC;AAEF,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAAE,GACpE,OAAO,CAAC,qBAAqB,CAAC,CAoDhC;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,sEAyDpE"}