@samsara-dev/appwright 0.7.2 → 0.7.4
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/.prettierrc +4 -0
- package/CHANGELOG.md +49 -4
- package/README.md +28 -1
- package/dist/device/index.d.ts +2 -1
- package/dist/device/index.d.ts.map +1 -1
- package/dist/device/index.js +12 -1
- package/dist/providers/awsDeviceFarm/index.d.ts +35 -0
- package/dist/providers/awsDeviceFarm/index.d.ts.map +1 -0
- package/dist/providers/awsDeviceFarm/index.js +445 -0
- package/dist/providers/awsDeviceFarm/index.spec.d.ts +2 -0
- package/dist/providers/awsDeviceFarm/index.spec.d.ts.map +1 -0
- package/dist/providers/awsDeviceFarm/index.spec.js +801 -0
- package/dist/providers/browserstack/index.d.ts.map +1 -1
- package/dist/providers/browserstack/index.js +8 -5
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -0
- package/dist/providers/lambdatest/index.d.ts.map +1 -1
- package/dist/providers/lambdatest/index.js +8 -5
- package/dist/types/index.d.ts +71 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +12 -9
|
@@ -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
|
+
});
|