Files
Mykola Mokhnach ae549c8613 feat(plugin-test-support)!: Migrate to ESM (#22592)
BREAKING CHANGE: @appium/plugin-test-support is now an ESM-only package. It can no longer be loaded via CommonJS require(); consumers must use import or dynamic import(). Deep imports into the package's internals are no longer possible — only the public entry point is exposed via exports.
2026-08-04 23:46:58 +02:00

170 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {createRequire} from 'node:module';
import net from 'node:net';
/* eslint-disable no-console */
import type {AppiumServer} from '@appium/types';
import {main as appiumServer} from 'appium';
import {fs} from 'appium/support.js';
import AsyncLock from 'async-lock';
import {exec} from 'teen_process';
import type {AppiumEnv, E2ESetupOpts} from './types.js';
const _require = createRequire(import.meta.url);
const APPIUM_BIN = _require.resolve('appium') as string;
const lock = new AsyncLock();
const logSymbols = {
info: 'ℹ',
success: '✔',
warning: '⚠',
error: '✖',
} as const;
/**
* Creates hooks to install a driver and a plugin and starts an Appium server w/ the given extensions.
*
* @param opts - Options for the plugin E2E harness
* @returns An object with `setup` and `teardown` callbacks
* @throws {Error} If a free port could not be found
* @throws {Error} If the Appium server could not be started
* @throws {Error} If the driver could not be installed
* @throws {Error} If the plugin could not be installed
*/
export function pluginE2EHarness(opts: E2ESetupOpts): {
setup: () => Promise<{
server: AppiumServer;
}>;
teardown: () => Promise<void>;
} {
const {
appiumHome,
serverArgs = {},
driverSource,
driverPackage,
driverName,
driverSpec,
pluginSource,
pluginPackage,
pluginSpec,
pluginName,
port,
host,
} = opts;
let server: AppiumServer | undefined;
const setup = async function setup() {
const setupAppiumHome = async (): Promise<AppiumEnv> => {
const env: AppiumEnv = {...process.env};
if (appiumHome) {
env.APPIUM_HOME = appiumHome;
await fs.mkdirp(appiumHome);
console.log(`${logSymbols.info} Set \`APPIUM_HOME\` to ${appiumHome}`);
}
return env;
};
const installDriver = async (env: AppiumEnv): Promise<void> => {
console.log(`${logSymbols.info} Checking if driver "${driverName}" is installed...`);
const driverListArgs = [APPIUM_BIN, 'driver', 'list', '--json'];
console.log(`${logSymbols.info} Running: ${process.execPath} ${driverListArgs.join(' ')}`);
const {stdout: driverListJson} = await exec(process.execPath, driverListArgs, {
env,
});
const installedDrivers = JSON.parse(driverListJson) as Record<string, {installed?: boolean}>;
if (!installedDrivers[driverName]?.installed) {
console.log(`${logSymbols.warning} Driver "${driverName}" not installed; installing...`);
const driverArgs = [APPIUM_BIN, 'driver', 'install', '--source', driverSource, driverSpec];
if (driverPackage) {
driverArgs.push('--package', driverPackage);
}
console.log(`${logSymbols.info} Running: ${process.execPath} ${driverArgs.join(' ')}`);
await exec(process.execPath, driverArgs, {
env,
});
}
console.log(`${logSymbols.success} Installed driver "${driverName}"`);
};
const installPlugin = async (env: AppiumEnv): Promise<void> => {
console.log(`${logSymbols.info} Checking if plugin "${pluginName}" is installed...`);
const pluginListArgs = [APPIUM_BIN, 'plugin', 'list', '--json'];
const {stdout: pluginListJson} = await exec(process.execPath, pluginListArgs, {
env,
});
const installedPlugins = JSON.parse(pluginListJson) as Record<string, {installed?: boolean}>;
if (!installedPlugins[pluginName]?.installed) {
console.log(`${logSymbols.warning} Plugin "${pluginName}" not installed; installing...`);
const pluginArgs = [APPIUM_BIN, 'plugin', 'install', '--source', pluginSource, pluginSpec];
if (pluginPackage) {
pluginArgs.push('--package', pluginPackage);
}
console.log(`${logSymbols.info} Running: ${process.execPath} ${pluginArgs.join(' ')}`);
await exec(process.execPath, pluginArgs, {
env,
});
}
console.log(`${logSymbols.success} Installed plugin "${pluginName}"`);
};
const startAppiumServer = async (): Promise<void> => {
const resolvedPort = port ?? (await getPort());
console.log(`${logSymbols.info} Will use port ${resolvedPort} for Appium server`);
const args = {
port: resolvedPort,
address: host,
usePlugins: [pluginName],
useDrivers: [driverName],
appiumHome,
...serverArgs,
};
server = (await appiumServer(args)) as AppiumServer;
};
const env = await setupAppiumHome();
await installDriver(env);
await installPlugin(env);
await startAppiumServer();
return {server: server as AppiumServer};
};
const teardown = async function teardown() {
await server?.close();
};
return {
setup,
teardown,
};
}
/**
* Returns a first available free port number on the local machine.
* The function call is race-free and thread-safe.
*
* @returns Port number
* @throws {Error} If a free port could not be found
*/
export async function getPort(): Promise<number> {
return await lock.acquire('getPort', async () => {
const server = net.createServer();
return await new Promise<number>((resolve, reject) => {
server.once('error', reject);
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Could not resolve a free port')));
return;
}
server.close((err) => (err ? reject(err) : resolve(address.port)));
});
});
});
}