(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:
Christian Bromann 2023-11-08 15:32:11 -08:00 committed by GitHub
parent da339851dc
commit c53480b266
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 330 additions and 0 deletions

View file

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

View file

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

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

View 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\', {')
})
})
})

View file

@ -45,6 +45,7 @@
"bestpractices",
"selectors",
"timeouts",
"emulation",
"accessibility",
"mocksandspies",
"customcommands",

68
website/docs/Emulation.md Normal file
View 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).