Skip to content

Commit

Permalink
feat: Determine chromedriver version from the /status API output (#456)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The detectWebDriverProtocol method of the Chromedriver class has been removed
BREAKING CHANGE: The desiredProtocol property of the Chromedriver class has been renamed to _desiredProtocol
BREAKING CHANGE: The driverVersion property of the Chromedriver has been changed to a getter
  • Loading branch information
mykola-mokhnach authored Jan 24, 2025
1 parent 5a066ac commit 797c8cc
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 144 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: lts/*
- run: npm install
name: Install dev dependencies
- run: |
export CHROME_VERSION=$(google-chrome --version | python -c "import sys, re; print(re.search(r'[0-9.]+', sys.stdin.read()).group(0))")
echo "Version number of the installed Chrome browser: $CHROME_VERSION"
Expand All @@ -24,9 +26,6 @@ jobs:
export CHROMEDRIVER_VERSION=$(grep -m 1 -n "$MAJOR_CHROME_VERSION" config/mapping.json | cut -d' ' -f4 | tr -d ',"')
echo "Matching Chromedriver version: $CHROMEDRIVER_VERSION"
fi
npm install
name: Install dev dependencies
- run: |
sudo Xvfb -ac $DISPLAY -screen 0 1280x1024x24 > /dev/null 2>&1 &
npm run e2e-test
name: Run functional tests
Expand Down
149 changes: 67 additions & 82 deletions lib/chromedriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import _ from 'lodash';
import path from 'path';
import {compareVersions} from 'compare-versions';
import { ChromedriverStorageClient } from './storage-client/storage-client';
import {toW3cCapNames, getCapValue} from './protocol-helpers';
import {toW3cCapNames, getCapValue, toW3cCapName} from './protocol-helpers';

const NEW_CD_VERSION_FORMAT_MAJOR_VERSION = 73;
const DEFAULT_HOST = '127.0.0.1';
Expand All @@ -27,7 +27,6 @@ const CHROME_BUNDLE_ID = 'com.android.chrome';
const WEBVIEW_SHELL_BUNDLE_ID = 'org.chromium.webview_shell';
const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview'];
const VERSION_PATTERN = /([\d.]+)/;
const WEBDRIVER_VERSION_PATTERN = /Starting (ChromeDriver|Microsoft Edge WebDriver) ([.\d]+)/;

const CD_VERSION_TIMEOUT = 5000;

Expand Down Expand Up @@ -92,17 +91,27 @@ export class Chromedriver extends events.EventEmitter {
this.details = details;
/** @type {any} */
this.capabilities = {};
/** @type {keyof PROTOCOLS} */
this.desiredProtocol = PROTOCOLS.MJSONWP;
/** @type {keyof PROTOCOLS | null} */
this._desiredProtocol = null;

// Store the running driver version
this.driverVersion = /** @type {string|null} */ null;
/** @type {string|null} */
this._driverVersion = null;
/** @type {Record<string, any> | null} */
this._onlineStatus = null;
}

get log() {
return this._log;
}

/**
* @returns {string | null}
*/
get driverVersion() {
return this._driverVersion;
}

async getDriversMapping() {
let mapping = _.cloneDeep(CHROMEDRIVER_CHROME_MAPPING);
if (this.mappingPath) {
Expand Down Expand Up @@ -477,8 +486,8 @@ export class Chromedriver extends events.EventEmitter {
`capable of automating Chrome '${chromeVersion}'.\nChoosing the most recent, '${binPath}'.`,
);
this.log.debug(
'If a specific version is required, specify it with the `chromedriverExecutable`' +
'desired capability.',
`If a specific version is required, specify it with the 'chromedriverExecutable'` +
` capability.`,
);
return binPath;
// eslint-disable-next-line no-constant-condition
Expand Down Expand Up @@ -512,72 +521,45 @@ export class Chromedriver extends events.EventEmitter {
}

/**
* Sync the WebDriver protocol if current on going protocol is W3C or MJSONWP.
* Does nothing if this.driverVersion is null.
* Determines the driver communication protocol
* based on various validation rules.
*
* @returns {typeof PROTOCOLS[keyof typeof PROTOCOLS]}
* @returns {keyof PROTOCOLS}
*/
syncProtocol() {
if (!this.driverVersion) {
// Keep the default protocol if the driverVersion was unsure.
return this.desiredProtocol;
if (this.driverVersion) {
const coercedVersion = semver.coerce(this.driverVersion);
if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) {
this.log.info(
`The ChromeDriver v. ${this.driverVersion} does not fully support ${PROTOCOLS.W3C} protocol. ` +
`Defaulting to ${PROTOCOLS.MJSONWP}`,
);
this._desiredProtocol = PROTOCOLS.MJSONWP;
return this._desiredProtocol;
}
}

this.desiredProtocol = PROTOCOLS.MJSONWP;
const coercedVersion = semver.coerce(this.driverVersion);
if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) {
this.log.info(
`The ChromeDriver v. ${this.driverVersion} does not fully support ${PROTOCOLS.W3C} protocol. ` +
`Defaulting to ${PROTOCOLS.MJSONWP}`,
);
return this.desiredProtocol;
}
// Check only chromeOptions for now.
const chromeOptions = getCapValue(this.capabilities, 'chromeOptions', {});
if (chromeOptions.w3c === false) {
const isOperaDriver = _.includes(this._onlineStatus?.message, 'OperaDriver');
const chromeOptions = getCapValue(this.capabilities, 'chromeOptions');
if (_.isPlainObject(chromeOptions) && chromeOptions.w3c === false) {
this.log.info(
`The ChromeDriver v. ${this.driverVersion} supports ${PROTOCOLS.W3C} protocol, ` +
`but ${PROTOCOLS.MJSONWP} one has been explicitly requested`,
);
return this.desiredProtocol;
}

this.desiredProtocol = PROTOCOLS.W3C;
this.log.info(`Set ChromeDriver communication protocol to ${PROTOCOLS.W3C}`);
return this.desiredProtocol;
}

/**
* Sync the protocol by reading the given output
*
* @param {string} line The output of ChromeDriver process
* @returns {typeof PROTOCOLS[keyof typeof PROTOCOLS] | null}
*/
detectWebDriverProtocol(line) {
if (this.driverVersion) {
return this.syncProtocol();
}

// also print chromedriver version to logs
// will output something like
// Starting ChromeDriver 2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2) on port 9515
// Or MSEdge:
// Starting Microsoft Edge WebDriver 111.0.1661.41 (57be51b50d1be232a9e8186a10017d9e06b1fd16) on port 9515
const match = WEBDRIVER_VERSION_PATTERN.exec(line);
if (match && match.length === 3) {
this.log.debug(`${match[1]} version: '${match[2]}'`);
this.driverVersion = match[2];
try {
return this.syncProtocol();
} catch (e) {
this.driverVersion = null;
this.log.error(`Stopping the chromedriver process. Cannot determinate the protocol: ${e}`);
this.stop();
this._desiredProtocol = PROTOCOLS.MJSONWP;
return this._desiredProtocol;
} else if (isOperaDriver) {
// OperaDriver needs the W3C protocol to be requested explcitly,
// otherwise it defaults to JWP
if (_.isPlainObject(chromeOptions)) {
chromeOptions.w3c = true;
} else {
this.capabilities[toW3cCapName('chromeOptions')] = {w3c: true};
}
// Does not print else condition log since the log could be
// very noisy when this.verbose option is true.
}
return null;

this._desiredProtocol = PROTOCOLS.W3C;
return this._desiredProtocol;
}

/**
Expand Down Expand Up @@ -619,7 +601,6 @@ export class Chromedriver extends events.EventEmitter {
let processIsAlive = false;
/** @type {string|undefined} */
let webviewVersion;
let didDetectProtocol = false;
try {
const chromedriverPath = await this.initChromedriverPath();
await this.killAll();
Expand Down Expand Up @@ -648,17 +629,6 @@ export class Chromedriver extends events.EventEmitter {
}
}

if (!didDetectProtocol) {
const proto = this.detectWebDriverProtocol(line);
if (proto === PROTOCOLS.W3C) {
// given caps might not be properly prefixed
// so we try to fix them in order to properly init
// the new W3C session
this.capabilities = toW3cCapNames(this.capabilities);
}
didDetectProtocol = true;
}

if (this.verbose) {
// give the output if it is requested
this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
Expand All @@ -668,7 +638,9 @@ export class Chromedriver extends events.EventEmitter {

// handle out-of-bound exit by simply emitting a stopped state
this.proc.once('exit', (code, signal) => {
this.driverVersion = null;
this._driverVersion = null;
this._desiredProtocol = null;
this._onlineStatus = null;
processIsAlive = false;
if (
this.state !== Chromedriver.STATE_STOPPED &&
Expand All @@ -682,10 +654,11 @@ export class Chromedriver extends events.EventEmitter {
this.proc?.removeAllListeners();
this.proc = null;
});
this.log.info(`Spawning chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
// start subproc and wait for startDetector
await this.proc.start(startDetector);
await this.waitForOnline();
this.syncProtocol();
return await this.startSession();
} catch (e) {
const err = /** @type {Error} */ (e);
Expand Down Expand Up @@ -743,7 +716,19 @@ export class Chromedriver extends events.EventEmitter {
chromedriverStopped = true;
return;
}
await this.getStatus();
/** @type {any} */
const status = await this.getStatus();
if (!_.isPlainObject(status) || !status.ready) {
throw new Error(`The response to the /status API is not valid: ${JSON.stringify(status)}`);
}
this._onlineStatus = status;
const versionMatch = VERSION_PATTERN.exec(status.build?.version ?? '');
if (versionMatch) {
this._driverVersion = versionMatch[1];
this.log.info(`Chromedriver version: ${this._driverVersion}`);
} else {
this.log.info('Chromedriver version cannot be determined from the /status API response');
}
});
if (chromedriverStopped) {
throw new Error('ChromeDriver crashed during startup.');
Expand All @@ -756,19 +741,19 @@ export class Chromedriver extends events.EventEmitter {

async startSession() {
const sessionCaps =
this.desiredProtocol === PROTOCOLS.W3C
? {capabilities: {alwaysMatch: this.capabilities}}
this._desiredProtocol === PROTOCOLS.W3C
? {capabilities: {alwaysMatch: toW3cCapNames(this.capabilities)}}
: {desiredCapabilities: this.capabilities};
this.log.info(
`Starting ${this.desiredProtocol} Chromedriver session with capabilities: ` +
`Starting ${this._desiredProtocol} Chromedriver session with capabilities: ` +
JSON.stringify(sessionCaps, null, 2),
);
const {capabilities} = /** @type {NewSessionResponse} */ (
const response = /** @type {NewSessionResponse} */ (
await this.jwproxy.command('/session', 'POST', sessionCaps)
);
this.log.prefix = generateLogPrefix(this, this.jwproxy.sessionId);
this.changeState(Chromedriver.STATE_ONLINE);
return capabilities;
return _.has(response, 'capabilities') ? response.capabilities : response;
}

async stop(emitStates = true) {
Expand Down
6 changes: 3 additions & 3 deletions lib/protocol-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const W3C_PREFIX = 'goog:';
*
* @param {string} capName
*/
function toW3cCapName (capName) {
export function toW3cCapName (capName) {
return (_.isString(capName) && !capName.includes(':') && !isStandardCap(capName))
? `${W3C_PREFIX}${capName}`
: capName;
Expand All @@ -17,8 +17,8 @@ function toW3cCapName (capName) {
*
* @param {Record<string,any>} allCaps
* @param {string} rawCapName
* @param {any} defaultValue
* @returns
* @param {any} [defaultValue]
* @returns {any}
*/
function getCapValue (allCaps = {}, rawCapName, defaultValue) {
for (const [capName, capValue] of _.toPairs(allCaps)) {
Expand Down
9 changes: 1 addition & 8 deletions test/functional/chromedriver-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,7 @@ describe('chromedriver binary setup', function () {
chai.should();
chai.use(chaiAsPromised.default);

let cd = new Chromedriver({});
try {
await cd.initChromedriverPath();
} catch (err) {
if (err.message.indexOf('Trying to use') !== -1) {
await install();
}
}
await install();
});

it('should start with a binary that exists', async function () {
Expand Down
49 changes: 1 addition & 48 deletions test/unit/chromedriver-specs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {Chromedriver} from '../../lib/chromedriver';
import * as utils from '../../lib/utils';
import sinon from 'sinon';
import {PROTOCOLS} from '@appium/base-driver';
import {fs} from '@appium/support';
import * as tp from 'teen_process';
import path from 'path';
import _ from 'lodash';
import * as utils from '../../lib/utils';

describe('chromedriver', function () {
let sandbox;
Expand Down Expand Up @@ -280,52 +279,6 @@ describe('chromedriver', function () {
});
});

describe('detectWebDriverProtocol', function () {
it('sync with chrome as mjsonwp', function () {
const cd = new Chromedriver({});
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
(cd.driverVersion === null).should.be.true;
cd.detectWebDriverProtocol(
'Starting ChromeDriver 2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2) on port 9515'
);
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
cd.driverVersion.should.eql('2.33.506106');
});

it('sync with chrome as w3c', function () {
const cd = new Chromedriver({});
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
(cd.driverVersion === null).should.be.true;
cd.detectWebDriverProtocol(
'Starting ChromeDriver 111.0.1661.41 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2) on port 9515'
);
cd.desiredProtocol.should.eql(PROTOCOLS.W3C);
cd.driverVersion.should.eql('111.0.1661.41');
});

it('sync with msedge', function () {
const cd = new Chromedriver({});
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
(cd.driverVersion === null).should.be.true;
cd.detectWebDriverProtocol(
'Starting Microsoft Edge WebDriver 111.0.1661.41 (57be51b50d1be232a9e8186a10017d9e06b1fd16) on port 9515'
);
cd.desiredProtocol.should.eql(PROTOCOLS.W3C);
cd.driverVersion.should.eql('111.0.1661.41');
});

it('sync with unknown driver', function () {
const cd = new Chromedriver({});
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
(cd.driverVersion === null).should.be.true;
cd.detectWebDriverProtocol(
'Starting Unknown WebDriver 111.0.1661.41 (57be51b50d1be232a9e8186a10017d9e06b1fd16) on port 9515'
);
cd.desiredProtocol.should.eql(PROTOCOLS.MJSONWP);
(cd.driverVersion === null).should.be.true;
});
});

describe('getMostRecentChromedriver', function () {
it('should get a value by default', function () {
utils.getMostRecentChromedriver().should.be.a.string;
Expand Down

0 comments on commit 797c8cc

Please sign in to comment.