good by fibers and @wdio/sync 👋

This commit is contained in:
Christian Bromann 2022-05-20 09:26:26 +00:00
parent 775b5f1921
commit 80337515a4
44 changed files with 21 additions and 1573 deletions

View file

@ -81,7 +81,6 @@ __Did you build a WebdriverIO service or reporter?__ That's awesome! Please add
- [@wdio/repl](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-repl) - A WDIO helper utility to provide a repl interface for WebdriverIO
- [@wdio/reporter](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-reporter) - A WebdriverIO utility to help reporting all events
- [@wdio/runner](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-runner) - A WebdriverIO service that runs tests in arbitrary environments
- [@wdio/sync](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-sync) - A WebdriverIO plugin. Helper module to run WebdriverIO commands synchronously
- [@wdio/utils](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-utils) - A WDIO helper utility to provide several utility functions used across the project
### Reporter

View file

@ -1 +0,0 @@
throw new Error('Unable to load @wdio/sync package.')

View file

@ -19,16 +19,6 @@ const coveragePathIgnorePatterns = [
'packages/eslint-plugin-wdio',
]
/**
* check Node.js version and ignore `@wdio/sync` test if we
* are on v16 or higher
*/
const [major] = process.versions.node.split('.')
if (parseInt(major) >= 16) {
testPathIgnorePatterns.push('<rootDir>/packages/wdio-sync')
coveragePathIgnorePatterns.push('/packages/wdio-sync')
}
module.exports = {
globals: {
'ts-jest': {

View file

@ -48,12 +48,7 @@ export const builder = (yargs: yargs.Argv) => {
export const handler = async (argv: ReplCommandArguments) => {
const caps = getCapabilities(argv)
/**
* runner option required to wrap commands within Fibers context
*/
const execMode = hasWdioSyncSupport ? { runner: 'local' as const } : {}
const client = await remote({ ...argv, ...caps, ...execMode })
const client = await remote({ ...argv, ...caps })
// @ts-ignore
global.$ = client.$.bind(client)

View file

@ -400,12 +400,6 @@ class Launcher {
// If an arg appears multiple times the last occurrence is used
let execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs]
// set '--no-wasm-code-gc' deliberatively as it causes problems with
// @wdio/sync and recent TypeScript compiles
if (!execArgv.includes('--no-wasm-code-gc')) {
execArgv.push('--no-wasm-code-gc')
}
// bump up worker count
this._runnerStarted++

View file

@ -65,11 +65,6 @@ type SingleCapability = { capabilities: Capabilities.RemoteCapability }
interface SingleConfigOption extends Omit<Options.Testrunner, 'capabilities'>, SingleCapability {}
type MultiRemoteCaps = Record<string, (Capabilities.DesiredCapabilities | Capabilities.W3CCapabilities) & { sessionId?: string }>
// Todo(Christian): move to a central place
declare global {
var _HAS_FIBER_CONTEXT: boolean
}
export default class Runner extends EventEmitter {
private _configParser = new ConfigParser()
private _sigintWasCalled = false
@ -362,12 +357,6 @@ export default class Runner extends EventEmitter {
return
}
/**
* suppress @wdio/sync warnings of not running commands inside of
* a Fibers context
*/
global._HAS_FIBER_CONTEXT = true
let logTypes
try {
logTypes = await global.browser.getLogTypes()

View file

@ -1,4 +0,0 @@
src
tests
tsconfig.json
tsconfig.prod.json

View file

@ -1,20 +0,0 @@
Copyright (c) OpenJS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,110 +0,0 @@
WebdriverIO Sync
================
> A WebdriverIO plugin. Helper module to run WebdriverIO commands synchronously
A WebdriverIO plugin. Helper module to run WebdriverIO commands synchronously. It overwrites global functions depending on the test framework (e.g. for Mocha describe and it) and uses Fibers to make commands of WebdriverIO using the wdio testrunner synchronous. This package is consumed by all wdio framework adapters.
## Usage
### Using WDIO Testrunner
If you are using the WDIO testrunner all you need to make all your specs run synchronous is to have `@wdio/sync` installed in your project. The testrunner automatically will detect it and transform the commands to make a test like this:
```js
describe('webdriver.io page', () => {
it('should have the right title', async () => {
await browser.url('https://webdriver.io')
await expect(browser)
.toHaveTitle('WebdriverIO · Next-gen browser and mobile automation test framework for Node.js | WebdriverIO')
})
})
```
easier to read and write like this:
```js
describe('webdriver.io page', () => {
it('should have the right title', () => {
browser.url('https://webdriver.io')
expect(browser).toHaveTitle('WebdriverIO · Next-gen browser and mobile automation test framework for Node.js | WebdriverIO')
})
})
```
### Using WebdriverIO as standalone package
Given you have a simple standalone WebdriverIO script like this:
```js
// standalone.js
const { remote } = require('webdriverio')
const sync = require('@wdio/sync').default
;(() => {
const browser = await remote({
outputDir: __dirname,
capabilities: {
browserName: 'chrome'
}
})
await browser.url('https://webdriver.io')
console.log(await browser.getTitle())
await browser.deleteSession()
})().catch(console.error)
```
With this package you can make WebdriverIO commands synchronous by using the `sync` wrapper:
```js
// standalone.js
const { remote } = require('webdriverio')
const sync = require('@wdio/sync').default
remote({
runner: 'local',
outputDir: __dirname,
capabilities: {
browserName: 'chrome'
}
}).then((browser) => sync(() => {
/**
* sync code from here on
*/
browser.url('https://webdriver.io')
console.log(browser.getTitle())
browser.deleteSession()
}))
```
## Switching Between Sync And Async
While using `@wdio/sync` you can still switch between both by using the `browser.call()` command. It allows you to run async code and return the result into a synchronous environment. For example:
```js
describe('webdriver.io page', () => {
it('should have the right title', () => {
/**
* synchronous execution here
*/
browser.url('https://webdriver.io')
const result = browser.call(async () => {
/**
* asynchronous execution here
*/
await browser.url('https://google.com')
return Promise.all([
browser.getTitle(),
browser.getUrl()
])
})
/**
* synchronous execution here
*/
console.log(result) // returns: ['Google', 'https://www.google.com/']
})
})
```

View file

@ -1,39 +0,0 @@
{
"name": "@wdio/sync",
"version": "7.20.7",
"description": "A WebdriverIO plugin. Helper module to run WebdriverIO commands synchronously",
"author": "Christian Bromann <mail@bromann.dev>",
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-sync",
"license": "MIT",
"main": "./build/index",
"engines": {
"node": ">=12.0.0 <16"
},
"typeScriptVersion": "3.8.3",
"repository": {
"type": "git",
"url": "git://github.com/webdriverio/webdriverio.git"
},
"keywords": [
"webdriverio",
"wdio",
"fibers",
"sync",
"wdio-plugin"
],
"bugs": {
"url": "https://github.com/webdriverio/webdriverio/issues"
},
"dependencies": {
"@types/fibers": "^3.1.0",
"@types/puppeteer": "^5.4.0",
"@wdio/logger": "7.19.0",
"@wdio/types": "7.20.7",
"fibers": "^5.0.0",
"webdriverio": "7.20.7"
},
"publishConfig": {
"access": "public"
},
"types": "./build/index.d.ts"
}

View file

@ -1,23 +0,0 @@
export const STACK_START = /^\s+at /
export const STACKTRACE_FILTER = [
// exclude @wdio/sync from stack traces
'node_modules/@wdio/sync/',
// exclude webdriverio and webdriver stack traces
'node_modules/webdriverio/build/',
'node_modules/webdriver/build/',
// exclude request
'node_modules/request/request',
// exclude EventEmitter
' (events.js:',
' (domain.js:',
// other excludes
'(internal/process/next_tick.js',
'new Promise (<anonymous>)',
'Generator.next (<anonymous>)',
'__awaiter ('
] as const

View file

@ -1,65 +0,0 @@
import Fiber from './fibers'
import logger from '@wdio/logger'
const log = logger('@wdio/sync')
/**
* Helper method to execute a row of hooks with certain parameters.
* It will return with a reject promise due to a design decision to not let hooks/service intefer the
* actual test process.
*
* @param {Function|Function[]} hooks list of hooks
* @param {Object[]} args list of parameter for hook functions
* @return {Promise} promise that gets resolved once all hooks finished running
*/
export default async function executeHooksWithArgs (hookName?: string, hooks: Function | Function[] = [], args?: any | undefined) {
/**
* make sure hooks are an array of functions
*/
if (typeof hooks === 'function') {
hooks = [hooks]
}
/**
* make sure args is an array since we are calling apply
*/
if (!Array.isArray(args)) {
args = [args]
}
const hookPromise = hooks.map((hook) => new Promise((resolve) => {
let result
const execHook = () => {
delete global.browser._NOT_FIBER
try {
result = hook.apply(null, args)
} catch (err: any) {
log.error(err.stack)
return resolve(err)
}
if (result && typeof result.then === 'function') {
return result.then(resolve, (err: Error) => {
log.error(err.stack)
resolve(err)
})
}
resolve(result)
}
/**
* after command hooks require additional Fiber environment
*/
return hook.constructor.name === 'AsyncFunction' ? execHook() : Fiber(execHook).run()
}))
const start = Date.now()
const result = await Promise.all(hookPromise)
if (hookPromise.length) {
log.debug(`Finished to run "${hookName}" hook in ${Date.now() - start}ms`)
}
return result
}

View file

@ -1,57 +0,0 @@
import logger from '@wdio/logger'
import type { FiberConstructor, FutureConstructor } from './types'
const log = logger('@wdio/sync')
let Fiber: FiberConstructor
let Future: FutureConstructor
global._HAS_FIBER_CONTEXT = false
const origErrorFn = console.error.bind(console)
const errors: Error[] = []
console.error = /* istanbul ignore next */ (...args: Error[]) => errors.push(...args)
/**
* only load @wdio/sync with Node.js v15 and below
*/
const [major] = process.versions.node.split('.')
/* istanbul ignore if */
if (parseInt(major) >= 16) {
throw new Error(
'@wdio/sync is not support with NodeJS v16 and above. Please migrate your code base ' +
'using async/await for command execution. Read more about this in our migration ' +
'guide: https://webdriver.io/docs/async-migration.\n'
)
}
/**
* Helper method to retrieve a version of `fibers` for your Node version.
*/
try {
/**
* try original fibers package first
*/
Fiber = require('fibers')
Future = require('fibers/future')
} catch (err: any) {
log.debug('Couldn\'t load fibers package for Node v12 and above')
}
console.error = origErrorFn
/**
* throw if no fibers could be loaded
*/
// @ts-ignore
if (!Fiber || !Future) {
throw new Error(
'No proper `fibers` package could be loaded. It might be not ' +
'supported with your current Node version. Please ensure to use ' +
`only WebdriverIOs recommended Node versions.\n${errors.join('\n')}`
)
}
export default Fiber
export { Future }

View file

@ -1,89 +0,0 @@
import Fiber from './fibers'
import type { Browser } from 'webdriverio'
import executeHooksWithArgs from './executeHooksWithArgs'
import runFnInFiberContext from './runFnInFiberContext'
import wrapCommand from './wrapCommand'
import { stackTraceFilter } from './utils'
const defaultRetries = { attempts: 0, limit: 0 }
declare global {
var _HAS_FIBER_CONTEXT: boolean
var browser: any
}
/**
* execute test or hook synchronously
*
* @param {Function} fn spec or hook method
* @param {Number} retries { limit: number, attempts: number }
* @param {Array} args arguments passed to hook
* @return {Promise} that gets resolved once test/hook is done or was retried enough
*/
async function executeSync (this: Browser<'async'>, fn: Function, retries = defaultRetries, args: any[] = []): Promise<any> {
/**
* User can also use the `@wdio/sync` package directly to run commands
* synchronously in standalone mode. In this case we neither have
* `global.browser` nor `this`
*/
if (global.browser) {
delete global.browser._NOT_FIBER
}
if (this) {
this.wdioRetries = retries.attempts
}
try {
global._HAS_FIBER_CONTEXT = true
let res = fn.apply(this, args)
global._HAS_FIBER_CONTEXT = false
/**
* sometimes function result is Promise,
* we need to await result before proceeding
*/
if (res instanceof Promise) {
return await res
}
return res
} catch (err: any) {
if (retries.limit > retries.attempts) {
retries.attempts++
return await executeSync.call(this, fn, retries, args)
}
/**
* no need to modify stack if no stack available
*/
if (!err.stack) {
return Promise.reject(err)
}
err.stack = err.stack.split('\n').filter(stackTraceFilter).join('\n')
return Promise.reject(err)
}
}
/**
* run hook or spec via executeSync
*/
function runSync (this: any, fn: Function, repeatTest?: typeof defaultRetries, args: any[] = []) {
return (resolve: (value: any) => void, reject: (err: Error) => void) =>
Fiber(() => executeSync.call(this, fn, repeatTest, args).then(resolve, reject)).run()
}
export {
executeHooksWithArgs,
wrapCommand,
runFnInFiberContext,
executeSync,
runSync,
}
export default function sync(testFn: Function) {
return new Promise((resolve, reject) => {
return runSync(testFn)(resolve, reject)
})
}

View file

@ -1,25 +0,0 @@
import type { Browser } from 'webdriverio'
import Fiber from './fibers'
/**
* global function to wrap callbacks into Fiber context
* @param {Function} fn function to wrap around
* @return {Function} wrapped around function
*/
export default function runFnInFiberContext (fn: Function) {
return function (this: Browser<'async'>, ...args: any[]) {
delete global.browser._NOT_FIBER
return new Promise((resolve, reject) => Fiber(() => {
try {
global._HAS_FIBER_CONTEXT = true
const result = fn.apply(this, args)
global._HAS_FIBER_CONTEXT = false
return resolve(result)
} catch (err: any) {
return reject(err)
}
}).run())
}
}

View file

@ -1,22 +0,0 @@
import type Fiber from 'fibers'
import type Future from 'fibers/future'
export interface FiberConstructor {
/**
* Instantiate a new Fiber. You may invoke this either as a function or as
* a constructor; the behavior is the same.
*
* When run() is called on this fiber for the first time, `fn` will be
* invoked as the first frame on a new stack. Execution will continue on
* this new stack until `fn` returns, or Fiber.yield() is called.
*
* After the function returns the fiber is reset to original state and
* may be restarted with another call to run().
*/
new (fn: Function): Fiber
(fn: Function): Fiber
}
export interface FutureConstructor {
new <T> (): Future<T>
}

View file

@ -1,57 +0,0 @@
import { STACK_START, STACKTRACE_FILTER } from './constants'
/**
* filter stack array
* @param {string} stackRow
* @returns {boolean}
*/
export const stackTraceFilter = (stackRow: string) => {
if (stackRow.match(STACK_START)) {
return !STACKTRACE_FILTER.some(r => stackRow.includes(r))
}
return true
}
/**
* Cleanup stack traces, merge and remove duplicates
* @param {Error|*} commandError Error object or anything else including undefined
* @param {Error} savedError Error with root stack trace
* @returns {Error}
*/
export function sanitizeErrorMessage (commandError: Error, savedError: Error) {
let name, stack, message
if (commandError instanceof Error) {
({ name, message, stack } = commandError)
} else {
name = 'Error'
message = commandError
}
const err = new Error(message)
err.name = name
let stackArr = savedError.stack?.split('\n') || []
/**
* merge stack traces if `commandError` has stack trace
*/
if (stack) {
// remove duplicated error name from stack trace
stack = stack.replace(`${err.name}: ${err.name}`, err.name)
// remove first stack trace line from second stack trace
stackArr[0] = '\n'
// merge
stackArr = [...stack.split('\n'), ...stackArr]
}
err.stack = stackArr
// filter stack trace
.filter(stackTraceFilter)
// remove duplicates from stack traces
.reduce((acc, currentValue) => {
return acc.includes(currentValue) ? acc : `${acc}\n${currentValue}`
}, '')
.trim()
return err
}

View file

@ -1,180 +0,0 @@
import logger from '@wdio/logger'
import type { Options } from '@wdio/types'
import type { Browser, Element, MultiRemoteBrowser } from 'webdriverio'
import executeHooksWithArgs from './executeHooksWithArgs'
import { sanitizeErrorMessage } from './utils'
import { Future } from './fibers'
const log = logger('@wdio/sync')
let inCommandHook = false
const timers: any[] = []
const elements: Set<Element<'async'>> = new Set()
declare global {
var WDIO_WORKER: boolean
}
/**
* resets `_NOT_FIBER` if Timer has timed out
*/
process.on('WDIO_TIMER', (payload) => {
if (payload.start) {
return timers.push(payload.id)
}
if (timers.includes(payload.id)) {
while (timers.pop() !== payload.id);
}
if (payload.timeout) {
elements.forEach(/* istanbul ignore next */ element => { delete element._NOT_FIBER })
}
if (timers.length === 0) {
elements.clear()
}
})
/**
* wraps a function into a Fiber ready context to enable sync execution and hooks
* @param {Function} fn function to be executed
* @param {String} commandName name of that function
* @param {Function[]} beforeCommand method to be executed before calling the actual function
* @param {Function[]} afterCommand method to be executed after calling the actual function
* @return {Function} actual wrapped function
*/
export default function wrapCommand (commandName: string, fn: Function) {
return function wrapCommandFn(this: Browser<'async'> | Element<'async'>, ...args: any[]) {
/**
* print error if a user is using a fiberized command outside of the Fibers context
*/
if (!global._HAS_FIBER_CONTEXT && global.WDIO_WORKER) {
log.warn(
`Can't return command result of ${commandName} synchronously because command ` +
'was executed outside of an it block, hook or step definition!'
)
}
/**
* store element if Timer is running to reset `_NOT_FIBER` if timeout has occurred
*/
if (timers.length > 0) {
elements.add(this as Element<'async'>)
}
/**
* Avoid running some functions in Future that are not in Fiber.
*/
if (this._NOT_FIBER === true) {
this._NOT_FIBER = isNotInFiber(this as Element<'async'>, fn.name)
return fn.apply(this, args)
}
/**
* all named nested functions run in parent Fiber context
*/
this._NOT_FIBER = fn.name !== ''
const future = new Future()
const result = runCommandWithHooks.apply(this, [commandName, fn, ...args])
result.then(future.return.bind(future), future.throw.bind(future))
try {
const futureResult = future.wait()
inFiber(this)
return futureResult
} catch (err: any) {
/**
* in case some 3rd party lib rejects without bundling into an error
*/
if (typeof err === 'string') {
throw new Error(err)
}
/**
* in case we run commands where no fiber function was used
* e.g. when we call deleteSession
*/
if (err.message.includes('Can\'t wait without a fiber')) {
return result
}
inFiber(this)
throw err
}
}
}
/**
* helper method that runs the command with before/afterCommand hook
*/
async function runCommandWithHooks(
this: Browser<'async'> | Element<'async'>,
commandName: string,
fn: Function,
...args: any[]
) {
// save error for getting full stack in case of failure
// should be before any async calls
const stackError = new Error()
await runCommandHook.call(this, 'beforeCommand', (this.options as Options.Testrunner).beforeCommand, [commandName, args])
let commandResult
let commandError
try {
commandResult = await fn.apply(this, args)
} catch (err: any) {
commandError = sanitizeErrorMessage(err, stackError)
}
await runCommandHook.call(this, 'afterCommand', (this.options as Options.Testrunner).afterCommand, [commandName, args, commandResult, commandError])
if (commandError) {
throw commandError
}
return commandResult
}
async function runCommandHook(hookName: string, hookFn?: Function | Function[], args?: any[]) {
if (!inCommandHook) {
inCommandHook = true
await executeHooksWithArgs(hookName, hookFn, args)
inCommandHook = false
}
}
/**
* isNotInFiber
* if element or its parent has element id then we are in parent's Fiber
* @param {object} context browser or element
* @param {string} fnName function name
*/
function isNotInFiber(context: Element<'async'>, fnName: string) {
return fnName !== '' && !!(context.elementId || (context.parent && (context.parent as Element<'async'>).elementId))
}
/**
* set `_NOT_FIBER` to `false` for element and its parents
* @param {object} context browser or element
*/
function inFiber (context: Browser<'async'> | Element<'async'> | MultiRemoteBrowser<'async'>) {
const multiRemoteContext = context as MultiRemoteBrowser<'async'>
if (multiRemoteContext.constructor.name === 'MultiRemoteDriver') {
return multiRemoteContext.instances.forEach(instance => {
multiRemoteContext[instance]._NOT_FIBER = false
let parent = (multiRemoteContext[instance] as Element<'async'>).parent
while (parent && parent._NOT_FIBER) {
parent._NOT_FIBER = false
parent = (parent as Element<'async'>).parent
}
})
}
context._NOT_FIBER = false
let parent = (context as Element<'async'>).parent
while (parent && parent._NOT_FIBER) {
parent._NOT_FIBER = false
parent = (parent as Element<'async'>).parent
}
}

View file

@ -1,5 +0,0 @@
{
"env": {
"jest": true
}
}

View file

@ -1 +0,0 @@
export const anotherError = new Error('AnotherError')

View file

@ -1,5 +0,0 @@
const FiberMock = jest.fn().mockImplementation((fn) => {
return { run: fn }
})
module.exports = FiberMock

View file

@ -1,18 +0,0 @@
const returnValues = {
wait: undefined
}
class Future {
public wait = jest.fn().mockImplementation(
() => (returnValues.wait instanceof Error)
? Promise.reject(returnValues.wait)
: Promise.resolve(returnValues.wait)
)
public return = jest.fn()
public throw = jest.fn()
}
// @ts-expect-error
Future.returnValues = returnValues
module.exports = Future

View file

@ -1,61 +0,0 @@
import executeHooksWithArgs from '../src/executeHooksWithArgs'
describe('executeHooksWithArgs', () => {
beforeEach(() => {
global.browser = {} as any
})
it('multiple hooks, multiple args', async () => {
const hookHoge = () => { return 'hoge' }
const hookFuga = () => { return 'fuga' }
const argHoge = { hoge: 'hoge' }
const argFuga = { fuga: 'fuga' }
const res = await executeHooksWithArgs('someHook', [hookHoge, hookFuga], [argHoge, argFuga])
expect(res).toEqual(['hoge', 'fuga'])
})
it('one hook, one arg', async () => {
const hook = () => { return 'hoge' }
const arg = { hoge: 'hoge' }
const res = await executeHooksWithArgs('someHook', hook, arg)
expect(res).toHaveLength(1)
expect(res).toContain('hoge')
})
it('with error', async () => {
const hook = () => { throw new Error('Hoge') }
const res = await executeHooksWithArgs('someHook', hook, [])
expect(res).toHaveLength(1)
expect(res).toEqual([new Error('Hoge')])
})
it('return promise with error', async () => {
const hook = () => {
return new Promise(() => { throw new Error('Hoge') })
}
const res = await executeHooksWithArgs('someHook', hook, [])
expect(res).toHaveLength(1)
expect(res).toEqual([new Error('Hoge')])
})
it('async functions', async () => {
global.browser._NOT_FIBER = true
const hookHoge = () => {
return new Promise(reject => setTimeout(reject, 5, new Error('Hoge')))
}
const hookFuga = async () => new Promise(resolve => setTimeout(resolve, 10, 'fuga'))
const res = await executeHooksWithArgs('someHook', [hookHoge, hookFuga], [])
expect(res).toEqual([new Error('Hoge'), 'fuga'])
expect(global.browser._NOT_FIBER).toBe(undefined)
})
it('should do nothing if there were no hooks passed', async () => {
const res = await executeHooksWithArgs()
expect(res).toHaveLength(0)
})
afterEach(() => {
// @ts-expect-error
delete global.browser
})
})

View file

@ -1,40 +0,0 @@
export {}
declare global {
var requireFibers: boolean | undefined
}
global.requireFibers = true
const mockFibers = () => {
jest.mock('fibers', () => {
if (!global.requireFibers) {
throw new Error('package not found')
}
return { version: 4 }
})
jest.mock('fibers/future', () => {
if (!global.requireFibers) {
throw new Error('package not found')
}
return { version: 4 }
})
}
test('should load fibers 4', () => {
mockFibers()
const fibers = require('../src/fibers')
expect(fibers.default.version).toBe(4)
})
test('should throw error if package can not be installed', () => {
global.requireFibers = false
jest.resetModules()
mockFibers()
expect(() => require('../src/fibers'))
.toThrow(/No proper `fibers` package could be loaded/)
})
afterEach(() => {
delete global.requireFibers
})

View file

@ -1,132 +0,0 @@
import { Browser } from 'webdriverio'
import sync, { executeSync, runSync } from '../src'
beforeEach(() => {
global.browser = {} as any as Browser
})
it('exports a function to make async tests sync', async () => {
const fakeFn = jest.fn().mockReturnValue('foobar')
expect(await sync(fakeFn)).toBe('foobar')
})
describe('executeSync', () => {
it('should pass with default values and regular fn', async () => {
global.browser._NOT_FIBER = true
expect(await executeSync.call(browser, () => 1, {} as any)).toEqual(1)
expect(global.browser._NOT_FIBER).toBe(undefined)
})
it('should pass with args and async fn', async () => {
const fn = async (arg: any) => arg
const scope = {} as any
expect(await executeSync.call(scope, fn, { limit: 1, attempts: 0 }, [2])).toEqual(2)
expect(scope.wdioRetries).toBe(0)
})
it('should pass without scope', async () => {
const fn = async (arg: any) => arg
expect(await executeSync.call(browser, fn, { limit: 1, attempts: 0 }, [2])).toEqual(2)
})
it('should filter stack on failure', async () => {
global.browser._NOT_FIBER = true
let error
try {
await executeSync.call(browser, () => { throw new Error('foobar') }, {} as any)
} catch (err: any) {
error = err
}
expect(error.stack).not.toContain('at new Promise (<anonymous>)')
expect(global.browser._NOT_FIBER).toBe(undefined)
})
it('should not filter stack on failure if it is missing', async () => {
let error
try {
await executeSync.call(browser, () => {
const err = new Error('foobar')
err.stack = 'false'
throw err
}, {} as any)
} catch (err: any) {
error = err
}
expect(error.stack).toEqual('false')
})
it('should repeat step on failure', async () => {
let counter = 3
const scope = {} as any
const repeatTest = { limit: counter, attempts: 0 }
const fn = () => {
if (counter > 0) {
counter--
throw new Error('foobar')
}
return true
}
expect(await executeSync.call(scope, fn, repeatTest)).toEqual(true)
expect(counter).toEqual(0)
expect(repeatTest).toEqual({ limit: 3, attempts: 3 })
expect(scope.wdioRetries).toBe(3)
})
it('should throw if repeatTest attempts exceeded', async () => {
let counter = 3
const scope = {} as any
const repeatTest = { limit: counter - 1, attempts: 0 }
let error
try {
await executeSync.call(scope, () => {
if (counter > 0) {
counter--
throw new Error('foobar')
}
return true
}, repeatTest)
} catch (err: any) {
error = err
}
expect(error.message).toEqual('foobar')
expect(repeatTest).toEqual({ limit: 2, attempts: 2 })
expect(scope.wdioRetries).toBe(2)
})
})
describe('runSync', () => {
it('should return value', async () => {
const resolveFn = jest.fn()
const rejectFn = jest.fn()
const scope = {}
const fibersFn = runSync.call(scope, (arg: string) => 'foo' + arg, {} as any, ['bar'])
await fibersFn(resolveFn, rejectFn)
expect(rejectFn).not.toBeCalled()
expect(resolveFn).toBeCalledWith('foobar')
})
it('should reject promise on error', async () => {
const resolveFn = jest.fn()
const rejectFn = jest.fn()
const scope = {}
const fn = jest.fn().mockImplementation(() => { throw error })
const error = new Error('foo')
const fibersFn = runSync.call(scope, fn, { limit: 1, attempts: 0 })
await fibersFn(resolveFn, rejectFn)
expect(resolveFn).not.toBeCalled()
expect(rejectFn).toBeCalledWith(error)
expect(fn).toBeCalledTimes(2)
})
})
afterEach(() => {
// @ts-expect-error
delete global.browser
})

View file

@ -1,34 +0,0 @@
import { Browser } from 'webdriverio'
import runFnInFiberContext from '../src/runFnInFiberContext'
beforeAll(() => {
if (!global.browser) {
global.browser = {} as any as Browser
}
})
interface Scope extends Browser{
scopedVar: string
}
test('should wrap a successful running command', async () => {
global.browser._NOT_FIBER = true
const wrappedFn = runFnInFiberContext(function (this: Scope, arg: string) {
return this.scopedVar + arg
})
expect(await wrappedFn.call({ scopedVar: 'foo' } as Scope, 'bar')).toBe('foobar')
expect(global.browser._NOT_FIBER).toBe(undefined)
})
test('should wrap a failing running command', async () => {
global.browser._NOT_FIBER = true
const wrappedFn = runFnInFiberContext(function (this: Scope, arg: string) {
throw new Error(this.scopedVar + arg)
})
await expect(wrappedFn.call({ scopedVar: 'foo' } as Scope, 'bar'))
.rejects.toThrow(new Error('foobar'))
expect(global.browser._NOT_FIBER).toBe(undefined)
})

View file

@ -1,224 +0,0 @@
import { Future } from '../src/fibers'
import wrapCommand from '../src/wrapCommand'
import { anotherError } from './__mocks__/errors'
import type FutureType from 'fibers/future'
jest.mock('../src/executeHooksWithArgs', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => true)
}))
describe('wrapCommand:runCommand', () => {
beforeEach(() => {
jest.resetAllMocks()
delete global.WDIO_WORKER
})
it('should return result', async () => {
process.emit('WDIO_TIMER' as any, { id: 0, start: true } as any)
// @ts-expect-error mock
Future.returnValues.wait = 'barbar'
global.WDIO_WORKER = true
const fn = jest.fn(x => (x + x))
const runCommand = wrapCommand('foo', fn)
const result = await runCommand.call({ options: {} } as any, 'bar')
expect(result).toEqual('barbar')
process.emit('WDIO_TIMER' as any, { id: 0 } as any)
})
it('should set _NOT_FIBER to false if elementId is missing', async () => {
const fn = jest.fn()
const runCommand = wrapCommand('foo', fn)
const context = { options: {}, _NOT_FIBER: true } as any
await runCommand.call(context, 'bar')
expect(context._NOT_FIBER).toBe(false)
})
it('should set _NOT_FIBER to true if elementId is exist', async () => {
const fn = jest.fn()
const runCommand = wrapCommand('foo', fn)
const context = { options: {}, _NOT_FIBER: true, elementId: 'foo' } as any
await runCommand.call(context, 'bar')
expect(context._NOT_FIBER).toBe(true)
})
it('should set _NOT_FIBER to false if elementId is exist but function is anonymous', async () => {
const runCommand = wrapCommand('foo', () => { })
const context = { options: {}, _NOT_FIBER: true, elementId: 'foo' } as any
await runCommand.call(context, 'bar')
expect(context._NOT_FIBER).toBe(false)
})
it('should set _NOT_FIBER to true if parent.elementId is exist', async () => {
const fn = jest.fn()
const runCommand = wrapCommand('foo', fn)
const context = { options: {}, _NOT_FIBER: true, parent: { elementId: 'foo' } } as any
await runCommand.call(context, 'bar')
expect(context._NOT_FIBER).toBe(true)
})
it('should set _NOT_FIBER to false for element and every parent', async () => {
Future.prototype.wait = () => {}
const runCommand = wrapCommand('foo', jest.fn())
const context = {
_NOT_FIBER: undefined,
options: {}, elementId: 'foo', parent: { _NOT_FIBER: true }
} as any
await runCommand.call(context)
expect(context._NOT_FIBER).toEqual(false)
expect(context.parent._NOT_FIBER).toEqual(false)
})
it('should set _NOT_FIBER to false function with empty name', async () => {
Future.prototype.wait = () => {}
const runCommand = wrapCommand('foo', () => {})
const context = {
options: {}, elementId: 'foo', _hidden_: null, _hidden_changes_: [],
get _NOT_FIBER () { return this._hidden_ },
set _NOT_FIBER (val) {
this._hidden_changes_.push(val)
this._hidden_ = val
}
} as any
await runCommand.call(context)
expect(context._hidden_changes_).toEqual([false, false])
})
it('should set _NOT_FIBER to multiremote instance', async () => {
Future.prototype.wait = () => {}
const runCommand = wrapCommand('waitUntil', jest.fn())
const context = {
_hidden_: null,
_hidden_changes_: [],
constructor: { name: 'MultiRemoteDriver' },
instances: ['browserA', 'browserB'],
browserA: {
_hidden_: true,
get _NOT_FIBER () { return context.browserA._hidden_ },
set _NOT_FIBER (val) {
context._hidden_changes_.push(`browserA ${val}`)
context._hidden_ = val
},
parent: {
_hidden_: true,
get _NOT_FIBER () { return context.browserA.parent._hidden_ },
set _NOT_FIBER (val) {
context._hidden_changes_.push(`parent ${val}`)
context._hidden_ = val
}
}
},
browserB: {
_hidden_: true,
get _NOT_FIBER () { return context.browserB._hidden_ },
set _NOT_FIBER (val) {
context._hidden_changes_.push(`browserB ${val}`)
context._hidden_ = val
}
}
} as any
await runCommand.call(context)
expect(context._hidden_changes_).toEqual(['browserA false', 'parent false', 'browserB false'])
})
it('should throw error with proper message', async () => {
// @ts-expect-error mock
Future.returnValues.wait = new Error('bar')
const fn = jest.fn(x => { throw new Error(x) })
const runCommand = wrapCommand('foo', fn)
const result = runCommand.call({ options: {} } as any, 'bar')
await expect(result).rejects.toEqual(new Error('bar'))
})
it('should contain merged error stack', async () => {
expect.assertions(3)
// @ts-expect-error mock
Future.returnValues.wait = new Error('AnotherError')
const fn = jest.fn(() => { throw anotherError })
const runCommand = wrapCommand('foo', fn)
const result = runCommand.call({ options: {} } as any, 'bar')
try {
await result
} catch (err: any) {
expect(err).toEqual(new Error('AnotherError'))
expect(err.name).toBe('Error')
expect(err.stack.split(__filename)).toHaveLength(2)
}
})
it('should accept non Error objects', async () => {
// @ts-expect-error mock
Future.returnValues.wait = new Error('bar')
const fn = jest.fn(x => Promise.reject(x))
const runCommand = wrapCommand('foo', fn)
const result = runCommand.call({ options: {} } as any, 'bar')
try {
await result
} catch (err: any) {
expect(err).toEqual(new Error('bar'))
expect(err.name).toBe('Error')
expect(err.stack.split(__filename)).toHaveLength(2)
}
expect.assertions(3)
})
it('should accept undefined', async () => {
expect.assertions(3)
// @ts-expect-error mock
Future.returnValues.wait = new Error()
const fn = jest.fn(() => Promise.reject())
const runCommand = wrapCommand('foo', fn)
const result = runCommand.call({ options: {} } as any)
try {
await result
} catch (err: any) {
expect(err).toEqual(new Error())
expect(err.name).toBe('Error')
expect(err.stack.split(__filename)).toHaveLength(2)
}
})
describe.skip('future', () => {
beforeEach(() => {
(Future as any as FutureType<any>).wait = jest.fn(() => { throw new Error() })
})
it('should throw regular error', () => {
const fn = jest.fn(() => {})
const runCommand = wrapCommand('foo', fn)
const context = { options: {}, _NOT_FIBER: undefined } as any
try {
runCommand.call(context, 'bar')
} catch (err: any) {
expect((Future as any as FutureType<any>).wait).toThrow()
}
expect(context._NOT_FIBER).toBe(false)
expect.assertions(2)
})
})
/**
* actual testing is done with smoke tests
* only branch coverage here
*/
describe('WDIO_TIMER', () => {
it('WDIO_TIMER listener', () => {
process.emit('WDIO_TIMER' as any, { id: 1, start: true } as any)
process.emit('WDIO_TIMER' as any, { id: 1 } as any)
process.emit('WDIO_TIMER' as any, { id: 2, start: true } as any)
process.emit('WDIO_TIMER' as any, { id: 2, start: true } as any)
process.emit('WDIO_TIMER' as any, { id: 3, start: true } as any)
process.emit('WDIO_TIMER' as any, { id: 2, timeout: true } as any)
process.emit('WDIO_TIMER' as any, { id: 123, timeout: true } as any)
})
})
})

View file

@ -1,12 +0,0 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"baseUrl": ".",
"outDir": "./build",
"rootDir": "./src"
},
"include": [
"src/**/*",
"../../@types"
]
}

View file

@ -1,12 +0,0 @@
{
"extends": "../../tsconfig.prod",
"compilerOptions": {
"baseUrl": ".",
"outDir": "./build",
"rootDir": "./src"
},
"include": [
"src/**/*",
"../../@types"
]
}

View file

@ -7,9 +7,7 @@ import {
commandCallStructure, isValidParameter, getArgumentType, safeImport,
isFunctionAsync, transformCommandLogResult, canAccess, sleep
} from './utils.js'
import {
wrapCommand, executeHooksWithArgs, executeSync, executeAsync
} from './shim.js'
import { wrapCommand, executeHooksWithArgs, executeAsync } from './shim.js'
import { testFnWrapper, runTestInFiberContext } from './test-framework/index.js'
import {
isW3C, capabilitiesEnvironmentDetector,
@ -32,10 +30,9 @@ export {
sleep,
/**
* wdio-sync shim
* runner shim
*/
wrapCommand,
executeSync,
executeAsync,
runTestInFiberContext,
testFnWrapper,

View file

@ -12,7 +12,6 @@ interface Retries {
}
declare global {
var _HAS_FIBER_CONTEXT: boolean
var browser: any
var expectAsync: any
}
@ -67,15 +66,7 @@ const ELEMENT_PROPS = [
]
const PROMISE_METHODS = ['then', 'catch', 'finally']
/**
* shim to make sure that we only wrap commands if wdio-sync is installed as dependency
*/
export let runAsync = true
export let asyncSpec = true
let executeHooksWithArgs = async function executeHooksWithArgsShim<T> (hookName: string, hooks: Function | Function[] = [], args: any[] = []): Promise<(T | Error)[]> {
runAsync = true
/**
* make sure hooks are an array of functions
*/
@ -290,13 +281,8 @@ let wrapCommand = function wrapCommand<T>(commandName: string, fn: Function): (.
return function (this: Clients.Browser, ...args: any[]) {
/**
* use sync mode if:
* - @wdio/sync package is installed and can be resolved
* - if a global.browser is define so we run with wdio testrunner
* - we are in a fiber context (flag is set when outer function is wrapped into fibers context)
*
* also if we run command asynchronous and the command suppose to return an element, we
* apply `chainElementQuery` to allow chaining of these promises.
* if the command suppose to return an element, we apply `chainElementQuery` to allow
* chaining of these promises.
*/
const command = ELEMENT_QUERY_COMMANDS.includes(commandName) || commandName.endsWith('$')
? chainElementQuery
@ -306,40 +292,6 @@ let wrapCommand = function wrapCommand<T>(commandName: string, fn: Function): (.
}
}
/**
* execute test or hook synchronously
*
* @param {Function} fn spec or hook method
* @param {Number} retries { limit: number, attempts: number }
* @param {Array} args arguments passed to hook
* @return {Promise} that gets resolved once test/hook is done or was retried enough
*/
async function executeSyncFn (this: any, fn: Function, retries: Retries, args: any[] = []): Promise<unknown> {
this.wdioRetries = retries.attempts
try {
runAsync = true
let res = fn.apply(this, args)
/**
* sometimes function result is Promise,
* we need to await result before proceeding
*/
if (res instanceof Promise) {
return await res
}
return res
} catch (err: any) {
if (retries.limit > retries.attempts) {
retries.attempts++
return await executeSyncFn.call(this, fn, retries, args)
}
return Promise.reject(err)
}
}
/**
* execute test or hook asynchronously
*
@ -351,7 +303,6 @@ async function executeSyncFn (this: any, fn: Function, retries: Retries, args: a
async function executeAsync(this: any, fn: Function, retries: Retries, args: any[] = []): Promise<unknown> {
// @ts-expect-error
const isJasmine = global.jasmine && global.expectAsync
const asyncSpecBefore = asyncSpec
this.wdioRetries = retries.attempts
// @ts-ignore
@ -365,16 +316,10 @@ async function executeAsync(this: any, fn: Function, retries: Retries, args: any
}
try {
runAsync = true
asyncSpec = true
const result = fn.apply(this, args)
if (result && typeof result.finally === 'function') {
result
.finally(() => (asyncSpec = asyncSpecBefore))
.catch((err: any) => err)
} else {
asyncSpec = asyncSpecBefore
result.catch((err: any) => err)
}
return await result
@ -393,47 +338,8 @@ async function executeAsync(this: any, fn: Function, retries: Retries, args: any
}
}
let executeSync = executeSyncFn
/**
* Method to switch between sync and async execution. It allows to have async
* tests in between synchronous tests. `fn` can either return a promise (e.g. for `executeSync`)
* or a function (e.g. for `runSync`). In both cases we need to make sure that
* we flip `runAsync` flag to true to that commands are wrapped with the @wdio/sync
* wrapper.
*/
export function switchSyncFlag (fn: Function) {
return function (this: unknown, ...args: any[]) {
const switchFlag = runAsync
runAsync = false
const result = fn.apply(this, args)
if (typeof result.finally === 'function') {
runAsync = switchFlag
return result
}
if (typeof result === 'function') {
return function (this: any, ...args: any[]) {
const switchFlagWithinFn = runAsync
const res = result.apply(this, args)
if (typeof result.finally === 'function') {
return result.finally(() => (runAsync = switchFlagWithinFn))
}
runAsync = switchFlagWithinFn
return res
}
}
runAsync = switchFlag
return result
}
}
export {
executeHooksWithArgs,
wrapCommand,
executeSync,
executeAsync,
}

View file

@ -20,8 +20,7 @@ import type {
const MOCHA_COMMANDS: ['skip', 'only'] = ['skip', 'only']
/**
* runs a hook within fibers context (if function name is not async)
* it also executes before/after hook
* runs a hook and execute before/after hook
*
* @param {Function} hookFn function that was passed to the framework hook
* @param {Function} origFn original framework hook function
@ -78,7 +77,7 @@ export const runHook = function (
}
/**
* runs a spec function (test function) within the fibers context
* runs a spec function (test function)
*
* @param {string} specTitle test description
* @param {Function} specFn test function that got passed in from the user

View file

@ -63,8 +63,6 @@ export const remote = async function (params: RemoteOptions, remoteModifier?: Fu
/**
* we need to overwrite the original addCommand and overwriteCommand
* in order to wrap the function within Fibers (only if webdriverio
* is used with @wdio/cli)
*/
if ((params as Options.Testrunner).framework && !isStub(automationProtocol)) {
const origAddCommand = instance.addCommand.bind(instance)

View file

@ -9,7 +9,7 @@ import { ELEMENT_KEY } from './constants.js'
* This method is an command wrapper for elements that checks if a command is called
* that wasn't found on the page and automatically waits for it
*
* @param {Function} fn commandWrap from wdio-sync package (or shim if not running in sync)
* @param {Function} fn command shim
*/
export const elementErrorHandler = (fn: Function) => (commandName: string, commandFn: Function) => {
return function elementErrorHandlerCallback (this: WebdriverIO.Element, ...args: any[]) {

View file

@ -46,11 +46,7 @@ export const getPrototype = (scope: 'browser' | 'element') => {
/**
* used to store the puppeteer instance in the browser scope
*/
puppeteer: { value: null, writable: true },
/**
* for handling sync execution in @wdio/sync
*/
_NOT_FIBER: { value: false, writable: true, configurable: true }
puppeteer: { value: null, writable: true }
}
/**

View file

@ -59,15 +59,6 @@ describe('timer', () => {
expect(await timer).toBe('foobar')
})
it('should wrap fn into fibers', async () => {
global.browser = {} as any as WebdriverIO.Browser
setWdioSyncSupport(true)
;(runFnInFiberContext as jest.Mock).mockReturnValueOnce(() => 'called')
let timer = new Timer(20, 30, () => 'foobar')
await triggerDelay()
expect(await timer).toBe('called')
})
afterAll(() => {
setWdioSyncSupport(false)
delete global.browser

View file

@ -1,6 +1,5 @@
import assert from 'assert'
import os from 'os'
import sync from '../../packages/wdio-sync'
import assert from 'node:assert'
import os from 'node:os'
import { remote } from '../../packages/webdriverio'
describe('Mocha smoke test', () => {
@ -150,22 +149,6 @@ describe('Mocha smoke test', () => {
assert.equal(JSON.stringify(await elem.getSize()), JSON.stringify({ width: 1, height: 2 }))
})
it('should allow to run standalone mode synchronously', () => {
browser.clickScenario()
return remote({
runner: 'local',
hostname: 'localhost',
port: 4444,
path: '/',
capabilities: {
browserName: 'chrome'
}
}).then((remoteBrowser) => sync(() => {
assert.equal(remoteBrowser.getTitle(), 'Mock Page Title')
}))
})
describe('isDisplayed', () => {
it('should return false if element is never found', () => {
browser.isNeverDisplayedScenario()

View file

@ -470,6 +470,12 @@ const severeErrorTest = async () => {
(async () => {
const smokeTests = [
mochaTestrunner,
jasmineTestrunner,
multiremote,
wdioHooks,
sharedStoreServiceTest,
mochaSpecGrouping,
cucumberTestrunner,
cucumberFailAmbiguousDefinitions,
cucumberReporter,
@ -487,22 +493,6 @@ const severeErrorTest = async () => {
severeErrorTest
]
/**
* add smoke tests that run in sync mode and need Fibers and therefor
* aren't supported in Node.js v16 and above
*/
const [major] = process.versions.node.split('.')
if (parseInt(major) < 16) {
smokeTests.push(
mochaTestrunner,
jasmineTestrunner,
multiremote,
wdioHooks,
sharedStoreServiceTest,
mochaSpecGrouping
)
}
console.log('\nRunning smoke tests...\n')
await runTests(smokeTests)

View file

@ -37,5 +37,5 @@ TypeScript compilation is used to do this kind of static analysis.
Typings should appear in node_modules folder, same as in real project. In order to achieve that typings are copied to package folders accordingly.
1. Delete `node_modules` folder in every project
2. Copy every typings and `package.json` file of package to `node_modules`, ex: `packages/wdio-sync` to `node_modules/@wdio/sync`, etc.
2. Copy every typings and `package.json` file of package to `node_modules`, ex: `packages/wdio-types` to `node_modules/@wdio/types`, etc.
3. Copy referenced typings that are required by webdriverio typings

View file

@ -17,7 +17,6 @@ const packages = {
'devtools': 'packages/devtools',
'webdriver': 'packages/webdriver',
'webdriverio': 'packages/webdriverio',
'@wdio/sync': 'packages/wdio-sync',
'@wdio/reporter': 'packages/wdio-reporter',
'@wdio/allure-reporter': 'packages/wdio-allure-reporter',
'@wdio/appium-service': 'packages/wdio-appium-service',

View file

@ -7,7 +7,6 @@
],
"Core Concepts": [
"setuptypes",
"sync-vs-async",
"automationProtocols",
"multiremote",
"autowait"

View file

@ -6,34 +6,9 @@ title: Introduction
Welcome to the WebdriverIO docs page. These pages contain reference materials for all implemented selenium bindings and commands. WebdriverIO has all [JSONWire protocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/) commands implemented and also supports special bindings for [Appium](http://appium.io).
:::info
These are the docs for the latest version (__>=7.x__) of WebdriverIO. If you are still using an older version, please visit the [old documentation websites](/versions)!
These are the docs for the latest version (__>=8.x__) of WebdriverIO. If you are still using an older version, please visit the [old documentation websites](/versions)!
:::
## Examples
Each command documentation usually comes with an example that demonstrates its usage in WebdriverIO's testrunner in [sync mode](sync-vs-async#sync-mode). If you run WebdriverIO in standalone mode, you can still use all commands but you'll need to make sure that the execution order is handled properly by handling promises.
While you e.g. assign the result of a command in sync mode directly to a variable:
```js
it('can handle commands synchronously', () => {
let value = $('#input').getValue()
console.log(value) // outputs: some value
})
```
you need use `async/await` when running in standalone or not using sync mode at all:
```js
it('can handle commands using async/await', async function () {
const inputElement = await $('#input')
let value = await inputElement.getValue()
console.log(value) // outputs: some value
})
```
It is recommended to use the testrunner to scale up your test suite, as it comes with a lot of useful add-ons like the [Sauce Service](_sauce-service.md) that save you from writing a lot of boilerplate code by yourself.
## Contribute
If you feel like you have a good example for a command, don't hesitate to open a PR and submit it. Just click on the orange button on the top right with the label _“EDIT”_. Make sure you understand the way we write these docs by checking the [Contributing](https://github.com/webdriverio/webdriverio/blob/main/CONTRIBUTING.md) section.

View file

@ -100,4 +100,4 @@ You’ll need [Node.js](http://nodejs.org) installed.
- Install at least v12.16.1 or higher as this is the oldest active LTS version
- Only releases that are or will become an LTS release are officially supported
If you don't have Node installed, we recommend installing [NVM](https://github.com/creationix/nvm) to assist managing multiple active Node.js versions. If you are using the [WDIO Testrunner](/docs/setuptypes#the-wdio-testrunner) in [sync mode](/docs/sync-vs-async#sync-mode) you also need Python v3 or higher installed.
If you don't have Node installed, we recommend installing [NVM](https://github.com/creationix/nvm) to assist managing multiple active Node.js versions.

View file

@ -1,116 +0,0 @@
---
id: sync-vs-async
title: Sync vs. Async Mode
---
:::caution
As of __14/04/2021__ sync mode will not be supported anymore starting from Node.js v16 due to [changes](https://chromium-review.googlesource.com/c/v8/v8/+/2537690) in Chromium. If you start a project with WebdriverIO we recommend to not use sync mode anymore. The community is working on a transition plan, [read more on that in this RFC](https://github.com/webdriverio/webdriverio/discussions/6702).
:::
WebdriverIO runs a set of asynchronous commands to interact with the browser or mobile device. In JavaScript asynchronous operations are handled via `async/await`. This however can be a confusing concept for people unfamiliar with the language. In addition to that it can make tests very verbose as almost every operation is asynchronous. To simplify its usage WebdriverIO provides the ability to run commands synchronous through [`node-fibers`](https://www.npmjs.com/package/fibers).
## How to enable/disable sync mode
To enable sync mode you only need to add the [`@wdio/sync`](https://www.npmjs.com/package/@wdio/sync) package to your dev dependencies:
```bash npm2yarn
npm install --save-dev @wdio/sync
```
The package will be automatically detected by the framework and the environment properly set up to run synchronous.
## Sync mode
If you're using [`@wdio/sync`](https://www.npmjs.com/package/@wdio/sync) then you can avoid awaiting for command calls. It is still required to deal with Promises from 3rd-party libraries, you should use [browser.call](api/browser/call) for this to wrap them and make them synchronous too.
:::caution
Synchronous Mode will depcrecated with Node.js v16. With an update to the
underlying Chromium version it became technically impossible to provide the
same synchronous behavior. We recommend to start transition to asynchronous
command execution. For more information, see our <a href="https://github.com/webdriverio/webdriverio/discussions/6702">RFC</a>.
:::
```js
// 3rd-party library example
// https://www.npmjs.com/package/got#gotgeturl-options
const { get } = require('got')
describe('suite sync', () => {
// make sure to remove `async` keyword otherwise function treated as async
// and you have to await every browser call.
// The same is applicable to hooks in `wdio.conf.js` as well.
it('test sync', () => {
browser.pause(500)
// wrap 3rd-party library calls with `browser.call`
const response = browser.call(
() => get('https://cat-fact.herokuapp.com/facts/', {
responseType: 'json'
})
)
console.log(response.body[0].type) // outputs: "cat"
$('body').click() // You can chain functions in sync mode
})
// If you have `@wdio/sync` installed and configured, it is still possible to use async functions.
// However, in such case you have to await every browser/element call like in async mode, and this can
// be confusing when other tests are sync, so we discourage mixing modes, but it is possible to do so.
// The best practice in sync mode is to wrap anything async with `browser.call`.
it('using async function in sync mode', async () => {
await browser.pause(500)
const response = await get('https://cat-fact.herokuapp.com/facts/')
console.log(response.body[0].type) // outputs: "cat"
const el = await $('body')
await el.click()
})
})
```
### Common issues in sync mode
- `fibers` failed to install properly. The package usually comes with pre-built binaries but if your environment doesn't support it these need to be compiled which require [node-gyp](https://github.com/nodejs/node-gyp) and Python.
## Async Mode
If you decide to run in async mode all WebdriverIO commands return a Promise and need to be awaited to get the result, e.g.:
```js
describe('suite async', () => {
it('test', async () => { // pay attention to `async` keyword
const el = await $('body') // note `await` keyword
await el.click()
await browser.pause(500)
console.log(browser.capabilities) // static properties should not be awaited
await $('body').click()
})
})
```
### Common issues in async mode
There can be quite some confusion when handling asynchronous commands manually. The usual problems are:
- previous command was not awaited:
```js
const el = await $('body')
el.waitForExist() // ERROR: await is missing here, you'll get `Unhandled promise rejection`.
await el.click()
```
- Array loops (e.g., `forEach` & `map`) don't need `Promise.all`
```js
const links = await $$('a')
const linksText = await links.map((link) => {
return link.getText();
})
```