Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Determine chromedriver version from the /status API output #456

Merged
merged 10 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be nice to add ms:edgeOptions for MSEdge as well?
MSEdge supports chormeOptions naming still, so I don't have any strong opinion for this. I don't remember well, but probably edge prior edgeOptions than chormeOptions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edgeOptions map does not contain the w3c flag, so I assume this logic could remain unchanged for now: https://learn.microsoft.com/de-de/microsoft-edge/webdriver-chromium/capabilities-edge-options

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

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
Loading