diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 0c39e2ee..3f2434c7 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -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" @@ -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 diff --git a/lib/chromedriver.js b/lib/chromedriver.js index b67d4b1d..4e7de5c7 100644 --- a/lib/chromedriver.js +++ b/lib/chromedriver.js @@ -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'; @@ -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; @@ -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 | 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) { @@ -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 @@ -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; } /** @@ -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(); @@ -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}`); @@ -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 && @@ -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); @@ -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.'); @@ -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) { diff --git a/lib/protocol-helpers.js b/lib/protocol-helpers.js index 02352cf8..4cc03464 100644 --- a/lib/protocol-helpers.js +++ b/lib/protocol-helpers.js @@ -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; @@ -17,8 +17,8 @@ function toW3cCapName (capName) { * * @param {Record} 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)) { diff --git a/test/functional/chromedriver-e2e-specs.js b/test/functional/chromedriver-e2e-specs.js index 59e2fe0f..0b70e21e 100644 --- a/test/functional/chromedriver-e2e-specs.js +++ b/test/functional/chromedriver-e2e-specs.js @@ -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 () { diff --git a/test/unit/chromedriver-specs.js b/test/unit/chromedriver-specs.js index 92d4cba7..d90eb060 100644 --- a/test/unit/chromedriver-specs.js +++ b/test/unit/chromedriver-specs.js @@ -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; @@ -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;