(webdriverio): support emulating Web APIs for geolocation, colorScheme, userAgent and onLine (#11592)
* (webdriverio): new emulate command * add docs * add tests
This commit is contained in:
parent
da339851dc
commit
c53480b266
6 changed files with 330 additions and 0 deletions
|
|
@ -12,4 +12,5 @@ const mock = () => logMock
|
|||
mock.setLevel = vi.fn()
|
||||
mock.setLogLevelsConfig = vi.fn()
|
||||
mock.waitForBuffer = vi.fn()
|
||||
mock.clearLogger = vi.fn()
|
||||
export default mock
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export * from './browser/custom$$.js'
|
|||
export * from './browser/custom$.js'
|
||||
export * from './browser/debug.js'
|
||||
export * from './browser/deleteCookies.js'
|
||||
export * from './browser/emulate.js'
|
||||
export * from './browser/execute.js'
|
||||
export * from './browser/executeAsync.js'
|
||||
export * from './browser/getCookies.js'
|
||||
|
|
|
|||
149
packages/webdriverio/src/commands/browser/emulate.ts
Normal file
149
packages/webdriverio/src/commands/browser/emulate.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
type SupportedScopes = 'geolocation' | 'userAgent' | 'colorScheme' | 'onLine'
|
||||
|
||||
interface EmulationOptions {
|
||||
geolocation: GeolocationPosition | Error
|
||||
userAgent: string
|
||||
colorScheme: 'light' | 'dark'
|
||||
onLine: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* WebdriverIO allows you to emulate Web APIs using the `emulate` command. These Web APIs can then
|
||||
* behave exactly as you specify it.
|
||||
*
|
||||
* Read more on this in the [Emulation](emulation) guidelines.
|
||||
*
|
||||
* :::info
|
||||
*
|
||||
* This feature requires WebDriver Bidi support for the browser. While recent versions of Chrome, Edge
|
||||
* and Firefox have such support, Safari __does not__. For updates follow [wpt.fyi](https://wpt.fyi/results/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py?label=experimental&label=master&aligned).
|
||||
* Furthermore if you use a cloud vendor for spawning browsers, make sure your vendor also supports WebDriver Bidi.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* <example>
|
||||
:emulateColorScheme.js
|
||||
it('should open WebdriverIO using light color scheme', async () => {
|
||||
await browser.emulate('colorScheme', 'light')
|
||||
await browser.url('https://webdriver.io')
|
||||
const backgroundColor = await browser.$('nav').getCSSProperty('background-color')
|
||||
console.log(backgroundColor.parsed.hex) // outputs: "#efefef"
|
||||
})
|
||||
it('should open WebdriverIO using dark color scheme'm async () => {
|
||||
await browser.emulate('colorScheme', 'dark')
|
||||
await browser.url('https://webdriver.io')
|
||||
const backgroundColor = await browser.$('nav').getCSSProperty('background-color')
|
||||
console.log(backgroundColor.parsed.hex) // outputs: "#000000"
|
||||
})
|
||||
* </example>
|
||||
*
|
||||
* <example>
|
||||
:emulateGeoLocation.js
|
||||
it('should find my emulated geo location', async () => {
|
||||
await browser.emulate('geolocation', {
|
||||
latitude: 52.52,
|
||||
longitude: 13.39,
|
||||
accuracy: 100
|
||||
})
|
||||
await browser.url('https://www.google.com/maps')
|
||||
await browser.$('aria/Show Your Location').click()
|
||||
await browser.pause(5000)
|
||||
console.log(await browser.getUrl()) // outputs: "https://www.google.com/maps/@52.52,13.39,16z?entry=ttu"
|
||||
})
|
||||
* </example>
|
||||
*
|
||||
* @param {string} scope feature of the browser you like to emulate, can be either `geolocation`, `userAgent`, `colorScheme` or `onLine`
|
||||
* @param {EmulationOptions} options emulation option for specific scope
|
||||
* @returns `void`
|
||||
*/
|
||||
export async function emulate<Scope extends SupportedScopes> (
|
||||
this: WebdriverIO.Browser,
|
||||
scope: Scope,
|
||||
options: EmulationOptions[Scope]
|
||||
) {
|
||||
if (!this.isBidi) {
|
||||
throw new Error('emulate command is only supported for Bidi')
|
||||
}
|
||||
|
||||
if (scope === 'geolocation') {
|
||||
if (!options) {
|
||||
throw new Error('Missing geolocation emulation options')
|
||||
}
|
||||
|
||||
const patchedFn = options instanceof Error
|
||||
? `cbError(new Error(${JSON.stringify(options.message)}))`
|
||||
: `cbSuccess({
|
||||
coords: ${JSON.stringify(options)},
|
||||
timestamp: Date.now()
|
||||
})`
|
||||
await this.scriptAddPreloadScriptCommand({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator.geolocation, 'getCurrentPosition', {
|
||||
value: (cbSuccess, cbError) => ${patchedFn}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (scope === 'userAgent') {
|
||||
if (typeof options !== 'string') {
|
||||
throw new Error(`Expected userAgent emulation options to be a string, received ${typeof options}`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScriptCommand({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: ${JSON.stringify(options)}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (scope === 'colorScheme') {
|
||||
if (options !== 'light' && options !== 'dark') {
|
||||
throw new Error(`Expected "colorScheme" emulation options to be either "light" or "dark", received "${options}"`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScriptCommand({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
const originalMatchMedia = window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: (query) => {
|
||||
const colorSchemeQuery = query.match(/\\(prefers-color-scheme:(\\s)*(dark|light)\\)/i)
|
||||
if (colorSchemeQuery) {
|
||||
const result = originalMatchMedia(query)
|
||||
Object.defineProperty(result, 'matches', {
|
||||
value: colorSchemeQuery[2] === "${options}",
|
||||
configurable: true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
return originalMatchMedia(query)
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (scope === 'onLine') {
|
||||
if (typeof options !== 'boolean') {
|
||||
throw new Error(`Expected "onLine" emulation options to be a boolean, received "${typeof options}"`)
|
||||
}
|
||||
|
||||
await this.scriptAddPreloadScriptCommand({
|
||||
functionDeclaration: /*js*/`() => {
|
||||
Object.defineProperty(navigator, 'onLine', {
|
||||
value: ${options}
|
||||
})
|
||||
}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
110
packages/webdriverio/tests/commands/browser/emulate.test.ts
Normal file
110
packages/webdriverio/tests/commands/browser/emulate.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { expect, describe, it, vi } from 'vitest'
|
||||
import { remote } from '../../../src/index.js'
|
||||
|
||||
vi.mock('got')
|
||||
|
||||
describe('emulate', () => {
|
||||
it('should fail if bidi is not supported', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
scriptAddPreloadScriptCommand: 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 })
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator.geolocation, \'getCurrentPosition\', {')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow to emulate userAgent', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScriptCommand: 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')
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator, \'userAgent\', {')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow to emulate colorScheme', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScriptCommand: 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')
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(window, \'matchMedia\', {')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow to emulate onLine', async () => {
|
||||
const browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const fakeScope = {
|
||||
isBidi: true,
|
||||
scriptAddPreloadScriptCommand: 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)
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledTimes(1)
|
||||
expect(fakeScope.scriptAddPreloadScriptCommand).toBeCalledWith({
|
||||
functionDeclaration: expect.stringContaining('Object.defineProperty(navigator, \'onLine\', {')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
"bestpractices",
|
||||
"selectors",
|
||||
"timeouts",
|
||||
"emulation",
|
||||
"accessibility",
|
||||
"mocksandspies",
|
||||
"customcommands",
|
||||
|
|
|
|||
68
website/docs/Emulation.md
Normal file
68
website/docs/Emulation.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
id: emulation
|
||||
title: Emulation
|
||||
---
|
||||
|
||||
With WebdriverIO you can emulate Web APIs using the [`emulate`](/docs/api/browser/emulate) command to return custom values that help you emulate certain browser behaviors. Note that this requires your application to explicitly use these APIs.
|
||||
|
||||
:::info
|
||||
|
||||
This feature requires WebDriver Bidi support for the browser. While recent versions of Chrome, Edge and Firefox have such support, Safari __does not__. For updates follow [wpt.fyi](https://wpt.fyi/results/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py?label=experimental&label=master&aligned). Furthermore if you use a cloud vendor for spawning browsers, make sure your vendor also supports WebDriver Bidi.
|
||||
|
||||
:::
|
||||
|
||||
## Geolocation
|
||||
|
||||
Change the browser geolocation to a specific area, e.g.:
|
||||
|
||||
```ts
|
||||
await browser.emulate('geolocation', {
|
||||
latitude: 52.52,
|
||||
longitude: 13.39,
|
||||
accuracy: 100
|
||||
})
|
||||
await browser.url('https://www.google.com/maps')
|
||||
await browser.$('aria/Show Your Location').click()
|
||||
await browser.pause(5000)
|
||||
console.log(await browser.getUrl()) // outputs: "https://www.google.com/maps/@52.52,13.39,16z?entry=ttu"
|
||||
```
|
||||
|
||||
This will monkey patch how [`navigator.geolocation.getCurrentPosition`](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) works and returns the location provided by you.
|
||||
|
||||
## Color Scheme
|
||||
|
||||
Change the default color scheme setup of the browser via:
|
||||
|
||||
```ts
|
||||
await browser.emulate('colorScheme', 'light')
|
||||
await browser.url('https://webdriver.io')
|
||||
const backgroundColor = await browser.$('nav').getCSSProperty('background-color')
|
||||
console.log(backgroundColor.parsed.hex) // outputs: "#efefef"
|
||||
|
||||
await browser.emulate('colorScheme', 'dark')
|
||||
await browser.url('https://webdriver.io')
|
||||
const backgroundColor = await browser.$('nav').getCSSProperty('background-color')
|
||||
console.log(backgroundColor.parsed.hex) // outputs: "#000000"
|
||||
```
|
||||
|
||||
This will monkey patch how [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) behaves when you query the color scheme via `(prefers-color-scheme: dark)`.
|
||||
|
||||
## User Agent
|
||||
|
||||
Change the user agent of the browser to a different string via:
|
||||
|
||||
```ts
|
||||
await browser.emulate('userAgent', 'Chrome/1.2.3.4 Safari/537.36')
|
||||
```
|
||||
|
||||
This will change the value of [`navigator.userAgent`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent). Note that browser vendors progressively deprecating the User Agent.
|
||||
|
||||
## onLine Property
|
||||
|
||||
Change the online status of the browser via:
|
||||
|
||||
```ts
|
||||
await browser.emulate('userAgent', 'Chrome/1.2.3.4 Safari/537.36')
|
||||
```
|
||||
|
||||
This will __not__ turn off network traffic between the browser and the internet and only changes the return value of [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine).
|
||||
Loading…
Reference in a new issue