From 68b011f8e5d06225c0b3804aa6a814d9c1bfedea Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 27 Jan 2023 18:56:06 +0100 Subject: [PATCH] Various browser runner improvements (#9673) * Various browser runner improvements * fix missing commands * allow HTMLElement set in $$ command * fix unit tests --- __mocks__/yargs.ts | 1 + .../wdio-browser-runner/src/browser/driver.ts | 12 +++++------ .../src/vite/plugins/testrunner.ts | 17 ++++++--------- .../wdio-browser-runner/src/vite/server.ts | 10 ++++++++- .../tests/vite/plugins/testrunner.test.ts | 2 +- packages/wdio-cli/src/run.ts | 3 ++- packages/wdio-runner/src/browser.ts | 8 +++---- .../webdriverio/src/commands/browser/$$.ts | 21 ++++++++++++++----- .../webdriverio/src/utils/getElementObject.ts | 2 +- packages/webdriverio/src/utils/index.ts | 4 ++-- 10 files changed, 48 insertions(+), 32 deletions(-) diff --git a/__mocks__/yargs.ts b/__mocks__/yargs.ts index 1c7ad6003..324f53fb4 100644 --- a/__mocks__/yargs.ts +++ b/__mocks__/yargs.ts @@ -2,6 +2,7 @@ import { vi } from 'vitest' const yargs: any = {} yargs.command = vi.fn(() => yargs) yargs.example = vi.fn(() => yargs) +yargs.version = vi.fn(() => yargs) yargs.hideBin = vi.fn(() => yargs) yargs.argv = { _: ['wdio.conf.js'] } yargs.epilogue = vi.fn().mockReturnValue(yargs) diff --git a/packages/wdio-browser-runner/src/browser/driver.ts b/packages/wdio-browser-runner/src/browser/driver.ts index 67c2035df..61de76fdc 100644 --- a/packages/wdio-browser-runner/src/browser/driver.ts +++ b/packages/wdio-browser-runner/src/browser/driver.ts @@ -1,9 +1,8 @@ import stringify from 'fast-safe-stringify' import { commands } from 'virtual:wdio' -import { webdriverMonad } from '@wdio/utils' +import { webdriverMonad, sessionEnvironmentDetector } from '@wdio/utils' import { getEnvironmentVars } from 'webdriver' -import type { ErrorObject } from 'serialize-error' import { MESSAGE_TYPES } from '../constants.js' import type { SocketMessage, SocketMessagePayload, ConsoleEvent, CommandRequestEvent } from '../vite/types.js' @@ -12,7 +11,7 @@ const COMMAND_TIMEOUT = 30 * 1000 // 30s const CONSOLE_METHODS = ['log', 'info', 'warn', 'error', 'debug'] as const interface CommandMessagePromise { resolve: (value: unknown) => void - reject: (err: ErrorObject) => void + reject: (err: Error) => void commandTimeout?: NodeJS.Timeout } @@ -42,7 +41,8 @@ export default class ProxyDriver { socket.addEventListener('message', this.#handleServerMessage.bind(this)) let commandId = 0 - const environmentPrototype: Record = getEnvironmentVars(params) + const environment = sessionEnvironmentDetector({ capabilities: params.capabilities, requestedCapabilities: {} }) + const environmentPrototype: Record = getEnvironmentVars(environment) // have debug command const commandsProcessedInNodeWorld = [...commands, 'debug'] const protocolCommands = commandsProcessedInNodeWorld.reduce((prev, commandName) => { @@ -126,8 +126,8 @@ export default class ProxyDriver { return console.error(`Unknown command id "${value.id}"`) } if (value.error) { - console.log(`[WDIO] ${(new Date()).toISOString()} - id: ${value.id} - ERROR: ${JSON.stringify(value.result)}`) - return commandMessage.reject(value.error) + console.log(`[WDIO] ${(new Date()).toISOString()} - id: ${value.id} - ERROR: ${JSON.stringify(value.error.message)}`) + return commandMessage.reject(new Error(value.error.message || 'unknown error')) } if (commandMessage.commandTimeout) { clearTimeout(commandMessage.commandTimeout) diff --git a/packages/wdio-browser-runner/src/vite/plugins/testrunner.ts b/packages/wdio-browser-runner/src/vite/plugins/testrunner.ts index c000896ee..73e271773 100644 --- a/packages/wdio-browser-runner/src/vite/plugins/testrunner.ts +++ b/packages/wdio-browser-runner/src/vite/plugins/testrunner.ts @@ -2,6 +2,7 @@ import url from 'node:url' import path from 'node:path' import logger from '@wdio/logger' +import { deepmerge } from 'deepmerge-ts' import { resolve } from 'import-meta-resolve' import type { Plugin } from 'vite' @@ -17,17 +18,11 @@ import { getTemplate, getErrorTemplate } from '../utils.js' const log = logger('@wdio/browser-runner:plugin') const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) -const commands = { - ...WebDriverProtocol, - ...MJsonWProtocol, - ...JsonWProtocol, - ...AppiumProtocol, - ...ChromiumProtocol, - ...SauceLabsProtocol, - ...SeleniumProtocol, - ...GeckoProtocol, - ...WebDriverBidiProtocol -} +const commands = deepmerge( + WebDriverProtocol, MJsonWProtocol, JsonWProtocol, AppiumProtocol, + ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol, GeckoProtocol, + WebDriverBidiProtocol +) const protocolCommandList = Object.values(commands).map( (endpoint) => Object.values(endpoint).map( ({ command }) => command diff --git a/packages/wdio-browser-runner/src/vite/server.ts b/packages/wdio-browser-runner/src/vite/server.ts index 5d5ddb698..7fc53fce3 100644 --- a/packages/wdio-browser-runner/src/vite/server.ts +++ b/packages/wdio-browser-runner/src/vite/server.ts @@ -175,7 +175,7 @@ export class ViteServer extends EventEmitter { } async #handleCommand (ws: WebSocket, payload: CommandRequestEvent) { - log.info(`Received browser message: ${payload}`) + log.debug(`Received browser message: ${JSON.stringify(payload)}`) const cid = payload.cid if (typeof cid !== 'string') { const error = serializeError(new Error(`No "cid" property passed into command message with id "${payload.id}"`)) @@ -194,6 +194,14 @@ export class ViteServer extends EventEmitter { if (payload.commandName === 'debug') { this.emit('debugState', true) } + + /** + * double check if function is registered + */ + if (typeof browser[payload.commandName as keyof typeof browser] !== 'function') { + throw new Error(`browser.${payload.commandName} is not a function`) + } + const result = await (browser[payload.commandName as keyof typeof browser] as Function)(...payload.args) const resultMsg = JSON.stringify(this.#commandResponse({ id: payload.id, result })) diff --git a/packages/wdio-browser-runner/tests/vite/plugins/testrunner.test.ts b/packages/wdio-browser-runner/tests/vite/plugins/testrunner.test.ts index f493b035d..2f1366aaa 100644 --- a/packages/wdio-browser-runner/tests/vite/plugins/testrunner.test.ts +++ b/packages/wdio-browser-runner/tests/vite/plugins/testrunner.test.ts @@ -54,7 +54,7 @@ test('resolveId', async () => { test('load', () => { const plugin = testrunner({}) const js = (plugin.load as Function)('\0virtual:wdio') - expect(js).toContain('export const commands = ["newSession","getSession","deleteSession"') + expect(js).toContain('export const commands = ["newSession","deleteSession","getSession"') expect(js).toContain('export const automationProtocolPath =') expect((plugin.load as Function)('something else')).toBe(undefined) }) diff --git a/packages/wdio-cli/src/run.ts b/packages/wdio-cli/src/run.ts index a41de4a44..55b5c51df 100644 --- a/packages/wdio-cli/src/run.ts +++ b/packages/wdio-cli/src/run.ts @@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers' import { commands } from './commands/index.js' import { handler, cmdArgs } from './commands/run.js' -import { CLI_EPILOGUE } from './constants.js' +import { CLI_EPILOGUE, pkg } from './constants.js' import type { RunCommandArguments } from './types.js' const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) @@ -37,6 +37,7 @@ export default async function run() { .example('wdio install reporter spec', 'Install @wdio/spec-reporter') .example('wdio repl chrome -u -k ', 'Run repl in Sauce Labs cloud') .updateStrings({ 'Commands:': `${DESCRIPTION.join('\n')}\n\nCommands:` }) + .version(pkg.version) .epilogue(CLI_EPILOGUE) /** diff --git a/packages/wdio-runner/src/browser.ts b/packages/wdio-runner/src/browser.ts index 0e29222c0..f8866d34e 100644 --- a/packages/wdio-runner/src/browser.ts +++ b/packages/wdio-runner/src/browser.ts @@ -28,7 +28,6 @@ declare global { } const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)) -const TEST_TIMEOUT = 15 * 1000 export default class BrowserFramework implements Omit { #inDebugMode = false @@ -75,11 +74,12 @@ export default class BrowserFramework implements Omit { } async #loop () { + const timeout = this._config.mochaOpts!.timeout + /** * start tests */ let failures = 0 - let uid = 0 for (const spec of this._specs) { log.info(`Run spec file ${spec} for cid ${this._cid}`) @@ -101,7 +101,7 @@ export default class BrowserFramework implements Omit { const now = Date.now() await browser.waitUntil(async () => { while (typeof state.failures !== 'number' && (!state.errors || state.errors.length === 0)) { - if ((Date.now() - now) > TEST_TIMEOUT) { + if ((Date.now() - now) > timeout) { return false } @@ -137,7 +137,7 @@ export default class BrowserFramework implements Omit { return true }, { timeoutMsg: 'browser test timed out', - timeout: TEST_TIMEOUT + timeout }) if (state.errors?.length) { diff --git a/packages/webdriverio/src/commands/browser/$$.ts b/packages/webdriverio/src/commands/browser/$$.ts index d0e9b785b..a4b7355da 100644 --- a/packages/webdriverio/src/commands/browser/$$.ts +++ b/packages/webdriverio/src/commands/browser/$$.ts @@ -1,6 +1,6 @@ import type { ElementReference } from '@wdio/protocols' -import { findElements, enhanceElementsArray } from '../../utils/index.js' +import { findElements, enhanceElementsArray, isElement, findElement } from '../../utils/index.js' import { getElements } from '../../utils/getElementObject.js' import type { Selector, ElementArray } from '../../types.js' @@ -61,11 +61,22 @@ import type { Selector, ElementArray } from '../../types.js' */ export async function $$ ( this: WebdriverIO.Browser | WebdriverIO.Element, - selector: Selector | ElementReference[] | WebdriverIO.Element[] + selector: Selector | ElementReference[] | WebdriverIO.Element[] | HTMLElement[] ) { - const res = Array.isArray(selector) + let res: (ElementReference | Error)[] = Array.isArray(selector) ? selector as ElementReference[] : await findElements.call(this, selector) - const elements = await getElements.call(this, selector, res) - return enhanceElementsArray(elements, this, selector) as ElementArray + + /** + * allow user to transform a set of HTMLElements into a set of WebdriverIO elements + */ + if (Array.isArray(selector) && isElement(selector[0])) { + res = [] + for (const el of selector) { + res.push(await findElement.call(this, el)) + } + } + + const elements = await getElements.call(this, selector as Selector, res) + return enhanceElementsArray(elements, this, selector as Selector) as ElementArray } diff --git a/packages/webdriverio/src/utils/getElementObject.ts b/packages/webdriverio/src/utils/getElementObject.ts index 857d3acf0..17f9210f6 100644 --- a/packages/webdriverio/src/utils/getElementObject.ts +++ b/packages/webdriverio/src/utils/getElementObject.ts @@ -85,7 +85,7 @@ export const getElement = function findElement( export const getElements = function getElements( this: WebdriverIO.Browser | WebdriverIO.Element, selector: Selector | ElementReference[] | WebdriverIO.Element[], - elemResponse: ElementReference[], + elemResponse: (ElementReference | Error)[], isReactElement = false ): ElementArray { const browser = getBrowserObject(this as WebdriverIO.Element) diff --git a/packages/webdriverio/src/utils/index.ts b/packages/webdriverio/src/utils/index.ts index fe32e11ed..ed7fb356e 100644 --- a/packages/webdriverio/src/utils/index.ts +++ b/packages/webdriverio/src/utils/index.ts @@ -81,7 +81,7 @@ export const getElementFromResponse = (res: ElementReference) => { * deprecated JSONWireProtocol response */ if ((res as any).ELEMENT) { - return (res as any).ELEMENT + return (res as any as { ELEMENT: string }).ELEMENT } /** @@ -207,7 +207,7 @@ function fetchElementByJSFunction ( return getBrowserObject(scope).execute(`return (${script}).apply(null, arguments)`, scope, referenceId) } -function isElement (o: Selector){ +export function isElement (o: Selector){ return ( typeof HTMLElement === 'object' ? o instanceof HTMLElement