appwright
Version:
E2E mobile app testing done right, with the Playwright test runner
277 lines (276 loc) • 11.7 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LambdaTestDeviceProvider = void 0;
const async_retry_1 = __importDefault(require("async-retry"));
const fs_1 = __importDefault(require("fs"));
const form_data_1 = __importDefault(require("form-data"));
const path_1 = __importDefault(require("path"));
const device_1 = require("../../device");
const logger_1 = require("../../logger");
const utils_1 = require("./utils");
const browserStackToLambdaTest = {
deviceName: {
"Google Pixel 8": "Pixel 8",
},
osVersion: {
"14.0": "14",
},
};
const API_BASE_URL = "https://mobile-api.lambdatest.com/mobile-automation/api/v1";
const envVarKeyForBuild = (projectName) => `LAMBDATEST_APP_URL_${projectName.toUpperCase()}`;
async function getSessionDetails(sessionId) {
const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}`, {
method: "GET",
headers: {
Authorization: (0, utils_1.getAuthHeader)(),
},
});
if (!response.ok) {
throw new Error(`Error fetching session details: ${response.statusText}`);
}
const data = await response.json();
return data;
}
class LambdaTestDeviceProvider {
project;
appBundleId;
sessionDetails;
sessionId;
projectName = path_1.default.basename(process.cwd());
constructor(project, appBundleId) {
this.project = project;
this.appBundleId = appBundleId;
if (!appBundleId) {
throw new Error("App Bundle ID is required for running tests on LambdaTest. Set the `appBundleId` for your projects that run on this provider.");
}
}
async globalSetup() {
if (!this.project.use.buildPath) {
throw new Error(`Build path not found. Please set the build path in the config file.`);
}
if (!(process.env.LAMBDATEST_USERNAME && process.env.LAMBDATEST_ACCESS_KEY)) {
throw new Error("LAMBDATEST_USERNAME and LAMBDATEST_ACCESS_KEY are required environment variables for this device provider. Please set the LAMBDATEST_USERNAME and LAMBDATEST_ACCESS_KEY environment variables.");
}
const buildPath = this.project.use.buildPath;
const isHttpUrl = buildPath.startsWith("http");
const isLambdaTestUrl = buildPath.startsWith("lt://");
let appUrl = undefined;
if (isLambdaTestUrl) {
appUrl = buildPath;
}
else {
let body;
let headers = {
Authorization: (0, utils_1.getAuthHeader)(),
};
if (isHttpUrl) {
body = new URLSearchParams({
url: buildPath,
visibility: "team",
storage: "url",
name: this.projectName,
});
}
else {
if (!fs_1.default.existsSync(buildPath)) {
throw new Error(`Build file not found: ${buildPath}`);
}
const form = new form_data_1.default();
form.append("visibility", "team");
form.append("storage", "file");
form.append("appFile", fs_1.default.createReadStream(buildPath));
form.append("name", this.projectName);
headers = { ...headers, ...form.getHeaders() };
body = form;
}
logger_1.logger.log(`Uploading: ${buildPath}`);
const fetch = (await import("node-fetch")).default;
const response = await fetch(`https://manual-api.lambdatest.com/app/upload/realDevice`, {
method: "POST",
headers,
body,
});
const data = await response.json();
appUrl = data.app_url;
if (!appUrl) {
logger_1.logger.error("Uploading the build failed:", data);
}
}
process.env[envVarKeyForBuild(this.project.name)] = appUrl;
}
async getDevice() {
this.validateConfig();
const config = this.createConfig();
return await this.createDriver(config);
}
validateConfig() {
const device = this.project.use.device;
if (!device.name || !device.osVersion) {
throw new Error("Device name and osVersion are required for running tests on LambdaTest. Please set the device name and osVersion in the `appwright.config.ts` file.");
}
}
async createDriver(config) {
const WebDriver = (await import("webdriver")).default;
const webDriverClient = await WebDriver.newSession(config);
this.sessionId = webDriverClient.sessionId;
const testOptions = {
expectTimeout: this.project.use.expectTimeout,
};
return new device_1.Device(webDriverClient, this.appBundleId, testOptions, this.project.use.device?.provider);
}
static async downloadVideo(sessionId, outputDir, fileName) {
const sessionData = await getSessionDetails(sessionId);
const sessionDetails = sessionData?.data;
const videoURL = sessionDetails?.video_url;
const pathToTestVideo = path_1.default.join(outputDir, `${fileName}.mp4`);
const tempPathForWriting = `${pathToTestVideo}.part`;
const dir = path_1.default.dirname(pathToTestVideo);
fs_1.default.mkdirSync(dir, { recursive: true });
const fileStream = fs_1.default.createWriteStream(tempPathForWriting);
//To catch the lambdatest error in case all retries fails
try {
if (videoURL) {
await (0, async_retry_1.default)(async () => {
const response = await fetch(videoURL, {
method: "GET",
});
if (response.status !== 200) {
// Retry if not 200
throw new Error(`Video not found: ${response.status} (URL: ${videoURL})`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Failed to get reader from response body.");
}
const streamToFile = async () => {
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done)
break;
fileStream.write(value);
}
};
await streamToFile();
fileStream.close();
}, {
retries: 10,
minTimeout: 3_000,
onRetry: (err, i) => {
if (i > 5) {
logger_1.logger.warn(`Retry attempt ${i} failed: ${err.message}`);
}
},
});
return new Promise((resolve, reject) => {
// Ensure file stream is closed even in case of an error
fileStream.on("finish", () => {
try {
fs_1.default.renameSync(tempPathForWriting, pathToTestVideo);
logger_1.logger.log(`Download finished and file closed: ${pathToTestVideo}`);
resolve({ path: pathToTestVideo, contentType: "video/mp4" });
}
catch (err) {
logger_1.logger.error(`Failed to rename file: `, err);
reject(err);
}
});
fileStream.on("error", (err) => {
logger_1.logger.error(`Failed to write file: ${err.message}`);
reject(err);
});
});
}
else {
return null;
}
}
catch (e) {
logger_1.logger.log(`Error Downloading video: `, e);
return null;
}
}
async syncTestDetails(details) {
const response = await fetch(`${API_BASE_URL}/sessions/${this.sessionId}`, {
method: "PATCH",
headers: {
Authorization: (0, utils_1.getAuthHeader)(),
"Content-Type": "application/json",
},
body: details.status
? JSON.stringify({
name: details.name,
status_ind: details.status,
custom_data: details.reason,
})
: JSON.stringify({
name: details.name,
}),
});
if (!response.ok) {
//TODO: Check whether add retry here or leave it as is because while setting the name of test
//sometimes the session is not getting created till then thus this fails.
// throw new Error(`Error setting session details: ${response.statusText}`);
}
const responseData = await response.json();
return responseData;
}
deviceInfoForSession() {
let deviceName = this.project.use.device?.name;
let osVersion = this.project.use.device.osVersion;
if (deviceName &&
Object.keys(browserStackToLambdaTest.deviceName).includes(deviceName)) {
// we map BrowserStack names to LambdaTest for better usability
deviceName = browserStackToLambdaTest.deviceName[deviceName];
}
if (osVersion &&
Object.keys(browserStackToLambdaTest.osVersion).includes(osVersion)) {
osVersion = browserStackToLambdaTest.osVersion[osVersion];
}
return {
deviceName,
platformVersion: osVersion,
deviceOrientation: this.project.use.device?.orientation,
};
}
createConfig() {
const platformName = this.project.use.platform;
const envVarKey = envVarKeyForBuild(this.project.name);
if (!process.env[envVarKey]) {
throw new Error(`process.env.${envVarKey} is not set. Did the file upload work?`);
}
return {
port: 443,
protocol: "https",
path: "/wd/hub",
logLevel: "warn",
user: process.env.LAMBDATEST_USERNAME,
key: process.env.LAMBDATEST_ACCESS_KEY,
hostname: "mobile-hub.lambdatest.com",
capabilities: {
...this.deviceInfoForSession(),
appiumVersion: "2.3.0",
platformName: platformName,
queueTimeout: 600,
idleTimeout: 600,
app: process.env[envVarKey],
devicelog: true,
video: true,
build: `${this.projectName} ${platformName} ${process.env.GITHUB_ACTIONS === "true"
? `CI ${process.env.GITHUB_RUN_ID}`
: process.env.USER}`,
project: this.projectName,
autoGrantPermissions: true,
autoAcceptAlerts: true,
isRealMobile: true,
enableImageInjection: this.project.use.device
?.enableCameraImageInjection,
"settings[snapshotMaxDepth]": 62,
},
};
}
}
exports.LambdaTestDeviceProvider = LambdaTestDeviceProvider;
;