UNPKG

appwright

Version:

E2E mobile app testing done right, with the Playwright test runner

275 lines (274 loc) 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.installDriver = installDriver; exports.startAppiumServer = startAppiumServer; exports.stopAppiumServer = stopAppiumServer; exports.isEmulatorInstalled = isEmulatorInstalled; exports.startAndroidEmulator = startAndroidEmulator; exports.getAppBundleId = getAppBundleId; exports.getConnectedIOSDeviceUDID = getConnectedIOSDeviceUDID; exports.getActiveAndroidDevices = getActiveAndroidDevices; exports.getApkDetails = getApkDetails; const child_process_1 = require("child_process"); const path_1 = __importDefault(require("path")); const types_1 = require("../types"); const logger_1 = require("../logger"); const promises_1 = __importDefault(require("fs/promises")); const util_1 = require("util"); const utils_1 = require("../utils"); const execPromise = (0, util_1.promisify)(child_process_1.exec); async function installDriver(driverName) { // uninstall the driver first to avoid conflicts await new Promise((resolve) => { const installProcess = (0, child_process_1.spawn)("npx", ["appium", "driver", "uninstall", driverName], { stdio: "pipe", }); installProcess.on("exit", (code) => { resolve(code); }); }); // install the driver await new Promise((resolve) => { const installProcess = (0, child_process_1.spawn)("npx", ["appium", "driver", "install", driverName], { stdio: "pipe", }); installProcess.on("exit", (code) => { resolve(code); }); }); } async function startAppiumServer(provider) { let emulatorStartRequested = false; return new Promise((resolve, reject) => { const appiumProcess = (0, child_process_1.spawn)("npx", ["appium"], { stdio: "pipe", }); appiumProcess.stderr.on("data", async (data) => { console.log(data.toString()); }); appiumProcess.stdout.on("data", async (data) => { const output = data.toString(); console.log(output); if (output.includes("Error: listen EADDRINUSE")) { // TODO: Kill the appium server if it is already running logger_1.logger.error(`Appium: ${data}`); throw new Error(`Appium server is already running. Please stop the server before running tests.`); } if (output.includes("Could not find online devices")) { if (!emulatorStartRequested && provider == "emulator") { emulatorStartRequested = true; await startAndroidEmulator(); } } if (output.includes("Appium REST http interface listener started")) { logger_1.logger.log("Appium server is up and running."); resolve(appiumProcess); } }); appiumProcess.on("error", (error) => { logger_1.logger.error(`Appium: ${error}`); reject(error); }); process.on("exit", () => { logger_1.logger.log("Main process exiting. Killing Appium server..."); appiumProcess.kill(); }); appiumProcess.on("close", (code) => { logger_1.logger.log(`Appium server exited with code ${code}`); }); }); } function stopAppiumServer() { return new Promise((resolve, reject) => { (0, child_process_1.exec)(`pkill -f appium`, (error, stdout) => { if (error) { logger_1.logger.error(`Error stopping Appium server: ${error.message}`); reject(error); } logger_1.logger.log("Appium server stopped successfully."); resolve(stdout); }); }); } function isEmulatorInstalled(platform) { return new Promise((resolve) => { if (platform == types_1.Platform.ANDROID) { const androidHome = process.env.ANDROID_HOME; const emulatorPath = path_1.default.join(androidHome, "emulator", "emulator"); (0, child_process_1.exec)(`${emulatorPath} -list-avds`, (error, stdout, stderr) => { if (error) { throw new Error(`Error fetching emulator list.\nPlease install emulator from Android SDK Tools. Follow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`); } if (stderr) { logger_1.logger.error(`Emulator: ${stderr}`); } const lines = stdout.trim().split("\n"); const deviceNames = lines.filter((line) => line.trim() && !line.startsWith("INFO") && !line.includes("/tmp/")); if (deviceNames.length > 0) { resolve(true); } else { throw new Error(`No installed emulators found. Follow this guide to install emulators: https://community.neptune-software.com/topics/tips--tricks/blogs/how-to-install--android-emulator-without--android--st`); } }); } }); } async function startAndroidEmulator() { return new Promise((resolve, reject) => { const androidHome = process.env.ANDROID_HOME; const emulatorPath = path_1.default.join(androidHome, "emulator", "emulator"); (0, child_process_1.exec)(`${emulatorPath} -list-avds`, (error, stdout, stderr) => { if (error) { 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`); } if (stderr) { logger_1.logger.error(`Emulator: ${stderr}`); } const lines = stdout.trim().split("\n"); // Filter out lines that do not contain device names const deviceNames = lines.filter((line) => line.trim() && !line.startsWith("INFO") && !line.includes("/tmp/")); if (deviceNames.length === 0) { 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`); } else { logger_1.logger.log(`Available Emulators: ${deviceNames}`); } const emulatorToStart = deviceNames[0]; const emulatorProcess = (0, child_process_1.spawn)(emulatorPath, ["-avd", emulatorToStart], { stdio: "pipe", }); emulatorProcess.stdout?.on("data", (data) => { logger_1.logger.log(`Emulator: ${data}`); if (data.includes("Successfully loaded snapshot 'default_boot'")) { logger_1.logger.log("Emulator started successfully."); resolve(); } }); emulatorProcess.on("error", (err) => { logger_1.logger.error(`Emulator: ${err.message}`); reject(`Failed to start emulator: ${err.message}`); }); emulatorProcess.on("close", (code) => { if (code !== 0) { reject(`Emulator process exited with code: ${code}`); } }); // Ensure the emulator process is killed when the main process exits process.on("exit", () => { logger_1.logger.log("Main process exiting. Killing the emulator process..."); emulatorProcess.kill(); }); }); }); } function getAppBundleId(path) { return new Promise((resolve, reject) => { const command = `osascript -e 'id of app "${path}"'`; (0, child_process_1.exec)(command, (error, stdout, stderr) => { if (error) { logger_1.logger.error("osascript:", error.message); return reject(error); } if (stderr) { logger_1.logger.error(`osascript: ${stderr}`); return reject(new Error(stderr)); } const bundleId = stdout.trim(); if (bundleId) { resolve(bundleId); } else { reject(new Error("Bundle ID not found")); } }); }); } async function getConnectedIOSDeviceUDID() { try { const { stdout } = await execPromise(`xcrun xctrace list devices`); const iphoneDevices = stdout .split("\n") .filter((line) => line.includes("iPhone")); const realDevices = iphoneDevices.filter((line) => !line.includes("Simulator")); if (!realDevices.length) { throw new Error(`No connected iPhone detected. Please ensure your device is connected and try again.`); } const deviceLine = realDevices[0]; //the output from above looks like this: User’s iPhone (18.0) (00003110-002A304e3A53C41E) //where `00003110-000A304e3A53C41E` is the UDID of the device const matches = deviceLine.match(/\(([\da-fA-F-]+)\)$/); if (matches && matches[1]) { return matches[1]; } else { throw new Error(`Please check your iPhone device connection. To check for connected devices run "xcrun xctrace list devices | grep iPhone | grep -v Simulator"`); } } catch (error) { //@ts-ignore throw new Error(`getConnectedIOSDeviceUDID: ${error.message}`); } } async function getActiveAndroidDevices() { try { const { stdout } = await execPromise("adb devices"); const lines = stdout.trim().split("\n"); const deviceLines = lines.filter((line) => line.includes("\tdevice")); return deviceLines.length; } catch (error) { throw new Error( //@ts-ignore `getActiveAndroidDevices: ${error.message}`); } } async function getLatestBuildToolsVersion() { const androidHome = process.env.ANDROID_HOME; const buildToolsPath = path_1.default.join(androidHome, "build-tools"); try { const files = await promises_1.default.readdir(buildToolsPath); const versions = files.filter((file) => /^\d+\.\d+\.\d+(-rc\d+)?$/.test(file)); if (versions.length === 0) { throw new Error(`No valid build-tools found in ${buildToolsPath}. Please download from Android Studio: https://developer.android.com/studio/intro/update#required`); } return (0, utils_1.getLatestBuildToolsVersions)(versions); } catch (err) { logger_1.logger.error(`getLatestBuildToolsVersion: ${err}`); throw new Error(`Error reading ${buildToolsPath}. Ensure it exists or download from Android Studio: https://developer.android.com/studio/intro/update#required`); } } async function getApkDetails(buildPath) { const androidHome = process.env.ANDROID_HOME; const buildToolsVersion = await getLatestBuildToolsVersion(); if (!buildToolsVersion) { throw new Error(`No valid build-tools found in ${buildToolsVersion}. Please download from Android Studio: https://developer.android.com/studio/intro/update#required`); } const aaptPath = path_1.default.join(androidHome, "build-tools", buildToolsVersion, "aapt"); const command = `${aaptPath} dump badging ${buildPath}`; try { const { stdout, stderr } = await execPromise(command); if (stderr) { logger_1.logger.error(`getApkDetails: ${stderr}`); throw new Error(`Error executing aapt: ${stderr}`); } const packageMatch = stdout.match(/package: name='(\S+)'/); const activityMatch = stdout.match(/launchable-activity: name='(\S+)'/); if (!packageMatch || !activityMatch) { throw new Error(`Unable to retrieve package or launchable activity from the APK. Please verify that the provided file is a valid APK.`); } const packageName = packageMatch[1]; const launchableActivity = activityMatch[1]; return { packageName, launchableActivity }; } catch (error) { throw new Error(`getApkDetails: ${error.message}`); } }