diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f51c4848..9b5fd194e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,9 @@ jobs: - name: Lint / Typings / Unit / Smoke Tests run: npm run test - name: Interop Tests - run: npm run test:e2e:cjs + run: + npm i chromedriver@104 + npm run test:e2e:cjs e2e-windows: runs-on: windows-latest steps: diff --git a/__mocks__/which.ts b/__mocks__/which.ts new file mode 100644 index 000000000..d0dcec723 --- /dev/null +++ b/__mocks__/which.ts @@ -0,0 +1,6 @@ +import { vi } from 'vitest' + +const which = vi.fn() +// @ts-expect-error +which.sync = vi.fn() +export default which diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 069f5c4f8..0c0663410 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -45,7 +45,8 @@ "puppeteer-core": "^17.0.0", "query-selector-shadow-dom": "^1.0.0", "ua-parser-js": "^1.0.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "which": "^2.0.2" }, "devDependencies": { "@types/node": "^18.0.0", diff --git a/packages/devtools/src/launcher.ts b/packages/devtools/src/launcher.ts index 5195e7600..f7095fc38 100644 --- a/packages/devtools/src/launcher.ts +++ b/packages/devtools/src/launcher.ts @@ -1,4 +1,4 @@ -import { launch as launchChromeBrowser } from 'chrome-launcher' +import { launch as launchChromeBrowser, Options } from 'chrome-launcher' import puppeteer, { PuppeteerLaunchOptions } from 'puppeteer-core' import logger from '@wdio/logger' import type { Browser } from 'puppeteer-core/lib/cjs/puppeteer/common/Browser' @@ -6,7 +6,7 @@ import type { Capabilities } from '@wdio/types' import { QueryHandler } from 'query-selector-shadow-dom/plugins/puppeteer/index.js' import browserFinder from './finder/index.js' -import { getPages } from './utils.js' +import { getPages, launchChromeUsingWhich } from './utils.js' import { CHROME_NAMES, FIREFOX_NAMES, @@ -106,7 +106,7 @@ async function launchChrome (capabilities: ExtendedCapabilities) { } log.info(`Launch Google Chrome with flags: ${chromeFlags.join(' ')}`) - const chrome = await launchChromeBrowser({ + const launchOptions: Options = { prefs: chromeOptions.prefs, chromePath: chromeOptions.binary, ignoreDefaultFlags: true, @@ -114,7 +114,9 @@ async function launchChrome (capabilities: ExtendedCapabilities) { userDataDir, envVars: devtoolsOptions.env, ...(devtoolsOptions.customPort ? { port: devtoolsOptions.customPort } : {}) - }) + } + const chrome = await launchChromeBrowser(launchOptions).catch( + (err: Error) => launchChromeUsingWhich(err, launchOptions)) log.info(`Connect Puppeteer with browser on port ${chrome.port}`) const browser = await puppeteer.connect({ diff --git a/packages/devtools/src/utils.ts b/packages/devtools/src/utils.ts index a738e6462..38ed3dc9c 100644 --- a/packages/devtools/src/utils.ts +++ b/packages/devtools/src/utils.ts @@ -1,10 +1,11 @@ import fs from 'node:fs' import { createRequire } from 'node:module' -import { execFileSync } from 'node:child_process' +import which from 'which' import logger from '@wdio/logger' import { commandCallStructure, isValidParameter, getArgumentType, canAccess } from '@wdio/utils' import { WebDriverProtocol, CommandParameters, CommandPathVariables, ElementReference } from '@wdio/protocols' +import { launch as launchChromeBrowser, Options } from 'chrome-launcher' import type { Logger } from '@wdio/logger/build/node' import type { ElementHandle } from 'puppeteer-core/lib/cjs/puppeteer/common/ElementHandle' import type { Browser } from 'puppeteer-core/lib/cjs/puppeteer/common/Browser' @@ -302,12 +303,7 @@ export function findByWhich (executables: string[], priorities: Priorities[]) { const installations: string[] = [] executables.forEach((executable) => { try { - const browserPath = execFileSync( - 'which', - [executable], - { stdio: 'pipe' } - ).toString().split(/\r?\n/)[0] - + const browserPath = which.sync(executable) if (canAccess(browserPath)) { installations.push(browserPath) } @@ -374,3 +370,29 @@ export async function patchDebug (scoppedLogger: Logger) { export const sleep = (time = 0) => new Promise( (resolve) => setTimeout(resolve, time)) + +export const launchChromeUsingWhich = async (err: Error, launchOptions: Options, ) => { + /** + * we only handle the error if + */ + if ( + /** + * the installation could not be found + */ + !err.message.includes('No Chrome installations found.') || + /** + * the user specified a binary path that could not be found + */ + typeof launchOptions.chromePath === 'string' + ) { + throw err + } + + /** + * try to use node-which to resolve a Chrome path + */ + const chromePath = await which('chrome') + .catch(() => which('chromium') + .catch(() => which('google-chrome'))) + return launchChromeBrowser({ ...launchOptions, chromePath }) +} diff --git a/packages/devtools/tests/utils.test.ts b/packages/devtools/tests/utils.test.ts index 784a2351a..094bf0fc0 100644 --- a/packages/devtools/tests/utils.test.ts +++ b/packages/devtools/tests/utils.test.ts @@ -1,17 +1,20 @@ import { describe, it, expect, afterEach, vi, test } from 'vitest' import path from 'node:path' -import childProcess from 'node:child_process' // @ts-ignore no types needed import debug from 'debug' +import which from 'which' +import { launch } from 'chrome-launcher' import { canAccess } from '@wdio/utils' import { validate, getPrototype, findElement, findElements, getStaleElementError, sanitizeError, transformExecuteArgs, transformExecuteResult, getPages, - uniq, findByWhich, patchDebug, sleep + uniq, findByWhich, patchDebug, sleep, launchChromeUsingWhich } from '../src/utils.js' +vi.mock('which') +vi.mock('chrome-launcher') vi.mock('@wdio/logger', async () => { const pathModule = await vi.importActual('node:path') as typeof path return import(pathModule.join(process.cwd(), '__mocks__', '@wdio/logger')) @@ -85,24 +88,6 @@ vi.mock('fs', () => { } }) -vi.mock('node:child_process', () => { - let returnValue = false - const execFileSync = vi.fn().mockImplementation(() => { - if (!returnValue) { - throw new Error('foo not found') - } - return returnValue - }) - - return { - execFileSync, - default: { - execFileSync, - shouldReturn: (value: any) => (returnValue = value) - } - } -}) - vi.mock('debug', () => ({ default: { log: vi.fn() } })) @@ -400,8 +385,7 @@ test('findByWhich', () => { expect(findByWhich(['firefox'], [{ regex: /firefox/, weight: 51 }])) .toEqual([]) - // @ts-expect-error mock feature - vi.mocked(childProcess).shouldReturn('/path/to/other/firefox\n') + vi.mocked(which.sync).mockReturnValue('/path/to/other/firefox') expect(findByWhich(['firefox'], [{ regex: /firefox/, weight: 51 }])) .toEqual(['/path/to/other/firefox']) @@ -430,3 +414,28 @@ test('sleep', async () => { await sleep(100) expect(Date.now() - start).toBeGreaterThanOrEqual(90) }) + +describe('launchChromeUsingWhich', () => { + it('should throw if error is not related to chrome binary not found', async () => { + await expect(() => launchChromeUsingWhich(new Error('ups'), {})) + .rejects.toThrow('ups') + }) + + it('should throw if user has specified path explicitly', async () => { + await expect(() => launchChromeUsingWhich( + new Error('No Chrome installations found.'), + { chromePath: '/foo/bar' } + )).rejects.toThrow('No Chrome installations found.') + }) + + it('should use which properly to find path', async () => { + vi.mocked(which).mockImplementation(async (binary: string) => { + if (binary === 'google-chrome') { + return '/foo/bar/google-chrome' + } + throw new Error('not found') + }) + await launchChromeUsingWhich(new Error('No Chrome installations found.'), {}) + expect(launch).toBeCalledWith({ chromePath: '/foo/bar/google-chrome' }) + }) +})