appwright
Version:
E2E mobile app testing done right, with the Playwright test runner
275 lines (274 loc) • 12.5 kB
JavaScript
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}`);
}
}
;