appium-geckodriver
Version:
Appium driver for Gecko-based browsers and web views
155 lines (135 loc) • 4.64 kB
text/typescript
import type {
RouteMatcher,
DefaultCreateSessionResult,
InitialOpts,
StringRecord,
ExternalDriver,
W3CDriverCaps,
} from '@appium/types';
import {BaseDriver, errors} from 'appium/driver';
import {GECKO_SERVER_HOST, GeckoDriverServer} from './gecko';
import {desiredCapConstraints} from './desired-caps';
import {INSECURE_FEAT_CUSTOM_GECKODRIVER_EXECUTABLE} from './constants';
import * as findCommands from './commands/find';
import {formatCapsForServer} from './utils';
const NO_PROXY: RouteMatcher[] = [
['GET', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/element/[^/]+/elements?$')],
['POST', new RegExp('^/session/[^/]+/elements?$')],
];
export type GeckoConstraints = typeof desiredCapConstraints;
export class GeckoDriver
extends BaseDriver<GeckoConstraints, StringRecord>
implements ExternalDriver<GeckoConstraints, string, StringRecord>
{
public proxyReqRes: (...args: any) => any = null as any;
findElOrEls = findCommands.findElOrEls;
private isProxyActive: boolean = false;
private _gecko: GeckoDriverServer | null = null;
private _bidiProxyUrl: string | null = null;
constructor(opts: InitialOpts = {} as InitialOpts) {
super(opts);
this.desiredCapConstraints = structuredClone(desiredCapConstraints);
this.locatorStrategies = [
'xpath',
'tag name',
'link text',
'partial link text',
'css selector',
// Let these two reach Gecko Driver and fail there with a proper error message
'id',
'name',
];
this.resetState();
}
get gecko(): GeckoDriverServer {
if (!this._gecko) {
throw new Error('Gecko driver is not initialized');
}
return this._gecko;
}
override get bidiProxyUrl(): string | null {
return this._bidiProxyUrl;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override proxyActive(sessionId?: string): boolean {
return this.isProxyActive;
}
override getProxyAvoidList(): RouteMatcher[] {
return NO_PROXY;
}
override canProxy(): boolean {
return true;
}
override validateDesiredCaps(caps: any): caps is any {
const isValid = super.validateDesiredCaps(caps);
if (!isValid) {
return false;
}
if (
caps.geckodriverExecutable &&
!this.isFeatureEnabled(INSECURE_FEAT_CUSTOM_GECKODRIVER_EXECUTABLE)
) {
throw new errors.SessionNotCreatedError(
`The 'geckodriverExecutable' capability requires the ` +
`'${INSECURE_FEAT_CUSTOM_GECKODRIVER_EXECUTABLE}' insecure feature to be enabled ` +
`on the Appium server.`,
);
}
return true;
}
override async createSession(
w3cCaps1: W3CDriverCaps<GeckoConstraints>,
w3cCaps2?: W3CDriverCaps<GeckoConstraints>,
...args: any[]
): Promise<DefaultCreateSessionResult<GeckoConstraints>> {
const [sessionId, processedCaps] = await super.createSession(w3cCaps1, w3cCaps2, ...args);
this._gecko = new GeckoDriverServer(this.log, processedCaps);
let response: StringRecord;
try {
response = await this._gecko.start(formatCapsForServer(processedCaps), {
reqBasePath: this.basePath,
});
} catch (e) {
await this.deleteSession();
throw e;
}
this.proxyReqRes = this._gecko.proxy.proxyReqRes.bind(this._gecko.proxy);
this._bidiProxyUrl = this._extractWebSocketUrl(response);
if (this._bidiProxyUrl) {
this.log.info(`Set proxy BiDi URL to ${this._bidiProxyUrl}`);
}
this.isProxyActive = true;
return [sessionId, processedCaps];
}
override async deleteSession(): Promise<void> {
this.log.info('Ending Gecko Driver session');
await this._gecko?.stop();
this.resetState();
await super.deleteSession();
}
private resetState(): void {
this._gecko = null;
this.proxyReqRes = null as any;
this.isProxyActive = false;
this._bidiProxyUrl = null;
}
private _extractWebSocketUrl(response: StringRecord): string | null {
const webSocketUrl = (response?.capabilities as any)?.webSocketUrl;
if (typeof webSocketUrl !== 'string' || webSocketUrl.length === 0) {
return null;
}
try {
const asUrl = new URL(webSocketUrl);
return asUrl.hostname !== GECKO_SERVER_HOST
? webSocketUrl.replace(asUrl.host, GECKO_SERVER_HOST)
: webSocketUrl;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
this.log.warn(`Failed to parse WebSocket URL from '${webSocketUrl}': ${msg}`);
return null;
}
}
}
export default GeckoDriver;