WebDriver BiDi Implementation (#8505)

* initial bidi implementation

* add unit tests

* remove only

* fix vitest path in windows
This commit is contained in:
Christian Bromann 2022-07-21 13:09:42 +02:00 committed by GitHub
parent b98aa2a136
commit b7f5d06fd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 403 additions and 35 deletions

View file

@ -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

View file

@ -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 = [

View file

@ -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) {}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -9,7 +9,7 @@ export const config = {
/**
* specify test files
*/
specs: [path.resolve(__dirname, 'headless', '*.e2e.ts')],
specs: ['./headless/*.e2e.ts'],
/**
* capabilities

View 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
}]
}
}
}

View file

@ -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'
]

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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"
}
}

View 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 }))
}
}

View file

@ -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 })

View file

@ -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

View file

@ -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
*/

View 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]`;

View 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()
})
})
})

View file

@ -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)

View file

@ -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', () => {

View file

@ -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',

View file

@ -24,5 +24,6 @@ export const returnTypeMap = {
findElements: 'ElementReference[]',
findElementFromElement: 'ElementReference',
findElementsFromElement: 'ElementReference[]',
getAllCookies: 'Cookie[]'
getAllCookies: 'Cookie[]',
send: 'BidiResponse'
}