@samsara-dev/appwright 0.7.1 → 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,11 @@
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
+
3
9
  ## 0.7.1
4
10
 
5
11
  ### Patch Changes
@@ -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.1",
3
+ "version": "0.7.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"