WebDriver BiDi Implementation (#8505)
* initial bidi implementation * add unit tests * remove only * fix vitest path in windows
This commit is contained in:
parent
b98aa2a136
commit
b7f5d06fd4
23 changed files with 403 additions and 35 deletions
|
|
@ -12,4 +12,4 @@ RUN \
|
|||
microsoft-edge-dev
|
||||
|
||||
RUN \
|
||||
npm i -g http-server diff-so-fancy chromedriver@103
|
||||
npm i -g http-server diff-so-fancy chromedriver@103 geckodriver
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ const requestMock: any = vi.fn().mockImplementation((uri, params) => {
|
|||
const script = Function(params.json.script)
|
||||
const args = params.json.args.map((arg: any) => arg.ELEMENT || arg[ELEMENT_KEY] || arg)
|
||||
|
||||
let result = null
|
||||
let result: any = null
|
||||
if (params.json.script.includes('resq')) {
|
||||
if (params.json.script.includes('react$$')) {
|
||||
result = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { vi } from 'vitest'
|
||||
|
||||
export default class WebSocketMock {
|
||||
on = vi.fn()
|
||||
on = vi.fn().mockReturnValue({ off: vi.fn() })
|
||||
off = vi.fn()
|
||||
send = vi.fn()
|
||||
|
||||
constructor (public wsUrl: string) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import path from 'node:path'
|
|||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 1000 * 60,
|
||||
include: [path.resolve(__dirname, '*.e2e.ts')],
|
||||
include: ['./**/*.e2e.ts'],
|
||||
hookTimeout: 60 * 1000,
|
||||
threads: false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import path from 'node:path'
|
|||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 1000 * 60,
|
||||
include: [path.resolve(__dirname, '**', '*.test.ts')],
|
||||
include: ['./**/*.test.ts'],
|
||||
hookTimeout: 60 * 1000,
|
||||
threads: false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const config = {
|
|||
/**
|
||||
* specify test files
|
||||
*/
|
||||
specs: [path.resolve(__dirname, 'headless', '*.e2e.ts')],
|
||||
specs: ['./headless/*.e2e.ts'],
|
||||
|
||||
/**
|
||||
* capabilities
|
||||
|
|
|
|||
33
packages/wdio-protocols/protocols/webdriverBidi.json
Normal file
33
packages/wdio-protocols/protocols/webdriverBidi.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"sendCommand": {
|
||||
"socket": {
|
||||
"command": "send",
|
||||
"description": "Send socket commands via WebDriver Bidi",
|
||||
"ref": "https://github.com/w3c/webdriver-bidi",
|
||||
"parameters": [{
|
||||
"name": "params",
|
||||
"type": "object",
|
||||
"description": "socket payload",
|
||||
"required": true
|
||||
}],
|
||||
"returns": {
|
||||
"type": "Object",
|
||||
"name": "response",
|
||||
"description": "WebDriver Bidi response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendAsyncCommand": {
|
||||
"socket": {
|
||||
"command": "sendAsync",
|
||||
"description": "Send asynchronous socket commands via WebDriver Bidi",
|
||||
"ref": "https://github.com/w3c/webdriver-bidi",
|
||||
"parameters": [{
|
||||
"name": "params",
|
||||
"type": "object",
|
||||
"description": "socket payload",
|
||||
"required": true
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,11 @@ import type MJSONWPCommands from './commands/mjsonwp'
|
|||
import type SauceLabsCommands from './commands/saucelabs'
|
||||
import type SeleniumCommands from './commands/selenium'
|
||||
import type WebDriverCommands from './commands/webdriver'
|
||||
import type WebDriverBidiCommands from './commands/webdriverBidi'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const WebDriverProtocol: Protocol = require('../protocols/webdriver.json')
|
||||
const WebDriverBidiProtocol: Protocol = require('../protocols/webdriverBidi.json')
|
||||
const MJsonWProtocol: Protocol = require('../protocols/mjsonwp.json')
|
||||
const JsonWProtocol: Protocol = require('../protocols/jsonwp.json')
|
||||
const AppiumProtocol: Protocol = require('../protocols/appium.json')
|
||||
|
|
@ -24,6 +26,10 @@ type WebDriverCommandsAsync = {
|
|||
[K in keyof WebDriverCommands]:
|
||||
(...args: Parameters<WebDriverCommands[K]>) => Promise<ReturnType<WebDriverCommands[K]>>
|
||||
}
|
||||
type WebDriverBidiCommandsAsync = {
|
||||
[K in keyof WebDriverBidiCommands]:
|
||||
(...args: Parameters<WebDriverBidiCommands[K]>) => Promise<ReturnType<WebDriverBidiCommands[K]>>
|
||||
}
|
||||
type AppiumCommandsAsync = {
|
||||
[K in keyof AppiumCommands]:
|
||||
(...args: Parameters<AppiumCommands[K]>) => Promise<ReturnType<AppiumCommands[K]>>
|
||||
|
|
@ -61,17 +67,20 @@ export {
|
|||
// protocols
|
||||
WebDriverProtocol, MJsonWProtocol, JsonWProtocol, AppiumProtocol,
|
||||
ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol, GeckoProtocol,
|
||||
WebDriverBidiProtocol,
|
||||
// sync commands
|
||||
AppiumCommands, ChromiumCommands, JSONWPCommands, MJSONWPCommands,
|
||||
SauceLabsCommands, SeleniumCommands, WebDriverCommands, GeckoCommands,
|
||||
WebDriverBidiCommands,
|
||||
// async commands
|
||||
WebDriverCommandsAsync, AppiumCommandsAsync, ChromiumCommandsAsync,
|
||||
JSONWPCommandsAsync, MJSONWPCommandsAsync, SauceLabsCommandsAsync,
|
||||
SeleniumCommandsAsync, GeckoCommandsAsync
|
||||
SeleniumCommandsAsync, GeckoCommandsAsync,
|
||||
WebDriverBidiCommandsAsync
|
||||
}
|
||||
|
||||
export const CAPABILITY_KEYS = [
|
||||
'browserName', 'browserVersion', 'platformName', 'acceptInsecureCerts',
|
||||
'pageLoadStrategy', 'proxy', 'setWindowRect', 'timeouts', 'strictFileInteractability',
|
||||
'unhandledPromptBehavior'
|
||||
'unhandledPromptBehavior', 'webSocketUrl'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -176,3 +176,47 @@ export type Environments = 'XCUITest' | 'UIAutomation' | 'UiAutomator'
|
|||
* ```
|
||||
*/
|
||||
export type SupportedEnvironments = Record<Platform, Record<Environments, string>>
|
||||
|
||||
export type SupportedMethods = (
|
||||
'session.status' |
|
||||
'session.new' |
|
||||
'session.subscribe' |
|
||||
'session.unsubscribe' |
|
||||
'browsingContext.captureScreenshot' |
|
||||
'browsingContext.close' |
|
||||
'browsingContext.create' |
|
||||
'browsingContext.getTree' |
|
||||
'browsingContext.handleUserPrompt' |
|
||||
'browsingContext.navigate' |
|
||||
'browsingContext.reload' |
|
||||
'browsingContext.contextCreated' |
|
||||
'browsingContext.contextDestroyed' |
|
||||
'browsingContext.navigationStarted' |
|
||||
'browsingContext.fragmentNavigated' |
|
||||
'browsingContext.domContentLoaded' |
|
||||
'browsingContext.load' |
|
||||
'browsingContext.downloadWillBegin' |
|
||||
'browsingContext.navigationAborted' |
|
||||
'browsingContext.navigationFailed' |
|
||||
'browsingContext.userPromptClosed' |
|
||||
'browsingContext.userPromptOpened' |
|
||||
'script.disown' |
|
||||
'script.callFunction' |
|
||||
'script.evaluate' |
|
||||
'script.getRealms' |
|
||||
'script.realmCreated' |
|
||||
'script.realmDestoyed' |
|
||||
'log.entryAdded'
|
||||
)
|
||||
|
||||
export interface BidiRequest {
|
||||
method: SupportedMethods
|
||||
/**
|
||||
* types will be more defined later
|
||||
*/
|
||||
params: Record<string, any>
|
||||
}
|
||||
|
||||
export interface BidiResponse {
|
||||
id: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ export default function WebDriver (options: Record<string, any>, modifier?: Func
|
|||
/**
|
||||
* WebDriver monad
|
||||
*/
|
||||
function unit (this: void, sessionId: string, commandWrapper?: Function) {
|
||||
function unit (this: void, sessionId: string, commandWrapper?: Function, eventMiddleware?: { socket: Partial<EventEmitter> }) {
|
||||
if (eventMiddleware) {
|
||||
prototype.eventMiddleware = eventMiddleware
|
||||
}
|
||||
|
||||
/**
|
||||
* capabilities attached to the instance prototype not being shown if
|
||||
* logging the instance
|
||||
|
|
@ -182,6 +186,13 @@ export default function WebDriver (options: Record<string, any>, modifier?: Func
|
|||
for (let eventCommand in EVENTHANDLER_FUNCTIONS) {
|
||||
prototype[eventCommand] = function (...args: [any, any]) {
|
||||
eventHandler[eventCommand as keyof EventEmitter](...args as [never, any])
|
||||
|
||||
if (prototype.eventMiddleware) {
|
||||
if (typeof prototype.eventMiddleware[eventCommand as keyof EventEmitter] === 'function') {
|
||||
prototype.eventMiddleware.socket[eventCommand as keyof EventEmitter]!(...args as [never, any])
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
WebDriver
|
||||
=========
|
||||
|
||||
> A lightweight, non-opinionated implementation of the [WebDriver specification](https://w3c.github.io/webdriver/webdriver-spec.html) including mobile commands supported by [Appium](http://appium.io/)
|
||||
> A lightweight, non-opinionated implementation of the [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) and [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) specification including mobile commands supported by [Appium](http://appium.io/)
|
||||
|
||||
There are [tons](https://github.com/christian-bromann/awesome-selenium#javascript) of Selenium and WebDriver binding implementations in the Node.js world. Every one of them have an opinionated API and recommended way to use. This binding is the most non-opinionated you will find as it just represents the [WebDriver specification](https://w3c.github.io/webdriver/webdriver-spec.html) and doesn't come with any extra or higher level abstraction. It is lightweight and comes with support for the [WebDriver specification](https://w3c.github.io/webdriver/webdriver-spec.html) and Appium's [Mobile JSONWire Protocol](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md).
|
||||
|
||||
The package supports the following protocols:
|
||||
|
||||
- [WebDriver](https://w3c.github.io/webdriver/)
|
||||
- [WebDriver Bidi](https://w3c.github.io/webdriver-bidi/)
|
||||
- [Appium](http://appium.io/)
|
||||
- [Chromium](http://chromedriver.chromium.org/) (additional Chromedriver specific commands)
|
||||
- [Selenium](https://www.selenium.dev/) (additional Selenium WebDriver specific commands)
|
||||
|
|
@ -25,31 +26,57 @@ To install this package from NPM run:
|
|||
npm i webdriver
|
||||
```
|
||||
|
||||
## Example
|
||||
## WebDriver Example
|
||||
|
||||
The following example demonstrates a simple Google Search scenario:
|
||||
|
||||
```js
|
||||
import WebDriver from 'webdriver';
|
||||
|
||||
(async () => {
|
||||
const client = await WebDriver.newSession({
|
||||
path: '/',
|
||||
capabilities: { browserName: 'firefox' }
|
||||
})
|
||||
const client = await WebDriver.newSession({
|
||||
path: '/',
|
||||
capabilities: { browserName: 'firefox' }
|
||||
})
|
||||
|
||||
await client.navigateTo('https://www.google.com/ncr')
|
||||
await client.navigateTo('https://www.google.com/ncr')
|
||||
|
||||
const searchInput = await client.findElement('css selector', '#lst-ib')
|
||||
await client.elementSendKeys(searchInput['element-6066-11e4-a52e-4f735466cecf'], 'WebDriver')
|
||||
const searchInput = await client.findElement('css selector', '#lst-ib')
|
||||
await client.elementSendKeys(searchInput['element-6066-11e4-a52e-4f735466cecf'], 'WebDriver')
|
||||
|
||||
const searchBtn = await client.findElement('css selector', 'input[value="Google Search"]')
|
||||
await client.elementClick(searchBtn['element-6066-11e4-a52e-4f735466cecf'])
|
||||
const searchBtn = await client.findElement('css selector', 'input[value="Google Search"]')
|
||||
await client.elementClick(searchBtn['element-6066-11e4-a52e-4f735466cecf'])
|
||||
|
||||
console.log(await client.getTitle()) // outputs "WebDriver - Google Search"
|
||||
console.log(await client.getTitle()) // outputs "WebDriver - Google Search"
|
||||
|
||||
await client.deleteSession()
|
||||
})()
|
||||
await client.deleteSession()
|
||||
```
|
||||
|
||||
## WebDriver Bidi Example
|
||||
|
||||
To connect to the WebDriver Bidi protocol you have to send along a `webSocketUrl` flag to tell the browser driver to opt-in to the protocol:
|
||||
|
||||
```js
|
||||
import WebDriver from './packages/webdriver/build/index.js'
|
||||
|
||||
const browser = await WebDriver.newSession({
|
||||
capabilities: {
|
||||
webSocketUrl: true,
|
||||
browserName: 'firefox'
|
||||
}
|
||||
})
|
||||
|
||||
await browser.send({
|
||||
method: 'session.subscribe',
|
||||
params: { events: ['log.entryAdded'] }
|
||||
})
|
||||
|
||||
/**
|
||||
* returns: {"method":"log.entryAdded","params":{"type":"console","method":"log","realm":null,"args":[{"type":"string","value":"Hello Bidi"}],"level":"info","text":"Hello Bidi","timestamp":1657282076037}}
|
||||
*/
|
||||
browser.on('message', (data) => console.log('received %s', data))
|
||||
|
||||
await browser.executeScript('console.log("Hello Bidi")', [])
|
||||
await browser.deleteSession()
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"types": "./build/index.d.ts",
|
||||
"typeScriptVersion": "3.8.3",
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
"node": "^16.13 || >=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.merge": "^4.6.7"
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/ws": "^8.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^18.0.0",
|
||||
|
|
@ -43,7 +44,8 @@
|
|||
"@wdio/types": "7.20.7",
|
||||
"@wdio/utils": "7.20.7",
|
||||
"got": "^12.1.0",
|
||||
"ky": "0.31.1",
|
||||
"lodash.merge": "^4.6.2"
|
||||
"ky": "^0.31.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"ws": "^8.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
72
packages/webdriver/src/bidi.ts
Normal file
72
packages/webdriver/src/bidi.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { EventEmitter } from 'node:events'
|
||||
import WebSocket from 'ws'
|
||||
import logger from '@wdio/logger'
|
||||
import { BidiRequest, BidiResponse } from '@wdio/protocols'
|
||||
|
||||
const log = logger('webdriver:BidiHandler')
|
||||
const RESPONSE_TIMEOUT = 1000 * 60
|
||||
|
||||
export class BidiHandler extends EventEmitter {
|
||||
#id = 0
|
||||
#ws: WebSocket
|
||||
#isConnected = false
|
||||
|
||||
constructor (private _webSocketUrl: string) {
|
||||
super()
|
||||
log.info(`Connect to webSocketUrl ${this._webSocketUrl}`)
|
||||
this.#ws = new WebSocket(this._webSocketUrl)
|
||||
}
|
||||
|
||||
public connect () {
|
||||
return new Promise<void>((resolve) => this.#ws.on('open', () => {
|
||||
this.#isConnected = true
|
||||
resolve()
|
||||
}))
|
||||
}
|
||||
|
||||
get socket () {
|
||||
return this.#ws
|
||||
}
|
||||
|
||||
get isConnected () {
|
||||
return this.#isConnected
|
||||
}
|
||||
|
||||
public send (params: BidiRequest) {
|
||||
if (!this.#isConnected) {
|
||||
throw new Error('No connection to WebDriver Bidi was established')
|
||||
}
|
||||
|
||||
const id = ++this.#id
|
||||
this.#ws.send(JSON.stringify({ id, ...params }))
|
||||
return new Promise<BidiResponse>((resolve, reject) => {
|
||||
const t = setTimeout(() => {
|
||||
reject(new Error(`Request with id ${id} timed out`))
|
||||
h.off('message', listener)
|
||||
}, RESPONSE_TIMEOUT)
|
||||
|
||||
const listener = (data: WebSocket.RawData) => {
|
||||
try {
|
||||
const payload = JSON.parse(data.toString()) as BidiResponse
|
||||
if (payload.id === id) {
|
||||
clearTimeout(t)
|
||||
h.off('message', listener)
|
||||
resolve(payload)
|
||||
}
|
||||
} catch (err: any) {
|
||||
log.error(`Failed parse message: ${err.message}`)
|
||||
}
|
||||
}
|
||||
const h = this.#ws.on('message', listener)
|
||||
})
|
||||
}
|
||||
|
||||
public sendAsync (params: BidiRequest) {
|
||||
if (!this.#isConnected) {
|
||||
throw new Error('No connection to WebDriver Bidi was established')
|
||||
}
|
||||
|
||||
const id = ++this.#id
|
||||
this.#ws.send(JSON.stringify({ id, ...params }))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
import logger from '@wdio/logger'
|
||||
import { commandCallStructure, isValidParameter, getArgumentType } from '@wdio/utils'
|
||||
import type { CommandEndpoint } from '@wdio/protocols'
|
||||
import type { CommandEndpoint, BidiResponse } from '@wdio/protocols'
|
||||
|
||||
import RequestFactory from './request/factory.js'
|
||||
import { BidiHandler } from './bidi.js'
|
||||
import type { WebDriverResponse } from './request'
|
||||
import type { BaseClient } from './types'
|
||||
|
||||
const log = logger('webdriver')
|
||||
const BIDI_COMMANDS = ['send', 'sendAsync'] as const
|
||||
|
||||
interface BaseClientWithEventHandler extends BaseClient {
|
||||
eventMiddleware: BidiHandler
|
||||
}
|
||||
|
||||
export default function (
|
||||
method: string,
|
||||
|
|
@ -16,7 +22,7 @@ export default function (
|
|||
) {
|
||||
const { command, ref, parameters, variables = [], isHubCommand = false } = commandInfo
|
||||
|
||||
return async function protocolCommand (this: BaseClient, ...args: any[]): Promise<WebDriverResponse> {
|
||||
return async function protocolCommand (this: BaseClientWithEventHandler, ...args: any[]): Promise<WebDriverResponse | BidiResponse | void> {
|
||||
let endpoint = endpointUri // clone endpointUri in case we change it
|
||||
const commandParams = [...variables.map((v) => Object.assign(v, {
|
||||
/**
|
||||
|
|
@ -88,6 +94,17 @@ export default function (
|
|||
body[commandParams[i].name] = arg
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Bidi calls
|
||||
*/
|
||||
if (this.sessionId && BIDI_COMMANDS.includes(command as typeof BIDI_COMMANDS[number])) {
|
||||
if (!this.eventMiddleware) {
|
||||
throw new Error('Your WebDriver session doesn\'t support WebDriver Bidi')
|
||||
}
|
||||
|
||||
return this.eventMiddleware[command as typeof BIDI_COMMANDS[number]](body.params)
|
||||
}
|
||||
|
||||
const request = await RequestFactory.getInstance(method, endpoint, body, isHubCommand)
|
||||
request.on('performance', (...args) => this.emit('request.performance', ...args))
|
||||
this.emit('command', { method, endpoint, body })
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { validateConfig } from '@wdio/config'
|
|||
import type { Options, Capabilities } from '@wdio/types'
|
||||
|
||||
import command from './command.js'
|
||||
import { BidiHandler } from './bidi.js'
|
||||
import { DEFAULTS } from './constants.js'
|
||||
import {
|
||||
startWebDriverSession, getPrototype, getEnvironmentVars, setupDirectConnect
|
||||
|
|
@ -48,7 +49,15 @@ export default class WebDriver {
|
|||
modifier,
|
||||
prototype
|
||||
)
|
||||
const client = monad(sessionId, customCommandWrapper)
|
||||
|
||||
let handler: BidiHandler | undefined
|
||||
if (capabilities.webSocketUrl) {
|
||||
log.info(`Register BiDi handler for session with id ${sessionId}`)
|
||||
const socketUrl = (capabilities.webSocketUrl as any as string).replace('localhost', '127.0.0.1')
|
||||
handler = new BidiHandler(socketUrl)
|
||||
await handler.connect()
|
||||
}
|
||||
const client = monad(sessionId, customCommandWrapper, handler)
|
||||
|
||||
/**
|
||||
* if the server responded with direct connect information, update the
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logger from '@wdio/logger'
|
|||
import Protocols from '@wdio/protocols'
|
||||
import {
|
||||
WebDriverProtocol, MJsonWProtocol, JsonWProtocol, AppiumProtocol, ChromiumProtocol,
|
||||
SauceLabsProtocol, SeleniumProtocol, GeckoProtocol
|
||||
SauceLabsProtocol, SeleniumProtocol, GeckoProtocol, WebDriverBidiProtocol
|
||||
} from '@wdio/protocols'
|
||||
import { transformCommandLogResult } from '@wdio/utils'
|
||||
import { CAPABILITY_KEYS } from '@wdio/protocols'
|
||||
|
|
@ -178,6 +178,10 @@ export function getPrototype ({ isW3C, isChrome, isFirefox, isMobile, isSauce, i
|
|||
isMobile
|
||||
? merge({}, JsonWProtocol, WebDriverProtocol)
|
||||
: isW3C ? WebDriverProtocol : JsonWProtocol,
|
||||
/**
|
||||
* enable Bidi protocol for W3C sessions
|
||||
*/
|
||||
isW3C ? WebDriverBidiProtocol : {},
|
||||
/**
|
||||
* only apply mobile protocol if session is actually for mobile
|
||||
*/
|
||||
|
|
|
|||
13
packages/webdriver/tests/__snapshots__/bidi.test.ts.snap
Normal file
13
packages/webdriver/tests/__snapshots__/bidi.test.ts.snap
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`BidiHandler > send > fails if sending a message while not connected 1`] = `[Error: No connection to WebDriver Bidi was established]`;
|
||||
|
||||
exports[`BidiHandler > sendAsync > can send without getting an result 1`] = `
|
||||
[
|
||||
[
|
||||
"{\\"id\\":1,\\"method\\":\\"session.new\\",\\"params\\":{}}",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`BidiHandler > sendAsync > fails if sending a message while not connected 1`] = `[Error: No connection to WebDriver Bidi was established]`;
|
||||
69
packages/webdriver/tests/bidi.test.ts
Normal file
69
packages/webdriver/tests/bidi.test.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import path from 'node:path'
|
||||
import { describe, it, vi, expect } from 'vitest'
|
||||
import { BidiHandler } from '../src/bidi'
|
||||
|
||||
vi.mock('ws')
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
describe('BidiHandler', () => {
|
||||
it('iniates with a WebSocket', () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
// @ts-expect-error "wsUrl" is a mock property
|
||||
expect(handler.socket.wsUrl).toBe('ws://foo/bar')
|
||||
expect(handler.isConnected).toBe(false)
|
||||
})
|
||||
|
||||
it('can connect', async () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
handler.connect()
|
||||
expect(handler.socket.on).toBeCalledWith('open', expect.any(Function))
|
||||
|
||||
const [, cb] = vi.mocked(handler.socket.on).mock.calls[0]
|
||||
cb.call(this as any)
|
||||
expect(handler.isConnected).toBe(true)
|
||||
})
|
||||
|
||||
describe('send', () => {
|
||||
it('fails if sending a message while not connected', async () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
await expect(async () => handler.send({ method: 'session.new', params: {} }))
|
||||
.rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('sends and waits for result', async () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
handler.connect()
|
||||
const [, cb] = vi.mocked(handler.socket.on).mock.calls[0]
|
||||
cb.call(this as any)
|
||||
|
||||
vi.mocked(handler.socket.on).mockClear()
|
||||
const promise = handler.send({ method: 'session.new', params: {} })
|
||||
expect(handler.socket.on).toBeCalledWith('message', expect.any(Function))
|
||||
const [, messageCallback] = vi.mocked(handler.socket.on).mock.calls[0]
|
||||
|
||||
messageCallback.call(this as any, Buffer.from('{somewrongmessage'))
|
||||
messageCallback.call(this as any, Buffer.from(JSON.stringify({ id: 1, result: 'foobar' })))
|
||||
const result = await promise
|
||||
expect(result).toEqual({ id: 1, result: 'foobar' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendAsync', () => {
|
||||
it('fails if sending a message while not connected', async () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
await expect(async () => handler.sendAsync({ method: 'session.new', params: {} }))
|
||||
.rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('can send without getting an result', async () => {
|
||||
const handler = new BidiHandler('ws://foo/bar')
|
||||
handler.connect()
|
||||
const [, cb] = vi.mocked(handler.socket.on).mock.calls[0]
|
||||
cb.call(this as any)
|
||||
|
||||
expect(handler.sendAsync({ method: 'session.new', params: {} }))
|
||||
.toEqual(undefined)
|
||||
expect(vi.mocked(handler.socket.send).mock.calls).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -176,6 +176,30 @@ describe('command wrapper', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Bidi support', () => {
|
||||
it('fails if bidi is not supported', async () => {
|
||||
scope.sessionId = 'foo123'
|
||||
const commandFn = commandWrapper('POST', 'sendCommand', {
|
||||
command: 'send',
|
||||
parameters: []
|
||||
} as any)
|
||||
await expect(async () => commandFn.call(scope))
|
||||
.rejects.toThrow(/doesn't support WebDriver Bidi/)
|
||||
})
|
||||
|
||||
it('it propagates command to middleware', async () => {
|
||||
(scope as any).eventMiddleware = { send: vi.fn() }
|
||||
const commandFn = commandWrapper('POST', 'sendCommand', {
|
||||
command: 'send',
|
||||
required: true,
|
||||
parameters: [{ type: 'object', name: 'params' }]
|
||||
} as any)
|
||||
await commandFn.call(scope, { params: 'foobar' })
|
||||
expect((scope as any).eventMiddleware.send).toHaveBeenCalledTimes(1)
|
||||
expect((scope as any).eventMiddleware.send).toHaveBeenCalledWith({ params: 'foobar' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('command wrapper result log', () => {
|
||||
async function getRequestCallback (method: string, path: string, endpoint: Protocols.CommandEndpoint) {
|
||||
const commandFn = commandWrapper(method, path, endpoint)
|
||||
|
|
|
|||
|
|
@ -7,13 +7,29 @@ import { sessionEnvironmentDetector } from '@wdio/utils'
|
|||
import type { Capabilities } from '@wdio/types'
|
||||
|
||||
import WebDriver, { getPrototype, DEFAULTS, command } from '../src'
|
||||
// @ts-expect-error mock feature
|
||||
import { initCount } from '../src/bidi'
|
||||
import { Client } from '../src/types'
|
||||
|
||||
vi.mock('@wdio/utils', () => import(path.join(process.cwd(), '__mocks__', '@wdio/utils')))
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
vi.mock('fs')
|
||||
vi.mock('ws')
|
||||
vi.mock('got')
|
||||
|
||||
vi.mock('../src/bidi', () => {
|
||||
let initCount = 0
|
||||
return {
|
||||
BidiHandler: class BidiHandlerMock {
|
||||
connect = vi.fn()
|
||||
constructor () {
|
||||
++initCount
|
||||
}
|
||||
},
|
||||
initCount: () => initCount
|
||||
}
|
||||
})
|
||||
|
||||
const sessionOptions = {
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
|
|
@ -181,6 +197,17 @@ describe('WebDriver', () => {
|
|||
expect((browser.capabilities as Capabilities.DesiredCapabilities).browserName).toBe('mockBrowser')
|
||||
expect((browser.requestedCapabilities as Capabilities.DesiredCapabilities).browserName).toBe('firefox')
|
||||
})
|
||||
|
||||
it('attaches bidi handler if socket url is given', async () => {
|
||||
vi.mocked(got).mockResolvedValue({
|
||||
body: { value: { webSocketUrl: 'ws://foo/bar' } }
|
||||
})
|
||||
await WebDriver.newSession({
|
||||
path: '/',
|
||||
capabilities: { browserName: 'firefox' }
|
||||
})
|
||||
expect(initCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('attachToSession', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import webdriver from '../packages/wdio-protocols/protocols/webdriver.json' assert { type: 'json' }
|
||||
import webdriverBidi from '../packages/wdio-protocols/protocols/webdriverBidi.json' assert { type: 'json' }
|
||||
import appium from '../packages/wdio-protocols/protocols/appium.json' assert { type: 'json' }
|
||||
import mjsonwp from '../packages/wdio-protocols/protocols/mjsonwp.json' assert { type: 'json' }
|
||||
import chromium from '../packages/wdio-protocols/protocols/chromium.json' assert { type: 'json' }
|
||||
|
|
@ -8,14 +9,15 @@ import selenium from '../packages/wdio-protocols/protocols/selenium.json' assert
|
|||
import jsonwp from '../packages/wdio-protocols/protocols/jsonwp.json' assert { type: 'json' }
|
||||
|
||||
export const PROTOCOLS = {
|
||||
webdriver, appium, mjsonwp, chromium,
|
||||
gecko, saucelabs, selenium, jsonwp
|
||||
webdriver, webdriverBidi, appium, mjsonwp,
|
||||
chromium, gecko, saucelabs, selenium, jsonwp
|
||||
}
|
||||
export const PROTOCOL_NAMES = {
|
||||
appium: 'Appium',
|
||||
jsonwp: 'JSON Wire Protocol',
|
||||
mjsonwp: 'Mobile JSON Wire Protocol',
|
||||
webdriver: 'WebDriver Protocol',
|
||||
webdriverBidi: 'WebDriver Bidi Protocol',
|
||||
chromium: 'Chromium',
|
||||
gecko: 'Firefox',
|
||||
saucelabs: 'Sauce Labs',
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ export const returnTypeMap = {
|
|||
findElements: 'ElementReference[]',
|
||||
findElementFromElement: 'ElementReference',
|
||||
findElementsFromElement: 'ElementReference[]',
|
||||
getAllCookies: 'Cookie[]'
|
||||
getAllCookies: 'Cookie[]',
|
||||
send: 'BidiResponse'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue