@empiricalrun/playwright-utils 0.65.0 → 0.66.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api-worker-client.d.ts +25 -6
  3. package/dist/api-worker-client.d.ts.map +1 -1
  4. package/dist/api-worker-client.js +73 -9
  5. package/dist/generated/api-worker-openapi.d.ts +2568 -1384
  6. package/dist/generated/api-worker-openapi.d.ts.map +1 -1
  7. package/dist/kv.d.ts +31 -1
  8. package/dist/kv.d.ts.map +1 -1
  9. package/dist/kv.js +79 -5
  10. package/dist/overlay-tests/typesafe.spec.js +5 -3
  11. package/dist/overlay-tests/visual.spec.js +1 -1
  12. package/dist/reporter/attachment-cleanup-test-reporter.d.ts +3 -1
  13. package/dist/reporter/attachment-cleanup-test-reporter.d.ts.map +1 -1
  14. package/dist/reporter/attachment-cleanup-test-reporter.js +5 -1
  15. package/dist/reporter/attachment-cleanup.d.ts +1 -1
  16. package/dist/reporter/attachment-cleanup.d.ts.map +1 -1
  17. package/dist/reporter/attachment-cleanup.js +14 -7
  18. package/dist/reporter/attachment-key.d.ts +3 -0
  19. package/dist/reporter/attachment-key.d.ts.map +1 -0
  20. package/dist/reporter/attachment-key.js +31 -0
  21. package/dist/reporter/empirical-reporter.d.ts +1 -1
  22. package/dist/reporter/empirical-reporter.d.ts.map +1 -1
  23. package/dist/reporter/empirical-reporter.js +7 -6
  24. package/dist/reporter/harness.d.ts +6 -1
  25. package/dist/reporter/harness.d.ts.map +1 -1
  26. package/dist/reporter/harness.js +22 -13
  27. package/dist/reporter/incremental-blob-reporter.d.ts +1 -0
  28. package/dist/reporter/incremental-blob-reporter.d.ts.map +1 -1
  29. package/dist/reporter/incremental-blob-reporter.js +8 -4
  30. package/dist/reporter/util.d.ts +3 -3
  31. package/dist/reporter/util.d.ts.map +1 -1
  32. package/dist/reporter/util.js +10 -8
  33. package/dist/test/cross-shard-locks.d.ts +22 -0
  34. package/dist/test/cross-shard-locks.d.ts.map +1 -0
  35. package/dist/test/cross-shard-locks.js +110 -0
  36. package/dist/test/expect/visual.d.ts.map +1 -1
  37. package/dist/test/expect/visual.js +6 -2
  38. package/dist/test/index.d.ts +1 -0
  39. package/dist/test/index.d.ts.map +1 -1
  40. package/dist/test/index.js +13 -0
  41. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts.map +1 -1
  42. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.js +0 -2
  43. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts +0 -1
  44. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts.map +1 -1
  45. package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.js +0 -20
  46. package/dist/test/scripts/pw-locator-patch/vision/query.d.ts.map +1 -1
  47. package/dist/test/scripts/pw-locator-patch/vision/query.js +57 -5
  48. package/docs/kv.md +66 -2
  49. package/package.json +4 -5
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -22,6 +22,7 @@ const mime_1 = __importDefault(require("mime"));
22
22
  const path_1 = __importDefault(require("path"));
23
23
  const api_worker_client_1 = require("../api-worker-client");
24
24
  const logger_1 = require("../logger");
25
+ const attachment_key_1 = require("./attachment-key");
25
26
  // Mirrors https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/tools/cli-client/program.ts#L440
26
27
  function calculateSha1(buffer) {
27
28
  const hash = crypto_1.default.createHash("sha1");
@@ -173,8 +174,8 @@ async function sendTestCaseBeginToDashboard(params) {
173
174
  function safelySerialiseJSON(obj, keyFilter) {
174
175
  return JSON.parse(JSON.stringify(obj, (key, value) => (keyFilter?.(key) ? undefined : value)));
175
176
  }
176
- async function updateHtmlZipFileAttachmentPaths(jsonFilePath, htmlFilePath, repoPath) {
177
- const testAttachmentMap = buildTestAttachmentMapFromSummary(jsonFilePath, repoPath);
177
+ async function updateHtmlZipFileAttachmentPaths(jsonFilePath, htmlFilePath, outputDirs) {
178
+ const testAttachmentMap = buildTestAttachmentMapFromSummary(jsonFilePath, outputDirs);
178
179
  let htmlFile;
179
180
  try {
180
181
  htmlFile = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
@@ -232,7 +233,7 @@ async function updateHtmlZipFileAttachmentPaths(jsonFilePath, htmlFilePath, repo
232
233
  finally {
233
234
  }
234
235
  }
235
- function buildTestAttachmentMapFromSummary(summaryJsonFilePath, repoPath, doNotNormalize = false) {
236
+ function buildTestAttachmentMapFromSummary(summaryJsonFilePath, outputDirs, doNotNormalize = false) {
236
237
  const summaryJson = JSON.parse(fs_1.default.readFileSync(summaryJsonFilePath, "utf8"));
237
238
  return (0, reporter_1.buildTestAttachmentMap)(summaryJson, {
238
239
  transformAttachment: doNotNormalize
@@ -241,8 +242,10 @@ function buildTestAttachmentMapFromSummary(summaryJsonFilePath, repoPath, doNotN
241
242
  if (!attachment?.path) {
242
243
  return attachment;
243
244
  }
244
- const relativePath = path_1.default.relative(repoPath, attachment.path);
245
- return { ...attachment, path: path_1.default.join("data", relativePath) };
245
+ return {
246
+ ...attachment,
247
+ path: path_1.default.join("data", (0, attachment_key_1.attachmentKey)(attachment.path, outputDirs)),
248
+ };
246
249
  },
247
250
  });
248
251
  }
@@ -255,7 +258,7 @@ async function checkFileExistsAsync(filePath) {
255
258
  return false;
256
259
  }
257
260
  }
258
- async function updateSummaryJsonAttachmentPaths(filePath, repoPath, urlPrefix) {
261
+ async function updateSummaryJsonAttachmentPaths(filePath, outputDirs, urlPrefix) {
259
262
  if (!process.env.PROJECT_NAME || !process.env.TEST_RUN_GITHUB_ACTION_ID) {
260
263
  console.error("PROJECT_NAME and TEST_RUN_GITHUB_ACTION_ID must be set");
261
264
  return;
@@ -266,8 +269,7 @@ async function updateSummaryJsonAttachmentPaths(filePath, repoPath, urlPrefix) {
266
269
  for (const result of test.results || []) {
267
270
  for (const attachment of result.attachments || []) {
268
271
  if (attachment?.path) {
269
- const relativePath = path_1.default.relative(repoPath, attachment.path);
270
- attachment.path = `${urlPrefix}/${relativePath}`;
272
+ attachment.path = `${urlPrefix}/${(0, attachment_key_1.attachmentKey)(attachment.path, outputDirs)}`;
271
273
  }
272
274
  }
273
275
  }
@@ -0,0 +1,22 @@
1
+ import { type TestLocksByTestId } from "@empiricalrun/shared-types/playwright-utils";
2
+ import type { TestInfo } from "@playwright/test";
3
+ import { type KVClient } from "../kv";
4
+ type LockKv = Pick<KVClient, "set" | "refresh" | "delete">;
5
+ export declare const LOCK_LEASE_SECONDS = 60;
6
+ export declare function lockNamesFor(testId: string, locks?: TestLocksByTestId): string[];
7
+ export declare function lockKey(runId: string, name: string): string;
8
+ export declare function holdLocks({ kv, runId, names, holder, annotate, now, sleep, }: {
9
+ kv: LockKv;
10
+ runId: string;
11
+ names: string[];
12
+ holder: unknown;
13
+ annotate: (annotation: {
14
+ type: string;
15
+ description: string;
16
+ }) => void;
17
+ now?: () => number;
18
+ sleep?: (ms: number) => Promise<void>;
19
+ }): Promise<() => Promise<void>>;
20
+ export declare function holdCrossShardLocks(testInfo: TestInfo, createKv: () => LockKv): Promise<() => Promise<void>>;
21
+ export {};
22
+ //# sourceMappingURL=cross-shard-locks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-shard-locks.d.ts","sourceRoot":"","sources":["../../src/test/cross-shard-locks.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,KAAK,QAAQ,EAA6B,MAAM,OAAO,CAAC;AAEjE,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAI3D,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAkBrC,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,iBACa,GACnB,MAAM,EAAE,CAEV;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3D;AAuBD,wBAAsB,SAAS,CAAC,EAC9B,EAAE,EACF,KAAK,EACL,KAAK,EACL,MAAM,EACN,QAAQ,EACR,GAAc,EACd,KAAiE,GAClE,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA6C/B;AAED,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,MAAM,GACrB,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAmB9B"}
@@ -0,0 +1,110 @@
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
+ exports.LOCK_LEASE_SECONDS = void 0;
7
+ exports.lockNamesFor = lockNamesFor;
8
+ exports.lockKey = lockKey;
9
+ exports.holdLocks = holdLocks;
10
+ exports.holdCrossShardLocks = holdCrossShardLocks;
11
+ const node_crypto_1 = require("node:crypto");
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const playwright_utils_1 = require("@empiricalrun/shared-types/playwright-utils");
14
+ const kv_1 = require("../kv");
15
+ exports.LOCK_LEASE_SECONDS = 60;
16
+ const RENEW_EVERY_MS = 20_000;
17
+ const MAX_POLL_MS = 2_000;
18
+ let sharedLocks;
19
+ function readSharedLocks() {
20
+ const file = process.env[playwright_utils_1.TEST_LOCKS_FILE_ENV];
21
+ if (!file) {
22
+ return {};
23
+ }
24
+ try {
25
+ return JSON.parse(node_fs_1.default.readFileSync(file, "utf-8"));
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ function lockNamesFor(testId, locks = (sharedLocks ??=
32
+ readSharedLocks())) {
33
+ return [...new Set(locks[testId] ?? [])].sort();
34
+ }
35
+ function lockKey(runId, name) {
36
+ const readable = name.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 200);
37
+ const digest = (0, node_crypto_1.createHash)("sha256").update(name).digest("hex").slice(0, 12);
38
+ return `pw-lock:${runId}:${readable}:${digest}`;
39
+ }
40
+ async function acquire(kv, key, holder, sleep) {
41
+ for (let attempt = 0;; attempt++) {
42
+ try {
43
+ const entry = await kv.set(key, holder, exports.LOCK_LEASE_SECONDS, {
44
+ ifAbsent: true,
45
+ });
46
+ return entry.version;
47
+ }
48
+ catch (error) {
49
+ if (!(error instanceof kv_1.KvPreconditionFailedError)) {
50
+ throw error;
51
+ }
52
+ await sleep(Math.min(MAX_POLL_MS, 250 * 2 ** attempt));
53
+ }
54
+ }
55
+ }
56
+ async function holdLocks({ kv, runId, names, holder, annotate, now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), }) {
57
+ const held = [];
58
+ for (const name of names) {
59
+ const key = lockKey(runId, name);
60
+ const startedAt = now();
61
+ const version = await acquire(kv, key, holder, sleep);
62
+ held.push({ name, key, version });
63
+ annotate({
64
+ type: "lock",
65
+ description: `${name}: waited ${Math.round((now() - startedAt) / 1000)}s across shards`,
66
+ });
67
+ }
68
+ const leaseLost = (lock) => annotate({ type: "lock", description: `${lock.name}: lease lost` });
69
+ let renewing = Promise.resolve();
70
+ const renewal = setInterval(() => {
71
+ renewing = Promise.allSettled(held.map((lock) => kv
72
+ .refresh(lock.key, exports.LOCK_LEASE_SECONDS, { ifVersion: lock.version })
73
+ .then((entry) => {
74
+ if (entry) {
75
+ lock.version = entry.version;
76
+ }
77
+ else {
78
+ leaseLost(lock);
79
+ }
80
+ }, () => leaseLost(lock))));
81
+ }, RENEW_EVERY_MS);
82
+ renewal.unref();
83
+ return async () => {
84
+ clearInterval(renewal);
85
+ await renewing;
86
+ await Promise.allSettled(held
87
+ .reverse()
88
+ .map((lock) => kv.delete(lock.key, { ifVersion: lock.version })));
89
+ };
90
+ }
91
+ async function holdCrossShardLocks(testInfo, createKv) {
92
+ const names = lockNamesFor(testInfo.testId);
93
+ const runId = process.env.TEST_RUN_GITHUB_ACTION_ID;
94
+ if (names.length === 0 || !runId || !process.env.EMPIRICALRUN_API_KEY) {
95
+ return async () => { };
96
+ }
97
+ return holdLocks({
98
+ kv: createKv(),
99
+ runId,
100
+ names,
101
+ holder: {
102
+ test_id: testInfo.testId,
103
+ title: testInfo.titlePath.join(" › "),
104
+ retry: testInfo.retry,
105
+ shard: testInfo.config.shard?.current ?? null,
106
+ worker_index: testInfo.workerIndex,
107
+ },
108
+ annotate: (annotation) => testInfo.annotations.push(annotation),
109
+ });
110
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAOhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAgE7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CAsFxB"}
1
+ {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../../src/test/expect/visual.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAY,MAAM,kBAAkB,CAAC;AAOhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAsE7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,CAAC,CAsFxB"}
@@ -4,13 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.toLookRight = toLookRight;
7
- const vision_1 = require("@empiricalrun/llm/vision");
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const openai_1 = __importDefault(require("openai"));
10
9
  const os_1 = __importDefault(require("os"));
11
10
  const path_1 = __importDefault(require("path"));
12
11
  const api_worker_client_1 = require("../../api-worker-client");
13
12
  const telemetry_1 = require("../../telemetry");
13
+ function asImageDataUrl(image) {
14
+ return image.startsWith("data:image")
15
+ ? image
16
+ : `data:image/png;base64,${image}`;
17
+ }
14
18
  function getCallerLine() {
15
19
  const stack = new Error().stack;
16
20
  if (!stack) {
@@ -91,7 +95,7 @@ async function toLookRight(target, pageDescription) {
91
95
  {
92
96
  type: "image_url",
93
97
  image_url: {
94
- url: (0, vision_1.imageFormatForProvider)("anthropic", base64Image),
98
+ url: asImageDataUrl(base64Image),
95
99
  },
96
100
  },
97
101
  ],
@@ -21,6 +21,7 @@ type TestOptions = {
21
21
  page: Page;
22
22
  }>;
23
23
  saveVideos: void;
24
+ crossShardLocks: void;
24
25
  };
25
26
  declare const baseTestFixture: (testFn: TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>, options?: BaseTestFixtureOpts) => TestType<PlaywrightTestArgs & PlaywrightTestOptions & TestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
26
27
  export { baseTestFixture, extendExpect, injectLocatorHighlightScripts, setVideoLabel, };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,IAAI,EACJ,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAA8B,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,cAAc,CAAC;QACvB,UAAU,QAAQ,CAAC,CAAC;YAClB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YAC7C,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;SACnE;KACF;CACF;AAED,QAAA,MAAM,YAAY,mBAA6B,OAAO,MAAM,0CAG3D,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,QAAQ,CAAC;IACb,yBAAyB,EAAE,CACzB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,UAAU,EAAE,IAAI,CAAC;CAClB,CAAC;AAIF,QAAA,MAAM,eAAe,WACX,QAAQ,CACd,kBAAkB,GAAG,qBAAqB,EAC1C,oBAAoB,GAAG,uBAAuB,CAC/C,YACQ,mBAAmB,uHA6G7B,CAAC;AAEF,OAAO,EACL,eAAe,EACf,YAAY,EACZ,6BAA6B,EAC7B,aAAa,GACd,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,IAAI,EACJ,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGjC,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAA8B,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,cAAc,CAAC;QACvB,UAAU,QAAQ,CAAC,CAAC;YAClB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YAC7C,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;SACnE;KACF;CACF;AAED,QAAA,MAAM,YAAY,mBAA6B,OAAO,MAAM,0CAG3D,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,QAAQ,CAAC;IACb,yBAAyB,EAAE,CACzB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,UAAU,EAAE,IAAI,CAAC;IACjB,eAAe,EAAE,IAAI,CAAC;CACvB,CAAC;AAIF,QAAA,MAAM,eAAe,WACX,QAAQ,CACd,kBAAkB,GAAG,qBAAqB,EAC1C,oBAAoB,GAAG,uBAAuB,CAC/C,YACQ,mBAAmB,uHA2H7B,CAAC;AAEF,OAAO,EACL,eAAe,EACf,YAAY,EACZ,6BAA6B,EAC7B,aAAa,GACd,CAAC"}
@@ -7,6 +7,7 @@ exports.setVideoLabel = exports.injectLocatorHighlightScripts = exports.extendEx
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const kv_1 = require("../kv");
9
9
  const coverage_1 = require("./coverage");
10
+ const cross_shard_locks_1 = require("./cross-shard-locks");
10
11
  const expect_1 = require("./expect");
11
12
  const scripts_1 = require("./scripts");
12
13
  Object.defineProperty(exports, "injectLocatorHighlightScripts", { enumerable: true, get: function () { return scripts_1.injectLocatorHighlightScripts; } });
@@ -112,6 +113,18 @@ const baseTestFixture = function (testFn, options = {}) {
112
113
  },
113
114
  { auto: true },
114
115
  ],
116
+ crossShardLocks: [
117
+ async ({}, use, testInfo) => {
118
+ const release = await (0, cross_shard_locks_1.holdCrossShardLocks)(testInfo, () => new kv_1.KVClient());
119
+ try {
120
+ await use();
121
+ }
122
+ finally {
123
+ await release();
124
+ }
125
+ },
126
+ { auto: true, timeout: 0 },
127
+ ],
115
128
  });
116
129
  return extendedTestFn;
117
130
  };
@@ -1 +1 @@
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
+ {"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;AAU9C,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;CAgCvB;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
@@ -91,11 +91,9 @@ class OverlayDismissAgent {
91
91
  }
92
92
  async runAgent(element, text, testFn) {
93
93
  const task = await (0, prompt_1.getTask)(this.page, element, text);
94
- const trace = (0, utils_1.traceThis)(task, testFn);
95
94
  const result = await testFn.step(utils_1.DISMISSAL_AGENT_STEP, () => (0, cua_1.createTestUsingComputerUseAgent)({
96
95
  page: this.page,
97
96
  task,
98
- trace,
99
97
  // We don't want to use page.pause approach here because
100
98
  // overlay dismissal can kick in during a browser agent tool call
101
99
  // and we need app to be able to use page.pause for agent actions.
@@ -8,5 +8,4 @@ export declare function dismissOverlayStepTitle(element: OverlayElement | undefi
8
8
  export declare function isProductFruitsOverlay(element: OverlayElement | undefined): boolean | undefined;
9
9
  export declare function textContent(pageRef: Page, element: OverlayElement | undefined): Promise<string | undefined>;
10
10
  export declare function annotateForReport(testFn: TestFn, description: string): void;
11
- export declare function traceThis(task: string, testFn: TestFn): import("@empiricalrun/llm").TraceClient | undefined;
12
11
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,sBASrE;AAED,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,oBAAoB,gCAAgC,CAAC;AAElE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,UAO1E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,uBAKzE;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,+BAgDpC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAMpE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,uDAiBrD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,sBASrE;AAED,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,oBAAoB,gCAAgC,CAAC;AAElE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,UAO1E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,uBAKzE;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,+BAgDpC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAMpE"}
@@ -6,8 +6,6 @@ exports.dismissOverlayStepTitle = dismissOverlayStepTitle;
6
6
  exports.isProductFruitsOverlay = isProductFruitsOverlay;
7
7
  exports.textContent = textContent;
8
8
  exports.annotateForReport = annotateForReport;
9
- exports.traceThis = traceThis;
10
- const llm_1 = require("@empiricalrun/llm");
11
9
  function overlayDescription(element) {
12
10
  if (element) {
13
11
  const { interceptor, parent } = element;
@@ -85,21 +83,3 @@ function annotateForReport(testFn, description) {
85
83
  description,
86
84
  });
87
85
  }
88
- function traceThis(task, testFn) {
89
- const metadata = {
90
- projectName: process.env.PROJECT_NAME ?? "",
91
- testRunId: process.env.TEST_RUN_GITHUB_ACTION_ID ?? "",
92
- testCase: testFn.info().title,
93
- };
94
- const trace = (0, llm_1.createLangfuseTrace)({
95
- name: "overlay_dismiss",
96
- input: task,
97
- tags: ["overlay_dismiss"],
98
- metadata,
99
- });
100
- if (trace) {
101
- const traceUrl = trace.getTraceUrl();
102
- console.log(`Starting computer use agent: ${traceUrl}`);
103
- }
104
- return trace;
105
- }
@@ -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;AAIhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QASzE"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/vision/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAiEhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAMzE"}
@@ -1,15 +1,67 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.patchQuery = patchQuery;
4
- const vision_1 = require("@empiricalrun/llm/vision");
7
+ const openai_1 = __importDefault(require("openai"));
5
8
  const api_worker_client_1 = require("../../../../api-worker-client");
9
+ const telemetry_1 = require("../../../../telemetry");
10
+ const QUERY_MODEL = "gpt-4o-mini";
11
+ const SYSTEM_PROMPT = `
12
+ You are given an image and an instruction. Use the "explanation" to
13
+ think it through, and then respond with the "answer".`;
14
+ async function queryScreenshot(base64Image, instruction) {
15
+ const openai = new openai_1.default({ maxRetries: 5, ...(0, api_worker_client_1.llmProxyOpenAIConfig)() });
16
+ const completion = await openai.chat.completions.create({
17
+ model: QUERY_MODEL,
18
+ messages: [
19
+ { role: "system", content: SYSTEM_PROMPT },
20
+ {
21
+ role: "user",
22
+ content: [
23
+ {
24
+ type: "image_url",
25
+ image_url: { url: `data:image/png;base64,${base64Image}` },
26
+ },
27
+ { type: "text", text: `Instruction: ${instruction}` },
28
+ ],
29
+ },
30
+ ],
31
+ temperature: 0.5,
32
+ response_format: {
33
+ type: "json_schema",
34
+ json_schema: {
35
+ name: "your_response",
36
+ strict: true,
37
+ schema: {
38
+ type: "object",
39
+ properties: {
40
+ explanation: { type: "string" },
41
+ answer: { type: "string" },
42
+ },
43
+ required: ["explanation", "answer"],
44
+ additionalProperties: false,
45
+ },
46
+ },
47
+ },
48
+ });
49
+ await (0, telemetry_1.trackLLMResponseGenerated)({
50
+ modelName: QUERY_MODEL,
51
+ tokensInput: completion.usage?.prompt_tokens ?? 0,
52
+ tokensOutput: completion.usage?.completion_tokens ?? 0,
53
+ chatSessionSource: "vision_query",
54
+ });
55
+ const content = completion.choices[0]?.message?.content;
56
+ if (!content) {
57
+ throw new Error("Query failed: no response content from LLM.");
58
+ }
59
+ return JSON.parse(content).answer;
60
+ }
6
61
  function patchQuery(LocatorClass) {
7
62
  LocatorClass.prototype.query = async function (instruction) {
8
63
  await this.waitFor({ state: "visible" });
9
64
  const screenshot = await this.screenshot();
10
- const answer = await (0, vision_1.query)(screenshot.toString("base64"), instruction, {
11
- clientConfig: (0, api_worker_client_1.llmProxyOpenAIConfig)(),
12
- });
13
- return answer;
65
+ return queryScreenshot(screenshot.toString("base64"), instruction);
14
66
  };
15
67
  }
package/docs/kv.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # KV Store
2
2
 
3
- A simple key-value store for sharing data across test runs.
3
+ A project-scoped key-value store for sharing state across tests, shards, test
4
+ runs and agent sessions. Every entry expires, and conditional writes make it
5
+ safe to coordinate between parallel shards.
4
6
 
5
7
  ## Fixture usage
6
8
 
@@ -17,10 +19,72 @@ test("example", async ({ page, kv }) => {
17
19
  import { KVClient } from "@empiricalrun/playwright-utils";
18
20
 
19
21
  const kv = new KVClient();
20
- await kv.set("my-key", { some: "data" }, 3600); // ttl in seconds, max 2 days
22
+ await kv.set("my-key", { some: "data" }, 3600);
21
23
  const value = await kv.get<{ some: string }>("my-key");
22
24
  ```
23
25
 
26
+ ## Keys, values and expiry
27
+
28
+ - Keys are 1-512 characters from `A-Z a-z 0-9 . _ : -`. Use `:` to namespace
29
+ them, e.g. `async-checks:analytics:1234`.
30
+ - Values are any JSON value, up to 1 MB serialized.
31
+ - `ttl` is required, in seconds, up to 30 days.
32
+
33
+ ## Entries and versions
34
+
35
+ `getEntry` returns the value with its metadata. `version` increases on every
36
+ write.
37
+
38
+ ```ts
39
+ const entry = await kv.getEntry("my-key");
40
+ // { key, value, version, expires_at, created_at, updated_at, written_by }
41
+ ```
42
+
43
+ ## Conditional writes
44
+
45
+ `set` with `ifAbsent` writes only if the key does not exist; `ifVersion` writes
46
+ only if the entry is still at that version. A failed condition throws
47
+ `KvPreconditionFailedError`, whose `current` is the entry as it is now.
48
+
49
+ ```ts
50
+ import { KvPreconditionFailedError } from "@empiricalrun/playwright-utils";
51
+
52
+ try {
53
+ const lease = await kv.set("lock:checkout", testInfo.testId, 60, {
54
+ ifAbsent: true,
55
+ });
56
+ await kv.refresh("lock:checkout", 60, { ifVersion: lease.version });
57
+ } catch (error) {
58
+ if (error instanceof KvPreconditionFailedError) {
59
+ console.log("held by", error.current?.value);
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Refresh, delete and list
65
+
66
+ ```ts
67
+ await kv.refresh("my-key", 600);
68
+ await kv.delete("my-key", { ifVersion: 3 });
69
+
70
+ const page = await kv.list({ prefix: "async-checks:" });
71
+ for await (const entry of kv.entries("async-checks:")) {
72
+ console.log(entry.key, entry.value);
73
+ }
74
+ ```
75
+
76
+ `refresh` returns `null` and `delete` returns `false` when the key does not
77
+ exist. `list` returns metadata only; `entries` pages through every entry with
78
+ its value.
79
+
80
+ ## Locks across shards
81
+
82
+ Tests declared with Playwright's `lock` option (`test("...", { lock: "name" }, ...)`)
83
+ also hold that lock across every shard of an Empirical test run. The
84
+ `baseTestFixture` fixtures take a KV lease per lock name while the test runs and
85
+ add a `lock` annotation with the time spent waiting. Locks are scoped to the
86
+ test run.
87
+
24
88
  ## Requirements
25
89
 
26
90
  - `EMPIRICALRUN_API_KEY` environment variable must be set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -45,7 +45,7 @@
45
45
  "playwright-core": "1.63.0",
46
46
  "serve-handler": "^6.1.6",
47
47
  "typescript": "7.0.2",
48
- "@empiricalrun/shared-types": "0.23.0"
48
+ "@empiricalrun/shared-types": "0.23.2"
49
49
  },
50
50
  "dependencies": {
51
51
  "async-retry": "^1.3.3",
@@ -56,10 +56,9 @@
56
56
  "openapi-fetch": "0.14.1",
57
57
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
58
58
  "ts-morph": "^28.0.0",
59
- "@empiricalrun/cua": "^0.5.4",
60
- "@empiricalrun/llm": "^0.28.3",
61
59
  "@empiricalrun/r2-uploader": "^0.11.5",
62
- "@empiricalrun/reporter": "^0.30.3"
60
+ "@empiricalrun/reporter": "^0.30.3",
61
+ "@empiricalrun/cua": "^0.6.0"
63
62
  },
64
63
  "scripts": {
65
64
  "dev": "pnpm gen:api-worker-openapi && tsc --build --watch",