appium-chromium-driver
Version:
Appium driver for Chromium-based browsers that work with Chromedriver
245 lines (221 loc) • 7.98 kB
text/typescript
import type {
DefaultCreateSessionResult,
DriverData,
ExternalDriver,
HTTPMethod,
InitialOpts,
StringRecord,
} from '@appium/types';
import {BaseDriver, STANDARD_CAPS} from 'appium/driver';
import {Chromedriver, type ChromedriverOpts} from 'appium-chromedriver';
import {detectChromeBrowserVersion} from './chrome';
import {desiredCapConstraints, type CDConstraints} from './desired-caps';
import * as msedge from './msedge/index';
import type {W3CChromiumDriverCaps, ChromiumDriverCaps, BrowserInfo} from './types';
import path from 'node:path';
const STANDARD_CAPS_LOWER = new Set([...STANDARD_CAPS].map((cap) => cap.toLowerCase()));
const CHROME_VENDOR_PREFIX = 'goog:';
const EDGE_VENDOR_PREFIX = 'ms:';
interface BrowserDriverStrategy {
discoverBrowserVersion(browserBinary?: string): Promise<string>;
resolveExecutable(
browserVersionInfo: BrowserInfo | undefined,
isAutodownloadEnabled: boolean,
): Promise<string | undefined>;
getDefaultExecutableDir(): string;
}
export class ChromiumDriver
extends BaseDriver<CDConstraints, StringRecord>
implements ExternalDriver<CDConstraints, string, StringRecord>
{
desiredCapConstraints = desiredCapConstraints;
proxyReqRes: ((...args: any[]) => any) | null = null;
proxyCommand?: <TReq = any, TRes = unknown>(
url: string,
method: HTTPMethod,
body?: TReq,
) => Promise<TRes>;
doesSupportBidi = true;
private _proxyActive = false;
private _cd: Chromedriver | null = null;
private _bidiProxyUrl: string | null = null;
constructor(opts: InitialOpts = {} as InitialOpts) {
super(opts);
}
get bidiProxyUrl(): string | null {
return this._bidiProxyUrl;
}
get cd(): Chromedriver {
if (!this._cd) {
throw new Error('Chromedriver not started');
}
return this._cd;
}
override proxyActive(): boolean {
return this._proxyActive;
}
override canProxy(): boolean {
return true;
}
override validateDesiredCaps(caps: any): caps is ChromiumDriverCaps {
return super.validateDesiredCaps(this.excludeBrowserPrefixCaps(caps));
}
override async createSession(
jsonwpDesiredCapabilities: W3CChromiumDriverCaps,
jsonwpRequiredCaps?: W3CChromiumDriverCaps,
w3cCapabilities?: W3CChromiumDriverCaps,
driverData?: DriverData[],
): Promise<DefaultCreateSessionResult<CDConstraints>> {
const [sessionId] = await super.createSession(
jsonwpDesiredCapabilities,
jsonwpRequiredCaps,
w3cCapabilities,
driverData,
);
const returnedCaps = await this.startChromedriverSession();
if (returnedCaps.webSocketUrl) {
this._bidiProxyUrl = String(returnedCaps.webSocketUrl);
}
return [sessionId, returnedCaps];
}
async startChromedriverSession(): Promise<ChromiumDriverCaps> {
const isAutodownloadEnabled = this.opts.autodownloadEnabled ?? true;
const browserVersionInfo = await this.getBrowserInfo();
const cdOpts: ChromedriverOpts = {
port: this.opts.chromedriverPort?.toString(),
useSystemExecutable: this.opts.useSystemExecutable,
executable: await this.getExecutable(browserVersionInfo, isAutodownloadEnabled),
executableDir: this.getExecutableDir(),
verbose: this.opts.verbose,
logPath: this.opts.logPath,
disableBuildCheck: this.opts.disableBuildCheck,
details: browserVersionInfo,
isAutodownloadEnabled,
};
if (this.basePath) {
cdOpts.reqBasePath = this.basePath;
}
this._cd = new Chromedriver(cdOpts);
const cdStartRes = (await this._cd.start(this.getSessionCaps())) as ChromiumDriverCaps;
this._proxyActive = true;
this.proxyReqRes = this._cd.proxyReq.bind(this._cd);
this.proxyCommand = this._cd.sendCommand.bind(this._cd);
return cdStartRes;
}
override async deleteSession(sessionId?: string): Promise<void> {
try {
await super.deleteSession(sessionId);
} finally {
this._proxyActive = false;
this._bidiProxyUrl = null;
this.proxyReqRes = null;
this.proxyCommand = undefined;
if (this._cd) {
try {
await this._cd.stop();
} catch (err) {
this.log.warn(`Failed to stop Chromedriver: ${(err as Error).message}`);
}
this._cd = null;
}
}
}
/**
* Exclude browser-specific capabilities (e.g. `goog:chromeOptions` and `ms:edgeOptions`)
* from the capabilities to skip validation error for unrecognized capabilities.
* @param caps
* @returns
*/
private excludeBrowserPrefixCaps(caps: Record<string, any>): Record<string, any> {
const browserCapKeys = Object.keys(caps).filter(
(key) => key.startsWith(CHROME_VENDOR_PREFIX) || key.startsWith(EDGE_VENDOR_PREFIX),
);
return Object.keys(caps).reduce(
(acc, capName) => {
if (!browserCapKeys.includes(capName)) {
acc[capName] = caps[capName];
}
return acc;
},
{} as Record<string, any>,
);
}
private async getBrowserInfo(): Promise<BrowserInfo | undefined> {
const opts = this.opts as StringRecord;
const browserBinary: string | undefined =
(opts['goog:chromeOptions'] as Record<string, any>)?.binary ??
(opts['ms:edgeOptions'] as Record<string, any>)?.binary;
try {
const bv = await this.getBrowserDriverStrategy().discoverBrowserVersion(browserBinary);
this.log.info(`Detected browser version: ${bv}`);
return {info: {Browser: bv}};
} catch (err) {
this.log.warn(`Failed to get browser version from binary: ${(err as Error).message}`);
}
}
/**
* FIXME: Please use this driver's local storage instead of the node_modules path
* to avoid potential read-only issue.
* Please update the `appium driver run chromium install-chromedriver` command behavior
* also to reflect the change.
* This change is a breaking change.
*/
private getDefaultChromeDriverDir(): string {
const pkgJson = require.resolve('appium-chromedriver/package.json');
const packageDir = path.dirname(pkgJson);
return path.join(packageDir, 'chromedriver');
}
private async getExecutable(
browserVersionInfo?: BrowserInfo | undefined,
isAutodownloadEnabled: boolean = true,
): Promise<string | undefined> {
if (this.opts.executable) {
return this.opts.executable;
}
return await this.getBrowserDriverStrategy().resolveExecutable(
browserVersionInfo,
isAutodownloadEnabled,
);
}
private getExecutableDir(): string | undefined {
if (this.opts.executableDir) {
return this.opts.executableDir;
}
return this.getBrowserDriverStrategy().getDefaultExecutableDir();
}
private getBrowserDriverStrategy(): BrowserDriverStrategy {
if (msedge.isMsEdge(this.opts.browserName)) {
return {
discoverBrowserVersion: async (browserBinary?: string) =>
await msedge.discoverMsEdgeBrowserVersion(browserBinary),
resolveExecutable: async (browserVersionInfo, isAutodownloadEnabled) =>
await msedge.determineDriverExecutable(
this.opts,
browserVersionInfo,
isAutodownloadEnabled,
),
getDefaultExecutableDir: () => msedge.getDefaultDriverDir(),
};
}
return {
discoverBrowserVersion: async (browserBinary?: string) =>
await detectChromeBrowserVersion(browserBinary),
resolveExecutable: async () => undefined,
getDefaultExecutableDir: () => this.getDefaultChromeDriverDir(),
};
}
private getSessionCaps(): StringRecord {
const opts = this.opts as StringRecord;
return Object.keys(opts).reduce((acc, capName) => {
if (
STANDARD_CAPS_LOWER.has(capName.toLowerCase()) ||
capName.startsWith(CHROME_VENDOR_PREFIX) ||
capName.startsWith(EDGE_VENDOR_PREFIX)
) {
acc[capName] = opts[capName];
}
return acc;
}, {} as StringRecord);
}
}
export default ChromiumDriver;