@samsara-dev/appwright 0.7.2 → 0.7.3

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.
@@ -0,0 +1,801 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const vitest_1 = require("vitest");
27
+ const index_1 = require("./index");
28
+ const fs = __importStar(require("fs"));
29
+ const logger_1 = require("../../logger");
30
+ //@ts-ignore
31
+ const client_device_farm_1 = require("@aws-sdk/client-device-farm");
32
+ // Helper function to cast test project to expected type
33
+ const asProject = (project) => project;
34
+ // Create a mock fetch function
35
+ const mockFetch = vitest_1.vi.fn();
36
+ vitest_1.vi.stubGlobal("fetch", mockFetch);
37
+ // Mock utils module
38
+ vitest_1.vi.mock("../../utils", async () => {
39
+ const actual = await vitest_1.vi.importActual("../../utils");
40
+ return {
41
+ ...actual,
42
+ validateBuildPath: vitest_1.vi.fn(),
43
+ };
44
+ });
45
+ // Mock async-retry
46
+ vitest_1.vi.mock("async-retry", () => ({
47
+ default: vitest_1.vi.fn((fn) => fn()),
48
+ }));
49
+ // Mock node-fetch
50
+ vitest_1.vi.mock("node-fetch", () => ({
51
+ default: mockFetch,
52
+ }));
53
+ // Mock stream/promises pipeline
54
+ vitest_1.vi.mock("stream/promises", () => ({
55
+ pipeline: vitest_1.vi.fn().mockResolvedValue(undefined),
56
+ }));
57
+ // Mock webdriver
58
+ vitest_1.vi.mock("webdriver", () => ({
59
+ default: {
60
+ newSession: vitest_1.vi.fn().mockResolvedValue({
61
+ sessionId: "test-session-id",
62
+ }),
63
+ },
64
+ }));
65
+ // Mock AWS SDK
66
+ vitest_1.vi.mock("@aws-sdk/client-device-farm", () => {
67
+ const mockDeviceFarmClient = vitest_1.vi.fn();
68
+ mockDeviceFarmClient.prototype.send = vitest_1.vi.fn();
69
+ // Create mock command constructors that store their input
70
+ const mockCommandConstructors = {};
71
+ const createMockCommand = (name) => {
72
+ const mockConstructor = vitest_1.vi.fn((input) => {
73
+ const command = { input, constructor: { name } };
74
+ // Add the constructor as a property for instanceof checks
75
+ Object.defineProperty(command, "constructor", {
76
+ value: mockCommandConstructors[name],
77
+ writable: false,
78
+ enumerable: false,
79
+ configurable: true,
80
+ });
81
+ return command;
82
+ });
83
+ mockCommandConstructors[name] = mockConstructor;
84
+ return mockConstructor;
85
+ };
86
+ const commands = {
87
+ CreateUploadCommand: createMockCommand("CreateUploadCommand"),
88
+ GetUploadCommand: createMockCommand("GetUploadCommand"),
89
+ CreateRemoteAccessSessionCommand: createMockCommand("CreateRemoteAccessSessionCommand"),
90
+ GetRemoteAccessSessionCommand: createMockCommand("GetRemoteAccessSessionCommand"),
91
+ StopRemoteAccessSessionCommand: createMockCommand("StopRemoteAccessSessionCommand"),
92
+ ListArtifactsCommand: createMockCommand("ListArtifactsCommand"),
93
+ };
94
+ return {
95
+ DeviceFarmClient: mockDeviceFarmClient,
96
+ UploadStatus: {
97
+ FAILED: "FAILED",
98
+ SUCCEEDED: "SUCCEEDED",
99
+ INITIALIZED: "INITIALIZED",
100
+ PROCESSING: "PROCESSING",
101
+ },
102
+ ...commands,
103
+ };
104
+ });
105
+ // Mock AWS STS SDK
106
+ vitest_1.vi.mock("@aws-sdk/client-sts", () => {
107
+ const mockSTSClientClass = vitest_1.vi.fn();
108
+ mockSTSClientClass.prototype.send = vitest_1.vi.fn();
109
+ mockSTSClientClass.prototype.destroy = vitest_1.vi.fn();
110
+ const mockAssumeRoleCommand = vitest_1.vi.fn((input) => ({
111
+ input,
112
+ constructor: { name: "AssumeRoleCommand" },
113
+ }));
114
+ return {
115
+ STSClient: mockSTSClientClass,
116
+ AssumeRoleCommand: mockAssumeRoleCommand,
117
+ };
118
+ });
119
+ // Mock fs
120
+ vitest_1.vi.mock("fs", () => {
121
+ const mockFs = {
122
+ createReadStream: vitest_1.vi.fn(),
123
+ statSync: vitest_1.vi.fn(),
124
+ existsSync: vitest_1.vi.fn(),
125
+ mkdirSync: vitest_1.vi.fn(),
126
+ createWriteStream: vitest_1.vi.fn(),
127
+ renameSync: vitest_1.vi.fn(),
128
+ unlinkSync: vitest_1.vi.fn(),
129
+ rmSync: vitest_1.vi.fn(),
130
+ };
131
+ return {
132
+ default: mockFs,
133
+ ...mockFs,
134
+ };
135
+ });
136
+ (0, vitest_1.describe)("AWSDeviceFarmProvider", () => {
137
+ let provider;
138
+ let mockClient;
139
+ (0, vitest_1.beforeEach)(() => {
140
+ // Reset all mocks
141
+ vitest_1.vi.clearAllMocks();
142
+ mockFetch.mockReset();
143
+ // Setup mock client with send method
144
+ mockClient = {
145
+ send: vitest_1.vi.fn(),
146
+ destroy: vitest_1.vi.fn(),
147
+ };
148
+ vitest_1.vi.mocked(client_device_farm_1.DeviceFarmClient).mockImplementation(() => mockClient);
149
+ // Default fs mocks - use the default export since that's what the provider uses
150
+ const fsModule = fs;
151
+ const fsDefault = fsModule.default || fs;
152
+ vitest_1.vi.mocked(fsDefault.existsSync).mockReturnValue(true);
153
+ vitest_1.vi.mocked(fsDefault.statSync).mockReturnValue({ size: 1024 });
154
+ vitest_1.vi.mocked(fsDefault.createReadStream).mockReturnValue({});
155
+ });
156
+ (0, vitest_1.afterEach)(() => {
157
+ vitest_1.vi.clearAllMocks();
158
+ mockFetch.mockReset();
159
+ });
160
+ (0, vitest_1.describe)("Constructor & Setup", () => {
161
+ (0, vitest_1.test)("validates required fields (projectArn, deviceArn, platform)", () => {
162
+ // Missing projectArn
163
+ (0, vitest_1.expect)(() => {
164
+ const project = {
165
+ name: "test-project",
166
+ use: {
167
+ device: {
168
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
169
+ },
170
+ platform: "android",
171
+ },
172
+ };
173
+ new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
174
+ }).toThrow("AWS Device Farm: `projectArn` is required in the device configuration.");
175
+ // Missing deviceArn
176
+ (0, vitest_1.expect)(() => {
177
+ const project = {
178
+ name: "test-project",
179
+ use: {
180
+ device: {
181
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
182
+ },
183
+ platform: "android",
184
+ },
185
+ };
186
+ new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
187
+ }).toThrow("AWS Device Farm: `deviceArn` is required in the device configuration.");
188
+ // Missing platform
189
+ (0, vitest_1.expect)(() => {
190
+ const project = {
191
+ name: "test-project",
192
+ use: {
193
+ device: {
194
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
195
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
196
+ },
197
+ },
198
+ };
199
+ new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
200
+ }).toThrow("AWS Device Farm: `platform` must be specified in the project configuration.");
201
+ // Valid configuration
202
+ (0, vitest_1.expect)(() => {
203
+ const project = {
204
+ name: "test-project",
205
+ use: {
206
+ device: {
207
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
208
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
209
+ },
210
+ platform: "android",
211
+ },
212
+ };
213
+ new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
214
+ }).not.toThrow();
215
+ });
216
+ (0, vitest_1.test)("globalSetup uses existing appArn when provided", async () => {
217
+ const project = {
218
+ name: "test-project",
219
+ use: {
220
+ device: {
221
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
222
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
223
+ appArn: "arn:aws:devicefarm:upload:existing",
224
+ },
225
+ platform: "ios",
226
+ },
227
+ };
228
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
229
+ await provider.globalSetup();
230
+ // Should not create upload or upload file
231
+ (0, vitest_1.expect)(mockClient.send).not.toHaveBeenCalled();
232
+ (0, vitest_1.expect)(mockFetch).not.toHaveBeenCalled();
233
+ });
234
+ (0, vitest_1.test)("uses explicit appArn from config without globalSetup", async () => {
235
+ // This test simulates a runtime provider that doesn't call globalSetup
236
+ const project = {
237
+ name: "test-project",
238
+ use: {
239
+ device: {
240
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
241
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
242
+ appArn: "arn:aws:devicefarm:upload:explicit-arn",
243
+ },
244
+ platform: "ios",
245
+ },
246
+ };
247
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
248
+ // The provider should have uploadArn set from config even without globalSetup
249
+ (0, vitest_1.expect)(provider.uploadArn).toBe("arn:aws:devicefarm:upload:explicit-arn");
250
+ // Mock session creation to verify appArn is passed correctly
251
+ mockClient.send
252
+ .mockResolvedValueOnce({
253
+ remoteAccessSession: { arn: "arn:aws:devicefarm:session:123" },
254
+ })
255
+ .mockResolvedValueOnce({
256
+ remoteAccessSession: {
257
+ arn: "arn:aws:devicefarm:session:123",
258
+ status: "RUNNING",
259
+ endpoint: "wss://device.example.com",
260
+ device: { name: "iPhone 14", os: "16.0" },
261
+ },
262
+ });
263
+ // Create remote session without calling globalSetup
264
+ await provider.createRemoteSession();
265
+ // Verify that createRemoteSession used the appArn from config
266
+ const createSessionCall = mockClient.send.mock.calls[0][0];
267
+ (0, vitest_1.expect)(createSessionCall.input.appArn).toBe("arn:aws:devicefarm:upload:explicit-arn");
268
+ });
269
+ (0, vitest_1.test)("persists upload ARN across provider instances", async () => {
270
+ // Clean up any existing env var
271
+ delete process.env.AWS_DEVICE_FARM_APP_ARN_TEST_PROJECT;
272
+ // First provider instance (simulates globalSetup)
273
+ const project1 = {
274
+ name: "test-project",
275
+ use: {
276
+ device: {
277
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
278
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
279
+ },
280
+ platform: "android",
281
+ buildPath: "/path/to/app.apk",
282
+ },
283
+ };
284
+ const provider1 = new index_1.AWSDeviceFarmProvider(asProject(project1), "com.example.app");
285
+ // Simulate globalSetup storing the ARN
286
+ provider1.uploadArn =
287
+ "arn:aws:devicefarm:upload:123";
288
+ process.env.AWS_DEVICE_FARM_APP_ARN_TEST_PROJECT =
289
+ "arn:aws:devicefarm:upload:123";
290
+ // Second provider instance (simulates fixture creation)
291
+ // IMPORTANT: This has the same buildPath as the first instance
292
+ // This is the typical scenario - same config for all instances
293
+ const project2 = {
294
+ name: "test-project",
295
+ use: {
296
+ device: {
297
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
298
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
299
+ },
300
+ platform: "android",
301
+ buildPath: "/path/to/app.apk", // Same buildPath as first instance
302
+ },
303
+ };
304
+ const provider2 = new index_1.AWSDeviceFarmProvider(asProject(project2), "com.example.app");
305
+ // The constructor should have loaded the ARN from env var
306
+ // even though buildPath is provided
307
+ (0, vitest_1.expect)(provider2.uploadArn).toBe("arn:aws:devicefarm:upload:123");
308
+ // Mock session creation
309
+ mockClient.send
310
+ .mockResolvedValueOnce({
311
+ remoteAccessSession: { arn: "arn:aws:devicefarm:session:123" },
312
+ })
313
+ .mockResolvedValueOnce({
314
+ remoteAccessSession: {
315
+ arn: "arn:aws:devicefarm:session:123",
316
+ status: "RUNNING",
317
+ endpoint: "wss://device.example.com",
318
+ device: { name: "Pixel 6", os: "12" },
319
+ },
320
+ });
321
+ // Create remote session with second provider
322
+ await provider2.createRemoteSession();
323
+ // Verify that the second provider used the uploaded ARN from the first provider
324
+ (0, vitest_1.expect)(mockClient.send).toHaveBeenCalled();
325
+ // Check that the CreateRemoteAccessSessionCommand was called with the correct appArn
326
+ const createSessionCall = mockClient.send.mock.calls[0][0];
327
+ (0, vitest_1.expect)(createSessionCall.input).toBeDefined();
328
+ (0, vitest_1.expect)(createSessionCall.input.appArn).toBe("arn:aws:devicefarm:upload:123");
329
+ // Also verify the other expected properties
330
+ (0, vitest_1.expect)(createSessionCall.input.projectArn).toBe("arn:aws:devicefarm:us-west-2:123:project:456");
331
+ (0, vitest_1.expect)(createSessionCall.input.deviceArn).toBe("arn:aws:devicefarm:us-west-2::device:123");
332
+ // Clean up
333
+ delete process.env.AWS_DEVICE_FARM_APP_ARN_TEST_PROJECT;
334
+ });
335
+ });
336
+ (0, vitest_1.describe)("Session Management", () => {
337
+ (0, vitest_1.beforeEach)(() => {
338
+ const project = {
339
+ name: "test-project",
340
+ use: {
341
+ device: {
342
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
343
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
344
+ appArn: "arn:aws:devicefarm:upload:123",
345
+ },
346
+ platform: "android",
347
+ expectTimeout: 30000,
348
+ },
349
+ };
350
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
351
+ });
352
+ (0, vitest_1.test)("creates and starts remote session successfully", async () => {
353
+ const mockSession = {
354
+ arn: "arn:aws:devicefarm:session:123",
355
+ status: "RUNNING",
356
+ endpoint: "wss://device.example.com",
357
+ device: {
358
+ name: "Pixel 6",
359
+ os: "12",
360
+ },
361
+ };
362
+ // Mock session creation - returns ARN
363
+ mockClient.send.mockResolvedValueOnce({
364
+ remoteAccessSession: { arn: "arn:aws:devicefarm:session:123" },
365
+ });
366
+ // Mock session status check - returns RUNNING session
367
+ mockClient.send.mockResolvedValueOnce({
368
+ remoteAccessSession: mockSession,
369
+ });
370
+ const session = await provider.createRemoteSession();
371
+ (0, vitest_1.expect)(session).toEqual(mockSession);
372
+ (0, vitest_1.expect)(mockClient.send).toHaveBeenCalled();
373
+ const createSessionCall = mockClient.send.mock.calls[0][0];
374
+ (0, vitest_1.expect)(createSessionCall.input.projectArn).toBe("arn:aws:devicefarm:us-west-2:123:project:456");
375
+ (0, vitest_1.expect)(createSessionCall.input.deviceArn).toBe("arn:aws:devicefarm:us-west-2::device:123");
376
+ });
377
+ (0, vitest_1.test)("handles query parameters in endpoint correctly", async () => {
378
+ const project = {
379
+ name: "test-project",
380
+ use: {
381
+ device: {
382
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
383
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
384
+ appArn: "arn:aws:devicefarm:upload:123",
385
+ },
386
+ platform: "android",
387
+ expectTimeout: 30000,
388
+ },
389
+ };
390
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
391
+ // Mock session with query parameters in endpoint
392
+ mockClient.send
393
+ .mockResolvedValueOnce({
394
+ remoteAccessSession: { arn: "arn:aws:devicefarm:session:123" },
395
+ })
396
+ .mockResolvedValueOnce({
397
+ remoteAccessSession: {
398
+ arn: "arn:aws:devicefarm:session:123",
399
+ status: "RUNNING",
400
+ endpoint: "wss://device.example.com/wd/hub?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test&X-Amz-Date=20250101T000000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123",
401
+ device: { name: "Pixel 6", os: "12" },
402
+ },
403
+ });
404
+ // Mock WebDriver
405
+ const mockWebDriver = (await import("webdriver")).default;
406
+ const mockNewSession = vitest_1.vi.fn().mockResolvedValue({
407
+ sessionId: "mock-session-id",
408
+ });
409
+ vitest_1.vi.mocked(mockWebDriver.newSession).mockImplementation(mockNewSession);
410
+ await provider.getDevice();
411
+ // Verify WebDriver was called with correct connection options
412
+ (0, vitest_1.expect)(mockNewSession).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
413
+ protocol: "https",
414
+ hostname: "device.example.com",
415
+ port: 443,
416
+ path: "/wd/hub", // Path should NOT include query parameters
417
+ queryParams: {
418
+ "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
419
+ "X-Amz-Credential": "test",
420
+ "X-Amz-Date": "20250101T000000Z",
421
+ "X-Amz-Expires": "3600",
422
+ "X-Amz-SignedHeaders": "host",
423
+ "X-Amz-Signature": "abc123",
424
+ },
425
+ }));
426
+ });
427
+ (0, vitest_1.test)("builds correct capabilities for Android/iOS", async () => {
428
+ // Android
429
+ const androidProject = {
430
+ name: "test-project",
431
+ use: {
432
+ device: {
433
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
434
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
435
+ appArn: "arn:aws:devicefarm:upload:123",
436
+ },
437
+ platform: "android",
438
+ },
439
+ };
440
+ provider = new index_1.AWSDeviceFarmProvider(asProject(androidProject), "com.example.app");
441
+ const androidCaps = provider.buildCapabilities({
442
+ device: { name: "Pixel 6", os: "12" },
443
+ });
444
+ (0, vitest_1.expect)(androidCaps.platformName).toBe("Android");
445
+ (0, vitest_1.expect)(androidCaps["appium:automationName"]).toBe("UiAutomator2");
446
+ (0, vitest_1.expect)(androidCaps["appium:deviceName"]).toBe("Pixel 6");
447
+ (0, vitest_1.expect)(androidCaps["appium:platformVersion"]).toBe("12");
448
+ (0, vitest_1.expect)(androidCaps["appium:bundleId"]).toBeUndefined();
449
+ // Should include app capability with the upload ARN
450
+ (0, vitest_1.expect)(androidCaps["appium:app"]).toBe("arn:aws:devicefarm:upload:123");
451
+ // iOS
452
+ const iosProject = {
453
+ name: "test-project",
454
+ use: {
455
+ device: {
456
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
457
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
458
+ appArn: "arn:aws:devicefarm:upload:123",
459
+ },
460
+ platform: "ios",
461
+ },
462
+ };
463
+ provider = new index_1.AWSDeviceFarmProvider(asProject(iosProject), "com.example.app");
464
+ const iosCaps = provider.buildCapabilities({
465
+ device: { name: "iPhone 14", os: "16.0" },
466
+ });
467
+ (0, vitest_1.expect)(iosCaps.platformName).toBe("iOS");
468
+ (0, vitest_1.expect)(iosCaps["appium:automationName"]).toBe("XCUITest");
469
+ (0, vitest_1.expect)(iosCaps["appium:deviceName"]).toBe("iPhone 14");
470
+ (0, vitest_1.expect)(iosCaps["appium:platformVersion"]).toBe("16.0");
471
+ (0, vitest_1.expect)(iosCaps["appium:bundleId"]).toBe("com.example.app");
472
+ // Should include app capability with the upload ARN
473
+ (0, vitest_1.expect)(iosCaps["appium:app"]).toBe("arn:aws:devicefarm:upload:123");
474
+ });
475
+ (0, vitest_1.test)("includes app capability for Android without appPackage/appActivity", async () => {
476
+ // This tests the main bug scenario - user provides buildPath only
477
+ const project = {
478
+ name: "test-project",
479
+ use: {
480
+ device: {
481
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
482
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
483
+ appArn: "arn:aws:devicefarm:upload:android-app",
484
+ // No appPackage or appActivity provided
485
+ },
486
+ platform: "android",
487
+ },
488
+ };
489
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), undefined);
490
+ const caps = provider.buildCapabilities({
491
+ device: { name: "Pixel 6", os: "12" },
492
+ });
493
+ // Must include app capability for Appium to work
494
+ (0, vitest_1.expect)(caps["appium:app"]).toBe("arn:aws:devicefarm:upload:android-app");
495
+ (0, vitest_1.expect)(caps["appium:appPackage"]).toBeUndefined();
496
+ (0, vitest_1.expect)(caps["appium:appActivity"]).toBeUndefined();
497
+ });
498
+ (0, vitest_1.test)("stops session on cleanup", async () => {
499
+ provider.remoteSessionArn =
500
+ "arn:aws:devicefarm:session:123";
501
+ mockClient.send.mockResolvedValueOnce({});
502
+ await provider.stopRemoteSession();
503
+ (0, vitest_1.expect)(mockClient.send).toHaveBeenCalled();
504
+ const stopSessionCall = mockClient.send.mock.calls[0][0];
505
+ (0, vitest_1.expect)(stopSessionCall.input.arn).toBe("arn:aws:devicefarm:session:123");
506
+ });
507
+ (0, vitest_1.test)("stops remote session when WebDriver.newSession fails", async () => {
508
+ // Mock successful remote session creation
509
+ mockClient.send
510
+ .mockResolvedValueOnce({
511
+ remoteAccessSession: { arn: "arn:aws:devicefarm:session:123" },
512
+ })
513
+ .mockResolvedValueOnce({
514
+ remoteAccessSession: {
515
+ arn: "arn:aws:devicefarm:session:123",
516
+ status: "RUNNING",
517
+ endpoint: "wss://device.example.com",
518
+ device: { name: "Pixel 6", os: "12" },
519
+ },
520
+ });
521
+ // Mock WebDriver.newSession to fail
522
+ const mockWebDriver = (await import("webdriver")).default;
523
+ vitest_1.vi.mocked(mockWebDriver.newSession).mockRejectedValueOnce(new Error("Invalid capabilities: bad automation name"));
524
+ // Mock stopRemoteSession to verify it's called
525
+ mockClient.send.mockResolvedValueOnce({}); // for StopRemoteAccessSessionCommand
526
+ // Attempt to get device should throw
527
+ await (0, vitest_1.expect)(provider.getDevice()).rejects.toThrow("Invalid capabilities: bad automation name");
528
+ // Verify that stopRemoteSession was called to clean up
529
+ (0, vitest_1.expect)(mockClient.send).toHaveBeenCalledTimes(3); // create, get, stop
530
+ const stopSessionCall = mockClient.send.mock.calls[2][0];
531
+ (0, vitest_1.expect)(stopSessionCall.input.arn).toBe("arn:aws:devicefarm:session:123");
532
+ });
533
+ });
534
+ (0, vitest_1.describe)("Error Handling", () => {
535
+ (0, vitest_1.test)("throws error when both buildPath and appArn missing", async () => {
536
+ const project = {
537
+ name: "test-project",
538
+ use: {
539
+ device: {
540
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
541
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
542
+ },
543
+ platform: "android",
544
+ },
545
+ };
546
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
547
+ await (0, vitest_1.expect)(provider.globalSetup()).rejects.toThrow("AWS Device Farm: Either provide `buildPath` or `appArn` in the configuration.");
548
+ });
549
+ (0, vitest_1.test)("handles session creation failure gracefully", async () => {
550
+ const project = {
551
+ name: "test-project",
552
+ use: {
553
+ device: {
554
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
555
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
556
+ appArn: "arn:aws:devicefarm:upload:123",
557
+ },
558
+ platform: "android",
559
+ },
560
+ };
561
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
562
+ // Mock session creation returns empty response (no ARN)
563
+ mockClient.send.mockResolvedValueOnce({
564
+ remoteAccessSession: {},
565
+ });
566
+ await (0, vitest_1.expect)(provider.createRemoteSession()).rejects.toThrow("AWS Device Farm: Remote access session ARN was not returned.");
567
+ });
568
+ (0, vitest_1.test)("handles upload failure with proper error message", async () => {
569
+ const project = {
570
+ name: "test-project",
571
+ use: {
572
+ device: {
573
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
574
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:123",
575
+ },
576
+ platform: "android",
577
+ buildPath: "/path/to/app.apk",
578
+ },
579
+ };
580
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
581
+ // Mock upload creation failure
582
+ mockClient.send.mockRejectedValueOnce(new Error("Upload creation failed"));
583
+ await (0, vitest_1.expect)(provider.globalSetup()).rejects.toThrow("Upload creation failed");
584
+ });
585
+ });
586
+ (0, vitest_1.describe)("Video Download", () => {
587
+ (0, vitest_1.test)("downloadVideo handles node-fetch v3 stream correctly", async () => {
588
+ const sessionArn = "arn:aws:devicefarm:us-west-2:123:session:456";
589
+ const outputDir = "/tmp/test-output";
590
+ const fileName = "test-video";
591
+ // Mock fs operations
592
+ const fsDefault = fs.default || fs;
593
+ vitest_1.vi.mocked(fsDefault.mkdirSync).mockReturnValue(undefined);
594
+ vitest_1.vi.mocked(fsDefault.existsSync).mockReturnValue(false);
595
+ // Create a mock writable stream
596
+ const mockWriteStream = {};
597
+ vitest_1.vi.mocked(fsDefault.createWriteStream).mockReturnValue(mockWriteStream);
598
+ vitest_1.vi.mocked(fsDefault.renameSync).mockReturnValue(undefined);
599
+ // Mock client to return video artifact
600
+ mockClient.send.mockResolvedValueOnce({
601
+ artifacts: [
602
+ {
603
+ type: "VIDEO",
604
+ url: "https://presigned-url.example.com/video.mp4",
605
+ },
606
+ ],
607
+ });
608
+ // Mock fetch to return a Node.js Readable stream (node-fetch v3 behavior)
609
+ const { Readable } = await import("stream");
610
+ const mockVideoData = Buffer.from("mock-video-content");
611
+ const mockStream = Readable.from([mockVideoData]);
612
+ mockFetch.mockResolvedValueOnce({
613
+ ok: true,
614
+ body: mockStream, // This is a Node.js Readable, not a Web Stream with getReader()
615
+ });
616
+ // Mock the pipeline function
617
+ const { pipeline } = await import("stream/promises");
618
+ vitest_1.vi.mocked(pipeline).mockResolvedValueOnce(undefined);
619
+ // Call downloadVideo
620
+ const result = await index_1.AWSDeviceFarmProvider.downloadVideo(sessionArn, outputDir, fileName);
621
+ // Verify the video was downloaded successfully
622
+ (0, vitest_1.expect)(result).toEqual({
623
+ path: "/tmp/test-output/test-video.mp4",
624
+ contentType: "video/mp4",
625
+ });
626
+ // Verify fetch was called with the presigned URL
627
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith("https://presigned-url.example.com/video.mp4", vitest_1.expect.objectContaining({ method: "GET" }));
628
+ // Verify the stream was written to the file using pipeline
629
+ (0, vitest_1.expect)(fsDefault.createWriteStream).toHaveBeenCalledWith("/tmp/test-output/test-video.mp4.part");
630
+ // Verify pipeline was called with the stream and write stream
631
+ (0, vitest_1.expect)(pipeline).toHaveBeenCalledWith(mockStream, mockWriteStream);
632
+ });
633
+ (0, vitest_1.test)("downloadVideo returns null when video artifact is not found", async () => {
634
+ const sessionArn = "arn:aws:devicefarm:us-west-2:123:session:456";
635
+ const outputDir = "/tmp/test-output";
636
+ const fileName = "test-video";
637
+ // Mock fs operations
638
+ const fsDefault = fs.default || fs;
639
+ vitest_1.vi.mocked(fsDefault.mkdirSync).mockReturnValue(undefined);
640
+ // Mock client to return no video artifacts
641
+ mockClient.send.mockResolvedValueOnce({
642
+ artifacts: [],
643
+ });
644
+ // Mock async-retry to throw error immediately instead of retrying
645
+ const retryModule = await import("async-retry");
646
+ vitest_1.vi.mocked(retryModule.default).mockImplementationOnce(((fn) => fn(() => { }, 1)));
647
+ // Mock logger to suppress error logs during test
648
+ const originalError = logger_1.logger.error;
649
+ logger_1.logger.error = vitest_1.vi.fn();
650
+ // Call downloadVideo
651
+ const result = await index_1.AWSDeviceFarmProvider.downloadVideo(sessionArn, outputDir, fileName);
652
+ // Restore logger.error
653
+ logger_1.logger.error = originalError;
654
+ // Should return null when no video found
655
+ (0, vitest_1.expect)(result).toBeNull();
656
+ });
657
+ });
658
+ (0, vitest_1.describe)("IAM Role Assumption", () => {
659
+ let mockStsClient;
660
+ let STSClientMock;
661
+ (0, vitest_1.beforeEach)(async () => {
662
+ // Get the mocked STS client
663
+ const stsModule = await import("@aws-sdk/client-sts");
664
+ STSClientMock = stsModule.STSClient;
665
+ mockStsClient = {
666
+ send: vitest_1.vi.fn(),
667
+ destroy: vitest_1.vi.fn(),
668
+ };
669
+ vitest_1.vi.mocked(STSClientMock).mockImplementation(() => mockStsClient);
670
+ });
671
+ (0, vitest_1.test)("assumes IAM role when roleArn is provided", async () => {
672
+ // Setup STS mock to return credentials
673
+ mockStsClient.send.mockResolvedValueOnce({
674
+ Credentials: {
675
+ AccessKeyId: "AKIAIOSFODNN7EXAMPLE",
676
+ SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
677
+ SessionToken: "FwoGZXIvYXdzEBY...",
678
+ },
679
+ });
680
+ const project = {
681
+ name: "test-project",
682
+ use: {
683
+ device: {
684
+ provider: "aws-device-farm",
685
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
686
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:789",
687
+ roleArn: "arn:aws:iam::123456789012:role/DeviceFarmAccess",
688
+ roleSessionName: "test-session",
689
+ externalId: "test-external-id",
690
+ },
691
+ platform: "android",
692
+ buildPath: "/path/to/app.apk",
693
+ appBundleId: "com.example.app",
694
+ },
695
+ };
696
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
697
+ // Mock upload flow
698
+ mockClient.send
699
+ .mockResolvedValueOnce({
700
+ upload: {
701
+ arn: "arn:aws:devicefarm:upload:123",
702
+ url: "https://upload-url.example.com",
703
+ },
704
+ })
705
+ .mockResolvedValueOnce({
706
+ upload: { status: "SUCCEEDED" },
707
+ });
708
+ mockFetch.mockResolvedValueOnce({ ok: true });
709
+ await provider.globalSetup();
710
+ // Verify STS AssumeRole was called
711
+ (0, vitest_1.expect)(mockStsClient.send).toHaveBeenCalledTimes(1);
712
+ (0, vitest_1.expect)(mockStsClient.destroy).toHaveBeenCalled();
713
+ // Verify DeviceFarmClient was recreated (called twice - once in constructor, once after assume role)
714
+ (0, vitest_1.expect)(client_device_farm_1.DeviceFarmClient).toHaveBeenCalledTimes(2);
715
+ (0, vitest_1.expect)(client_device_farm_1.DeviceFarmClient).toHaveBeenLastCalledWith({
716
+ region: "us-west-2",
717
+ credentials: {
718
+ accessKeyId: "AKIAIOSFODNN7EXAMPLE",
719
+ secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
720
+ sessionToken: "FwoGZXIvYXdzEBY...",
721
+ },
722
+ });
723
+ });
724
+ (0, vitest_1.test)("uses default session name when roleSessionName not provided", async () => {
725
+ mockStsClient.send.mockResolvedValueOnce({
726
+ Credentials: {
727
+ AccessKeyId: "AKIAIOSFODNN7EXAMPLE",
728
+ SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
729
+ SessionToken: "FwoGZXIvYXdzEBY...",
730
+ },
731
+ });
732
+ const project = {
733
+ name: "test-project",
734
+ use: {
735
+ device: {
736
+ provider: "aws-device-farm",
737
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
738
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:789",
739
+ roleArn: "arn:aws:iam::123456789012:role/DeviceFarmAccess",
740
+ appArn: "arn:aws:devicefarm:upload:existing",
741
+ // No roleSessionName provided
742
+ },
743
+ platform: "android",
744
+ },
745
+ };
746
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
747
+ await provider.globalSetup();
748
+ // Verify AssumeRoleCommand was called with default session name
749
+ const { AssumeRoleCommand } = await import("@aws-sdk/client-sts");
750
+ (0, vitest_1.expect)(AssumeRoleCommand).toHaveBeenCalledWith({
751
+ RoleArn: "arn:aws:iam::123456789012:role/DeviceFarmAccess",
752
+ RoleSessionName: "appwright-device-farm",
753
+ ExternalId: undefined,
754
+ });
755
+ });
756
+ (0, vitest_1.test)("throws error when AssumeRole returns incomplete credentials", async () => {
757
+ mockStsClient.send.mockResolvedValueOnce({
758
+ Credentials: {
759
+ AccessKeyId: "AKIAIOSFODNN7EXAMPLE",
760
+ // Missing SecretAccessKey and SessionToken
761
+ },
762
+ });
763
+ const project = {
764
+ name: "test-project",
765
+ use: {
766
+ device: {
767
+ provider: "aws-device-farm",
768
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
769
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:789",
770
+ roleArn: "arn:aws:iam::123456789012:role/DeviceFarmAccess",
771
+ appArn: "arn:aws:devicefarm:upload:existing",
772
+ },
773
+ platform: "android",
774
+ },
775
+ };
776
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
777
+ await (0, vitest_1.expect)(provider.globalSetup()).rejects.toThrow("AWS Device Farm: AssumeRole returned incomplete credentials.");
778
+ });
779
+ (0, vitest_1.test)("does not assume role when roleArn is not provided", async () => {
780
+ const project = {
781
+ name: "test-project",
782
+ use: {
783
+ device: {
784
+ provider: "aws-device-farm",
785
+ projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
786
+ deviceArn: "arn:aws:devicefarm:us-west-2::device:789",
787
+ appArn: "arn:aws:devicefarm:upload:existing",
788
+ // No roleArn
789
+ },
790
+ platform: "android",
791
+ },
792
+ };
793
+ provider = new index_1.AWSDeviceFarmProvider(asProject(project), "com.example.app");
794
+ await provider.globalSetup();
795
+ // Verify STS was never called
796
+ (0, vitest_1.expect)(mockStsClient.send).not.toHaveBeenCalled();
797
+ // Verify DeviceFarmClient was only created once (in constructor)
798
+ (0, vitest_1.expect)(client_device_farm_1.DeviceFarmClient).toHaveBeenCalledTimes(1);
799
+ });
800
+ });
801
+ });