@samsara-dev/appwright 0.7.1 → 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.
- 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 +443 -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/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +8 -6
- package/dist/tests/reporter.spec.d.ts +2 -0
- package/dist/tests/reporter.spec.d.ts.map +1 -0
- package/dist/tests/reporter.spec.js +167 -0
- package/dist/types/index.d.ts +71 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# appwright
|
|
2
2
|
|
|
3
|
+
## 0.7.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 92deea8: Add AWS Device Farm provider support for mobile testing
|
|
8
|
+
|
|
9
|
+
This release introduces comprehensive AWS Device Farm integration for mobile app testing, enabling teams to run tests on real devices in AWS's device cloud.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
- **AWS Device Farm Provider**: Complete implementation with support for both Android and iOS testing
|
|
13
|
+
- **Automated App Upload**: Automatically uploads APK/IPA files to Device Farm during test setup
|
|
14
|
+
- **ARN Persistence**: Intelligently reuses uploaded builds across test runs to minimize upload time
|
|
15
|
+
- **Remote Session Management**: Creates and manages Device Farm remote access sessions
|
|
16
|
+
- **Video Recording**: Automatic video capture of test sessions with download capability
|
|
17
|
+
- **Flexible Configuration**: Support for custom capabilities, interaction modes, and session settings
|
|
18
|
+
|
|
19
|
+
### Technical Details
|
|
20
|
+
- Added comprehensive test suite with 15 tests covering all functionality
|
|
21
|
+
- Implemented proper async cleanup callbacks for resource management
|
|
22
|
+
- Enhanced error handling with descriptive error messages
|
|
23
|
+
- Added support for AWS region configuration
|
|
24
|
+
- Improved WebDriver connection handling with query parameter support
|
|
25
|
+
|
|
26
|
+
### Configuration Example
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
use: {
|
|
31
|
+
platform: Platform.ANDROID,
|
|
32
|
+
buildPath: "./app.apk",
|
|
33
|
+
device: {
|
|
34
|
+
provider: "aws-device-farm",
|
|
35
|
+
projectArn: "arn:aws:devicefarm:us-west-2:123:project:456",
|
|
36
|
+
deviceArn: "arn:aws:devicefarm:us-west-2::device:789",
|
|
37
|
+
region: "us-west-2",
|
|
38
|
+
sessionName: "My Test Session",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This provider enables teams to leverage AWS Device Farm's extensive device library for comprehensive mobile testing without maintaining physical device labs.
|
|
45
|
+
|
|
46
|
+
## 0.7.2
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- ecdfc42: Avoid video filename collisions in the Playwright reporter by including the provider session ID in downloaded video filenames.
|
|
51
|
+
|
|
3
52
|
## 0.7.1
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
|
@@ -7,18 +56,15 @@
|
|
|
7
56
|
- 75adf30: Add CI metadata support for BrowserStack build traceability
|
|
8
57
|
|
|
9
58
|
BrowserStack sessions now automatically include CI context in build names and session names:
|
|
10
|
-
|
|
11
59
|
- **Buildkite**: Build number, branch, commit from `BUILDKITE_*` env vars
|
|
12
60
|
- **GitHub Actions**: Run number, ref name, SHA from `GITHUB_*` env vars
|
|
13
61
|
- **GitLab CI**: Pipeline ID, ref name, commit from `CI_*` env vars
|
|
14
62
|
|
|
15
63
|
Example build names:
|
|
16
|
-
|
|
17
64
|
- CI: `driver-performance-tests android #35 (main)`
|
|
18
65
|
- Local: `driver-performance-tests android`
|
|
19
66
|
|
|
20
67
|
Environment variable overrides:
|
|
21
|
-
|
|
22
68
|
- `BROWSERSTACK_BUILD_NAME`: Override auto-generated build name
|
|
23
69
|
- `BROWSERSTACK_SESSION_NAME`: Override auto-generated session name
|
|
24
70
|
|
|
@@ -107,7 +153,6 @@
|
|
|
107
153
|
### Minor Changes
|
|
108
154
|
|
|
109
155
|
- 9415229: feat: add Visual Trace Service for automatic screenshot capture during test execution
|
|
110
|
-
|
|
111
156
|
- Implements automatic screenshot capture with smart defaults (only captures for failed tests)
|
|
112
157
|
- Adds screenshot deduplication using SHA-256 hashing
|
|
113
158
|
- Supports configurable screenshot limits (default: 50)
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ export default defineConfig({
|
|
|
77
77
|
- `platform`: The platform you want to test on, such as 'android' or 'ios'.
|
|
78
78
|
|
|
79
79
|
- `provider`: The device provider where you want to run your tests.
|
|
80
|
-
You can choose between `browserstack`, `lambdatest`, `emulator`, or `
|
|
80
|
+
You can choose between `browserstack`, `lambdatest`, `emulator`, `local-device`, or `aws-device-farm`.
|
|
81
81
|
|
|
82
82
|
- `buildPath`: The path to your build file. For Android, it should be an APK file.
|
|
83
83
|
For iOS, if you are running tests on real device, it should be an `.ipa` file. For running tests on an emulator, it should be a `.app` file.
|
|
@@ -170,6 +170,33 @@ the provider in your config.
|
|
|
170
170
|
},
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
#### Run tests on AWS Device Farm
|
|
174
|
+
|
|
175
|
+
Appwright can connect to AWS Device Farm remote access sessions. Configure the provider in your config and supply either a Device Farm `appArn` or a local `buildPath` that Appwright can upload during global setup.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
{
|
|
179
|
+
name: "ios",
|
|
180
|
+
use: {
|
|
181
|
+
platform: Platform.IOS,
|
|
182
|
+
appBundleId: "com.example.myapp",
|
|
183
|
+
buildPath: "./builds/MyApp.ipa",
|
|
184
|
+
device: {
|
|
185
|
+
provider: "aws-device-farm",
|
|
186
|
+
projectArn: "arn:aws:devicefarm:us-west-2:123456789012:project:abc123",
|
|
187
|
+
deviceArn:
|
|
188
|
+
"arn:aws:devicefarm:us-west-2::device:Apple:iPhone.15.Pro:17.5",
|
|
189
|
+
interactionMode: "VIDEO_ONLY",
|
|
190
|
+
sessionName: "smoke-suite",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and (optionally) `AWS_SESSION_TOKEN` & `AWS_REGION` environment variables before running the tests.
|
|
197
|
+
|
|
198
|
+
Appwright always requests Device Farm video recordings and attaches the MP4 to Playwright reports once the session completes.
|
|
199
|
+
|
|
173
200
|
## Run the sample project
|
|
174
201
|
|
|
175
202
|
To run the sample project:
|
package/dist/device/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export declare class Device {
|
|
|
15
15
|
private deviceProvider?;
|
|
16
16
|
private persistentSyncEnabled;
|
|
17
17
|
private activePersistentKey?;
|
|
18
|
-
|
|
18
|
+
private cleanupCallback?;
|
|
19
|
+
constructor(webDriverClient: WebDriverClient, bundleId: string | undefined, timeoutOpts: TimeoutOptions, provider: string, deviceProvider?: DeviceProvider, cleanupCallback?: () => Promise<void>);
|
|
19
20
|
attachDeviceProvider(provider: DeviceProvider): void;
|
|
20
21
|
enablePersistentStatusSync(): void;
|
|
21
22
|
ensurePersistentLifecycle(testInfo: TestInfo): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/device/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,cAAc,EACd,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAKlB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAO7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,KAAK,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,qBAAa,MAAM;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/device/index.ts"],"names":[],"mappings":"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"}
|
package/dist/device/index.js
CHANGED
|
@@ -103,12 +103,14 @@ let Device = (() => {
|
|
|
103
103
|
deviceProvider;
|
|
104
104
|
persistentSyncEnabled = false;
|
|
105
105
|
activePersistentKey;
|
|
106
|
-
|
|
106
|
+
cleanupCallback;
|
|
107
|
+
constructor(webDriverClient, bundleId, timeoutOpts, provider, deviceProvider, cleanupCallback) {
|
|
107
108
|
this.webDriverClient = webDriverClient;
|
|
108
109
|
this.bundleId = bundleId;
|
|
109
110
|
this.timeoutOpts = timeoutOpts;
|
|
110
111
|
this.provider = provider;
|
|
111
112
|
this.deviceProvider = deviceProvider;
|
|
113
|
+
this.cleanupCallback = cleanupCallback;
|
|
112
114
|
}
|
|
113
115
|
attachDeviceProvider(provider) {
|
|
114
116
|
this.deviceProvider = provider;
|
|
@@ -240,6 +242,15 @@ let Device = (() => {
|
|
|
240
242
|
(0, visualTrace_1.clearVisualTraceService)();
|
|
241
243
|
this.visualTraceService = undefined;
|
|
242
244
|
}
|
|
245
|
+
// Call the cleanup callback if provided
|
|
246
|
+
if (this.cleanupCallback) {
|
|
247
|
+
try {
|
|
248
|
+
await this.cleanupCallback();
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
logger_1.logger.error(`cleanup callback error:`, e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
243
254
|
}
|
|
244
255
|
/**
|
|
245
256
|
* Tap on the screen at the given coordinates, specified as x and y. The top left corner
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FullProject } from "@playwright/test";
|
|
2
|
+
import { AppwrightConfig, DeviceProvider } from "../../types";
|
|
3
|
+
import { Device } from "../../device";
|
|
4
|
+
export declare class AWSDeviceFarmProvider implements DeviceProvider {
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
private readonly project;
|
|
7
|
+
private readonly appBundleId;
|
|
8
|
+
private readonly deviceConfig;
|
|
9
|
+
private readonly platform;
|
|
10
|
+
private readonly region;
|
|
11
|
+
private client;
|
|
12
|
+
private uploadArn?;
|
|
13
|
+
private remoteSessionArn?;
|
|
14
|
+
constructor(project: FullProject<AppwrightConfig>, appBundleId: string | undefined);
|
|
15
|
+
globalSetup(): Promise<void>;
|
|
16
|
+
getDevice(): Promise<Device>;
|
|
17
|
+
syncTestDetails(): Promise<void>;
|
|
18
|
+
private validateBuildFile;
|
|
19
|
+
private uploadApplication;
|
|
20
|
+
private waitForUploadProcessing;
|
|
21
|
+
private createRemoteSession;
|
|
22
|
+
private waitForRemoteSession;
|
|
23
|
+
private normalizeEndpoint;
|
|
24
|
+
private buildCapabilities;
|
|
25
|
+
private assumeRole;
|
|
26
|
+
static downloadVideo(sessionArn: string, outputDir: string, fileName: string): Promise<{
|
|
27
|
+
path: string;
|
|
28
|
+
contentType: string;
|
|
29
|
+
} | null>;
|
|
30
|
+
private static findVideoArtifact;
|
|
31
|
+
private static downloadFromPresignedUrl;
|
|
32
|
+
private static extractRegionFromArn;
|
|
33
|
+
private stopRemoteSession;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/awsDeviceFarm/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA+BtC,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAC,CAAS;gBAGhC,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS;IAwD3B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B5B,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IA4D5B,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtC,OAAO,CAAC,iBAAiB;YASX,iBAAiB;YA6CjB,uBAAuB;YAmCvB,mBAAmB;YAgCnB,oBAAoB;IA0ClC,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,iBAAiB;YA8CX,UAAU;WA4CX,aAAa,CACxB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;mBA2EnC,iBAAiB;mBA6BjB,wBAAwB;IAwB7C,OAAO,CAAC,MAAM,CAAC,oBAAoB;YAQrB,iBAAiB;CAgBhC"}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AWSDeviceFarmProvider = void 0;
|
|
7
|
+
const async_retry_1 = __importDefault(require("async-retry"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const types_1 = require("../../types");
|
|
11
|
+
const device_1 = require("../../device");
|
|
12
|
+
const logger_1 = require("../../logger");
|
|
13
|
+
const client_device_farm_1 = require("@aws-sdk/client-device-farm");
|
|
14
|
+
const client_sts_1 = require("@aws-sdk/client-sts");
|
|
15
|
+
const utils_1 = require("../../utils");
|
|
16
|
+
const DEFAULT_REGION = "us-west-2";
|
|
17
|
+
const envVarKeyForUpload = (projectName) => `AWS_DEVICE_FARM_APP_ARN_${projectName.toUpperCase().replace(/-/g, "_")}`;
|
|
18
|
+
class AWSDeviceFarmProvider {
|
|
19
|
+
sessionId;
|
|
20
|
+
project;
|
|
21
|
+
appBundleId;
|
|
22
|
+
deviceConfig;
|
|
23
|
+
platform;
|
|
24
|
+
region;
|
|
25
|
+
client;
|
|
26
|
+
uploadArn;
|
|
27
|
+
remoteSessionArn;
|
|
28
|
+
constructor(project, appBundleId) {
|
|
29
|
+
this.project = project;
|
|
30
|
+
this.appBundleId = appBundleId;
|
|
31
|
+
this.deviceConfig = project.use.device;
|
|
32
|
+
if (!project.use.platform) {
|
|
33
|
+
throw new Error("AWS Device Farm: `platform` must be specified in the project configuration.");
|
|
34
|
+
}
|
|
35
|
+
this.platform = project.use.platform;
|
|
36
|
+
if (!this.deviceConfig.projectArn) {
|
|
37
|
+
throw new Error("AWS Device Farm: `projectArn` is required in the device configuration.");
|
|
38
|
+
}
|
|
39
|
+
if (!this.deviceConfig.deviceArn) {
|
|
40
|
+
throw new Error("AWS Device Farm: `deviceArn` is required in the device configuration.");
|
|
41
|
+
}
|
|
42
|
+
if (!this.deviceConfig.region && !process.env.AWS_REGION) {
|
|
43
|
+
logger_1.logger.warn("AWS Device Farm: region not specified. Falling back to us-west-2.");
|
|
44
|
+
}
|
|
45
|
+
if (this.platform === types_1.Platform.IOS && !this.appBundleId) {
|
|
46
|
+
throw new Error("AWS Device Farm: `appBundleId` is required for iOS projects.");
|
|
47
|
+
}
|
|
48
|
+
this.region =
|
|
49
|
+
this.deviceConfig.region ?? process.env.AWS_REGION ?? DEFAULT_REGION;
|
|
50
|
+
this.client = new client_device_farm_1.DeviceFarmClient({ region: this.region });
|
|
51
|
+
// Priority order for upload ARN:
|
|
52
|
+
// 1. Explicit appArn from device config (highest priority)
|
|
53
|
+
// 2. Persisted ARN from environment variable (for shared instances)
|
|
54
|
+
if (this.deviceConfig.appArn) {
|
|
55
|
+
// Use explicit appArn from config
|
|
56
|
+
this.uploadArn = this.deviceConfig.appArn;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Check for persisted upload ARN from a previous globalSetup
|
|
60
|
+
const envVarKey = envVarKeyForUpload(this.project.name);
|
|
61
|
+
const persistedArn = process.env[envVarKey];
|
|
62
|
+
if (persistedArn) {
|
|
63
|
+
this.uploadArn = persistedArn;
|
|
64
|
+
logger_1.logger.log(`AWS Device Farm: Using persisted app ARN from previous upload: ${persistedArn}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async globalSetup() {
|
|
69
|
+
// Handle IAM role assumption if configured
|
|
70
|
+
if (this.deviceConfig.roleArn) {
|
|
71
|
+
await this.assumeRole();
|
|
72
|
+
}
|
|
73
|
+
const buildPath = this.project.use.buildPath;
|
|
74
|
+
const providedAppArn = this.deviceConfig.appArn;
|
|
75
|
+
if (!buildPath && !providedAppArn) {
|
|
76
|
+
throw new Error("AWS Device Farm: Either provide `buildPath` or `appArn` in the configuration.");
|
|
77
|
+
}
|
|
78
|
+
if (buildPath) {
|
|
79
|
+
this.validateBuildFile(buildPath);
|
|
80
|
+
this.uploadArn = await this.uploadApplication(buildPath);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.uploadArn = providedAppArn;
|
|
84
|
+
}
|
|
85
|
+
// Persist the upload ARN for future provider instances
|
|
86
|
+
const envVarKey = envVarKeyForUpload(this.project.name);
|
|
87
|
+
process.env[envVarKey] = this.uploadArn;
|
|
88
|
+
logger_1.logger.log(`AWS Device Farm: Persisted app ARN for project "${this.project.name}": ${this.uploadArn}`);
|
|
89
|
+
}
|
|
90
|
+
async getDevice() {
|
|
91
|
+
const remoteSession = await this.createRemoteSession();
|
|
92
|
+
const endpointUrl = this.normalizeEndpoint(remoteSession);
|
|
93
|
+
const capabilities = this.buildCapabilities(remoteSession);
|
|
94
|
+
const WebDriver = (await import("webdriver")).default;
|
|
95
|
+
const connectionOptions = {
|
|
96
|
+
protocol: endpointUrl.protocol.replace(":", ""),
|
|
97
|
+
hostname: endpointUrl.hostname,
|
|
98
|
+
port: endpointUrl.port
|
|
99
|
+
? Number(endpointUrl.port)
|
|
100
|
+
: endpointUrl.protocol === "https:"
|
|
101
|
+
? 443
|
|
102
|
+
: 80,
|
|
103
|
+
path: endpointUrl.pathname,
|
|
104
|
+
};
|
|
105
|
+
// Add query parameters if present
|
|
106
|
+
if (endpointUrl.search) {
|
|
107
|
+
const queryParams = {};
|
|
108
|
+
const searchParams = new URLSearchParams(endpointUrl.search);
|
|
109
|
+
searchParams.forEach((value, key) => {
|
|
110
|
+
queryParams[key] = value;
|
|
111
|
+
});
|
|
112
|
+
connectionOptions.queryParams = queryParams;
|
|
113
|
+
}
|
|
114
|
+
let webDriverClient;
|
|
115
|
+
try {
|
|
116
|
+
webDriverClient = await WebDriver.newSession({
|
|
117
|
+
...connectionOptions,
|
|
118
|
+
capabilities: {
|
|
119
|
+
alwaysMatch: capabilities,
|
|
120
|
+
firstMatch: [{}],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Clean up the remote session if WebDriver connection fails
|
|
126
|
+
await this.stopRemoteSession();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
this.sessionId = this.remoteSessionArn ?? webDriverClient.sessionId;
|
|
130
|
+
const testOptions = {
|
|
131
|
+
expectTimeout: this.project.use.expectTimeout,
|
|
132
|
+
};
|
|
133
|
+
return new device_1.Device(webDriverClient, this.appBundleId, testOptions, this.project.use.device?.provider, this, async () => {
|
|
134
|
+
await this.stopRemoteSession();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async syncTestDetails() {
|
|
138
|
+
// AWS Device Farm does not currently expose an API to update session metadata like name or status.
|
|
139
|
+
// This is a no-op to satisfy the DeviceProvider interface.
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
validateBuildFile(buildPath) {
|
|
143
|
+
const expectedExtension = this.platform === types_1.Platform.ANDROID ? ".apk" : ".ipa";
|
|
144
|
+
(0, utils_1.validateBuildPath)(buildPath, expectedExtension);
|
|
145
|
+
if (!fs_1.default.existsSync(buildPath)) {
|
|
146
|
+
throw new Error(`AWS Device Farm: build file not found at ${buildPath}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async uploadApplication(buildPath) {
|
|
150
|
+
const uploadType = this.platform === types_1.Platform.ANDROID ? "ANDROID_APP" : "IOS_APP";
|
|
151
|
+
const fileName = path_1.default.basename(buildPath);
|
|
152
|
+
const createUploadResponse = await this.client.send(new client_device_farm_1.CreateUploadCommand({
|
|
153
|
+
name: fileName,
|
|
154
|
+
projectArn: this.deviceConfig.projectArn,
|
|
155
|
+
type: uploadType,
|
|
156
|
+
contentType: "application/octet-stream",
|
|
157
|
+
}));
|
|
158
|
+
const upload = createUploadResponse.upload;
|
|
159
|
+
if (!upload?.url || !upload?.arn) {
|
|
160
|
+
throw new Error("AWS Device Farm: Failed to create upload. Upload URL or ARN missing.");
|
|
161
|
+
}
|
|
162
|
+
const fetch = (await import("node-fetch")).default;
|
|
163
|
+
const fileStream = fs_1.default.createReadStream(buildPath);
|
|
164
|
+
const putResponse = await fetch(upload.url, {
|
|
165
|
+
method: "PUT",
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/octet-stream",
|
|
168
|
+
},
|
|
169
|
+
body: fileStream,
|
|
170
|
+
});
|
|
171
|
+
if (!putResponse.ok) {
|
|
172
|
+
throw new Error(`AWS Device Farm: Upload failed with status ${putResponse.status} ${putResponse.statusText}`);
|
|
173
|
+
}
|
|
174
|
+
await this.waitForUploadProcessing(upload.arn);
|
|
175
|
+
logger_1.logger.log(`AWS Device Farm: Uploaded ${fileName} (${upload.arn}) successfully.`);
|
|
176
|
+
return upload.arn;
|
|
177
|
+
}
|
|
178
|
+
async waitForUploadProcessing(uploadArn) {
|
|
179
|
+
await (0, async_retry_1.default)(async (bail) => {
|
|
180
|
+
const { upload } = await this.client.send(new client_device_farm_1.GetUploadCommand({ arn: uploadArn }));
|
|
181
|
+
if (!upload) {
|
|
182
|
+
return bail(new Error("AWS Device Farm: Unable to fetch upload status after upload."));
|
|
183
|
+
}
|
|
184
|
+
if (upload.status === client_device_farm_1.UploadStatus.FAILED) {
|
|
185
|
+
return bail(new Error(`AWS Device Farm: Upload processing failed. Reason: ${upload.message}`));
|
|
186
|
+
}
|
|
187
|
+
if (upload.status === client_device_farm_1.UploadStatus.SUCCEEDED) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`AWS Device Farm: Upload still processing (status: ${upload.status}).`);
|
|
191
|
+
}, {
|
|
192
|
+
retries: 20,
|
|
193
|
+
minTimeout: 5_000,
|
|
194
|
+
maxTimeout: 15_000,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async createRemoteSession() {
|
|
198
|
+
const createSessionResponse = await this.client.send(new client_device_farm_1.CreateRemoteAccessSessionCommand({
|
|
199
|
+
projectArn: this.deviceConfig.projectArn,
|
|
200
|
+
deviceArn: this.deviceConfig.deviceArn,
|
|
201
|
+
appArn: this.uploadArn,
|
|
202
|
+
interactionMode: this.deviceConfig.interactionMode ?? "VIDEO_ONLY",
|
|
203
|
+
name: this.deviceConfig.sessionName ?? `${this.project.name}-${Date.now()}`,
|
|
204
|
+
skipAppResign: this.deviceConfig.skipAppResign ?? false,
|
|
205
|
+
configuration: this.deviceConfig.configuration,
|
|
206
|
+
}));
|
|
207
|
+
const remoteSession = createSessionResponse.remoteAccessSession;
|
|
208
|
+
if (!remoteSession?.arn) {
|
|
209
|
+
throw new Error("AWS Device Farm: Remote access session ARN was not returned.");
|
|
210
|
+
}
|
|
211
|
+
this.remoteSessionArn = remoteSession.arn;
|
|
212
|
+
const session = await this.waitForRemoteSession(remoteSession.arn);
|
|
213
|
+
if (!session.endpoint) {
|
|
214
|
+
throw new Error("AWS Device Farm: Remote access session endpoint not available.");
|
|
215
|
+
}
|
|
216
|
+
return session;
|
|
217
|
+
}
|
|
218
|
+
async waitForRemoteSession(sessionArn) {
|
|
219
|
+
return await (0, async_retry_1.default)(async (bail) => {
|
|
220
|
+
const { remoteAccessSession } = await this.client.send(new client_device_farm_1.GetRemoteAccessSessionCommand({
|
|
221
|
+
arn: sessionArn,
|
|
222
|
+
}));
|
|
223
|
+
if (!remoteAccessSession) {
|
|
224
|
+
const error = new Error("AWS Device Farm: Remote access session not found.");
|
|
225
|
+
bail(error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (remoteAccessSession.status === "COMPLETED" ||
|
|
229
|
+
remoteAccessSession.status === "STOPPING") {
|
|
230
|
+
const error = new Error(`AWS Device Farm: Remote access session ended before it became RUNNING (result: ${remoteAccessSession.result ?? "unknown"}).`);
|
|
231
|
+
bail(error);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
if (remoteAccessSession.status !== "RUNNING") {
|
|
235
|
+
throw new Error(`AWS Device Farm: Waiting for remote session to be ready (status: ${remoteAccessSession.status}).`);
|
|
236
|
+
}
|
|
237
|
+
return remoteAccessSession;
|
|
238
|
+
}, {
|
|
239
|
+
retries: 30,
|
|
240
|
+
minTimeout: 10_000,
|
|
241
|
+
maxTimeout: 20_000,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
normalizeEndpoint(session) {
|
|
245
|
+
if (!session.endpoint) {
|
|
246
|
+
throw new Error("AWS Device Farm: Remote access session endpoint missing.");
|
|
247
|
+
}
|
|
248
|
+
const url = new URL(session.endpoint);
|
|
249
|
+
if (url.protocol === "wss:") {
|
|
250
|
+
url.protocol = "https:";
|
|
251
|
+
}
|
|
252
|
+
const sanitizedPath = url.pathname.endsWith("/")
|
|
253
|
+
? url.pathname.slice(0, -1)
|
|
254
|
+
: url.pathname;
|
|
255
|
+
if (!sanitizedPath.endsWith("/wd/hub")) {
|
|
256
|
+
url.pathname = `${sanitizedPath}/wd/hub`;
|
|
257
|
+
}
|
|
258
|
+
return url;
|
|
259
|
+
}
|
|
260
|
+
buildCapabilities(session) {
|
|
261
|
+
const isAndroid = this.platform === types_1.Platform.ANDROID;
|
|
262
|
+
const automationName = isAndroid ? "UiAutomator2" : "XCUITest";
|
|
263
|
+
const capabilities = {
|
|
264
|
+
platformName: isAndroid ? "Android" : "iOS",
|
|
265
|
+
"appium:automationName": automationName,
|
|
266
|
+
"appium:newCommandTimeout": 240,
|
|
267
|
+
"appium:noReset": false,
|
|
268
|
+
};
|
|
269
|
+
// CRITICAL: Include app capability for Appium to work
|
|
270
|
+
// This is required when appPackage/appActivity are not provided
|
|
271
|
+
if (this.uploadArn) {
|
|
272
|
+
capabilities["appium:app"] = this.uploadArn;
|
|
273
|
+
}
|
|
274
|
+
if (session.device?.name) {
|
|
275
|
+
capabilities["appium:deviceName"] = session.device.name;
|
|
276
|
+
}
|
|
277
|
+
if (session.device?.os) {
|
|
278
|
+
capabilities["appium:platformVersion"] = session.device.os;
|
|
279
|
+
}
|
|
280
|
+
if (this.appBundleId && !isAndroid) {
|
|
281
|
+
capabilities["appium:bundleId"] = this.appBundleId;
|
|
282
|
+
}
|
|
283
|
+
if (this.deviceConfig.appPackage) {
|
|
284
|
+
capabilities["appium:appPackage"] = this.deviceConfig.appPackage;
|
|
285
|
+
}
|
|
286
|
+
if (this.deviceConfig.appActivity) {
|
|
287
|
+
capabilities["appium:appActivity"] = this.deviceConfig.appActivity;
|
|
288
|
+
}
|
|
289
|
+
if (this.deviceConfig.additionalCapabilities) {
|
|
290
|
+
Object.assign(capabilities, this.deviceConfig.additionalCapabilities);
|
|
291
|
+
}
|
|
292
|
+
return capabilities;
|
|
293
|
+
}
|
|
294
|
+
async assumeRole() {
|
|
295
|
+
const roleArn = this.deviceConfig.roleArn;
|
|
296
|
+
const sessionName = this.deviceConfig.roleSessionName ?? "appwright-device-farm";
|
|
297
|
+
logger_1.logger.log(`AWS Device Farm: Assuming IAM role ${roleArn}`);
|
|
298
|
+
const stsClient = new client_sts_1.STSClient({ region: this.region });
|
|
299
|
+
try {
|
|
300
|
+
const command = new client_sts_1.AssumeRoleCommand({
|
|
301
|
+
RoleArn: roleArn,
|
|
302
|
+
RoleSessionName: sessionName,
|
|
303
|
+
ExternalId: this.deviceConfig.externalId,
|
|
304
|
+
});
|
|
305
|
+
const response = await stsClient.send(command);
|
|
306
|
+
const credentials = response.Credentials;
|
|
307
|
+
if (!credentials?.AccessKeyId ||
|
|
308
|
+
!credentials?.SecretAccessKey ||
|
|
309
|
+
!credentials?.SessionToken) {
|
|
310
|
+
throw new Error("AWS Device Farm: AssumeRole returned incomplete credentials.");
|
|
311
|
+
}
|
|
312
|
+
// Recreate the Device Farm client with assumed role credentials
|
|
313
|
+
this.client = new client_device_farm_1.DeviceFarmClient({
|
|
314
|
+
region: this.region,
|
|
315
|
+
credentials: {
|
|
316
|
+
accessKeyId: credentials.AccessKeyId,
|
|
317
|
+
secretAccessKey: credentials.SecretAccessKey,
|
|
318
|
+
sessionToken: credentials.SessionToken,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
logger_1.logger.log(`AWS Device Farm: Successfully assumed role ${roleArn}`);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
stsClient.destroy();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
static async downloadVideo(sessionArn, outputDir, fileName) {
|
|
328
|
+
if (!sessionArn) {
|
|
329
|
+
logger_1.logger.warn("AWS Device Farm: session ARN missing, skipping video download.");
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const region = AWSDeviceFarmProvider.extractRegionFromArn(sessionArn) ??
|
|
333
|
+
process.env.AWS_REGION ??
|
|
334
|
+
DEFAULT_REGION;
|
|
335
|
+
const client = new client_device_farm_1.DeviceFarmClient({ region });
|
|
336
|
+
const pathToVideo = path_1.default.join(outputDir, `${fileName}.mp4`);
|
|
337
|
+
const tempPath = `${pathToVideo}.part`;
|
|
338
|
+
const dir = path_1.default.dirname(pathToVideo);
|
|
339
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
340
|
+
try {
|
|
341
|
+
const downloadResult = await (0, async_retry_1.default)(async () => {
|
|
342
|
+
const artifact = await AWSDeviceFarmProvider.findVideoArtifact(client, sessionArn);
|
|
343
|
+
if (!artifact?.url) {
|
|
344
|
+
throw new Error("AWS Device Farm: Video artifact not ready yet.");
|
|
345
|
+
}
|
|
346
|
+
if (fs_1.default.existsSync(tempPath)) {
|
|
347
|
+
fs_1.default.rmSync(tempPath, { force: true });
|
|
348
|
+
}
|
|
349
|
+
await AWSDeviceFarmProvider.downloadFromPresignedUrl(artifact.url, tempPath);
|
|
350
|
+
if (fs_1.default.existsSync(pathToVideo)) {
|
|
351
|
+
fs_1.default.rmSync(pathToVideo, { force: true });
|
|
352
|
+
}
|
|
353
|
+
fs_1.default.renameSync(tempPath, pathToVideo);
|
|
354
|
+
return { path: pathToVideo, contentType: "video/mp4" };
|
|
355
|
+
}, {
|
|
356
|
+
retries: 10,
|
|
357
|
+
minTimeout: 5_000,
|
|
358
|
+
maxTimeout: 15_000,
|
|
359
|
+
onRetry: (error, attempt) => {
|
|
360
|
+
if (attempt > 3) {
|
|
361
|
+
logger_1.logger.warn(`AWS Device Farm: retrying video download (attempt ${attempt}): ${error.message}`);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
return downloadResult;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
logger_1.logger.error("AWS Device Farm: Failed to download video.", error);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
client.destroy();
|
|
373
|
+
if (fs_1.default.existsSync(tempPath)) {
|
|
374
|
+
try {
|
|
375
|
+
fs_1.default.rmSync(tempPath, { force: true });
|
|
376
|
+
}
|
|
377
|
+
catch (cleanupError) {
|
|
378
|
+
logger_1.logger.warn("AWS Device Farm: Unable to clean up temporary video file.", cleanupError);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
static async findVideoArtifact(client, sessionArn) {
|
|
384
|
+
const artifactTypesToCheck = ["FILE", "LOG"];
|
|
385
|
+
for (const type of artifactTypesToCheck) {
|
|
386
|
+
let nextToken;
|
|
387
|
+
do {
|
|
388
|
+
const response = await client.send(new client_device_farm_1.ListArtifactsCommand({
|
|
389
|
+
arn: sessionArn,
|
|
390
|
+
type,
|
|
391
|
+
nextToken,
|
|
392
|
+
}));
|
|
393
|
+
const artifacts = response.artifacts ?? [];
|
|
394
|
+
const videoArtifact = artifacts.find((artifact) => {
|
|
395
|
+
const artifactType = artifact.type ?? "";
|
|
396
|
+
return artifactType === "VIDEO" || artifactType === "VIDEO_LOG";
|
|
397
|
+
});
|
|
398
|
+
if (videoArtifact) {
|
|
399
|
+
return videoArtifact;
|
|
400
|
+
}
|
|
401
|
+
nextToken = response.nextToken;
|
|
402
|
+
} while (nextToken);
|
|
403
|
+
}
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
static async downloadFromPresignedUrl(url, tempPath) {
|
|
407
|
+
const fetch = (await import("node-fetch")).default;
|
|
408
|
+
const response = await fetch(url, {
|
|
409
|
+
method: "GET",
|
|
410
|
+
});
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
throw new Error(`AWS Device Farm: Unable to fetch video artifact. Status ${response.status}`);
|
|
413
|
+
}
|
|
414
|
+
// node-fetch v3 returns a Node.js Readable stream, not a Web Stream
|
|
415
|
+
// We can directly pipe it to the file
|
|
416
|
+
const { pipeline } = await import("stream/promises");
|
|
417
|
+
const fileStream = fs_1.default.createWriteStream(tempPath);
|
|
418
|
+
// Use pipeline to handle the stream properly with automatic cleanup
|
|
419
|
+
// Cast to Readable as node-fetch body is a Node.js stream
|
|
420
|
+
await pipeline(response.body, fileStream);
|
|
421
|
+
}
|
|
422
|
+
static extractRegionFromArn(arn) {
|
|
423
|
+
const arnParts = arn.split(":");
|
|
424
|
+
if (arnParts.length >= 4 && arnParts[2] === "devicefarm") {
|
|
425
|
+
return arnParts[3] || undefined;
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
async stopRemoteSession() {
|
|
430
|
+
if (!this.remoteSessionArn) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
await this.client.send(new client_device_farm_1.StopRemoteAccessSessionCommand({
|
|
435
|
+
arn: this.remoteSessionArn,
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
logger_1.logger.warn(`AWS Device Farm: Failed to stop remote access session (${String(error)})`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
exports.AWSDeviceFarmProvider = AWSDeviceFarmProvider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.spec.d.ts","sourceRoot":"","sources":["../../../src/providers/awsDeviceFarm/index.spec.ts"],"names":[],"mappings":""}
|