good by fibers and @wdio/sync 👋
This commit is contained in:
parent
775b5f1921
commit
80337515a4
44 changed files with 21 additions and 1573 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
throw new Error('Unable to load @wdio/sync package.')
|
||||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
src
|
||||
tests
|
||||
tsconfig.json
|
||||
tsconfig.prod.json
|
||||
|
|
@ -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.
|
||||
|
|
@ -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/']
|
||||
})
|
||||
})
|
||||
```
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const anotherError = new Error('AnotherError')
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const FiberMock = jest.fn().mockImplementation((fn) => {
|
||||
return { run: fn }
|
||||
})
|
||||
|
||||
module.exports = FiberMock
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../@types"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.prod",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../@types"
|
||||
]
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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[]) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
],
|
||||
"Core Concepts": [
|
||||
"setuptypes",
|
||||
"sync-vs-async",
|
||||
"automationProtocols",
|
||||
"multiremote",
|
||||
"autowait"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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();
|
||||
})
|
||||
```
|
||||
Loading…
Reference in a new issue