@samsara-dev/appwright 0.2.0

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.
Files changed (81) hide show
  1. package/.eslintrc.js +4 -0
  2. package/CHANGELOG.md +538 -0
  3. package/LICENSE +202 -0
  4. package/README.md +183 -0
  5. package/dist/bin/index.d.ts +3 -0
  6. package/dist/bin/index.d.ts.map +1 -0
  7. package/dist/bin/index.js +53 -0
  8. package/dist/config.d.ts +4 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +65 -0
  11. package/dist/device/index.d.ts +171 -0
  12. package/dist/device/index.d.ts.map +1 -0
  13. package/dist/device/index.js +415 -0
  14. package/dist/fixture/index.d.ts +38 -0
  15. package/dist/fixture/index.d.ts.map +1 -0
  16. package/dist/fixture/index.js +78 -0
  17. package/dist/fixture/workerInfo.d.ts +27 -0
  18. package/dist/fixture/workerInfo.d.ts.map +1 -0
  19. package/dist/fixture/workerInfo.js +87 -0
  20. package/dist/global-setup.d.ts +5 -0
  21. package/dist/global-setup.d.ts.map +1 -0
  22. package/dist/global-setup.js +30 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +25 -0
  26. package/dist/locator/index.d.ts +25 -0
  27. package/dist/locator/index.d.ts.map +1 -0
  28. package/dist/locator/index.js +296 -0
  29. package/dist/logger.d.ts +9 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +19 -0
  32. package/dist/providers/appium.d.ts +15 -0
  33. package/dist/providers/appium.d.ts.map +1 -0
  34. package/dist/providers/appium.js +274 -0
  35. package/dist/providers/browserstack/index.d.ts +26 -0
  36. package/dist/providers/browserstack/index.d.ts.map +1 -0
  37. package/dist/providers/browserstack/index.js +272 -0
  38. package/dist/providers/browserstack/utils.d.ts +2 -0
  39. package/dist/providers/browserstack/utils.d.ts.map +1 -0
  40. package/dist/providers/browserstack/utils.js +34 -0
  41. package/dist/providers/emulator/index.d.ts +13 -0
  42. package/dist/providers/emulator/index.d.ts.map +1 -0
  43. package/dist/providers/emulator/index.js +86 -0
  44. package/dist/providers/index.d.ts +5 -0
  45. package/dist/providers/index.d.ts.map +1 -0
  46. package/dist/providers/index.js +31 -0
  47. package/dist/providers/lambdatest/index.d.ts +27 -0
  48. package/dist/providers/lambdatest/index.d.ts.map +1 -0
  49. package/dist/providers/lambdatest/index.js +280 -0
  50. package/dist/providers/lambdatest/utils.d.ts +3 -0
  51. package/dist/providers/lambdatest/utils.d.ts.map +1 -0
  52. package/dist/providers/lambdatest/utils.js +36 -0
  53. package/dist/providers/local/index.d.ts +13 -0
  54. package/dist/providers/local/index.d.ts.map +1 -0
  55. package/dist/providers/local/index.js +86 -0
  56. package/dist/reporter.d.ts +13 -0
  57. package/dist/reporter.d.ts.map +1 -0
  58. package/dist/reporter.js +216 -0
  59. package/dist/tests/locator.spec.d.ts +2 -0
  60. package/dist/tests/locator.spec.d.ts.map +1 -0
  61. package/dist/tests/locator.spec.js +89 -0
  62. package/dist/tests/regex.spec.d.ts +2 -0
  63. package/dist/tests/regex.spec.d.ts.map +1 -0
  64. package/dist/tests/regex.spec.js +19 -0
  65. package/dist/tests/vitest.config.d.mts +3 -0
  66. package/dist/tests/vitest.config.d.mts.map +1 -0
  67. package/dist/tests/vitest.config.mjs +6 -0
  68. package/dist/types/errors.d.ts +7 -0
  69. package/dist/types/errors.d.ts.map +1 -0
  70. package/dist/types/errors.js +15 -0
  71. package/dist/types/index.d.ts +234 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +22 -0
  74. package/dist/utils.d.ts +8 -0
  75. package/dist/utils.d.ts.map +1 -0
  76. package/dist/utils.js +66 -0
  77. package/dist/vision/index.d.ts +64 -0
  78. package/dist/vision/index.d.ts.map +1 -0
  79. package/dist/vision/index.js +106 -0
  80. package/package.json +63 -0
  81. package/tsconfig.json +15 -0
@@ -0,0 +1,274 @@
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.installDriver = installDriver;
7
+ exports.startAppiumServer = startAppiumServer;
8
+ exports.stopAppiumServer = stopAppiumServer;
9
+ exports.isEmulatorInstalled = isEmulatorInstalled;
10
+ exports.startAndroidEmulator = startAndroidEmulator;
11
+ exports.getAppBundleId = getAppBundleId;
12
+ exports.getConnectedIOSDeviceUDID = getConnectedIOSDeviceUDID;
13
+ exports.getActiveAndroidDevices = getActiveAndroidDevices;
14
+ exports.getApkDetails = getApkDetails;
15
+ const child_process_1 = require("child_process");
16
+ const path_1 = __importDefault(require("path"));
17
+ const types_1 = require("../types");
18
+ const logger_1 = require("../logger");
19
+ const promises_1 = __importDefault(require("fs/promises"));
20
+ const util_1 = require("util");
21
+ const utils_1 = require("../utils");
22
+ const execPromise = (0, util_1.promisify)(child_process_1.exec);
23
+ async function installDriver(driverName) {
24
+ // uninstall the driver first to avoid conflicts
25
+ await new Promise((resolve) => {
26
+ const installProcess = (0, child_process_1.spawn)("npx", ["appium", "driver", "uninstall", driverName], {
27
+ stdio: "pipe",
28
+ });
29
+ installProcess.on("exit", (code) => {
30
+ resolve(code);
31
+ });
32
+ });
33
+ // install the driver
34
+ await new Promise((resolve) => {
35
+ const installProcess = (0, child_process_1.spawn)("npx", ["appium", "driver", "install", driverName], {
36
+ stdio: "pipe",
37
+ });
38
+ installProcess.on("exit", (code) => {
39
+ resolve(code);
40
+ });
41
+ });
42
+ }
43
+ async function startAppiumServer(provider) {
44
+ let emulatorStartRequested = false;
45
+ return new Promise((resolve, reject) => {
46
+ const appiumProcess = (0, child_process_1.spawn)("npx", ["appium"], {
47
+ stdio: "pipe",
48
+ });
49
+ appiumProcess.stderr.on("data", async (data) => {
50
+ console.log(data.toString());
51
+ });
52
+ appiumProcess.stdout.on("data", async (data) => {
53
+ const output = data.toString();
54
+ console.log(output);
55
+ if (output.includes("Error: listen EADDRINUSE")) {
56
+ // TODO: Kill the appium server if it is already running
57
+ logger_1.logger.error(`Appium: ${data}`);
58
+ throw new Error(`Appium server is already running. Please stop the server before running tests.`);
59
+ }
60
+ if (output.includes("Could not find online devices")) {
61
+ if (!emulatorStartRequested && provider == "emulator") {
62
+ emulatorStartRequested = true;
63
+ await startAndroidEmulator();
64
+ }
65
+ }
66
+ if (output.includes("Appium REST http interface listener started")) {
67
+ logger_1.logger.log("Appium server is up and running.");
68
+ resolve(appiumProcess);
69
+ }
70
+ });
71
+ appiumProcess.on("error", (error) => {
72
+ logger_1.logger.error(`Appium: ${error}`);
73
+ reject(error);
74
+ });
75
+ process.on("exit", () => {
76
+ logger_1.logger.log("Main process exiting. Killing Appium server...");
77
+ appiumProcess.kill();
78
+ });
79
+ appiumProcess.on("close", (code) => {
80
+ logger_1.logger.log(`Appium server exited with code ${code}`);
81
+ });
82
+ });
83
+ }
84
+ function stopAppiumServer() {
85
+ return new Promise((resolve, reject) => {
86
+ (0, child_process_1.exec)(`pkill -f appium`, (error, stdout) => {
87
+ if (error) {
88
+ logger_1.logger.error(`Error stopping Appium server: ${error.message}`);
89
+ reject(error);
90
+ }
91
+ logger_1.logger.log("Appium server stopped successfully.");
92
+ resolve(stdout);
93
+ });
94
+ });
95
+ }
96
+ function isEmulatorInstalled(platform) {
97
+ return new Promise((resolve) => {
98
+ if (platform == types_1.Platform.ANDROID) {
99
+ const androidHome = process.env.ANDROID_HOME;
100
+ const emulatorPath = path_1.default.join(androidHome, "emulator", "emulator");
101
+ (0, child_process_1.exec)(`${emulatorPath} -list-avds`, (error, stdout, stderr) => {
102
+ if (error) {
103
+ throw new Error(`Error fetching emulator list.\nPlease install emulator from Android SDK Tools.
104
+ Follow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`);
105
+ }
106
+ if (stderr) {
107
+ logger_1.logger.error(`Emulator: ${stderr}`);
108
+ }
109
+ const lines = stdout.trim().split("\n");
110
+ const deviceNames = lines.filter((line) => line.trim() && !line.startsWith("INFO") && !line.includes("/tmp/"));
111
+ if (deviceNames.length > 0) {
112
+ resolve(true);
113
+ }
114
+ else {
115
+ throw new Error(`No installed emulators found.
116
+ Follow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`);
117
+ }
118
+ });
119
+ }
120
+ });
121
+ }
122
+ async function startAndroidEmulator() {
123
+ return new Promise((resolve, reject) => {
124
+ const androidHome = process.env.ANDROID_HOME;
125
+ const emulatorPath = path_1.default.join(androidHome, "emulator", "emulator");
126
+ (0, child_process_1.exec)(`${emulatorPath} -list-avds`, (error, stdout, stderr) => {
127
+ if (error) {
128
+ throw new Error(`Error fetching emulator list.\nPlease install emulator from Android SDK Tools.\nFollow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`);
129
+ }
130
+ if (stderr) {
131
+ logger_1.logger.error(`Emulator: ${stderr}`);
132
+ }
133
+ const lines = stdout.trim().split("\n");
134
+ // Filter out lines that do not contain device names
135
+ const deviceNames = lines.filter((line) => line.trim() && !line.startsWith("INFO") && !line.includes("/tmp/"));
136
+ if (deviceNames.length === 0) {
137
+ throw new Error(`No installed emulators found.\nFollow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`);
138
+ }
139
+ else {
140
+ logger_1.logger.log(`Available Emulators: ${deviceNames}`);
141
+ }
142
+ const emulatorToStart = deviceNames[0];
143
+ const emulatorProcess = (0, child_process_1.spawn)(emulatorPath, ["-avd", emulatorToStart], {
144
+ stdio: "pipe",
145
+ });
146
+ emulatorProcess.stdout?.on("data", (data) => {
147
+ logger_1.logger.log(`Emulator: ${data}`);
148
+ if (data.includes("Successfully loaded snapshot 'default_boot'")) {
149
+ logger_1.logger.log("Emulator started successfully.");
150
+ resolve();
151
+ }
152
+ });
153
+ emulatorProcess.on("error", (err) => {
154
+ logger_1.logger.error(`Emulator: ${err.message}`);
155
+ reject(`Failed to start emulator: ${err.message}`);
156
+ });
157
+ emulatorProcess.on("close", (code) => {
158
+ if (code !== 0) {
159
+ reject(`Emulator process exited with code: ${code}`);
160
+ }
161
+ });
162
+ // Ensure the emulator process is killed when the main process exits
163
+ process.on("exit", () => {
164
+ logger_1.logger.log("Main process exiting. Killing the emulator process...");
165
+ emulatorProcess.kill();
166
+ });
167
+ });
168
+ });
169
+ }
170
+ function getAppBundleId(path) {
171
+ return new Promise((resolve, reject) => {
172
+ const command = `osascript -e 'id of app "${path}"'`;
173
+ (0, child_process_1.exec)(command, (error, stdout, stderr) => {
174
+ if (error) {
175
+ logger_1.logger.error("osascript:", error.message);
176
+ return reject(error);
177
+ }
178
+ if (stderr) {
179
+ logger_1.logger.error(`osascript: ${stderr}`);
180
+ return reject(new Error(stderr));
181
+ }
182
+ const bundleId = stdout.trim();
183
+ if (bundleId) {
184
+ resolve(bundleId);
185
+ }
186
+ else {
187
+ reject(new Error("Bundle ID not found"));
188
+ }
189
+ });
190
+ });
191
+ }
192
+ async function getConnectedIOSDeviceUDID() {
193
+ try {
194
+ const { stdout } = await execPromise(`xcrun xctrace list devices`);
195
+ const iphoneDevices = stdout
196
+ .split("\n")
197
+ .filter((line) => line.includes("iPhone"));
198
+ const realDevices = iphoneDevices.filter((line) => !line.includes("Simulator"));
199
+ if (!realDevices.length) {
200
+ throw new Error(`No connected iPhone detected. Please ensure your device is connected and try again.`);
201
+ }
202
+ const deviceLine = realDevices[0];
203
+ //the output from above looks like this: User’s iPhone (18.0) (00003110-002A304e3A53C41E)
204
+ //where `00003110-000A304e3A53C41E` is the UDID of the device
205
+ const matches = deviceLine.match(/\(([\da-fA-F-]+)\)$/);
206
+ if (matches && matches[1]) {
207
+ return matches[1];
208
+ }
209
+ else {
210
+ throw new Error(`Please check your iPhone device connection.
211
+ To check for connected devices run "xcrun xctrace list devices | grep iPhone | grep -v Simulator"`);
212
+ }
213
+ }
214
+ catch (error) {
215
+ //@ts-ignore
216
+ throw new Error(`getConnectedIOSDeviceUDID: ${error.message}`);
217
+ }
218
+ }
219
+ async function getActiveAndroidDevices() {
220
+ try {
221
+ const { stdout } = await execPromise("adb devices");
222
+ const lines = stdout.trim().split("\n");
223
+ const deviceLines = lines.filter((line) => line.includes("\tdevice"));
224
+ return deviceLines.length;
225
+ }
226
+ catch (error) {
227
+ throw new Error(
228
+ //@ts-ignore
229
+ `getActiveAndroidDevices: ${error.message}`);
230
+ }
231
+ }
232
+ async function getLatestBuildToolsVersion() {
233
+ const androidHome = process.env.ANDROID_HOME;
234
+ const buildToolsPath = path_1.default.join(androidHome, "build-tools");
235
+ try {
236
+ const files = await promises_1.default.readdir(buildToolsPath);
237
+ const versions = files.filter((file) => /^\d+\.\d+\.\d+(-rc\d+)?$/.test(file));
238
+ if (versions.length === 0) {
239
+ throw new Error(`No valid build-tools found in ${buildToolsPath}. Please download from Android Studio: https://developer.android.com/studio/intro/update#required`);
240
+ }
241
+ return (0, utils_1.getLatestBuildToolsVersions)(versions);
242
+ }
243
+ catch (err) {
244
+ logger_1.logger.error(`getLatestBuildToolsVersion: ${err}`);
245
+ throw new Error(`Error reading ${buildToolsPath}. Ensure it exists or download from Android Studio: https://developer.android.com/studio/intro/update#required`);
246
+ }
247
+ }
248
+ async function getApkDetails(buildPath) {
249
+ const androidHome = process.env.ANDROID_HOME;
250
+ const buildToolsVersion = await getLatestBuildToolsVersion();
251
+ if (!buildToolsVersion) {
252
+ throw new Error(`No valid build-tools found in ${buildToolsVersion}. Please download from Android Studio: https://developer.android.com/studio/intro/update#required`);
253
+ }
254
+ const aaptPath = path_1.default.join(androidHome, "build-tools", buildToolsVersion, "aapt");
255
+ const command = `${aaptPath} dump badging ${buildPath}`;
256
+ try {
257
+ const { stdout, stderr } = await execPromise(command);
258
+ if (stderr) {
259
+ logger_1.logger.error(`getApkDetails: ${stderr}`);
260
+ throw new Error(`Error executing aapt: ${stderr}`);
261
+ }
262
+ const packageMatch = stdout.match(/package: name='(\S+)'/);
263
+ const activityMatch = stdout.match(/launchable-activity: name='(\S+)'/);
264
+ if (!packageMatch || !activityMatch) {
265
+ throw new Error(`Unable to retrieve package or launchable activity from the APK. Please verify that the provided file is a valid APK.`);
266
+ }
267
+ const packageName = packageMatch[1];
268
+ const launchableActivity = activityMatch[1];
269
+ return { packageName, launchableActivity };
270
+ }
271
+ catch (error) {
272
+ throw new Error(`getApkDetails: ${error.message}`);
273
+ }
274
+ }
@@ -0,0 +1,26 @@
1
+ import { AppwrightConfig, DeviceProvider } from "../../types";
2
+ import { FullProject } from "@playwright/test";
3
+ import { Device } from "../../device";
4
+ export declare class BrowserStackDeviceProvider implements DeviceProvider {
5
+ private sessionDetails?;
6
+ sessionId?: string;
7
+ private project;
8
+ constructor(project: FullProject<AppwrightConfig>, appBundleId: string | undefined);
9
+ globalSetup(): Promise<void>;
10
+ getDevice(): Promise<Device>;
11
+ private validateConfig;
12
+ private createDriver;
13
+ private getSessionDetails;
14
+ private getAppBundleIdFromSession;
15
+ static downloadVideo(sessionId: string, outputDir: string, fileName: string): Promise<{
16
+ path: string;
17
+ contentType: string;
18
+ } | null>;
19
+ syncTestDetails(details: {
20
+ status?: string;
21
+ reason?: string;
22
+ name?: string;
23
+ }): Promise<any>;
24
+ private createConfig;
25
+ }
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqDtC,qBAAa,0BAA2B,YAAW,cAAc;IAC/D,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,OAAO,CAA+B;gBAG5C,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS;IAU3B,WAAW;IAwDX,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAMlC,OAAO,CAAC,cAAc;YASR,YAAY;YAgBZ,iBAAiB;YAKjB,yBAAyB;WAK1B,aAAa,CACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAuFlD,eAAe,CAAC,OAAO,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf;IA2BD,OAAO,CAAC,YAAY;CAiDrB"}
@@ -0,0 +1,272 @@
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.BrowserStackDeviceProvider = void 0;
7
+ const async_retry_1 = __importDefault(require("async-retry"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const form_data_1 = __importDefault(require("form-data"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const device_1 = require("../../device");
12
+ const logger_1 = require("../../logger");
13
+ const API_BASE_URL = "https://api-cloud.browserstack.com/app-automate";
14
+ const envVarKeyForBuild = (projectName) => `BROWSERSTACK_APP_URL_${projectName.toUpperCase()}`;
15
+ function getAuthHeader() {
16
+ const userName = process.env.BROWSERSTACK_USERNAME;
17
+ const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;
18
+ const key = Buffer.from(`${userName}:${accessKey}`).toString("base64");
19
+ return `Basic ${key}`;
20
+ }
21
+ async function getSessionDetails(sessionId) {
22
+ const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}.json`, {
23
+ method: "GET",
24
+ headers: {
25
+ Authorization: getAuthHeader(),
26
+ },
27
+ });
28
+ if (!response.ok) {
29
+ throw new Error(`Error fetching session details: ${response.statusText}`);
30
+ }
31
+ const data = await response.json();
32
+ return data;
33
+ }
34
+ class BrowserStackDeviceProvider {
35
+ sessionDetails;
36
+ sessionId;
37
+ project;
38
+ constructor(project, appBundleId) {
39
+ this.project = project;
40
+ if (appBundleId) {
41
+ logger_1.logger.log(`Bundle id is specified (${appBundleId}) but ignored for BrowserStack provider.`);
42
+ }
43
+ }
44
+ async globalSetup() {
45
+ if (!this.project.use.buildPath) {
46
+ throw new Error(`Build path not found. Please set the build path in the config file.`);
47
+ }
48
+ if (!(process.env.BROWSERSTACK_USERNAME && process.env.BROWSERSTACK_ACCESS_KEY)) {
49
+ throw new Error("BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY are required environment variables for this device provider.");
50
+ }
51
+ const buildPath = this.project.use.buildPath;
52
+ const isHttpUrl = buildPath.startsWith("http");
53
+ const isBrowserStackUrl = buildPath.startsWith("bs://");
54
+ let appUrl = undefined;
55
+ if (isBrowserStackUrl) {
56
+ appUrl = buildPath;
57
+ }
58
+ else {
59
+ // Upload the file to BrowserStack and get the appUrl
60
+ let body;
61
+ let headers = {
62
+ Authorization: getAuthHeader(),
63
+ };
64
+ if (isHttpUrl) {
65
+ body = new URLSearchParams({
66
+ url: buildPath,
67
+ });
68
+ }
69
+ else {
70
+ if (!fs_1.default.existsSync(buildPath)) {
71
+ throw new Error(`Build file not found: ${buildPath}`);
72
+ }
73
+ const form = new form_data_1.default();
74
+ form.append("file", fs_1.default.createReadStream(buildPath));
75
+ headers = { ...headers, ...form.getHeaders() };
76
+ body = form;
77
+ }
78
+ const fetch = (await import("node-fetch")).default;
79
+ logger_1.logger.log(`Uploading: ${buildPath}`);
80
+ const response = await fetch(`${API_BASE_URL}/upload`, {
81
+ method: "POST",
82
+ headers,
83
+ body,
84
+ });
85
+ const data = await response.json();
86
+ appUrl = data.app_url;
87
+ if (!appUrl) {
88
+ logger_1.logger.error("Uploading the build failed:", data);
89
+ }
90
+ }
91
+ process.env[envVarKeyForBuild(this.project.name)] = appUrl;
92
+ }
93
+ async getDevice() {
94
+ this.validateConfig();
95
+ const config = this.createConfig();
96
+ return await this.createDriver(config);
97
+ }
98
+ validateConfig() {
99
+ const device = this.project.use.device;
100
+ if (!device.name || !device.osVersion) {
101
+ throw new Error("Device name and osVersion are required for running tests on BrowserStack");
102
+ }
103
+ }
104
+ async createDriver(config) {
105
+ const WebDriver = (await import("webdriver")).default;
106
+ const webDriverClient = await WebDriver.newSession(config);
107
+ this.sessionId = webDriverClient.sessionId;
108
+ const bundleId = await this.getAppBundleIdFromSession();
109
+ const testOptions = {
110
+ expectTimeout: this.project.use.expectTimeout,
111
+ };
112
+ return new device_1.Device(webDriverClient, bundleId, testOptions, this.project.use.device?.provider);
113
+ }
114
+ async getSessionDetails() {
115
+ const data = await getSessionDetails(this.sessionId);
116
+ this.sessionDetails = data.automation_session;
117
+ }
118
+ async getAppBundleIdFromSession() {
119
+ await this.getSessionDetails();
120
+ return this.sessionDetails?.app_details.app_name ?? "";
121
+ }
122
+ static async downloadVideo(sessionId, outputDir, fileName) {
123
+ const sessionData = await getSessionDetails(sessionId);
124
+ const sessionDetails = sessionData?.automation_session;
125
+ const videoURL = sessionDetails?.video_url;
126
+ const pathToTestVideo = path_1.default.join(outputDir, `${fileName}.mp4`);
127
+ const tempPathForWriting = `${pathToTestVideo}.part`;
128
+ const dir = path_1.default.dirname(pathToTestVideo);
129
+ fs_1.default.mkdirSync(dir, { recursive: true });
130
+ /**
131
+ * The BrowserStack video URL initially returns a 200 status,
132
+ * but the video file may still be empty. To avoid downloading
133
+ * an incomplete file, we introduce a delay of 10_000 ms before attempting the download.
134
+ * After the wait, BrowserStack may return a 403 error if the video is not
135
+ * yet available. We handle this by retrying the download until we either
136
+ * receive a 200 response (indicating the video is ready) or reach a maximum
137
+ * of 10 retries, whichever comes first.
138
+ */
139
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
140
+ const fileStream = fs_1.default.createWriteStream(tempPathForWriting);
141
+ //To catch the browserstack error in case all retries fails
142
+ try {
143
+ if (videoURL) {
144
+ await (0, async_retry_1.default)(async () => {
145
+ const response = await fetch(videoURL, {
146
+ method: "GET",
147
+ });
148
+ if (response.status !== 200) {
149
+ // Retry if not 200
150
+ throw new Error(`Video not found: ${response.status} (URL: ${videoURL})`);
151
+ }
152
+ const reader = response.body?.getReader();
153
+ if (!reader) {
154
+ throw new Error("Failed to get reader from response body.");
155
+ }
156
+ const streamToFile = async () => {
157
+ // eslint-disable-next-line no-constant-condition
158
+ while (true) {
159
+ const { done, value } = await reader.read();
160
+ if (done)
161
+ break;
162
+ fileStream.write(value);
163
+ }
164
+ };
165
+ await streamToFile();
166
+ fileStream.close();
167
+ }, {
168
+ retries: 10,
169
+ minTimeout: 3_000,
170
+ onRetry: (err, i) => {
171
+ if (i > 5) {
172
+ logger_1.logger.warn(`Retry attempt ${i} failed: ${err.message}`);
173
+ }
174
+ },
175
+ });
176
+ return new Promise((resolve, reject) => {
177
+ // Ensure file stream is closed even in case of an error
178
+ fileStream.on("finish", () => {
179
+ try {
180
+ fs_1.default.renameSync(tempPathForWriting, pathToTestVideo);
181
+ logger_1.logger.log(`Download finished and file closed: ${pathToTestVideo}`);
182
+ resolve({ path: pathToTestVideo, contentType: "video/mp4" });
183
+ }
184
+ catch (err) {
185
+ logger_1.logger.error(`Failed to rename file: `, err);
186
+ reject(err);
187
+ }
188
+ });
189
+ fileStream.on("error", (err) => {
190
+ logger_1.logger.error(`Failed to write file: ${err.message}`);
191
+ reject(err);
192
+ });
193
+ });
194
+ }
195
+ else {
196
+ return null;
197
+ }
198
+ }
199
+ catch (e) {
200
+ logger_1.logger.log(`Error Downloading video: `, e);
201
+ return null;
202
+ }
203
+ }
204
+ async syncTestDetails(details) {
205
+ const response = await fetch(`${API_BASE_URL}/sessions/${this.sessionId}.json`, {
206
+ method: "PUT",
207
+ headers: {
208
+ Authorization: getAuthHeader(),
209
+ "Content-Type": "application/json",
210
+ },
211
+ body: details.status
212
+ ? JSON.stringify({
213
+ status: details.status,
214
+ reason: details.reason,
215
+ })
216
+ : JSON.stringify({
217
+ name: details.name,
218
+ }),
219
+ });
220
+ if (!response.ok) {
221
+ throw new Error(`Error setting session details: ${response.statusText}`);
222
+ }
223
+ const responseData = await response.json();
224
+ return responseData;
225
+ }
226
+ createConfig() {
227
+ const platformName = this.project.use.platform;
228
+ const projectName = path_1.default.basename(process.cwd());
229
+ const envVarKey = envVarKeyForBuild(this.project.name);
230
+ const deviceConfig = this.project.use.device;
231
+ const configuredAppiumVersion = deviceConfig.appiumVersion ??
232
+ process.env.BROWSERSTACK_APPIUM_VERSION ??
233
+ "3.1.0";
234
+ if (!process.env[envVarKey]) {
235
+ throw new Error(`process.env.${envVarKey} is not set. Did the file upload work?`);
236
+ }
237
+ return {
238
+ port: 443,
239
+ path: "/wd/hub",
240
+ protocol: "https",
241
+ logLevel: "warn",
242
+ user: process.env.BROWSERSTACK_USERNAME,
243
+ key: process.env.BROWSERSTACK_ACCESS_KEY,
244
+ hostname: "hub.browserstack.com",
245
+ capabilities: {
246
+ "bstack:options": {
247
+ debug: true,
248
+ interactiveDebugging: true,
249
+ networkLogs: true,
250
+ appiumVersion: configuredAppiumVersion,
251
+ enableCameraImageInjection: deviceConfig?.enableCameraImageInjection,
252
+ idleTimeout: 180,
253
+ deviceName: deviceConfig?.name,
254
+ osVersion: deviceConfig.osVersion,
255
+ platformName: platformName,
256
+ deviceOrientation: deviceConfig?.orientation,
257
+ buildName: `${projectName} ${platformName}`,
258
+ sessionName: `${projectName} ${platformName} test`,
259
+ buildIdentifier: process.env.GITHUB_ACTIONS === "true"
260
+ ? `CI ${process.env.GITHUB_RUN_ID}`
261
+ : process.env.USER,
262
+ },
263
+ "appium:autoGrantPermissions": true,
264
+ "appium:app": process.env[envVarKey],
265
+ "appium:autoAcceptAlerts": true,
266
+ "appium:fullReset": true,
267
+ "appium:settings[snapshotMaxDepth]": 62,
268
+ },
269
+ };
270
+ }
271
+ }
272
+ exports.BrowserStackDeviceProvider = BrowserStackDeviceProvider;
@@ -0,0 +1,2 @@
1
+ export declare function uploadImageToBS(imagePath: string): Promise<string>;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/utils.ts"],"names":[],"mappings":"AAgBA,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBxE"}
@@ -0,0 +1,34 @@
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.uploadImageToBS = uploadImageToBS;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const form_data_1 = __importDefault(require("form-data"));
9
+ function getAuthHeader() {
10
+ const userName = process.env.BROWSERSTACK_USERNAME;
11
+ const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;
12
+ const key = Buffer.from(`${userName}:${accessKey}`).toString("base64");
13
+ return `Basic ${key}`;
14
+ }
15
+ async function uploadImageToBS(imagePath) {
16
+ const formData = new form_data_1.default();
17
+ if (!fs_1.default.existsSync(imagePath)) {
18
+ throw new Error(`No image file found at the specified path: ${imagePath}. Please provide a valid image file.
19
+ Supported formats include JPG, JPEG, and PNG. Ensure the file exists and the path is correct.`);
20
+ }
21
+ formData.append("file", fs_1.default.createReadStream(imagePath));
22
+ formData.append("custom_id", "SampleMedia");
23
+ const fetch = (await import("node-fetch")).default;
24
+ const response = await fetch("https://api-cloud.browserstack.com/app-automate/upload-media", {
25
+ method: "POST",
26
+ headers: {
27
+ Authorization: getAuthHeader(),
28
+ },
29
+ body: formData,
30
+ });
31
+ const data = (await response.json());
32
+ const imageURL = data.media_url.trim();
33
+ return imageURL;
34
+ }
@@ -0,0 +1,13 @@
1
+ import { AppwrightConfig, DeviceProvider } from "../../types";
2
+ import { Device } from "../../device";
3
+ import { FullProject } from "@playwright/test";
4
+ export declare class EmulatorProvider implements DeviceProvider {
5
+ private project;
6
+ sessionId?: string;
7
+ constructor(project: FullProject<AppwrightConfig>, appBundleId: string | undefined);
8
+ getDevice(): Promise<Device>;
9
+ globalSetup(): Promise<void>;
10
+ private createDriver;
11
+ private createConfig;
12
+ }
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/emulator/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,cAAc,EAIf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAOtC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI/C,qBAAa,gBAAiB,YAAW,cAAc;IAInD,OAAO,CAAC,OAAO;IAHjB,SAAS,CAAC,EAAE,MAAM,CAAC;gBAGT,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EAC7C,WAAW,EAAE,MAAM,GAAG,SAAS;IAS3B,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAI5B,WAAW;YA8BH,YAAY;YAwBZ,YAAY;CAmC3B"}