@empiricalrun/playwright-utils 0.32.0 → 0.34.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 88dd018: feat: proxy endpoint for neon db on dash
8
+
9
+ ## 0.33.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 6c4f6de: feat: support html reporter for 1.57.0
14
+ - a94a7f7: fix: resolve multipart upload retry failures
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [a94a7f7]
19
+ - @empiricalrun/r2-uploader@0.5.0
20
+ - @empiricalrun/test-gen@0.78.3
21
+
3
22
  ## 0.32.0
4
23
 
5
24
  ### Minor Changes
package/README.md CHANGED
@@ -13,6 +13,8 @@ Playwright utils for test code repos of our customers
13
13
  - Captcha
14
14
  - [Email automation](./docs/email.md)
15
15
  - [Authentication](./docs/auth.md)
16
+ - [KV Store](./docs/kv.md)
17
+ - [Postgres](./docs/postgres.md)
16
18
 
17
19
  ## Development
18
20
 
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { baseConfig, chromeStablePath, devices } from "./config";
5
5
  export * from "./email";
6
6
  export * from "./kv";
7
7
  export * from "./playwright-extensions";
8
+ export * from "./postgres";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACjE,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC;AACrB,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACjE,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC;AACrB,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -26,3 +26,4 @@ Object.defineProperty(exports, "devices", { enumerable: true, get: function () {
26
26
  __exportStar(require("./email"), exports);
27
27
  __exportStar(require("./kv"), exports);
28
28
  __exportStar(require("./playwright-extensions"), exports);
29
+ __exportStar(require("./postgres"), exports);
@@ -0,0 +1,24 @@
1
+ type PostgresDatabase = {
2
+ projectId: string;
3
+ connectionUri: string;
4
+ };
5
+ type GetOptions = {
6
+ name?: string;
7
+ pgVersion?: number;
8
+ ttl?: number;
9
+ };
10
+ export declare class PostgresClient {
11
+ private client;
12
+ private kv;
13
+ constructor();
14
+ get(name: string, options?: GetOptions): Promise<PostgresDatabase>;
15
+ delete(name: string): Promise<void>;
16
+ query<T = Record<string, unknown>>(connectionUri: string, sql: string): Promise<T[]>;
17
+ execute(connectionUri: string, sql: string): Promise<{
18
+ statement: string;
19
+ rowCount: number;
20
+ }[]>;
21
+ private parseStatements;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AA6BA,KAAK,gBAAgB,GAAG;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAIF,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,EAAE,CAAW;;IAYf,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuClE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBnC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,CAAC,EAAE,CAAC;IAWT,OAAO,CACX,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAsBrD,OAAO,CAAC,eAAe;CAsBxB"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PostgresClient = void 0;
4
+ const dashboard_1 = require("@empiricalrun/test-gen/dashboard");
5
+ const pg_1 = require("pg");
6
+ const kv_1 = require("./kv");
7
+ const DEFAULT_TTL = 3600;
8
+ class PostgresClient {
9
+ client;
10
+ kv;
11
+ constructor() {
12
+ if (!process.env.EMPIRICALRUN_API_KEY) {
13
+ throw new Error("EMPIRICALRUN_API_KEY environment variable is required");
14
+ }
15
+ this.client = new dashboard_1.DashboardAPIClient({
16
+ authType: "project-api-key",
17
+ });
18
+ this.kv = new kv_1.KVClient();
19
+ }
20
+ async get(name, options) {
21
+ const kvKey = `postgres:${name}`;
22
+ const existing = await this.kv.get(kvKey);
23
+ if (existing) {
24
+ return existing;
25
+ }
26
+ const response = await this.client.request("/api/neon/proxy", {
27
+ method: "POST",
28
+ body: {
29
+ action: "create",
30
+ payload: {
31
+ name: options?.name ?? name,
32
+ pg_version: options?.pgVersion,
33
+ },
34
+ },
35
+ });
36
+ if (response.error) {
37
+ throw new Error(`Postgres create failed: ${response.error}`);
38
+ }
39
+ const projectId = response.data.project.id;
40
+ const connectionUri = response.data.connection_uris?.[0]?.connection_uri;
41
+ if (!connectionUri) {
42
+ throw new Error("No connection URI returned from Neon API");
43
+ }
44
+ const db = { projectId, connectionUri };
45
+ await this.kv.set(kvKey, db, options?.ttl ?? DEFAULT_TTL);
46
+ return db;
47
+ }
48
+ async delete(name) {
49
+ const kvKey = `postgres:${name}`;
50
+ const existing = await this.kv.get(kvKey);
51
+ if (!existing) {
52
+ return;
53
+ }
54
+ const response = await this.client.request("/api/neon/proxy", {
55
+ method: "POST",
56
+ body: {
57
+ action: "delete",
58
+ projectId: existing.projectId,
59
+ },
60
+ });
61
+ if (response.error) {
62
+ throw new Error(`Postgres delete failed: ${response.error}`);
63
+ }
64
+ }
65
+ async query(connectionUri, sql) {
66
+ const client = new pg_1.Client({ connectionString: connectionUri });
67
+ try {
68
+ await client.connect();
69
+ const result = await client.query(sql);
70
+ return result.rows;
71
+ }
72
+ finally {
73
+ await client.end();
74
+ }
75
+ }
76
+ async execute(connectionUri, sql) {
77
+ const client = new pg_1.Client({ connectionString: connectionUri });
78
+ const results = [];
79
+ try {
80
+ await client.connect();
81
+ const statements = this.parseStatements(sql);
82
+ for (const statement of statements) {
83
+ const result = await client.query(statement);
84
+ results.push({
85
+ statement: statement.substring(0, 80),
86
+ rowCount: result.rowCount ?? 0,
87
+ });
88
+ }
89
+ return results;
90
+ }
91
+ finally {
92
+ await client.end();
93
+ }
94
+ }
95
+ parseStatements(sql) {
96
+ const lines = sql.split("\n");
97
+ const cleanedLines = [];
98
+ for (const line of lines) {
99
+ const trimmed = line.trim();
100
+ if (trimmed && !trimmed.startsWith("--")) {
101
+ const commentIndex = line.indexOf("--");
102
+ if (commentIndex > 0) {
103
+ cleanedLines.push(line.substring(0, commentIndex));
104
+ }
105
+ else {
106
+ cleanedLines.push(line);
107
+ }
108
+ }
109
+ }
110
+ return cleanedLines
111
+ .join("\n")
112
+ .split(";")
113
+ .map((q) => q.trim())
114
+ .filter((q) => q.length > 0);
115
+ }
116
+ }
117
+ exports.PostgresClient = PostgresClient;
@@ -1 +1 @@
1
- {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAanC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,UAAU,CACwE;IAE1F,OAAO,CAAC,YAAY;;IAyBpB,OAAO,CAAC,6BAA6B,CA4BnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAsEtC,KAAK,CAAC,MAAM,EAAE,UAAU;CA+G/B;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAanC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,UAAU,CACwE;IAE1F,OAAO,CAAC,YAAY;;IAyBpB,OAAO,CAAC,6BAA6B,CA4BnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA8EtC,KAAK,CAAC,MAAM,EAAE,UAAU;CA+G/B;AAED,eAAe,iBAAiB,CAAC"}
@@ -67,49 +67,54 @@ class EmpiricalReporter {
67
67
  }
68
68
  try {
69
69
  const attachmentPromises = result.attachments.map(async (attachment) => {
70
- const fileMap = await this.enqueTestAttachmentUploadTask(attachment);
71
- if (!fileMap) {
72
- logger_1.logger.error(`No upload result for ${attachment}`);
73
- return undefined;
74
- }
75
- // Since we are uploading a single attachmnet, the fileMap will have one entry
76
- const url = Object.values(fileMap)[0];
77
- if (!url) {
78
- logger_1.logger.error(`No url in file map for ${attachment}`);
79
- return undefined;
70
+ try {
71
+ const fileMap = await this.enqueTestAttachmentUploadTask(attachment);
72
+ if (!fileMap) {
73
+ logger_1.logger.error(`No upload result for ${attachment.name}`);
74
+ return undefined;
75
+ }
76
+ const url = Object.values(fileMap)[0];
77
+ if (!url) {
78
+ logger_1.logger.error(`No url in file map for ${attachment.name}`);
79
+ return undefined;
80
+ }
81
+ if (!attachment.path) {
82
+ logger_1.logger.error(`No path in attachment for ${attachment.name}`);
83
+ return undefined;
84
+ }
85
+ return {
86
+ name: attachment.name,
87
+ contentType: attachment.contentType,
88
+ path: attachment.path,
89
+ url,
90
+ };
80
91
  }
81
- if (!attachment.path) {
82
- // This should never happen because we only upload if attachment.path is there
83
- logger_1.logger.error(`No path in file map for ${attachment}`);
92
+ catch (err) {
93
+ logger_1.logger.error(`[Empirical Reporter] Error uploading attachment ${attachment.name} for test ${test.title}:`, err);
84
94
  return undefined;
85
95
  }
86
- return {
87
- name: attachment.name,
88
- contentType: attachment.contentType,
89
- path: attachment.path,
90
- url,
91
- };
92
96
  });
93
97
  const testCaseRunEventTask = async () => {
94
- return Promise.all(attachmentPromises)
95
- .then((uploadedAttachments) => {
96
- logger_1.logger.debug(`[Empirical Reporter] Attachments for test ${test.title} are uploaded:`, uploadedAttachments);
98
+ const uploadedAttachments = await Promise.all(attachmentPromises);
99
+ const successfulAttachments = uploadedAttachments.filter((a) => a !== undefined);
100
+ try {
101
+ logger_1.logger.debug(`[Empirical Reporter] Attachments for test ${test.title} are uploaded:`, successfulAttachments);
97
102
  const { suites, projectName } = (0, util_1.suitesAndProjectForTest)(test);
98
103
  const params = {
99
104
  test,
100
105
  suites,
101
106
  result,
102
107
  projectName,
103
- attachments: uploadedAttachments.filter((a) => a !== undefined),
108
+ attachments: successfulAttachments,
104
109
  runId: process.env.TEST_RUN_GITHUB_ACTION_ID,
105
110
  };
106
111
  return (0, util_1.sendTestCaseUpdateToDashboard)(params);
107
- })
108
- .catch((error) => {
112
+ }
113
+ catch (error) {
109
114
  logger_1.logger.error(`[Empirical Reporter] Error sending test case event for: ${test.title}:`, error);
110
- });
115
+ }
111
116
  };
112
- void (0, r2_uploader_1.sendTaskToQueue)(testCaseRunEventTask);
117
+ void testCaseRunEventTask();
113
118
  return;
114
119
  }
115
120
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/reporter/util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EACL,cAAc,EAGd,UAAU,IAAI,oBAAoB,EAElC,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAoBnC,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAmB7D;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAAG,SAAS,UAMhC;AAED,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5E,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAyCD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ;;;EAwBrD;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAyDf;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GACnC,GAAG,CAIL;AAED,wBAAsB,gCAAgC,CACpD,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,iBAuFjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,EACb,iBAAiB,EAAE,GAAG,CACpB,MAAM,EACN;IAAE,WAAW,EAAE,2BAA2B,EAAE,CAAA;CAAE,EAAE,CACjD,QA+BF;AAGD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,oBAAoB,EACjC,MAAM,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,QAavC;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,cAAc,GAAE,OAAe,GAC9B,GAAG,CAAC,MAAM,EAAE;IAAE,WAAW,EAAE,2BAA2B,EAAE,CAAA;CAAE,EAAE,CAAC,CA4C/D;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gCAAgC,CACpD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,6CAoClB"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/reporter/util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EACL,cAAc,EAGd,UAAU,IAAI,oBAAoB,EAElC,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAoBnC,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAmB7D;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAAG,SAAS,UAMhC;AAED,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5E,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAyCD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ;;;EAwBrD;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAyDf;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GACnC,GAAG,CAIL;AAED,wBAAsB,gCAAgC,CACpD,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,iBAwGjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,EACb,iBAAiB,EAAE,GAAG,CACpB,MAAM,EACN;IAAE,WAAW,EAAE,2BAA2B,EAAE,CAAA;CAAE,EAAE,CACjD,QA+BF;AAGD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,oBAAoB,EACjC,MAAM,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,QAavC;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,cAAc,GAAE,OAAe,GAC9B,GAAG,CAAC,MAAM,EAAE;IAAE,WAAW,EAAE,2BAA2B,EAAE,CAAA;CAAE,EAAE,CAAC,CA4C/D;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gCAAgC,CACpD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,6CAoClB"}
@@ -166,7 +166,12 @@ async function updateHtmlZipFileAttachmentPaths(jsonFilePath, htmlFilePath, repo
166
166
  console.error(`❌ Failed to read HTML file at ${htmlFilePath}:`, err);
167
167
  return;
168
168
  }
169
- const base64 = htmlFile.match(/window\.playwrightReportBase64\s*=\s*"(?:data:application\/zip;base64,)?([^"]+)"/)?.[1];
169
+ // Support both old format (1.53.x) and new format (1.57.0+)
170
+ // Old: window.playwrightReportBase64 = "data:application/zip;base64,..."
171
+ // New: <script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,...</script>
172
+ const oldFormatMatch = htmlFile.match(/window\.playwrightReportBase64\s*=\s*"(?:data:application\/zip;base64,)?([^"]+)"/);
173
+ const newFormatMatch = htmlFile.match(/<script\s+id="playwrightReportBase64"[^>]*>(?:data:application\/zip;base64,)?([^<]+)<\/script>/);
174
+ const base64 = oldFormatMatch?.[1] || newFormatMatch?.[1];
170
175
  if (!base64) {
171
176
  console.error("❌ Base64 zip data not found in HTML.");
172
177
  return;
@@ -203,7 +208,15 @@ async function updateHtmlZipFileAttachmentPaths(jsonFilePath, htmlFilePath, repo
203
208
  const newZip = new adm_zip_1.default();
204
209
  newZip.addLocalFolder(tempDir);
205
210
  const newBase64 = newZip.toBuffer().toString("base64");
206
- const updatedHtml = htmlFile.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
211
+ let updatedHtml;
212
+ if (oldFormatMatch) {
213
+ // Old format (1.53.x): window.playwrightReportBase64 = "..."
214
+ updatedHtml = htmlFile.replace(/(window\.playwrightReportBase64\s*=\s*")(?:data:application\/zip;base64,)?[^"]*(")/, `$1data:application/zip;base64,${newBase64}$2`);
215
+ }
216
+ else {
217
+ // New format (1.57.0+): <script id="playwrightReportBase64" ...>...</script>
218
+ updatedHtml = htmlFile.replace(/(<script\s+id="playwrightReportBase64"[^>]*>)(?:data:application\/zip;base64,)?[^<]*(<\/script>)/, `$1data:application/zip;base64,${newBase64}$2`);
219
+ }
207
220
  await fs_1.default.promises.writeFile(htmlFilePath, updatedHtml, "utf8");
208
221
  logger_1.logger.debug("✅ HTML file updated with new base64 zip attachment.");
209
222
  }
@@ -1,5 +1,6 @@
1
1
  import type { BrowserContext, BrowserContextOptions, expect, Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestType } from "@playwright/test";
2
2
  import { KVClient } from "../kv";
3
+ import { PostgresClient } from "../postgres";
3
4
  import { injectLocatorHighlightScripts } from "./scripts";
4
5
  import { HighlighterOpts } from "./types";
5
6
  import { setVideoLabel } from "./video-labels";
@@ -14,6 +15,7 @@ declare const extendExpect: (expectInstance: typeof expect) => import("@playwrig
14
15
  type TestOptions = {
15
16
  page: Page;
16
17
  kv: KVClient;
18
+ postgres: PostgresClient;
17
19
  customContextPageProvider: (options?: BrowserContextOptions) => Promise<{
18
20
  context: BrowserContext;
19
21
  page: Page;
@@ -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,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,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;SAC9C;KACF;CACF;AAED,QAAA,MAAM,YAAY,GAAa,gBAAgB,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;AAYF,QAAA,MAAM,eAAe,GACnB,QAAQ,QAAQ,CACd,kBAAkB,GAAG,qBAAqB,EAC1C,oBAAoB,GAAG,uBAAuB,CAC/C,EACD,UAAS,eAAgC,uHAuG1C,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;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,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;SAC9C;KACF;CACF;AAED,QAAA,MAAM,YAAY,GAAa,gBAAgB,OAAO,MAAM,0CAG3D,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,QAAQ,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,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;AAYF,QAAA,MAAM,eAAe,GACnB,QAAQ,QAAQ,CACd,kBAAkB,GAAG,qBAAqB,EAC1C,oBAAoB,GAAG,uBAAuB,CAC/C,EACD,UAAS,eAAgC,uHA2G1C,CAAC;AAEF,OAAO,EACL,eAAe,EACf,YAAY,EACZ,6BAA6B,EAC7B,aAAa,GACd,CAAC"}
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setVideoLabel = exports.injectLocatorHighlightScripts = exports.extendExpect = exports.baseTestFixture = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const kv_1 = require("../kv");
9
+ const postgres_1 = require("../postgres");
9
10
  const expect_1 = require("./expect");
10
11
  const scripts_1 = require("./scripts");
11
12
  Object.defineProperty(exports, "injectLocatorHighlightScripts", { enumerable: true, get: function () { return scripts_1.injectLocatorHighlightScripts; } });
@@ -53,6 +54,10 @@ const baseTestFixture = function (testFn, options = defaultOptions) {
53
54
  const client = new kv_1.KVClient();
54
55
  await use(client);
55
56
  },
57
+ postgres: async ({}, use) => {
58
+ const client = new postgres_1.PostgresClient();
59
+ await use(client);
60
+ },
56
61
  customContextPageProvider: async ({ browser }, use, testInfo) => {
57
62
  const contexts = [];
58
63
  async function createContext(contextOptions = {}) {
@@ -0,0 +1,78 @@
1
+ # Postgres
2
+
3
+ A PostgreSQL client for creating and managing temporary Neon databases in tests.
4
+
5
+ ## Fixture usage
6
+
7
+ ```ts
8
+ test("example", async ({ page, postgres }) => {
9
+ // Get or create a database (cached via KV for 1 hour by default)
10
+ const { connectionUri } = await postgres.get("my-test-db");
11
+
12
+ // Run queries
13
+ const users = await postgres.query<{ id: number; name: string }>(
14
+ connectionUri,
15
+ "SELECT * FROM users",
16
+ );
17
+
18
+ // Execute multiple SQL statements
19
+ await postgres.execute(
20
+ connectionUri,
21
+ `
22
+ CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
23
+ INSERT INTO users (name) VALUES ('Alice');
24
+ `,
25
+ );
26
+
27
+ // Delete when done (optional - TTL will auto-expire)
28
+ await postgres.delete("my-test-db");
29
+ });
30
+ ```
31
+
32
+ ## Standalone usage
33
+
34
+ ```ts
35
+ import { PostgresClient } from "@empiricalrun/playwright-utils";
36
+
37
+ const postgres = new PostgresClient();
38
+
39
+ // Get or create a database
40
+ const { projectId, connectionUri } = await postgres.get("my-db", {
41
+ pgVersion: 15,
42
+ ttl: 7200, // 2 hours
43
+ });
44
+
45
+ // Run a query
46
+ const rows = await postgres.query(connectionUri, "SELECT NOW()");
47
+
48
+ // Delete when done
49
+ await postgres.delete("my-db");
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### `postgres.get(name, options?)`
55
+
56
+ Gets an existing database or creates a new one. Uses KV store to cache the connection.
57
+
58
+ - `name` - Unique identifier for the database
59
+ - `options.pgVersion` - PostgreSQL version (default: 15)
60
+ - `options.ttl` - Cache TTL in seconds (default: 3600)
61
+
62
+ Returns `{ projectId, connectionUri }`.
63
+
64
+ ### `postgres.delete(name)`
65
+
66
+ Deletes the database and removes it from cache.
67
+
68
+ ### `postgres.query(connectionUri, sql)`
69
+
70
+ Runs a SQL query and returns rows.
71
+
72
+ ### `postgres.execute(connectionUri, sql)`
73
+
74
+ Executes multiple SQL statements separated by `;`.
75
+
76
+ ## Requirements
77
+
78
+ - `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.32.0",
3
+ "version": "0.34.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -30,21 +30,23 @@
30
30
  "@types/node": "^20.14.9",
31
31
  "@types/serve-handler": "^6.1.4",
32
32
  "@types/adm-zip": "^0.5.7",
33
+ "@types/pg": "^8.11.6",
33
34
  "playwright-core": "1.53.2",
34
35
  "serve-handler": "^6.1.6",
35
36
  "@empiricalrun/shared-types": "0.12.0"
36
37
  },
37
38
  "dependencies": {
38
39
  "@babel/code-frame": "^7.24.7",
40
+ "pg": "^8.13.1",
39
41
  "adm-zip": "^0.5.16",
40
42
  "async-retry": "^1.3.3",
41
43
  "authenticator": "^1.1.5",
42
44
  "console-log-level": "^1.4.1",
43
45
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
44
46
  "rimraf": "^6.0.1",
47
+ "@empiricalrun/r2-uploader": "^0.5.0",
45
48
  "@empiricalrun/llm": "^0.25.1",
46
- "@empiricalrun/r2-uploader": "^0.4.0",
47
- "@empiricalrun/test-gen": "^0.78.2"
49
+ "@empiricalrun/test-gen": "^0.78.3"
48
50
  },
49
51
  "scripts": {
50
52
  "dev": "tsc --build --watch",
@@ -1 +1 @@
1
- {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/captcha/vision.ts","./src/config/index.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/empirical-reporter.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/expect.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
1
+ {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/captcha/vision.ts","./src/config/index.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/empirical-reporter.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/expect.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}