@samsara-dev/appwright 0.7.0 → 0.7.2

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,33 @@
1
1
  # appwright
2
2
 
3
+ ## 0.7.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ecdfc42: Avoid video filename collisions in the Playwright reporter by including the provider session ID in downloaded video filenames.
8
+
9
+ ## 0.7.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 75adf30: Add CI metadata support for BrowserStack build traceability
14
+
15
+ BrowserStack sessions now automatically include CI context in build names and session names:
16
+
17
+ - **Buildkite**: Build number, branch, commit from `BUILDKITE_*` env vars
18
+ - **GitHub Actions**: Run number, ref name, SHA from `GITHUB_*` env vars
19
+ - **GitLab CI**: Pipeline ID, ref name, commit from `CI_*` env vars
20
+
21
+ Example build names:
22
+
23
+ - CI: `driver-performance-tests android #35 (main)`
24
+ - Local: `driver-performance-tests android`
25
+
26
+ Environment variable overrides:
27
+
28
+ - `BROWSERSTACK_BUILD_NAME`: Override auto-generated build name
29
+ - `BROWSERSTACK_SESSION_NAME`: Override auto-generated session name
30
+
3
31
  ## 0.7.0
4
32
 
5
33
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsDtC,qBAAa,0BAA2B,YAAW,cAAc;IAC/D,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,OAAO,CAA+B;gBAG5C,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS;IAU3B,WAAW;IA0EX,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAMlC,OAAO,CAAC,cAAc;YASR,YAAY;YAiBZ,iBAAiB;YAKjB,yBAAyB;WAK1B,aAAa,CACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAuFlD,eAAe,CAAC,OAAO,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf;IA2BD,OAAO,CAAC,YAAY;CAuHrB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsDtC,qBAAa,0BAA2B,YAAW,cAAc;IAC/D,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,OAAO,CAA+B;gBAG5C,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS;IAU3B,WAAW;IA0EX,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAMlC,OAAO,CAAC,cAAc;YASR,YAAY;YAiBZ,iBAAiB;YAKjB,yBAAyB;WAK1B,aAAa,CACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAuFlD,eAAe,CAAC,OAAO,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf;IA2BD,OAAO,CAAC,YAAY;CAsJrB"}
@@ -251,6 +251,28 @@ class BrowserStackDeviceProvider {
251
251
  throw new Error(`process.env.${envVarKey} is not set. Did the file upload work?`);
252
252
  }
253
253
  const permissionPrompts = deviceConfig?.permissionPrompts;
254
+ // Build CI-aware metadata for better traceability between CI builds and BrowserStack sessions
255
+ const ciBuildIdentifier = process.env.BUILDKITE_BUILD_ID ||
256
+ process.env.GITHUB_RUN_ID ||
257
+ process.env.CI_JOB_ID || // GitLab CI
258
+ process.env.USER;
259
+ const ciBuildNumber = process.env.BUILDKITE_BUILD_NUMBER ||
260
+ process.env.GITHUB_RUN_NUMBER ||
261
+ process.env.CI_PIPELINE_IID; // GitLab CI
262
+ const ciBranch = process.env.BUILDKITE_BRANCH ||
263
+ process.env.GITHUB_REF_NAME ||
264
+ process.env.CI_COMMIT_REF_NAME; // GitLab CI
265
+ const ciCommit = (process.env.BUILDKITE_COMMIT ||
266
+ process.env.GITHUB_SHA ||
267
+ process.env.CI_COMMIT_SHA) // GitLab CI
268
+ ?.substring(0, 7);
269
+ // Allow env var override, otherwise build a descriptive name with CI context
270
+ const defaultBuildName = ciBuildNumber
271
+ ? `${projectName} ${platformName} #${ciBuildNumber}${ciBranch ? ` (${ciBranch})` : ""}`
272
+ : `${projectName} ${platformName}`;
273
+ const defaultSessionName = ciCommit
274
+ ? `${projectName} ${platformName} test @ ${ciCommit}`
275
+ : `${projectName} ${platformName} test`;
254
276
  const bstackOptions = {
255
277
  debug: true,
256
278
  interactiveDebugging: true,
@@ -262,11 +284,9 @@ class BrowserStackDeviceProvider {
262
284
  osVersion: deviceConfig.osVersion,
263
285
  platformName: platformName,
264
286
  deviceOrientation: deviceConfig?.orientation,
265
- buildName: `${projectName} ${platformName}`,
266
- sessionName: `${projectName} ${platformName} test`,
267
- buildIdentifier: process.env.GITHUB_ACTIONS === "true"
268
- ? `CI ${process.env.GITHUB_RUN_ID}`
269
- : process.env.USER,
287
+ buildName: process.env.BROWSERSTACK_BUILD_NAME || defaultBuildName,
288
+ sessionName: process.env.BROWSERSTACK_SESSION_NAME || defaultSessionName,
289
+ buildIdentifier: ciBuildIdentifier,
270
290
  };
271
291
  if (typeof deviceConfig?.appProfiling === "boolean") {
272
292
  bstackOptions.appProfiling = deviceConfig.appProfiling;
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAUhF,cAAM,eAAgB,YAAW,QAAQ;IACvC,OAAO,CAAC,gBAAgB,CAAsB;IAE9C,OAAO;IAQP,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAU9C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA0FtC,KAAK;YAKG,kCAAkC;IA4ChD,OAAO,CAAC,4BAA4B;IAyBpC,OAAO,CAAC,qBAAqB;CAI9B;AA8ED,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAUhF,cAAM,eAAgB,YAAW,QAAQ;IACvC,OAAO,CAAC,gBAAgB,CAAsB;IAE9C,OAAO;IAQP,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAU9C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA2FtC,KAAK;YAKG,kCAAkC;IA6ChD,OAAO,CAAC,4BAA4B;IAyBpC,OAAO,CAAC,qBAAqB;CAI9B;AA8ED,eAAe,eAAe,CAAC"}
package/dist/reporter.js CHANGED
@@ -51,7 +51,6 @@ class VideoDownloader {
51
51
  type: "workerInfo",
52
52
  description: `Ran on worker #${workerIndex}.`,
53
53
  });
54
- const expectedVideoPath = path_1.default.join((0, utils_1.basePath)(), `worker-${workerIndex}-video.mp4`);
55
54
  // The `onTestEnd` is method is called before the worker ends and
56
55
  // the worker's `endTime` is saved to disk. We add a 5 secs delay
57
56
  // to prevent a harmful race condition.
@@ -68,10 +67,11 @@ class VideoDownloader {
68
67
  if (!this.providerSupportsVideo(providerName)) {
69
68
  return; // Nothing to do here
70
69
  }
70
+ const workerVideoBaseName = `worker-${workerIndex}-${sessionId}-video`;
71
71
  if (endTime) {
72
72
  // This is the last test in the worker, so let's download the video
73
73
  const provider = (0, providers_1.getProviderClass)(providerName);
74
- const downloaded = await provider.downloadVideo(sessionId, (0, utils_1.basePath)(), `worker-${workerIndex}-video`);
74
+ const downloaded = await provider.downloadVideo(sessionId, (0, utils_1.basePath)(), workerVideoBaseName);
75
75
  if (!downloaded) {
76
76
  return;
77
77
  }
@@ -80,8 +80,9 @@ class VideoDownloader {
80
80
  else {
81
81
  // This is an intermediate test in the worker, so let's wait for the
82
82
  // video file to be found on disk. Once it is, we trim and attach it.
83
- await waitFor(() => fs_1.default.existsSync(expectedVideoPath));
84
- return this.trimAndAttachPersistentDeviceVideo(test, result, expectedVideoPath);
83
+ const expectedWorkerVideoPath = path_1.default.join((0, utils_1.basePath)(), `${workerVideoBaseName}.mp4`);
84
+ await waitFor(() => fs_1.default.existsSync(expectedWorkerVideoPath));
85
+ return this.trimAndAttachPersistentDeviceVideo(test, result, expectedWorkerVideoPath);
85
86
  }
86
87
  })
87
88
  .catch((e) => {
@@ -111,7 +112,8 @@ class VideoDownloader {
111
112
  }
112
113
  else {
113
114
  const trimSkipPoint = (testStart.getTime() - workerStart.getTime()) / 1000;
114
- const trimmedFileName = `worker-${workerIdx}-trimmed-${test.id}.mp4`;
115
+ const retryIndex = result.retry ?? 0;
116
+ const trimmedFileName = `worker-${workerIdx}-trimmed-${test.id}-retry-${retryIndex}.mp4`;
115
117
  try {
116
118
  pathToAttach = await trimVideo({
117
119
  originalVideoPath: workerVideoPath,
@@ -135,7 +137,7 @@ class VideoDownloader {
135
137
  });
136
138
  }
137
139
  downloadAndAttachDeviceVideo(test, result, providerClass, sessionId) {
138
- const videoFileName = `${test.id}`;
140
+ const videoFileName = `${sessionId}-${test.id}`;
139
141
  if (!providerClass.downloadVideo) {
140
142
  return;
141
143
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reporter.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.spec.d.ts","sourceRoot":"","sources":["../../src/tests/reporter.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
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 promises_1 = __importDefault(require("fs/promises"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ vitest_1.vi.mock("@ffmpeg-installer/ffmpeg", () => {
11
+ return {
12
+ default: { path: "/fake/ffmpeg" },
13
+ __esModule: true,
14
+ };
15
+ });
16
+ vitest_1.vi.mock("fluent-ffmpeg", () => {
17
+ return {
18
+ default: () => {
19
+ const handlers = {};
20
+ const chain = {
21
+ setFfmpegPath: () => chain,
22
+ setStartTime: () => chain,
23
+ setDuration: () => chain,
24
+ output: () => chain,
25
+ on: (event, cb) => {
26
+ handlers[event] = cb;
27
+ return chain;
28
+ },
29
+ run: () => {
30
+ void Promise.resolve().then(() => handlers.end?.());
31
+ },
32
+ };
33
+ return chain;
34
+ },
35
+ __esModule: true,
36
+ };
37
+ });
38
+ let mockBasePath = "";
39
+ const downloadVideoMock = vitest_1.vi.fn();
40
+ const getProviderClassMock = vitest_1.vi.fn(() => ({
41
+ downloadVideo: downloadVideoMock,
42
+ }));
43
+ vitest_1.vi.mock("../providers", () => {
44
+ return {
45
+ getProviderClass: getProviderClassMock,
46
+ __esModule: true,
47
+ };
48
+ });
49
+ vitest_1.vi.mock("../utils", () => {
50
+ return {
51
+ basePath: () => mockBasePath,
52
+ __esModule: true,
53
+ };
54
+ });
55
+ vitest_1.vi.mock("../logger", () => {
56
+ return {
57
+ logger: {
58
+ log: vitest_1.vi.fn(),
59
+ warn: vitest_1.vi.fn(),
60
+ error: vitest_1.vi.fn(),
61
+ },
62
+ __esModule: true,
63
+ };
64
+ });
65
+ let VideoDownloader;
66
+ (0, vitest_1.beforeAll)(async () => {
67
+ const reporterModule = await import("../reporter.js");
68
+ VideoDownloader = reporterModule.default;
69
+ });
70
+ (0, vitest_1.afterEach)(async () => {
71
+ const basePathToDelete = mockBasePath;
72
+ downloadVideoMock.mockReset();
73
+ mockBasePath = "";
74
+ getProviderClassMock.mockClear();
75
+ vitest_1.vi.useRealTimers();
76
+ if (basePathToDelete) {
77
+ await promises_1.default.rm(basePathToDelete, { recursive: true, force: true });
78
+ }
79
+ });
80
+ (0, vitest_1.describe)("VideoDownloader", () => {
81
+ (0, vitest_1.test)("downloads device videos with session-scoped filename", async () => {
82
+ mockBasePath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "appwright-videos-"));
83
+ const sessionId = "session-123";
84
+ const testId = "test-abc";
85
+ downloadVideoMock.mockResolvedValueOnce({
86
+ path: path_1.default.join(mockBasePath, `${sessionId}-${testId}.mp4`),
87
+ contentType: "video/mp4",
88
+ });
89
+ const reporter = new VideoDownloader();
90
+ const testCase = {
91
+ id: testId,
92
+ title: "example",
93
+ annotations: [
94
+ { type: "sessionId", description: sessionId },
95
+ { type: "providerName", description: "browserstack" },
96
+ ],
97
+ };
98
+ const testResult = {
99
+ workerIndex: 0,
100
+ duration: 1,
101
+ startTime: new Date(),
102
+ attachments: [],
103
+ };
104
+ reporter.onTestEnd(testCase, testResult);
105
+ (0, vitest_1.expect)(getProviderClassMock).toHaveBeenCalledWith("browserstack");
106
+ (0, vitest_1.expect)(downloadVideoMock).toHaveBeenCalledWith(sessionId, mockBasePath, `${sessionId}-${testId}`);
107
+ await reporter.onEnd();
108
+ (0, vitest_1.expect)(testResult.attachments).toEqual([
109
+ {
110
+ path: path_1.default.join(mockBasePath, `${sessionId}-${testId}.mp4`),
111
+ contentType: "video/mp4",
112
+ name: "video",
113
+ },
114
+ ]);
115
+ });
116
+ (0, vitest_1.test)("scopes persistentDevice worker video base name by sessionId", async () => {
117
+ vitest_1.vi.useFakeTimers();
118
+ mockBasePath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "appwright-videos-"));
119
+ const workerIndex = 0;
120
+ const sessionId = "session-xyz";
121
+ const providerName = "browserstack";
122
+ const workerVideoBaseName = `worker-${workerIndex}-${sessionId}-video`;
123
+ const workerStart = new Date("2025-01-01T00:00:00.000Z");
124
+ const testStart = new Date("2025-01-01T00:00:10.000Z");
125
+ await promises_1.default.writeFile(path_1.default.join(mockBasePath, `worker-info-${workerIndex}.json`), JSON.stringify({
126
+ idx: workerIndex,
127
+ sessionId,
128
+ providerName,
129
+ startTime: {
130
+ beforeAppiumSession: workerStart.toISOString(),
131
+ afterAppiumSession: workerStart.toISOString(),
132
+ },
133
+ endTime: new Date("2025-01-01T00:00:02.000Z").toISOString(),
134
+ tests: [],
135
+ }, null, 2));
136
+ const downloadedVideoPath = path_1.default.join(mockBasePath, `${workerVideoBaseName}.mp4`);
137
+ await promises_1.default.writeFile(downloadedVideoPath, "video-bytes");
138
+ downloadVideoMock.mockResolvedValueOnce({
139
+ path: downloadedVideoPath,
140
+ contentType: "video/mp4",
141
+ });
142
+ const reporter = new VideoDownloader();
143
+ const testCase = {
144
+ id: "test-1",
145
+ title: "persistent",
146
+ annotations: [],
147
+ };
148
+ const testResult = {
149
+ workerIndex,
150
+ duration: 1,
151
+ startTime: testStart,
152
+ retry: 1,
153
+ attachments: [],
154
+ };
155
+ reporter.onTestEnd(testCase, testResult);
156
+ await vitest_1.vi.advanceTimersByTimeAsync(5000);
157
+ await reporter.onEnd();
158
+ (0, vitest_1.expect)(downloadVideoMock).toHaveBeenCalledWith(sessionId, mockBasePath, workerVideoBaseName);
159
+ (0, vitest_1.expect)(testResult.attachments).toMatchObject([
160
+ {
161
+ contentType: "video/mp4",
162
+ name: "video",
163
+ path: vitest_1.expect.stringContaining(`-retry-1.mp4`),
164
+ },
165
+ ]);
166
+ });
167
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samsara-dev/appwright",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"