@samsara-dev/appwright 0.2.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # appwright
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - eb38761: Add `device.backgroundApp()` to delegate to Appium's `mobile: backgroundApp` command.
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 9415229: feat: add Visual Trace Service for automatic screenshot capture during test execution
14
+
15
+ - Implements automatic screenshot capture with smart defaults (only captures for failed tests)
16
+ - Adds screenshot deduplication using SHA-256 hashing
17
+ - Supports configurable screenshot limits (default: 50)
18
+ - Integrates with Playwright's trace configuration modes
19
+ - Works with both test-scoped device and worker-scoped persistentDevice fixtures
20
+ - Provides proper test isolation and retry support
21
+ - Includes comprehensive unit test coverage
22
+
3
23
  ## 0.2.1
4
24
 
5
25
  ### Patch Changes
@@ -1,13 +1,23 @@
1
1
  import type { Client as WebDriverClient } from "webdriver";
2
- import { AppwrightLocator, ExtractType, Platform, TimeoutOptions } from "../types";
2
+ import { AppwrightLocator, ExtractType, Platform, TimeoutOptions, VisualTraceConfig } from "../types";
3
3
  import { z } from "zod";
4
4
  import { LLMModel } from "@empiricalrun/llm";
5
+ import { TestInfo } from "@playwright/test";
5
6
  export declare class Device {
6
7
  private webDriverClient;
7
8
  private bundleId;
8
9
  private timeoutOpts;
9
10
  private provider;
11
+ private visualTraceService?;
10
12
  constructor(webDriverClient: WebDriverClient, bundleId: string | undefined, timeoutOpts: TimeoutOptions, provider: string);
13
+ /**
14
+ * Initialize Visual Trace Service for screenshot capture during test execution
15
+ */
16
+ initializeVisualTrace(testInfo: TestInfo, retryIndex: number, config?: VisualTraceConfig): void;
17
+ /**
18
+ * Take a screenshot - exposed for Visual Trace Service
19
+ */
20
+ takeScreenshot(): Promise<Buffer>;
11
21
  locator({ selector, findStrategy, textToMatch, }: {
12
22
  selector: string;
13
23
  findStrategy: string;
@@ -119,6 +129,25 @@ export declare class Device {
119
129
  getPlatform(): Platform;
120
130
  terminateApp(bundleId?: string): Promise<void>;
121
131
  activateApp(bundleId?: string): Promise<void>;
132
+ /**
133
+ * Sends the currently running app to the background.
134
+ *
135
+ * @param seconds - Number of seconds to keep app in background.
136
+ * Use -1 to background indefinitely (until manually reactivated).
137
+ * If positive number, app returns to foreground after specified seconds.
138
+ *
139
+ * @example
140
+ * ```js
141
+ * // Background for 10 seconds then auto-return
142
+ * await device.backgroundApp(10);
143
+ *
144
+ * // Background indefinitely (for battery tests)
145
+ * await device.backgroundApp(-1);
146
+ * await device.pause(30 * 60 * 1000); // Wait 30 minutes
147
+ * await device.activateApp(); // Manually bring back
148
+ * ```
149
+ */
150
+ backgroundApp(seconds?: number): Promise<void>;
122
151
  /**
123
152
  * Retrieves text content from the clipboard of the mobile device. This is useful
124
153
  * after a "copy to clipboard" action has been performed. This returns base64 encoded string.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/device/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/device/index.ts"],"names":[],"mappings":"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"}
@@ -42,11 +42,13 @@ const utils_1 = require("../utils");
42
42
  const utils_2 = require("../providers/browserstack/utils");
43
43
  const utils_3 = require("../providers/lambdatest/utils");
44
44
  const logger_1 = require("../logger");
45
+ const visualTrace_1 = require("../visualTrace");
45
46
  let Device = (() => {
46
47
  let _instanceExtraInitializers = [];
47
48
  let _tap_decorators;
48
49
  let _terminateApp_decorators;
49
50
  let _activateApp_decorators;
51
+ let _backgroundApp_decorators;
50
52
  let _getClipboardText_decorators;
51
53
  let _setMockCameraView_decorators;
52
54
  let _pause_decorators;
@@ -60,6 +62,7 @@ let Device = (() => {
60
62
  _tap_decorators = [utils_1.boxedStep];
61
63
  _terminateApp_decorators = [utils_1.boxedStep];
62
64
  _activateApp_decorators = [utils_1.boxedStep];
65
+ _backgroundApp_decorators = [utils_1.boxedStep];
63
66
  _getClipboardText_decorators = [utils_1.boxedStep];
64
67
  _setMockCameraView_decorators = [utils_1.boxedStep];
65
68
  _pause_decorators = [utils_1.boxedStep];
@@ -70,6 +73,7 @@ let Device = (() => {
70
73
  __esDecorate(this, null, _tap_decorators, { kind: "method", name: "tap", static: false, private: false, access: { has: obj => "tap" in obj, get: obj => obj.tap }, metadata: _metadata }, null, _instanceExtraInitializers);
71
74
  __esDecorate(this, null, _terminateApp_decorators, { kind: "method", name: "terminateApp", static: false, private: false, access: { has: obj => "terminateApp" in obj, get: obj => obj.terminateApp }, metadata: _metadata }, null, _instanceExtraInitializers);
72
75
  __esDecorate(this, null, _activateApp_decorators, { kind: "method", name: "activateApp", static: false, private: false, access: { has: obj => "activateApp" in obj, get: obj => obj.activateApp }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _backgroundApp_decorators, { kind: "method", name: "backgroundApp", static: false, private: false, access: { has: obj => "backgroundApp" in obj, get: obj => obj.backgroundApp }, metadata: _metadata }, null, _instanceExtraInitializers);
73
77
  __esDecorate(this, null, _getClipboardText_decorators, { kind: "method", name: "getClipboardText", static: false, private: false, access: { has: obj => "getClipboardText" in obj, get: obj => obj.getClipboardText }, metadata: _metadata }, null, _instanceExtraInitializers);
74
78
  __esDecorate(this, null, _setMockCameraView_decorators, { kind: "method", name: "setMockCameraView", static: false, private: false, access: { has: obj => "setMockCameraView" in obj, get: obj => obj.setMockCameraView }, metadata: _metadata }, null, _instanceExtraInitializers);
75
79
  __esDecorate(this, null, _pause_decorators, { kind: "method", name: "pause", static: false, private: false, access: { has: obj => "pause" in obj, get: obj => obj.pause }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -83,14 +87,28 @@ let Device = (() => {
83
87
  bundleId;
84
88
  timeoutOpts;
85
89
  provider;
90
+ visualTraceService;
86
91
  constructor(webDriverClient, bundleId, timeoutOpts, provider) {
87
92
  this.webDriverClient = webDriverClient;
88
93
  this.bundleId = bundleId;
89
94
  this.timeoutOpts = timeoutOpts;
90
95
  this.provider = provider;
91
96
  }
97
+ /**
98
+ * Initialize Visual Trace Service for screenshot capture during test execution
99
+ */
100
+ initializeVisualTrace(testInfo, retryIndex, config) {
101
+ this.visualTraceService = (0, visualTrace_1.initializeVisualTrace)(testInfo, retryIndex, config);
102
+ }
103
+ /**
104
+ * Take a screenshot - exposed for Visual Trace Service
105
+ */
106
+ async takeScreenshot() {
107
+ const base64Screenshot = await this.webDriverClient.takeScreenshot();
108
+ return Buffer.from(base64Screenshot, "base64");
109
+ }
92
110
  locator({ selector, findStrategy, textToMatch, }) {
93
- return new locator_1.Locator(this.webDriverClient, this.timeoutOpts, selector, findStrategy, textToMatch);
111
+ return new locator_1.Locator(this.webDriverClient, this.timeoutOpts, selector, findStrategy, textToMatch, this);
94
112
  }
95
113
  vision() {
96
114
  return new vision_1.VisionProvider(this, this.webDriverClient);
@@ -121,6 +139,11 @@ let Device = (() => {
121
139
  catch (e) {
122
140
  logger_1.logger.error(`close:`, e);
123
141
  }
142
+ // Clean up visual trace service
143
+ if (this.visualTraceService) {
144
+ (0, visualTrace_1.clearVisualTraceService)();
145
+ this.visualTraceService = undefined;
146
+ }
124
147
  }
125
148
  /**
126
149
  * Tap on the screen at the given coordinates, specified as x and y. The top left corner
@@ -290,6 +313,29 @@ let Device = (() => {
290
313
  },
291
314
  ]);
292
315
  }
316
+ /**
317
+ * Sends the currently running app to the background.
318
+ *
319
+ * @param seconds - Number of seconds to keep app in background.
320
+ * Use -1 to background indefinitely (until manually reactivated).
321
+ * If positive number, app returns to foreground after specified seconds.
322
+ *
323
+ * @example
324
+ * ```js
325
+ * // Background for 10 seconds then auto-return
326
+ * await device.backgroundApp(10);
327
+ *
328
+ * // Background indefinitely (for battery tests)
329
+ * await device.backgroundApp(-1);
330
+ * await device.pause(30 * 60 * 1000); // Wait 30 minutes
331
+ * await device.activateApp(); // Manually bring back
332
+ * ```
333
+ */
334
+ async backgroundApp(seconds = -1) {
335
+ await this.webDriverClient.executeScript("mobile: backgroundApp", [
336
+ seconds,
337
+ ]);
338
+ }
293
339
  /**
294
340
  * Retrieves text content from the clipboard of the mobile device. This is useful
295
341
  * after a "copy to clipboard" action has been performed. This returns base64 encoded string.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixture/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,aAAa,EAEd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAKnC,KAAK,iBAAiB,GAAG;IACvB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAE/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,IAAI,uRA4Df,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;2BACY,gBAAgB,YAAY,aAAa;;;;;;;EAUtE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixture/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,aAAa,EAGd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAKnC,KAAK,iBAAiB,GAAG;IACvB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAE/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,IAAI,uRAyEf,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;2BACY,gBAAgB,YAAY,aAAa;;;;;;;EAUtE,CAAC"}
@@ -21,6 +21,9 @@ exports.test = test_1.test.extend({
21
21
  type: "sessionId",
22
22
  description: deviceProvider.sessionId,
23
23
  });
24
+ // Initialize Visual Trace Service for screenshot capture
25
+ const visualTraceConfig = testInfo.project.use.visualTrace;
26
+ device.initializeVisualTrace(testInfo, testInfo.retry, visualTraceConfig);
24
27
  await deviceProvider.syncTestDetails?.({ name: testInfo.title });
25
28
  await use(device);
26
29
  await device.close();
@@ -46,6 +49,8 @@ exports.test = test_1.test.extend({
46
49
  }
47
50
  const providerName = project.use.device
48
51
  ?.provider;
52
+ // Note: For persistentDevice, Visual Trace is initialized lazily in boxedStep
53
+ // when test.info() is available, ensuring it works for worker-scoped fixtures.
49
54
  const afterSession = new Date();
50
55
  const workerInfoStore = new workerInfo_1.WorkerInfoStore();
51
56
  await workerInfoStore.saveWorkerStartTime(workerIndex, sessionId, providerName, beforeSession, afterSession);
@@ -6,7 +6,10 @@ export declare class Locator {
6
6
  private selector;
7
7
  private findStrategy;
8
8
  private textToMatch?;
9
- constructor(webDriverClient: WebDriverClient, timeoutOpts: TimeoutOptions, selector: string, findStrategy: string, textToMatch?: (string | RegExp) | undefined);
9
+ private device?;
10
+ constructor(webDriverClient: WebDriverClient, timeoutOpts: TimeoutOptions, selector: string, findStrategy: string, textToMatch?: (string | RegExp) | undefined, device?: {
11
+ takeScreenshot: () => Promise<Buffer>;
12
+ });
10
13
  fill(value: string, options?: ActionOptions): Promise<void>;
11
14
  sendKeyStrokes(value: string, options?: ActionOptions): Promise<void>;
12
15
  isVisible(options?: ActionOptions): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/locator/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EAEd,MAAM,UAAU,CAAC;AAIlB,qBAAa,OAAO;IAEhB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,WAAW;IAEnB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAEpB,OAAO,CAAC,WAAW,CAAC;gBANZ,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,cAAc,EAE3B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EAEpB,WAAW,CAAC,GAAE,MAAM,GAAG,MAAM,aAAA;IAIjC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB3D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCrE,SAAS,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAYpD,OAAO,CACX,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC;YAgCF,SAAS;IAiCjB,GAAG,CAAC,OAAO,CAAC,EAAE,aAAa;IAiB3B,OAAO,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBjD,MAAM,CAAC,SAAS,EAAE,eAAe;IAuBvC;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;CAkDrD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/locator/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EAEd,MAAM,UAAU,CAAC;AAIlB,qBAAa,OAAO;IAKhB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,WAAW;IAEnB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAEpB,OAAO,CAAC,WAAW,CAAC;IATtB,OAAO,CAAC,MAAM,CAAC,CAA4C;gBAGjD,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,cAAc,EAE3B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EAEpB,WAAW,CAAC,GAAE,MAAM,GAAG,MAAM,aAAA,EACrC,MAAM,CAAC,EAAE;QAAE,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE;IAM9C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB3D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCrE,SAAS,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAYpD,OAAO,CACX,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC;YAgCF,SAAS;IAiCjB,GAAG,CAAC,OAAO,CAAC,EAAE,aAAa;IAiB3B,OAAO,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBjD,MAAM,CAAC,SAAS,EAAE,eAAe;IAuBvC;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;CAkDrD"}
@@ -69,16 +69,19 @@ let Locator = (() => {
69
69
  selector;
70
70
  findStrategy;
71
71
  textToMatch;
72
+ // Store reference to the device for Visual Trace Service
73
+ device;
72
74
  constructor(webDriverClient, timeoutOpts,
73
75
  // Used for find elements request that is sent to Appium server
74
76
  selector, findStrategy,
75
77
  // Used to filter elements received from Appium server
76
- textToMatch) {
78
+ textToMatch, device) {
77
79
  this.webDriverClient = webDriverClient;
78
80
  this.timeoutOpts = timeoutOpts;
79
81
  this.selector = selector;
80
82
  this.findStrategy = findStrategy;
81
83
  this.textToMatch = textToMatch;
84
+ this.device = device;
82
85
  }
83
86
  async fill(value, options) {
84
87
  const isElementDisplayed = await this.isVisible(options);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=device.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.spec.d.ts","sourceRoot":"","sources":["../../src/tests/device.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
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 vitest_1 = require("vitest");
7
+ const test_1 = __importDefault(require("@playwright/test"));
8
+ const device_1 = require("../device");
9
+ // Override Playwright's test.step/info to work in Vitest environment
10
+ // so boxedStep decorator can execute without throwing.
11
+ test_1.default.step = vitest_1.vi.fn(async (_name, body) => await body());
12
+ test_1.default.info = () => undefined;
13
+ const createDevice = (executeScript = vitest_1.vi.fn()) => {
14
+ //@ts-ignore - providing partial WebDriver client for testing
15
+ const webDriverClient = {
16
+ executeScript,
17
+ };
18
+ const device = new device_1.Device(webDriverClient, "com.example.app", { expectTimeout: 1_000 }, "emulator");
19
+ return { device, executeScript };
20
+ };
21
+ (0, vitest_1.describe)("Device", () => {
22
+ (0, vitest_1.describe)("backgroundApp", () => {
23
+ (0, vitest_1.test)("backgrounds indefinitely by default", async () => {
24
+ const { device, executeScript } = createDevice();
25
+ await device.backgroundApp();
26
+ (0, vitest_1.expect)(executeScript).toHaveBeenCalledWith("mobile: backgroundApp", [-1]);
27
+ });
28
+ (0, vitest_1.test)("backgrounds for given duration", async () => {
29
+ const { device, executeScript } = createDevice();
30
+ await device.backgroundApp(30);
31
+ (0, vitest_1.expect)(executeScript).toHaveBeenCalledWith("mobile: backgroundApp", [30]);
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=visual-trace.service.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual-trace.service.spec.d.ts","sourceRoot":"","sources":["../../src/tests/visual-trace.service.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const service_1 = require("../visualTrace/service");
5
+ // Mock TestInfo object
6
+ function createMockTestInfo(options) {
7
+ return {
8
+ testId: options.testId || "test-1",
9
+ retry: options.retry || 0,
10
+ status: options.status,
11
+ errors: options.errors || [],
12
+ project: {
13
+ use: {
14
+ trace: options.trace,
15
+ screenshots: options.screenshots,
16
+ },
17
+ },
18
+ attach: vitest_1.vi.fn(),
19
+ };
20
+ }
21
+ // Mock screenshot function
22
+ const mockTakeScreenshot = vitest_1.vi.fn(() => Promise.resolve(Buffer.from("mock-screenshot-data")));
23
+ (0, vitest_1.describe)("VisualTraceService", () => {
24
+ (0, vitest_1.beforeEach)(() => {
25
+ (0, service_1.clearVisualTraceService)();
26
+ mockTakeScreenshot.mockClear();
27
+ });
28
+ (0, vitest_1.describe)("Configuration", () => {
29
+ (0, vitest_1.test)("should use default configuration when no config provided", () => {
30
+ const testInfo = createMockTestInfo({ testId: "test-1" });
31
+ const service = new service_1.VisualTraceService(testInfo, 0);
32
+ (0, vitest_1.expect)(service["config"]).toEqual({
33
+ enableScreenshots: "retain-on-failure",
34
+ maxScreenshots: 50,
35
+ dedupe: true,
36
+ });
37
+ });
38
+ (0, vitest_1.test)("should merge provided config with defaults", () => {
39
+ const testInfo = createMockTestInfo({ testId: "test-1" });
40
+ const config = {
41
+ enableScreenshots: "on",
42
+ maxScreenshots: 10,
43
+ };
44
+ const service = new service_1.VisualTraceService(testInfo, 0, config);
45
+ (0, vitest_1.expect)(service["config"]).toEqual({
46
+ enableScreenshots: "on",
47
+ maxScreenshots: 10,
48
+ dedupe: true, // Default value
49
+ });
50
+ });
51
+ });
52
+ (0, vitest_1.describe)("shouldCaptureScreenshot", () => {
53
+ (0, vitest_1.test)('should return true when trace mode is "on"', () => {
54
+ const testInfo = createMockTestInfo({ trace: "on" });
55
+ const service = new service_1.VisualTraceService(testInfo, 0);
56
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(true);
57
+ });
58
+ (0, vitest_1.test)('should return false when trace mode is "off"', () => {
59
+ const testInfo = createMockTestInfo({ trace: "off" });
60
+ const service = new service_1.VisualTraceService(testInfo, 0);
61
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(false);
62
+ });
63
+ (0, vitest_1.test)("should return false when screenshots are explicitly disabled", () => {
64
+ const testInfo = createMockTestInfo({ trace: "on" });
65
+ const service = new service_1.VisualTraceService(testInfo, 0, {
66
+ enableScreenshots: false,
67
+ });
68
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(false);
69
+ });
70
+ (0, vitest_1.test)('should return true for failed test with "retain-on-failure"', () => {
71
+ const testInfo = createMockTestInfo({
72
+ trace: "retain-on-failure",
73
+ status: "failed",
74
+ });
75
+ const service = new service_1.VisualTraceService(testInfo, 0);
76
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(true);
77
+ });
78
+ (0, vitest_1.test)('should return false for passed test with "retain-on-failure"', () => {
79
+ const testInfo = createMockTestInfo({
80
+ trace: "retain-on-failure",
81
+ status: "passed",
82
+ });
83
+ const service = new service_1.VisualTraceService(testInfo, 0);
84
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(false);
85
+ });
86
+ (0, vitest_1.test)('should return true when step fails with "retain-on-failure"', () => {
87
+ const testInfo = createMockTestInfo({
88
+ trace: "retain-on-failure",
89
+ status: undefined, // Status not set during execution
90
+ });
91
+ const service = new service_1.VisualTraceService(testInfo, 0);
92
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot(true)).toBe(true); // stepFailed = true
93
+ });
94
+ (0, vitest_1.test)('should handle object trace configuration with mode "on"', () => {
95
+ const testInfo = {
96
+ testId: "test-1",
97
+ retry: 0,
98
+ status: undefined,
99
+ errors: [],
100
+ project: {
101
+ use: {
102
+ trace: { mode: "on", screenshots: true },
103
+ },
104
+ },
105
+ attach: vitest_1.vi.fn(),
106
+ };
107
+ const service = new service_1.VisualTraceService(testInfo, 0);
108
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(true);
109
+ });
110
+ (0, vitest_1.test)("should respect screenshots: false in object trace config", () => {
111
+ const testInfo = {
112
+ testId: "test-1",
113
+ retry: 0,
114
+ status: undefined,
115
+ errors: [],
116
+ project: {
117
+ use: {
118
+ trace: { mode: "on", screenshots: false },
119
+ },
120
+ },
121
+ attach: vitest_1.vi.fn(),
122
+ };
123
+ const service = new service_1.VisualTraceService(testInfo, 0);
124
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(false);
125
+ });
126
+ (0, vitest_1.test)('should return true on retry with "on-first-retry"', () => {
127
+ const testInfo = createMockTestInfo({ trace: "on-first-retry" });
128
+ const service = new service_1.VisualTraceService(testInfo, 1);
129
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(true);
130
+ });
131
+ (0, vitest_1.test)('should return false on initial run with "on-first-retry"', () => {
132
+ const testInfo = createMockTestInfo({ trace: "on-first-retry" });
133
+ const service = new service_1.VisualTraceService(testInfo, 0);
134
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(false);
135
+ });
136
+ (0, vitest_1.test)('should return true on all retries with "on-all-retries"', () => {
137
+ const testInfo = createMockTestInfo({ trace: "on-all-retries" });
138
+ const service1 = new service_1.VisualTraceService(testInfo, 1);
139
+ const service2 = new service_1.VisualTraceService(testInfo, 2);
140
+ (0, vitest_1.expect)(service1.shouldCaptureScreenshot()).toBe(true);
141
+ (0, vitest_1.expect)(service2.shouldCaptureScreenshot()).toBe(true);
142
+ });
143
+ (0, vitest_1.test)("should return true when test has errors", () => {
144
+ const testInfo = createMockTestInfo({
145
+ trace: "retain-on-failure",
146
+ errors: [new Error("Test failed")],
147
+ });
148
+ const service = new service_1.VisualTraceService(testInfo, 0);
149
+ (0, vitest_1.expect)(service.shouldCaptureScreenshot()).toBe(true);
150
+ });
151
+ });
152
+ (0, vitest_1.describe)("captureScreenshot", () => {
153
+ (0, vitest_1.test)("should capture screenshot when conditions are met", async () => {
154
+ const testInfo = createMockTestInfo({ trace: "on" });
155
+ const service = new service_1.VisualTraceService(testInfo, 0);
156
+ await service.captureScreenshot(mockTakeScreenshot, "test-step");
157
+ (0, vitest_1.expect)(mockTakeScreenshot).toHaveBeenCalled();
158
+ (0, vitest_1.expect)(testInfo.attach).toHaveBeenCalledWith(vitest_1.expect.stringContaining("screenshot-"), vitest_1.expect.objectContaining({
159
+ body: vitest_1.expect.any(Buffer),
160
+ contentType: "image/png",
161
+ }));
162
+ });
163
+ (0, vitest_1.test)("should not capture screenshot when disabled", async () => {
164
+ const testInfo = createMockTestInfo({ trace: "off" });
165
+ const service = new service_1.VisualTraceService(testInfo, 0);
166
+ await service.captureScreenshot(mockTakeScreenshot, "test-step");
167
+ (0, vitest_1.expect)(mockTakeScreenshot).not.toHaveBeenCalled();
168
+ (0, vitest_1.expect)(testInfo.attach).not.toHaveBeenCalled();
169
+ });
170
+ (0, vitest_1.test)("should include step title in filename", async () => {
171
+ const testInfo = createMockTestInfo({ trace: "on" });
172
+ const service = new service_1.VisualTraceService(testInfo, 0);
173
+ await service.captureScreenshot(mockTakeScreenshot, "click-submit");
174
+ (0, vitest_1.expect)(testInfo.attach).toHaveBeenCalledWith(vitest_1.expect.stringContaining("click_submit"), vitest_1.expect.objectContaining({
175
+ body: vitest_1.expect.any(Buffer),
176
+ contentType: "image/png",
177
+ }));
178
+ });
179
+ (0, vitest_1.test)("should handle screenshot capture errors gracefully", async () => {
180
+ const testInfo = createMockTestInfo({ trace: "on" });
181
+ const service = new service_1.VisualTraceService(testInfo, 0);
182
+ const errorTakeScreenshot = vitest_1.vi.fn(() => Promise.reject(new Error("Screenshot failed")));
183
+ // Mock console.warn to verify it's called
184
+ const consoleWarnSpy = vitest_1.vi
185
+ .spyOn(console, "warn")
186
+ .mockImplementation(() => { });
187
+ await service.captureScreenshot(errorTakeScreenshot, "test-step");
188
+ (0, vitest_1.expect)(consoleWarnSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining("Failed to capture screenshot"));
189
+ (0, vitest_1.expect)(testInfo.attach).not.toHaveBeenCalled();
190
+ consoleWarnSpy.mockRestore();
191
+ });
192
+ });
193
+ (0, vitest_1.describe)("Screenshot limits", () => {
194
+ (0, vitest_1.test)("should respect max screenshot limit", async () => {
195
+ const testInfo = createMockTestInfo({ trace: "on" });
196
+ const service = new service_1.VisualTraceService(testInfo, 0, {
197
+ maxScreenshots: 2,
198
+ dedupe: false,
199
+ });
200
+ // Capture first two screenshots
201
+ await service.captureScreenshot(mockTakeScreenshot, "step-1");
202
+ await service.captureScreenshot(mockTakeScreenshot, "step-2");
203
+ // Third screenshot should be skipped
204
+ await service.captureScreenshot(mockTakeScreenshot, "step-3");
205
+ (0, vitest_1.expect)(mockTakeScreenshot).toHaveBeenCalledTimes(2);
206
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(2);
207
+ });
208
+ (0, vitest_1.test)("should track screenshot count correctly", async () => {
209
+ const testInfo = createMockTestInfo({ trace: "on" });
210
+ const service = new service_1.VisualTraceService(testInfo, 0, { dedupe: false });
211
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(0);
212
+ await service.captureScreenshot(mockTakeScreenshot, "step-1");
213
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(1);
214
+ await service.captureScreenshot(mockTakeScreenshot, "step-2");
215
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(2);
216
+ });
217
+ });
218
+ (0, vitest_1.describe)("Deduplication", () => {
219
+ (0, vitest_1.test)("should skip duplicate screenshots when dedupe is enabled", async () => {
220
+ const testInfo = createMockTestInfo({ trace: "on" });
221
+ const service = new service_1.VisualTraceService(testInfo, 0, { dedupe: true });
222
+ // Same screenshot data
223
+ const sameData = Buffer.from("same-screenshot");
224
+ const sameTakeScreenshot = vitest_1.vi.fn(() => Promise.resolve(sameData));
225
+ await service.captureScreenshot(sameTakeScreenshot, "step-1");
226
+ await service.captureScreenshot(sameTakeScreenshot, "step-2");
227
+ (0, vitest_1.expect)(sameTakeScreenshot).toHaveBeenCalledTimes(2);
228
+ (0, vitest_1.expect)(testInfo.attach).toHaveBeenCalledTimes(1); // Only attached once
229
+ });
230
+ (0, vitest_1.test)("should capture all screenshots when dedupe is disabled", async () => {
231
+ const testInfo = createMockTestInfo({ trace: "on" });
232
+ const service = new service_1.VisualTraceService(testInfo, 0, { dedupe: false });
233
+ // Same screenshot data
234
+ const sameData = Buffer.from("same-screenshot");
235
+ const sameTakeScreenshot = vitest_1.vi.fn(() => Promise.resolve(sameData));
236
+ await service.captureScreenshot(sameTakeScreenshot, "step-1");
237
+ await service.captureScreenshot(sameTakeScreenshot, "step-2");
238
+ (0, vitest_1.expect)(sameTakeScreenshot).toHaveBeenCalledTimes(2);
239
+ (0, vitest_1.expect)(testInfo.attach).toHaveBeenCalledTimes(2); // Attached both times
240
+ });
241
+ });
242
+ (0, vitest_1.describe)("State management", () => {
243
+ (0, vitest_1.test)("should maintain separate state for different test attempts", async () => {
244
+ const testInfo = createMockTestInfo({ testId: "test-1", trace: "on" });
245
+ // First attempt
246
+ const service1 = new service_1.VisualTraceService(testInfo, 0);
247
+ await service1.captureScreenshot(mockTakeScreenshot, "step-1");
248
+ (0, vitest_1.expect)(service1.getScreenshotCount()).toBe(1);
249
+ // Second attempt (retry)
250
+ const service2 = new service_1.VisualTraceService(testInfo, 1);
251
+ (0, vitest_1.expect)(service2.getScreenshotCount()).toBe(0); // Should start fresh
252
+ await service2.captureScreenshot(mockTakeScreenshot, "step-1");
253
+ (0, vitest_1.expect)(service2.getScreenshotCount()).toBe(1);
254
+ // First attempt state should be preserved
255
+ (0, vitest_1.expect)(service1.getScreenshotCount()).toBe(1);
256
+ });
257
+ (0, vitest_1.test)("should reset state when resetState is called", () => {
258
+ const testInfo = createMockTestInfo({ testId: "test-1", trace: "on" });
259
+ const service = new service_1.VisualTraceService(testInfo, 0);
260
+ service["getState"]().screenshotCount = 5;
261
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(5);
262
+ service.resetState();
263
+ (0, vitest_1.expect)(service.getScreenshotCount()).toBe(0);
264
+ });
265
+ });
266
+ (0, vitest_1.describe)("Singleton management", () => {
267
+ (0, vitest_1.test)("should initialize and get service instance", () => {
268
+ const testInfo = createMockTestInfo({ testId: "test-1" });
269
+ const service = (0, service_1.initializeVisualTrace)(testInfo, 0);
270
+ (0, vitest_1.expect)(service).toBeInstanceOf(service_1.VisualTraceService);
271
+ const retrievedService = (0, service_1.getVisualTraceService)();
272
+ (0, vitest_1.expect)(retrievedService).toBe(service);
273
+ });
274
+ (0, vitest_1.test)("should clear service instance", () => {
275
+ const testInfo = createMockTestInfo({ testId: "test-1" });
276
+ (0, service_1.initializeVisualTrace)(testInfo, 0);
277
+ (0, vitest_1.expect)((0, service_1.getVisualTraceService)()).not.toBeNull();
278
+ (0, service_1.clearVisualTraceService)();
279
+ (0, vitest_1.expect)((0, service_1.getVisualTraceService)()).toBeNull();
280
+ });
281
+ });
282
+ (0, vitest_1.describe)("Configuration update", () => {
283
+ (0, vitest_1.test)("should update configuration at runtime", () => {
284
+ const testInfo = createMockTestInfo({ testId: "test-1" });
285
+ const service = new service_1.VisualTraceService(testInfo, 0);
286
+ (0, vitest_1.expect)(service["config"].enableScreenshots).toBe("retain-on-failure");
287
+ service.updateConfig({ enableScreenshots: "on" });
288
+ (0, vitest_1.expect)(service["config"].enableScreenshots).toBe("on");
289
+ });
290
+ (0, vitest_1.test)("should merge partial config updates", () => {
291
+ const testInfo = createMockTestInfo({ testId: "test-1" });
292
+ const service = new service_1.VisualTraceService(testInfo, 0);
293
+ service.updateConfig({ maxScreenshots: 100 });
294
+ (0, vitest_1.expect)(service["config"].maxScreenshots).toBe(100);
295
+ (0, vitest_1.expect)(service["config"].dedupe).toBe(true); // Should preserve other settings
296
+ });
297
+ });
298
+ });
@@ -43,6 +43,29 @@ export type AppwrightConfig = {
43
43
  appBundleId: string;
44
44
  expectTimeout: number;
45
45
  };
46
+ /**
47
+ * Configuration for Visual Trace screenshot capture
48
+ */
49
+ export type VisualTraceConfig = {
50
+ /**
51
+ * Controls when screenshots are captured:
52
+ * - true | 'on': Always capture screenshots
53
+ * - false | 'off': Never capture screenshots
54
+ * - 'retain-on-failure': Only capture screenshots when tests fail (default)
55
+ */
56
+ enableScreenshots?: boolean | "on" | "off" | "retain-on-failure";
57
+ /**
58
+ * Maximum number of screenshots to capture per test.
59
+ * Default: 50
60
+ */
61
+ maxScreenshots?: number;
62
+ /**
63
+ * Whether to deduplicate screenshots based on content hash.
64
+ * Prevents capturing identical screenshots multiple times.
65
+ * Default: true
66
+ */
67
+ dedupe?: boolean;
68
+ };
46
69
  export type DeviceConfig = BrowserStackConfig | LambdaTestConfig | LocalDeviceConfig | EmulatorConfig;
47
70
  /**
48
71
  * Configuration for devices running on Browserstack.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,23 @@
1
+ import { TestInfo } from "@playwright/test";
1
2
  export declare function boxedStep(target: Function, context: ClassMethodDecoratorContext): (this: {
2
- selector: string | RegExp;
3
+ selector?: string | RegExp;
4
+ device?: {
5
+ takeScreenshot: () => Promise<Buffer>;
6
+ initializeVisualTrace?: (testInfo: any, retryIndex: number, config?: any) => void;
7
+ };
8
+ takeScreenshot?: () => Promise<Buffer>;
9
+ initializeVisualTrace?: (testInfo: any, retryIndex: number, config?: any) => void;
3
10
  }, ...args: any) => Promise<any>;
4
11
  export declare function validateBuildPath(buildPath: string | undefined, expectedExtension: string): void;
5
12
  export declare function getLatestBuildToolsVersions(versions: string[]): string | undefined;
6
13
  export declare function longestDeterministicGroup(pattern: RegExp): string | undefined;
7
14
  export declare function basePath(): string;
15
+ /**
16
+ * Check if tracing is enabled for the current test based on Playwright's trace configuration
17
+ */
18
+ export declare function isTracingEnabled(testInfo: TestInfo, retryIndex: number): boolean;
19
+ /**
20
+ * Determine if screenshots should be captured based on trace mode
21
+ */
22
+ export declare function shouldCaptureScreenshotsFromTrace(testInfo: TestInfo, retryIndex: number): boolean;
8
23
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,CACvB,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,2BAA2B,UAG5B;IACJ,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B,WACQ,GAAG,kBAmBf;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,iBAAiB,EAAE,MAAM,QAoB1B;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAuB7E;AAED,wBAAgB,QAAQ,WAEvB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAa,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAQlD,wBAAgB,SAAS,CACvB,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,2BAA2B,UAG5B;IACJ,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE;QACP,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,qBAAqB,CAAC,EAAE,CACtB,QAAQ,EAAE,GAAG,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,KACT,IAAI,CAAC;KACX,CAAC;IACF,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,qBAAqB,CAAC,EAAE,CACtB,QAAQ,EAAE,GAAG,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,KACT,IAAI,CAAC;CACX,WACQ,GAAG,kBAwEf;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,iBAAiB,EAAE,MAAM,QAoB1B;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAuB7E;AAED,wBAAgB,QAAQ,WAEvB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CA2BT;AAED;;GAEG;AACH,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAST"}
package/dist/utils.js CHANGED
@@ -8,9 +8,12 @@ exports.validateBuildPath = validateBuildPath;
8
8
  exports.getLatestBuildToolsVersions = getLatestBuildToolsVersions;
9
9
  exports.longestDeterministicGroup = longestDeterministicGroup;
10
10
  exports.basePath = basePath;
11
+ exports.isTracingEnabled = isTracingEnabled;
12
+ exports.shouldCaptureScreenshotsFromTrace = shouldCaptureScreenshotsFromTrace;
11
13
  const test_1 = __importDefault(require("@playwright/test"));
12
14
  const fs_1 = __importDefault(require("fs"));
13
15
  const path_1 = __importDefault(require("path"));
16
+ const visualTrace_1 = require("./visualTrace");
14
17
  function boxedStep(target, context) {
15
18
  return function replacementMethod(...args) {
16
19
  const path = this.selector ? `("${this.selector}")` : "";
@@ -23,7 +26,47 @@ function boxedStep(target, context) {
23
26
  : "";
24
27
  const name = `${context.name}${path}${argsString}`;
25
28
  return test_1.default.step(name, async () => {
26
- return await target.call(this, ...args);
29
+ let result;
30
+ let stepFailed = false;
31
+ try {
32
+ result = await target.call(this, ...args);
33
+ }
34
+ catch (error) {
35
+ stepFailed = true;
36
+ throw error; // Re-throw to preserve test failure
37
+ }
38
+ finally {
39
+ // Capture screenshot even if step throws an error
40
+ let visualTrace = (0, visualTrace_1.getVisualTraceService)();
41
+ // Check if we need to reinitialize for a new test (important for persistentDevice)
42
+ if (test_1.default.info) {
43
+ try {
44
+ const testInfo = test_1.default.info();
45
+ const device = this.device || this;
46
+ // Initialize or reinitialize if needed
47
+ if (device?.initializeVisualTrace && testInfo) {
48
+ const needsInit = (0, visualTrace_1.needsVisualTraceReinitialization)(testInfo.testId, testInfo.retry);
49
+ if (needsInit) {
50
+ // Get visual trace config from project
51
+ const visualTraceConfig = testInfo.project?.use
52
+ ?.visualTrace;
53
+ device.initializeVisualTrace(testInfo, testInfo.retry, visualTraceConfig);
54
+ visualTrace = (0, visualTrace_1.getVisualTraceService)();
55
+ }
56
+ }
57
+ }
58
+ catch (e) {
59
+ // test.info() might not be available in some contexts
60
+ }
61
+ }
62
+ // For Device methods, 'this' is the Device instance
63
+ // For Locator methods, 'this.device' is the Device instance
64
+ const takeScreenshot = this.device?.takeScreenshot || this.takeScreenshot;
65
+ if (visualTrace && takeScreenshot) {
66
+ await visualTrace.captureScreenshot(() => takeScreenshot.call(this.device || this), context.name, stepFailed);
67
+ }
68
+ }
69
+ return result;
27
70
  }, { box: true });
28
71
  };
29
72
  }
@@ -64,3 +107,40 @@ function longestDeterministicGroup(pattern) {
64
107
  function basePath() {
65
108
  return path_1.default.join(process.cwd(), "playwright-report", "data", "videos-store");
66
109
  }
110
+ /**
111
+ * Check if tracing is enabled for the current test based on Playwright's trace configuration
112
+ */
113
+ function isTracingEnabled(testInfo, retryIndex) {
114
+ const traceMode = testInfo.project.use?.trace;
115
+ if (!traceMode || traceMode === "off") {
116
+ return false;
117
+ }
118
+ if (traceMode === "on") {
119
+ return true;
120
+ }
121
+ if (traceMode === "retain-on-failure") {
122
+ // Tracing is enabled if the test has failed
123
+ return testInfo.status !== undefined && testInfo.status !== "passed";
124
+ }
125
+ if (traceMode === "on-first-retry") {
126
+ // Tracing is enabled on the first retry only
127
+ return retryIndex === 1;
128
+ }
129
+ if (traceMode === "on-all-retries" || traceMode === "retry-with-trace") {
130
+ // Tracing is enabled on all retries
131
+ return retryIndex > 0;
132
+ }
133
+ return false;
134
+ }
135
+ /**
136
+ * Determine if screenshots should be captured based on trace mode
137
+ */
138
+ function shouldCaptureScreenshotsFromTrace(testInfo, retryIndex) {
139
+ // Check if screenshots are explicitly disabled in project config
140
+ const screenshotsConfig = testInfo.project.use?.screenshot;
141
+ if (screenshotsConfig === "off") {
142
+ return false;
143
+ }
144
+ // Use trace configuration to determine screenshot behavior
145
+ return isTracingEnabled(testInfo, retryIndex);
146
+ }
@@ -0,0 +1,3 @@
1
+ export { VisualTraceService, initializeVisualTrace, getVisualTraceService, clearVisualTraceService, needsVisualTraceReinitialization, } from "./service";
2
+ export type { VisualTraceState } from "./service";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/visualTrace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,WAAW,CAAC;AAEnB,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.needsVisualTraceReinitialization = exports.clearVisualTraceService = exports.getVisualTraceService = exports.initializeVisualTrace = exports.VisualTraceService = void 0;
4
+ var service_1 = require("./service");
5
+ Object.defineProperty(exports, "VisualTraceService", { enumerable: true, get: function () { return service_1.VisualTraceService; } });
6
+ Object.defineProperty(exports, "initializeVisualTrace", { enumerable: true, get: function () { return service_1.initializeVisualTrace; } });
7
+ Object.defineProperty(exports, "getVisualTraceService", { enumerable: true, get: function () { return service_1.getVisualTraceService; } });
8
+ Object.defineProperty(exports, "clearVisualTraceService", { enumerable: true, get: function () { return service_1.clearVisualTraceService; } });
9
+ Object.defineProperty(exports, "needsVisualTraceReinitialization", { enumerable: true, get: function () { return service_1.needsVisualTraceReinitialization; } });
@@ -0,0 +1,75 @@
1
+ import { TestInfo } from "@playwright/test";
2
+ import { VisualTraceConfig } from "../types";
3
+ export interface VisualTraceState {
4
+ screenshotCount: number;
5
+ dedupeHashes: Set<string>;
6
+ }
7
+ export declare class VisualTraceService {
8
+ private states;
9
+ private testInfo;
10
+ readonly retryIndex: number;
11
+ private config;
12
+ readonly testId: string;
13
+ constructor(testInfo: TestInfo, retryIndex: number, config?: VisualTraceConfig);
14
+ /**
15
+ * Get or create state for the current test attempt
16
+ */
17
+ private getState;
18
+ /**
19
+ * Check if screenshots should be captured based on trace mode and test status
20
+ */
21
+ shouldCaptureScreenshot(stepFailed?: boolean): boolean;
22
+ /**
23
+ * Check our own config mode for screenshot capture
24
+ */
25
+ private checkConfigMode;
26
+ /**
27
+ * Check if the current test is failing
28
+ */
29
+ private isTestFailing;
30
+ /**
31
+ * Calculate hash for screenshot deduplication
32
+ */
33
+ private calculateHash;
34
+ /**
35
+ * Check if we've reached the screenshot limit
36
+ */
37
+ private hasReachedLimit;
38
+ /**
39
+ * Check if screenshot is a duplicate
40
+ */
41
+ private isDuplicate;
42
+ /**
43
+ * Capture and attach a screenshot to the test
44
+ */
45
+ captureScreenshot(takeScreenshot: () => Promise<Buffer>, stepTitle?: string, stepFailed?: boolean): Promise<void>;
46
+ /**
47
+ * Reset state for a test (useful for cleanup)
48
+ */
49
+ resetState(): void;
50
+ /**
51
+ * Get current screenshot count for the test
52
+ */
53
+ getScreenshotCount(): number;
54
+ /**
55
+ * Update configuration at runtime
56
+ */
57
+ updateConfig(config: Partial<VisualTraceConfig>): void;
58
+ }
59
+ /**
60
+ * Initialize the Visual Trace Service for a test
61
+ */
62
+ export declare function initializeVisualTrace(testInfo: TestInfo, retryIndex: number, config?: VisualTraceConfig): VisualTraceService;
63
+ /**
64
+ * Get the current Visual Trace Service instance
65
+ */
66
+ export declare function getVisualTraceService(): VisualTraceService | null;
67
+ /**
68
+ * Check if the Visual Trace Service needs reinitialization for a different test
69
+ */
70
+ export declare function needsVisualTraceReinitialization(testId: string, retryIndex: number): boolean;
71
+ /**
72
+ * Clear the Visual Trace Service instance
73
+ */
74
+ export declare function clearVisualTraceService(): void;
75
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/visualTrace/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,QAAQ,CAAW;IAC3B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,OAAO,CAAC,MAAM,CAAoB;IAClC,SAAgB,MAAM,EAAE,MAAM,CAAC;gBAG7B,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,iBAAiB;IAc5B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAahB;;OAEG;IACH,uBAAuB,CAAC,UAAU,GAAE,OAAe,GAAG,OAAO;IA6D7D;;OAEG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;IACH,OAAO,CAAC,aAAa;IAgBrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,eAAe;IAQvB;;OAEG;IACH,OAAO,CAAC,WAAW;IAgBnB;;OAEG;IACG,iBAAiB,CACrB,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACrC,SAAS,CAAC,EAAE,MAAM,EAClB,UAAU,GAAE,OAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IA0ChB;;OAEG;IACH,UAAU,IAAI,IAAI;IAKlB;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAK5B;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI;CAGvD;AAKD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,iBAAiB,GACzB,kBAAkB,CAGpB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,kBAAkB,GAAG,IAAI,CAEjE;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAST;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAK9C"}
@@ -0,0 +1,249 @@
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.VisualTraceService = void 0;
7
+ exports.initializeVisualTrace = initializeVisualTrace;
8
+ exports.getVisualTraceService = getVisualTraceService;
9
+ exports.needsVisualTraceReinitialization = needsVisualTraceReinitialization;
10
+ exports.clearVisualTraceService = clearVisualTraceService;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ class VisualTraceService {
13
+ states = new Map();
14
+ testInfo;
15
+ retryIndex;
16
+ config;
17
+ testId;
18
+ constructor(testInfo, retryIndex, config) {
19
+ this.testInfo = testInfo;
20
+ this.retryIndex = retryIndex;
21
+ this.testId = testInfo.testId;
22
+ // Default configuration: only capture on failure
23
+ this.config = {
24
+ enableScreenshots: config?.enableScreenshots ?? "retain-on-failure",
25
+ maxScreenshots: config?.maxScreenshots ?? 50,
26
+ dedupe: config?.dedupe ?? true,
27
+ };
28
+ }
29
+ /**
30
+ * Get or create state for the current test attempt
31
+ */
32
+ getState() {
33
+ const key = `${this.testInfo.testId}#${this.retryIndex}`;
34
+ if (!this.states.has(key)) {
35
+ this.states.set(key, {
36
+ screenshotCount: 0,
37
+ dedupeHashes: new Set(),
38
+ });
39
+ }
40
+ return this.states.get(key);
41
+ }
42
+ /**
43
+ * Check if screenshots should be captured based on trace mode and test status
44
+ */
45
+ shouldCaptureScreenshot(stepFailed = false) {
46
+ // Check if screenshots are explicitly disabled
47
+ if (this.config.enableScreenshots === false ||
48
+ this.config.enableScreenshots === "off") {
49
+ return false;
50
+ }
51
+ // Get trace configuration from Playwright
52
+ const traceConfig = this.testInfo.project.use?.trace;
53
+ // If no trace mode is set, use our config
54
+ if (!traceConfig) {
55
+ return this.checkConfigMode(stepFailed);
56
+ }
57
+ // Handle both string and object trace configurations
58
+ let traceMode;
59
+ if (typeof traceConfig === "string") {
60
+ traceMode = traceConfig;
61
+ }
62
+ else if (typeof traceConfig === "object" && traceConfig.mode) {
63
+ // Handle object configuration like { mode: 'on', screenshots: true }
64
+ traceMode = traceConfig.mode;
65
+ // If screenshots are explicitly disabled in trace config, respect that
66
+ if (traceConfig.screenshots === false) {
67
+ return false;
68
+ }
69
+ }
70
+ else {
71
+ // Unknown format, fall back to our config
72
+ return this.checkConfigMode(stepFailed);
73
+ }
74
+ // Map Playwright trace modes to screenshot behavior
75
+ switch (traceMode) {
76
+ case "on":
77
+ return true;
78
+ case "retain-on-failure":
79
+ // Capture if step failed, test already failed, or we're in a retry
80
+ return stepFailed || this.isTestFailing() || this.retryIndex > 0;
81
+ case "on-first-retry":
82
+ // Only capture on first retry
83
+ return this.retryIndex === 1;
84
+ case "on-all-retries":
85
+ // Capture on all retries
86
+ return this.retryIndex > 0;
87
+ case "retry-with-trace":
88
+ return this.retryIndex > 0;
89
+ case "off":
90
+ return false;
91
+ default:
92
+ return this.checkConfigMode(stepFailed);
93
+ }
94
+ }
95
+ /**
96
+ * Check our own config mode for screenshot capture
97
+ */
98
+ checkConfigMode(stepFailed = false) {
99
+ const mode = this.config.enableScreenshots;
100
+ if (mode === true || mode === "on") {
101
+ return true;
102
+ }
103
+ if (mode === "retain-on-failure") {
104
+ // Capture if step failed or test is already failing
105
+ return stepFailed || this.isTestFailing();
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * Check if the current test is failing
111
+ */
112
+ isTestFailing() {
113
+ // Check test status (will be 'failed', 'timedOut', etc. for failures)
114
+ if (this.testInfo.status && this.testInfo.status !== "passed") {
115
+ return true;
116
+ }
117
+ // Check if there are any errors
118
+ if (this.testInfo.errors && this.testInfo.errors.length > 0) {
119
+ return true;
120
+ }
121
+ // During test execution, we might not have status yet
122
+ // This will be handled by the retry detection
123
+ return false;
124
+ }
125
+ /**
126
+ * Calculate hash for screenshot deduplication
127
+ */
128
+ calculateHash(data) {
129
+ return crypto_1.default.createHash("sha256").update(data).digest("hex");
130
+ }
131
+ /**
132
+ * Check if we've reached the screenshot limit
133
+ */
134
+ hasReachedLimit() {
135
+ const state = this.getState();
136
+ return !!(this.config.maxScreenshots &&
137
+ state.screenshotCount >= this.config.maxScreenshots);
138
+ }
139
+ /**
140
+ * Check if screenshot is a duplicate
141
+ */
142
+ isDuplicate(data) {
143
+ if (!this.config.dedupe) {
144
+ return false;
145
+ }
146
+ const state = this.getState();
147
+ const hash = this.calculateHash(data);
148
+ if (state.dedupeHashes.has(hash)) {
149
+ return true;
150
+ }
151
+ state.dedupeHashes.add(hash);
152
+ return false;
153
+ }
154
+ /**
155
+ * Capture and attach a screenshot to the test
156
+ */
157
+ async captureScreenshot(takeScreenshot, stepTitle, stepFailed = false) {
158
+ // Check if we should capture screenshots
159
+ if (!this.shouldCaptureScreenshot(stepFailed)) {
160
+ return;
161
+ }
162
+ // Check if we've reached the screenshot limit
163
+ if (this.hasReachedLimit()) {
164
+ return;
165
+ }
166
+ try {
167
+ // Take the screenshot
168
+ const screenshotData = await takeScreenshot();
169
+ // Check if this is a duplicate screenshot
170
+ if (this.isDuplicate(screenshotData)) {
171
+ return;
172
+ }
173
+ // Update state
174
+ const state = this.getState();
175
+ state.screenshotCount++;
176
+ // Generate filename with step title if provided
177
+ const timestamp = Date.now();
178
+ const stepSuffix = stepTitle
179
+ ? `-${stepTitle.replace(/[^a-zA-Z0-9]/g, "_")}`
180
+ : "";
181
+ const filename = `screenshot-${timestamp}${stepSuffix}.png`;
182
+ // Attach to test
183
+ await this.testInfo.attach(filename, {
184
+ body: screenshotData,
185
+ contentType: "image/png",
186
+ });
187
+ }
188
+ catch (error) {
189
+ // Log error but don't fail the test
190
+ console.warn(`Failed to capture screenshot: ${error}`);
191
+ }
192
+ }
193
+ /**
194
+ * Reset state for a test (useful for cleanup)
195
+ */
196
+ resetState() {
197
+ const key = `${this.testInfo.testId}#${this.retryIndex}`;
198
+ this.states.delete(key);
199
+ }
200
+ /**
201
+ * Get current screenshot count for the test
202
+ */
203
+ getScreenshotCount() {
204
+ const state = this.getState();
205
+ return state.screenshotCount;
206
+ }
207
+ /**
208
+ * Update configuration at runtime
209
+ */
210
+ updateConfig(config) {
211
+ this.config = { ...this.config, ...config };
212
+ }
213
+ }
214
+ exports.VisualTraceService = VisualTraceService;
215
+ // Singleton instance management
216
+ let serviceInstance = null;
217
+ /**
218
+ * Initialize the Visual Trace Service for a test
219
+ */
220
+ function initializeVisualTrace(testInfo, retryIndex, config) {
221
+ serviceInstance = new VisualTraceService(testInfo, retryIndex, config);
222
+ return serviceInstance;
223
+ }
224
+ /**
225
+ * Get the current Visual Trace Service instance
226
+ */
227
+ function getVisualTraceService() {
228
+ return serviceInstance;
229
+ }
230
+ /**
231
+ * Check if the Visual Trace Service needs reinitialization for a different test
232
+ */
233
+ function needsVisualTraceReinitialization(testId, retryIndex) {
234
+ if (!serviceInstance) {
235
+ return true;
236
+ }
237
+ // Reinitialize if it's a different test or retry
238
+ return (serviceInstance.testId !== testId ||
239
+ serviceInstance.retryIndex !== retryIndex);
240
+ }
241
+ /**
242
+ * Clear the Visual Trace Service instance
243
+ */
244
+ function clearVisualTraceService() {
245
+ if (serviceInstance) {
246
+ serviceInstance.resetState();
247
+ serviceInstance = null;
248
+ }
249
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samsara-dev/appwright",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"