feat(webdriverio): support fake timers (#13252)
* feat(webdriverio): support fake timers * finish implementation and add tests * fix test * add docs * change __dirname * minor test improvements * resolve issues within tests * add comment * add typing tests * more docs
This commit is contained in:
parent
d1ba15487f
commit
a79210972e
24 changed files with 2546 additions and 65 deletions
|
|
@ -7,3 +7,4 @@
|
|||
*.d.ts
|
||||
!packages/**/src/cjs
|
||||
packages/wdio-protocols/src/protocols/webdriverBidi.ts
|
||||
packages/webdriverio/third_party/fake-timers.js
|
||||
|
|
|
|||
|
|
@ -356,4 +356,27 @@ describe('main suite 1', () => {
|
|||
expect(await browser.execute(() => Math.random())).not.toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
describe('emulate clock', () => {
|
||||
const now = new Date(2021, 3, 14)
|
||||
const getDateString = () => (new Date()).toString()
|
||||
|
||||
it('should allow to mock the clock', async () => {
|
||||
await browser.emulate('clock', { now })
|
||||
expect(await browser.execute(getDateString))
|
||||
.toBe(now.toString())
|
||||
await browser.url('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.execute(getDateString))
|
||||
.toBe(now.toString())
|
||||
})
|
||||
|
||||
it('should allow to restore the clock', async () => {
|
||||
await browser.restore('clock')
|
||||
expect(await browser.execute(getDateString))
|
||||
.not.toBe(now.toString())
|
||||
await browser.url('http://guinea-pig.webdriver.io/pointer.html')
|
||||
expect(await browser.execute(getDateString))
|
||||
.not.toBe(now.toString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default class ProxyDriver {
|
|||
const environment = sessionEnvironmentDetector({ capabilities: params.capabilities, requestedCapabilities: {} })
|
||||
const environmentPrototype: Record<string, PropertyDescriptor> = getEnvironmentVars(environment)
|
||||
// have debug command
|
||||
const commandsProcessedInNodeWorld = [...commands, 'debug', 'saveScreenshot', 'savePDF']
|
||||
const commandsProcessedInNodeWorld = [...commands, 'debug', 'saveScreenshot', 'savePDF', 'emulate', 'restore']
|
||||
const protocolCommands = commandsProcessedInNodeWorld.reduce((prev, commandName) => {
|
||||
prev[commandName] = {
|
||||
value: this.#getMockedCommand(commandName)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import logger from '@wdio/logger'
|
|||
import type { ClientOptions, RawData, WebSocket } from 'ws'
|
||||
|
||||
import Socket from './socket.js'
|
||||
import type * as remote from './remoteTypes.js'
|
||||
import type { CommandData } from './remoteTypes.js'
|
||||
import type { CommandResponse } from './localTypes.js'
|
||||
|
||||
|
|
@ -85,9 +86,20 @@ export class BidiCore {
|
|||
throw new Error('No connection to WebDriver Bidi was established')
|
||||
}
|
||||
|
||||
log.info('BIDI COMMAND', params.method, JSON.stringify(params.params))
|
||||
log.info('BIDI COMMAND', ...parseBidiCommand(params))
|
||||
const id = ++this.#id
|
||||
this.#ws.send(JSON.stringify({ id, ...params }))
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
function parseBidiCommand (params: Omit<CommandData, 'id'>) {
|
||||
const commandName = params.method
|
||||
if (commandName === 'script.addPreloadScript') {
|
||||
const param = params.params as remote.ScriptAddPreloadScriptParameters
|
||||
const logString = `{ functionDeclaration: <PreloadScript[${Buffer.byteLength(param.functionDeclaration, 'utf-8')} bytes]>, contexts: ${JSON.stringify(param.contexts)} }`
|
||||
return [commandName, logString]
|
||||
}
|
||||
|
||||
return [commandName, JSON.stringify(params.params)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
"types": "./build/index.d.ts",
|
||||
"typeScriptVersion": "3.8.3",
|
||||
"devDependencies": {
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/aria-query": "^5.0.4",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
|
|
@ -68,6 +69,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||
"@wdio/config": "workspace:*",
|
||||
"@wdio/logger": "workspace:*",
|
||||
"@wdio/protocols": "workspace:*",
|
||||
|
|
|
|||
164
packages/webdriverio/src/clock.ts
Normal file
164
packages/webdriverio/src/clock.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import logger from '@wdio/logger'
|
||||
import type { FakeTimerInstallOpts, InstalledClock, install } from '@sinonjs/fake-timers'
|
||||
|
||||
const log = logger('webdriverio:ClockManager')
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__clock: InstalledClock
|
||||
__wdio_sinon: {
|
||||
install: typeof install
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installFakeTimers (options: FakeTimerInstallOpts) {
|
||||
window.__clock = window.__wdio_sinon.install(options)
|
||||
}
|
||||
|
||||
function uninstallFakeTimers () {
|
||||
window.__clock.uninstall()
|
||||
}
|
||||
|
||||
export class ClockManager {
|
||||
#browser: WebdriverIO.Browser
|
||||
#resetFn: (() => Promise<any>) = () => Promise.resolve()
|
||||
#isInstalled = false
|
||||
|
||||
constructor(browser: WebdriverIO.Browser) {
|
||||
this.#browser = browser
|
||||
}
|
||||
|
||||
/**
|
||||
* Install fake timers on the browser. If you call the `emulate` command, WebdriverIO will automatically install
|
||||
* the fake timers for you. You can use this method to re-install the fake timers if you have called `restore`.
|
||||
*
|
||||
* @param options {FakeTimerInstallOpts} Options to pass to the fake clock
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async install(options?: FakeTimerInstallOpts) {
|
||||
if (this.#isInstalled) {
|
||||
return log.warn('Fake timers are already installed')
|
||||
}
|
||||
|
||||
/**
|
||||
* skip if we are not running in a Node.js environment
|
||||
*/
|
||||
if (globalThis.window) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* load Node.js specific modules dynamically to avoid loading them in the browser
|
||||
*/
|
||||
const url = await import('node:url')
|
||||
const path = await import('node:path')
|
||||
const fs = await import('node:fs/promises')
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
const emulateOptions = options || {} as FakeTimerInstallOpts
|
||||
const scriptPath = path.join(rootDir, 'third_party', 'fake-timers.js')
|
||||
const functionDeclaration = await fs.readFile(scriptPath, 'utf-8')
|
||||
const installOptions: FakeTimerInstallOpts = {
|
||||
...emulateOptions,
|
||||
now: emulateOptions.now && (emulateOptions.now instanceof Date) ? emulateOptions.now.getTime() : emulateOptions.now
|
||||
}
|
||||
|
||||
const [, libScript, restoreInstallScript] = await Promise.all([
|
||||
/**
|
||||
* install fake timers for current ex
|
||||
*/
|
||||
this.#browser.execute(`return (${functionDeclaration}).apply(null, arguments)`, []).then(() => (
|
||||
this.#browser.execute(installFakeTimers, installOptions)
|
||||
)),
|
||||
/**
|
||||
* add preload script to to emulate clock for upcoming page loads
|
||||
*/
|
||||
this.#browser.scriptAddPreloadScript({ functionDeclaration }),
|
||||
this.#browser.addInitScript(installFakeTimers, installOptions)
|
||||
])
|
||||
this.#resetFn = async () => Promise.all([
|
||||
this.#browser.scriptRemovePreloadScript({ script: libScript.script }),
|
||||
this.#browser.execute(uninstallFakeTimers),
|
||||
restoreInstallScript
|
||||
])
|
||||
this.#isInstalled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all overridden native functions. This is automatically called between tests, so should not
|
||||
* generally be needed.
|
||||
*
|
||||
* ```ts
|
||||
* it('should restore the clock', async () => {
|
||||
* console.log(new Date()) // returns e.g. 1722560447102
|
||||
*
|
||||
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
*
|
||||
* await clock.restore()
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1722560447102
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async restore() {
|
||||
await this.#resetFn()
|
||||
this.#isInstalled = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the clock the specified number of `milliseconds`. Any timers within the affected range of time will be called.
|
||||
* @param ms {number} The number of milliseconds to move the clock.
|
||||
*
|
||||
* ```ts
|
||||
* it('should move the clock', async () => {
|
||||
* console.log(new Date()) // returns e.g. 1722560447102
|
||||
*
|
||||
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
*
|
||||
* await clock.tick(1000)
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383601000
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param {number} ms The number of milliseconds to move the clock.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async tick(ms: number) {
|
||||
await this.#browser.execute((ms) => window.__clock.tick(ms), ms)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the system time to the new now. Now can be a timestamp, date object, or not passed in which defaults
|
||||
* to 0. No timers will be called, nor will the time left before they trigger change.
|
||||
*
|
||||
* ```ts
|
||||
* it('should set the system time', async () => {
|
||||
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
*
|
||||
* await clock.setSystemTime(new Date(2011, 3, 15))
|
||||
* console.log(await browser.execute(() => new Date().getTime())) // returns 1302850800000
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param date {Date|number} The new date to set the system time to.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setSystemTime(date: number | Date) {
|
||||
const serializableSystemTime = date instanceof Date ? date.getTime() : date
|
||||
await this.#browser.execute((date) => window.__clock.setSystemTime(date), serializableSystemTime)
|
||||
}
|
||||
}
|
||||
|
||||
export type SupportedScopes = 'geolocation' | 'userAgent' | 'colorScheme' | 'onLine' | 'clock'
|
||||
export type RestoreMap = Map<SupportedScopes, (() => Promise<any>)[]>
|
||||
|
||||
/**
|
||||
* store all preload scripts in a map (per instance) so that we can easily remove them
|
||||
*/
|
||||
export const restoreFunctions = new Map<WebdriverIO.Browser, RestoreMap>()
|
||||
|
|
@ -24,6 +24,7 @@ export * from './browser/pause.js'
|
|||
export * from './browser/react$$.js'
|
||||
export * from './browser/react$.js'
|
||||
export * from './browser/reloadSession.js'
|
||||
export * from './browser/restore.js'
|
||||
export * from './browser/savePDF.js'
|
||||
export * from './browser/saveRecordingScreen.js'
|
||||
export * from './browser/saveScreenshot.js'
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export async function addInitScript<ReturnValue, InnerArguments extends any[]> (
|
|||
const context = await this.getWindowHandle()
|
||||
const result = await this.scriptAddPreloadScript({
|
||||
functionDeclaration: `() => {
|
||||
const closure = new Function('return ${script.toString()}')
|
||||
const closure = new Function(\`return ${script.toString()}\`)
|
||||
return closure()(${serializedParameters.join(', ')})
|
||||
}`,
|
||||
arguments: [],
|
||||
|
|
|
|||
|
|
@ -1,21 +1,52 @@
|
|||
type SupportedScopes = 'geolocation' | 'userAgent' | 'colorScheme' | 'onLine'
|
||||
import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
|
||||
|
||||
import { type SupportedScopes, restoreFunctions, ClockManager } from '../../clock.js'
|
||||
|
||||
type RestoreFunction = () => Promise<any>
|
||||
type ColorScheme = 'light' | 'dark'
|
||||
|
||||
interface EmulationOptions {
|
||||
geolocation: Partial<GeolocationCoordinates>
|
||||
userAgent: string
|
||||
colorScheme: 'light' | 'dark'
|
||||
colorScheme: ColorScheme
|
||||
onLine: boolean
|
||||
clock?: FakeTimerInstallOpts
|
||||
}
|
||||
|
||||
function storeRestoreFunction (browser: WebdriverIO.Browser, scope: SupportedScopes, fn: RestoreFunction) {
|
||||
if (!restoreFunctions.has(browser)) {
|
||||
restoreFunctions.set(browser, new Map())
|
||||
}
|
||||
|
||||
const restoreFunctionsList = restoreFunctions.get(browser)?.get(scope)
|
||||
const updatedList = restoreFunctionsList ? [...restoreFunctionsList, fn] : [fn]
|
||||
restoreFunctions.get(browser)?.set(scope, updatedList)
|
||||
}
|
||||
|
||||
export async function emulate(scope: 'clock', options?: FakeTimerInstallOpts): Promise<ClockManager>
|
||||
export async function emulate(scope: 'geolocation', geolocation: Partial<GeolocationCoordinates>): Promise<RestoreFunction>
|
||||
export async function emulate(scope: 'userAgent', userAgent: string): Promise<RestoreFunction>
|
||||
export async function emulate(scope: 'colorScheme', colorScheme: ColorScheme): Promise<RestoreFunction>
|
||||
export async function emulate(scope: 'onLine', state: boolean): Promise<RestoreFunction>
|
||||
|
||||
/**
|
||||
* WebdriverIO allows you to emulate Web APIs using the `emulate` command. These Web APIs can then
|
||||
* behave exactly as you specify it.
|
||||
* behave exactly as you specify it. The following scopes are supported:
|
||||
*
|
||||
* - `geolocation`: Emulate the geolocation API
|
||||
* - `userAgent`: Emulate the user agent
|
||||
* - `colorScheme`: Emulate the color scheme
|
||||
* - `onLine`: Emulate the online status
|
||||
* - `clock`: Emulate the system clock
|
||||
*
|
||||
* The `emulate` command returns a function that can be called to reset the emulation. This is useful
|
||||
* when you want to reset the emulation after a test or a suite of tests.
|
||||
*
|
||||
* Read more on this in the [Emulation](/docs/emulation) guidelines.
|
||||
*
|
||||
* :::info
|
||||
*
|
||||
* It is not possible to change the emulated value without reloading the page.
|
||||
* Except for the `clock` scope it is not possible to change the emulated value without reloading the page.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
|
|
@ -27,11 +58,57 @@ interface EmulationOptions {
|
|||
*
|
||||
* :::
|
||||
*
|
||||
* @param {string} scope feature of the browser you like to emulate, can be either `geolocation`, `userAgent`, `colorScheme` or `onLine`
|
||||
* Based on the scope you can pass different options:
|
||||
*
|
||||
* | Scope | Options |
|
||||
* |---------------|--------------------------------------------------|
|
||||
* | `geolocation` | `{ latitude: number, longitude: number }` |
|
||||
* | `userAgent` | `string` |
|
||||
* | `colorScheme` | `'light' | 'dark'` |
|
||||
* | `onLine` | `boolean` |
|
||||
* | `clock` | `FakeTimerInstallOpts` |
|
||||
*
|
||||
* The `FakeTimerInstallOpts` object can have the following properties:
|
||||
*
|
||||
* ```ts
|
||||
* interface FakeTimerInstallOpts {
|
||||
* // Installs fake timers with the specified unix epoch
|
||||
* // @default: 0
|
||||
* now?: number | Date | undefined;
|
||||
|
||||
* // An array with names of global methods and APIs to fake. By default, WebdriverIO
|
||||
* // does not replace `nextTick()` and `queueMicrotask()`. For instance,
|
||||
* // `browser.emulate('clock', { toFake: ['setTimeout', 'nextTick'] })` will fake only
|
||||
* // `setTimeout()` and `nextTick()`
|
||||
* toFake?: FakeMethod[] | undefined;
|
||||
|
||||
* // The maximum number of timers that will be run when calling runAll() (default: 1000)
|
||||
* loopLimit?: number | undefined;
|
||||
|
||||
* // Tells WebdriverIO to increment mocked time automatically based on the real system
|
||||
* // time shift (e.g. the mocked time will be incremented by 20ms for every 20ms change
|
||||
* // in the real system time)
|
||||
* // @default false
|
||||
* shouldAdvanceTime?: boolean | undefined;
|
||||
|
||||
* // Relevant only when using with shouldAdvanceTime: true. increment mocked time by
|
||||
* // advanceTimeDelta ms every advanceTimeDelta ms change in the real system time
|
||||
* // @default: 20
|
||||
* advanceTimeDelta?: number | undefined;
|
||||
|
||||
* // Tells FakeTimers to clear 'native' (i.e. not fake) timers by delegating to their
|
||||
* // respective handlers. These are not cleared by default, leading to potentially
|
||||
* // unexpected behavior if timers existed prior to installing FakeTimers.
|
||||
* // @default: false
|
||||
* shouldClearNativeTimers?: boolean | undefined;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {string} scope feature of the browser you like to emulate, can be either `clock`, `geolocation`, `userAgent`, `colorScheme` or `onLine`
|
||||
* @param {EmulationOptions} options emulation option for specific scope
|
||||
* @example https://github.com/webdriverio/example-recipes/blob/9bff2baf8a0678c6886f8591d9fc8dea201895d3/emulate/example.js#L4-L18
|
||||
* @example https://github.com/webdriverio/example-recipes/blob/9bff2baf8a0678c6886f8591d9fc8dea201895d3/emulate/example.js#L20-L36
|
||||
* @returns `void`
|
||||
* @returns {Function} a function to reset the emulation
|
||||
*/
|
||||
export async function emulate<Scope extends SupportedScopes> (
|
||||
this: WebdriverIO.Browser,
|
||||
|
|
@ -53,14 +130,16 @@ export async function emulate<Scope extends SupportedScopes> (
|
|||
coords: ${JSON.stringify(options)},
|
||||
timestamp: Date.now()
|
||||
})`
|
||||
await this.scriptAddPreloadScript({
|
||||
const res = await this.scriptAddPreloadScript({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator.geolocation, 'getCurrentPosition', {
|
||||
value: (cbSuccess, cbError) => ${patchedFn}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script })
|
||||
storeRestoreFunction(this, 'geolocation', resetFn)
|
||||
return resetFn
|
||||
}
|
||||
|
||||
if (scope === 'userAgent') {
|
||||
|
|
@ -68,14 +147,25 @@ export async function emulate<Scope extends SupportedScopes> (
|
|||
throw new Error(`Expected userAgent emulation options to be a string, received ${typeof options}`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScript({
|
||||
const res = await this.scriptAddPreloadScript({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: ${JSON.stringify(options)}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
const resetFn = async () => {
|
||||
return this.scriptRemovePreloadScript({ script: res.script })
|
||||
}
|
||||
storeRestoreFunction(this, 'userAgent', resetFn)
|
||||
return resetFn
|
||||
}
|
||||
|
||||
if (scope === 'clock') {
|
||||
const clock = new ClockManager(this)
|
||||
await clock.install(options as FakeTimerInstallOpts)
|
||||
storeRestoreFunction(this, 'clock', clock.restore.bind(clock))
|
||||
return clock
|
||||
}
|
||||
|
||||
if (scope === 'colorScheme') {
|
||||
|
|
@ -83,7 +173,7 @@ export async function emulate<Scope extends SupportedScopes> (
|
|||
throw new Error(`Expected "colorScheme" emulation options to be either "light" or "dark", received "${options}"`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScript({
|
||||
const res = await this.scriptAddPreloadScript({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
const originalMatchMedia = window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
|
|
@ -104,7 +194,9 @@ export async function emulate<Scope extends SupportedScopes> (
|
|||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script })
|
||||
storeRestoreFunction(this, 'colorScheme', resetFn)
|
||||
return resetFn
|
||||
}
|
||||
|
||||
if (scope === 'onLine') {
|
||||
|
|
@ -112,15 +204,17 @@ export async function emulate<Scope extends SupportedScopes> (
|
|||
throw new Error(`Expected "onLine" emulation options to be a boolean, received "${typeof options}"`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScript({
|
||||
const res = await this.scriptAddPreloadScript({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator, 'onLine', {
|
||||
value: ${options}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script })
|
||||
storeRestoreFunction(this, 'onLine', resetFn)
|
||||
return resetFn
|
||||
}
|
||||
|
||||
return
|
||||
throw new Error(`Invalid scope "${scope}", expected one of "geolocation", "userAgent", "colorScheme", "onLine" or "clock"`)
|
||||
}
|
||||
|
|
|
|||
49
packages/webdriverio/src/commands/browser/restore.ts
Normal file
49
packages/webdriverio/src/commands/browser/restore.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { type SupportedScopes, restoreFunctions } from '../../clock.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* This command restores specific or all emulated behaviors that were set using the `emulate` command.
|
||||
*
|
||||
* <example>
|
||||
:restore.js
|
||||
before(async () => {
|
||||
await browser.emulate('geolocation', { latitude: 52.52, longitude: 13.405 })
|
||||
await browser.emulate('userAgent', 'foobar')
|
||||
await browser.emulate('colorScheme', 'dark')
|
||||
await browser.emulate('onLine', false)
|
||||
})
|
||||
|
||||
it('should restore all emulated behavior', async () => {
|
||||
await browser.url('https://webdriver.io')
|
||||
// test within an emulated environment...
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// restore all emulated behavior
|
||||
await browser.restore()
|
||||
// or only restore specific emulated behavior
|
||||
// await browser.restore(['geolocation', 'userAgent'])
|
||||
})
|
||||
* </example>
|
||||
*
|
||||
* @alias browser.restore
|
||||
* @type utility
|
||||
*
|
||||
*/
|
||||
export async function restore (
|
||||
this: WebdriverIO.Browser,
|
||||
scopes?: SupportedScopes | SupportedScopes[]
|
||||
) {
|
||||
const scopeArray = !scopes || Array.isArray(scopes) ? scopes : [scopes]
|
||||
const instanceRestoreFunctions = restoreFunctions.get(this)
|
||||
if (!instanceRestoreFunctions) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(Array.from(instanceRestoreFunctions.entries()).map(async ([scope, restoreFunctionsList]) => {
|
||||
if (!scopeArray || scopeArray.includes(scope)) {
|
||||
await Promise.all(restoreFunctionsList.map((fn) => fn()))
|
||||
instanceRestoreFunctions.set(scope, [])
|
||||
}
|
||||
}))
|
||||
}
|
||||
19
packages/webdriverio/src/commands/clock/restore.ts
Normal file
19
packages/webdriverio/src/commands/clock/restore.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Restore all overridden native functions. This is automatically called between tests, so should not
|
||||
* generally be needed.
|
||||
*
|
||||
* <example>
|
||||
:restore.js
|
||||
console.log(new Date()) // returns e.g. 1722560447102
|
||||
|
||||
const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
|
||||
await clock.restore()
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1722560447102
|
||||
* </example>
|
||||
*
|
||||
* @alias clock.restore
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
// actual implementation is located in packages/webdriverio/src/clock.ts
|
||||
18
packages/webdriverio/src/commands/clock/setSystemTime.ts
Normal file
18
packages/webdriverio/src/commands/clock/setSystemTime.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Change the system time to the new now. Now can be a timestamp, date object, or not passed in which defaults
|
||||
* to 0. No timers will be called, nor will the time left before they trigger change.
|
||||
*
|
||||
* <example>
|
||||
:setSystemTime.js
|
||||
const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
|
||||
await clock.setSystemTime(new Date(2011, 3, 15))
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1302850800000
|
||||
* </example>
|
||||
*
|
||||
* @alias clock.setSystemTime
|
||||
* @param { Date | number } date The new date to set the system time to.
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
// actual implementation is located in packages/webdriverio/src/clock.ts
|
||||
18
packages/webdriverio/src/commands/clock/tick.ts
Normal file
18
packages/webdriverio/src/commands/clock/tick.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Move the clock the specified number of `milliseconds`. Any timers within the affected range of time will be called.
|
||||
*
|
||||
* <example>
|
||||
:tick.js
|
||||
const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
||||
|
||||
await clock.tick(1000)
|
||||
console.log(await browser.execute(() => new Date().getTime())) // returns 1618383601000
|
||||
* </example>
|
||||
*
|
||||
* @alias clock.tick
|
||||
* @param { number } ms The number of milliseconds to move the clock.
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
// actual implementation is located in packages/webdriverio/src/clock.ts
|
||||
|
||||
|
|
@ -2,11 +2,11 @@ import type { RectReturn } from '@wdio/protocols'
|
|||
|
||||
import { getElementRect } from '../../utils/index.js'
|
||||
|
||||
export type Size = Pick<RectReturn, 'width' | 'height'>;
|
||||
export type Size = Pick<RectReturn, 'width' | 'height'>
|
||||
|
||||
export function getSize (this: WebdriverIO.Element): Promise<Size>;
|
||||
export function getSize (this: WebdriverIO.Element, prop: keyof RectReturn): Promise<number>;
|
||||
export function getSize (this: WebdriverIO.Element, prop?: keyof RectReturn): Promise<Size & number>;
|
||||
export function getSize (this: WebdriverIO.Element): Promise<Size>
|
||||
export function getSize (this: WebdriverIO.Element, prop: keyof RectReturn): Promise<number>
|
||||
export function getSize (this: WebdriverIO.Element, prop?: keyof RectReturn): Promise<Size & number>
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,88 +1,96 @@
|
|||
import path from 'node:path'
|
||||
|
||||
import { expect, describe, it, vi } from 'vitest'
|
||||
import { remote } from '../../../src/index.js'
|
||||
|
||||
vi.mock('fetch')
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
|
||||
describe('emulate', () => {
|
||||
it('should fail if bidi is not supported', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-expect-error invalid argument
|
||||
await expect(() => browser.emulate('geoLocation', {}))
|
||||
.rejects.toThrow(/emulate command is only supported for Bidi/)
|
||||
})
|
||||
|
||||
it('should allow to emulate geolocation', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn(),
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
}
|
||||
}
|
||||
await expect(() => browser.emulate.call(fakeScope, 'geolocation')).rejects.toThrow(/Missing geolocation emulation options/)
|
||||
await browser.emulate.call(fakeScope, 'geolocation', { latitude: 123, longitude: 456 })
|
||||
} as any as WebdriverIO.Browser
|
||||
fakeScope.emulate = browser.emulate.bind(fakeScope)
|
||||
|
||||
// @ts-expect-error missing argument
|
||||
await expect(() => fakeScope.emulate('geolocation')).rejects.toThrow(/Missing geolocation emulation options/)
|
||||
const restore = await fakeScope.emulate('geolocation', { latitude: 123, longitude: 456 })
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator.geolocation, \'getCurrentPosition\', {')
|
||||
})
|
||||
|
||||
expect(restore).toBeInstanceOf(Function)
|
||||
await restore()
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow to emulate userAgent', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn(),
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
}
|
||||
}
|
||||
await expect(() => browser.emulate.call(fakeScope, 'userAgent', 123)).rejects.toThrow(/Expected userAgent emulation options to be a string/)
|
||||
await browser.emulate.call(fakeScope, 'userAgent', 'foobar')
|
||||
} as any as WebdriverIO.Browser
|
||||
fakeScope.emulate = browser.emulate.bind(fakeScope)
|
||||
// @ts-expect-error invalid argument
|
||||
await expect(() => fakeScope.emulate('userAgent', 123)).rejects.toThrow(/Expected userAgent emulation options to be a string/)
|
||||
const restore = await fakeScope.emulate('userAgent', 'foobar')
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator, \'userAgent\', {')
|
||||
})
|
||||
|
||||
expect(restore).toBeInstanceOf(Function)
|
||||
await restore()
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow to emulate colorScheme', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn(),
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
}
|
||||
}
|
||||
await expect(() => browser.emulate.call(fakeScope, 'colorScheme', 123)).rejects.toThrow(/Expected "colorScheme" emulation options to be either "light" or "dark"/)
|
||||
await browser.emulate.call(fakeScope, 'colorScheme', 'light')
|
||||
} as any as WebdriverIO.Browser
|
||||
fakeScope.emulate = browser.emulate.bind(fakeScope)
|
||||
|
||||
// @ts-expect-error invalid argument
|
||||
await expect(() => fakeScope.emulate('colorScheme', 123)).rejects.toThrow(/Expected "colorScheme" emulation options to be either "light" or "dark"/)
|
||||
const restore = await fakeScope.emulate('colorScheme', 'light')
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(window, \'matchMedia\', {')
|
||||
})
|
||||
|
||||
expect(restore).toBeInstanceOf(Function)
|
||||
await restore()
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow to emulate onLine', async () => {
|
||||
|
|
@ -94,17 +102,63 @@ describe('emulate', () => {
|
|||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn(),
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
}
|
||||
}
|
||||
await expect(() => browser.emulate.call(fakeScope, 'onLine', 123)).rejects.toThrow(/Expected "onLine" emulation options to be a boolean/)
|
||||
await browser.emulate.call(fakeScope, 'onLine', false)
|
||||
} as any as WebdriverIO.Browser
|
||||
fakeScope.emulate = browser.emulate.bind(fakeScope)
|
||||
|
||||
// @ts-expect-error invalid
|
||||
await expect(() => fakeScope.emulate('onLine', 123)).rejects.toThrow(/Expected "onLine" emulation options to be a boolean/)
|
||||
const restore = await fakeScope.emulate('onLine', false)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator, \'onLine\', {')
|
||||
})
|
||||
|
||||
expect(restore).toBeInstanceOf(Function)
|
||||
await restore()
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow to emulate the clock', async () => {
|
||||
const now = new Date(2021, 3, 14)
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
addInitScript: vi.fn(),
|
||||
execute: vi.fn().mockResolvedValue({}),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
},
|
||||
} as any as WebdriverIO.Browser
|
||||
fakeScope.emulate = browser.emulate.bind(fakeScope)
|
||||
|
||||
const clock = await fakeScope.emulate('clock', { now })
|
||||
expect(fakeScope.execute).toBeCalledTimes(2)
|
||||
expect(fakeScope.addInitScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScript).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('function hijackMethod(target, method, clock) ')
|
||||
})
|
||||
expect(fakeScope.addInitScript).toBeCalledWith(
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ now: now.getTime() })
|
||||
)
|
||||
|
||||
expect(clock.restore).toBeInstanceOf(Function)
|
||||
await clock.restore()
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
69
packages/webdriverio/tests/commands/browser/restore.test.ts
Normal file
69
packages/webdriverio/tests/commands/browser/restore.test.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import path from 'node:path'
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
import { remote } from '../../../src/index.js'
|
||||
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
const browserA = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
|
||||
const browserB = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScript: vi.fn().mockResolvedValue({ script: 'foobar' }),
|
||||
scriptRemovePreloadScript: vi.fn(),
|
||||
addInitScript: vi.fn(),
|
||||
execute: vi.fn().mockResolvedValue({}),
|
||||
options: {
|
||||
beforeCommand: vi.fn(),
|
||||
afterCommand: vi.fn()
|
||||
}
|
||||
} as any as WebdriverIO.Browser
|
||||
|
||||
const scopeBrowserA = {
|
||||
...fakeScope,
|
||||
emulate: browserA.emulate.bind(browserA)
|
||||
}
|
||||
|
||||
const scopeBrowserB = {
|
||||
...fakeScope,
|
||||
emulate: browserB.emulate.bind(browserB)
|
||||
}
|
||||
|
||||
describe('restore', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(fakeScope.scriptRemovePreloadScript).mockClear()
|
||||
})
|
||||
|
||||
it('should restore all emulated behavior', async () => {
|
||||
await browserA.emulate.call(scopeBrowserA, 'geolocation', { latitude: 52.52, longitude: 13.405 })
|
||||
await browserA.emulate.call(scopeBrowserA, 'userAgent', 'foobar')
|
||||
await browserA.emulate.call(scopeBrowserA, 'colorScheme', 'dark')
|
||||
await browserB.emulate.call(scopeBrowserB, 'onLine', false)
|
||||
|
||||
await browserB.restore.call(scopeBrowserB)
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(1)
|
||||
await browserA.restore.call(scopeBrowserA)
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(4)
|
||||
})
|
||||
|
||||
it('should restore specific emulated behavior', async () => {
|
||||
await browserA.emulate.call(scopeBrowserA, 'geolocation', { latitude: 52.52, longitude: 13.405 })
|
||||
await browserA.emulate.call(scopeBrowserA, 'userAgent', 'foobar')
|
||||
await browserA.emulate.call(scopeBrowserA, 'colorScheme', 'dark')
|
||||
await browserA.emulate.call(scopeBrowserA, 'onLine', false)
|
||||
await browserA.restore.call(scopeBrowserA, ['geolocation', 'userAgent'])
|
||||
expect(fakeScope.scriptRemovePreloadScript).toBeCalledTimes(2)
|
||||
})
|
||||
})
|
||||
1846
packages/webdriverio/third_party/fake-timers.js
vendored
Normal file
1846
packages/webdriverio/third_party/fake-timers.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1406,6 +1406,9 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^20.11.30
|
||||
version: 20.14.2
|
||||
'@types/sinonjs__fake-timers':
|
||||
specifier: ^8.1.5
|
||||
version: 8.1.5
|
||||
'@wdio/config':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-config
|
||||
|
|
@ -1482,6 +1485,9 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../webdriver
|
||||
devDependencies:
|
||||
'@sinonjs/fake-timers':
|
||||
specifier: ^11.2.2
|
||||
version: 11.2.2
|
||||
'@types/archiver':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
|
|
@ -4337,6 +4343,12 @@ packages:
|
|||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
|
||||
|
||||
'@sinonjs/fake-timers@11.2.2':
|
||||
resolution: {integrity: sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==}
|
||||
|
||||
'@slorber/react-ideal-image@0.0.12':
|
||||
resolution: {integrity: sha512-u8KiDTEkMA7/KAeA5ywg/P7YG4zuKhWtswfVZDH8R8HXgQsFcHIYU2WaQnGuK/Du7Wdj90I+SdFmajSGFRvoKA==}
|
||||
engines: {node: '>= 8.9.0', npm: '> 3'}
|
||||
|
|
@ -5240,6 +5252,9 @@ packages:
|
|||
'@types/shelljs@0.8.15':
|
||||
resolution: {integrity: sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==}
|
||||
|
||||
'@types/sinonjs__fake-timers@8.1.5':
|
||||
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
|
||||
|
||||
'@types/sockjs@0.3.36':
|
||||
resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==}
|
||||
|
||||
|
|
@ -17195,6 +17210,14 @@ snapshots:
|
|||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
dependencies:
|
||||
type-detect: 4.0.8
|
||||
|
||||
'@sinonjs/fake-timers@11.2.2':
|
||||
dependencies:
|
||||
'@sinonjs/commons': 3.0.1
|
||||
|
||||
'@slorber/react-ideal-image@0.0.12(prop-types@15.8.1)(react-waypoint@10.3.0(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -18372,6 +18395,8 @@ snapshots:
|
|||
'@types/glob': 7.2.0
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@types/sinonjs__fake-timers@8.1.5': {}
|
||||
|
||||
'@types/sockjs@0.3.36':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function generateWdioDocs (sidebars: any) {
|
|||
element: ['api/element', fs.readdirSync(path.join(COMMAND_DIR, 'element'))],
|
||||
mock: ['api/mock', fs.readdirSync(path.join(COMMAND_DIR, 'mock'))],
|
||||
dialog: ['api/dialog', fs.readdirSync(path.join(COMMAND_DIR, 'dialog'))],
|
||||
clock: ['api/clock', fs.readdirSync(path.join(COMMAND_DIR, 'clock'))],
|
||||
}
|
||||
|
||||
const apiDocs: NavbarItem[] = []
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ This protocol command is embedded in the following convenient method<%= it.alter
|
|||
$(selector).<%= it.command %>(<%= it.paramString %>)
|
||||
<% } else if (it.isMockScope) { %>
|
||||
mock.<%= it.command %>(<%= it.paramString %>)
|
||||
<% } else if (it.isDialogScope) { %>
|
||||
await dialog.<%= it.command %>(<%= it.paramString %>)
|
||||
<% } else if (it.isClockScope) { %>
|
||||
const clock = await browser.emulate('clock', { ... })
|
||||
await clock.<%= it.command %>(<%= it.paramString %>)
|
||||
<% } else { %>
|
||||
<%= it.isMobile ? 'driver' : 'browser' %>.<%= it.command %>(<%= it.paramString %>)
|
||||
<% } %>
|
||||
|
|
|
|||
|
|
@ -220,7 +220,9 @@ export default function (docfile: any) {
|
|||
originalId: `api/${scope}/${name}`,
|
||||
isElementScope : scope === 'element',
|
||||
isNetworkScope : scope === 'network',
|
||||
isMockScope : scope === 'mock'
|
||||
isMockScope : scope === 'mock',
|
||||
isDialogScope : scope === 'dialog',
|
||||
isClockScope : scope === 'clock',
|
||||
}
|
||||
|
||||
return commandDescription
|
||||
|
|
|
|||
|
|
@ -526,6 +526,18 @@ async function bar() {
|
|||
const multiElementError = $$('selector').getElement()
|
||||
// @ts-expect-error
|
||||
const multiElementError2 = multiElements.getElement()
|
||||
|
||||
// Emulate tests
|
||||
let restore = await browser.emulate('geolocation', { latitude: 1, longitude: 2 })
|
||||
await restore()
|
||||
restore = await browser.emulate('userAgent', 'foobar')
|
||||
restore = await browser.emulate('onLine', true)
|
||||
// @ts-expect-error invalid param
|
||||
restore = await browser.emulate('onLine', 'dark')
|
||||
// @ts-expect-error invalid scope
|
||||
restore = await browser.emulate('foobar')
|
||||
const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
||||
await clock.restore()
|
||||
}
|
||||
|
||||
function testSevereServiceError_noParameters() {
|
||||
|
|
|
|||
40
website/docs/api/Clock.md
Normal file
40
website/docs/api/Clock.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
id: clock
|
||||
title: The Clock Object
|
||||
---
|
||||
|
||||
You can modify the browser system clock using the [`emulate`](/docs/emulation) command. It overrides native global functions related to time allowing them to be controlled synchronously via `clock.tick()` or the yielded clock object. This includes controlling:
|
||||
|
||||
- `setTimeout`
|
||||
- `clearTimeout`
|
||||
- `setInterval`
|
||||
- `clearInterval`
|
||||
- `Date Objects`
|
||||
|
||||
The clock starts at the unix epoch (timestamp of 0). This means that when you instantiate new Date in your application, it will have a time of January 1st, 1970 if you don't pass any other options to the `emulate` command.
|
||||
|
||||
## Example
|
||||
|
||||
When calling `browser.emulate('clock', { ... })` it will immediately overwrite the global functions for the current page as well as all following pages, e.g.:
|
||||
|
||||
```ts
|
||||
const clock = await browser.emulate('clock', { now: new Date(1989, 7, 4) })
|
||||
|
||||
console.log(await browser.execute(() => (new Date()).toString()))
|
||||
// returns "Fri Aug 04 1989 00:00:00 GMT-0700 (Pacific Daylight Time)"
|
||||
|
||||
await browser.url('https://webdriverio')
|
||||
console.log(await browser.execute(() => (new Date()).toString()))
|
||||
// returns "Fri Aug 04 1989 00:00:00 GMT-0700 (Pacific Daylight Time)"
|
||||
|
||||
await clock.restore()
|
||||
|
||||
console.log(await browser.execute(() => (new Date()).toString()))
|
||||
// returns "Thu Aug 01 2024 17:59:59 GMT-0700 (Pacific Daylight Time)"
|
||||
|
||||
await browser.url('http://guinea-pig.webdriver.io/pointer.html')
|
||||
console.log(await browser.execute(() => (new Date()).toString()))
|
||||
// returns "Thu Aug 01 2024 17:59:59 GMT-0700 (Pacific Daylight Time)"
|
||||
```
|
||||
|
||||
You can modify the system time by calling [`setSystemTime`](/) or [`tick`](/).
|
||||
26
website/docs/api/Dialog.md
Normal file
26
website/docs/api/Dialog.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: dialog
|
||||
title: The Dialog Object
|
||||
---
|
||||
|
||||
Dialog objects are dispatched by [`browser`](/docs/api/browser) via the `browser.on('dialog')` event.
|
||||
|
||||
An example of using the Dialog object:
|
||||
|
||||
```ts
|
||||
import { browser } from '@wdio/globals'
|
||||
|
||||
await browser.url('https://webdriver.io')
|
||||
browser.on('dialog', async (dialog) => {
|
||||
console.log(dialog.message()) // outputs: "Hello Dialog"
|
||||
await dialog.dismiss()
|
||||
})
|
||||
|
||||
await page.evaluate(() => alert('Hello Dialog'))
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Dialogs are dismissed automatically, unless there is a `page.on('dialog')` listener. When listener is present, it must either [`dialog.accept()`](/docs/api/dialog/accept) or [`dialog.dismiss()`](/docs/api/dialog/dismiss) the dialog - otherwise the page will freeze waiting for the dialog, and actions like click will never finish.
|
||||
|
||||
:::
|
||||
Loading…
Reference in a new issue