webdriverio/__mocks__/fetch.ts
Wim Selles aa49a4707d
fix: support windows/mac apps for isDisplayed (#14287)
* fix: support windows/mac apps for isDisplayed

- add 2 new session flags for windows and mac apps
- add UT's
- add Docs

* chore: forgot to push the docs
2025-03-20 00:00:44 -07:00

541 lines
17 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from 'vitest'
/**
* This flag helps to indicate that WebdriverIO is running in a unit test environment.
* Setting this environment changes the behavior of some functions to e.g. not exit
* the process or enter code sections that are hard to mock out.
*/
process.env.WDIO_UNIT_TESTS = '1'
globalThis.WDIO_RESQ_SCRIPT = ''
globalThis.WDIO_FAKER_SCRIPT = ''
const ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf'
const SHADOW_ELEMENT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf'
let manualMockResponse: any
const path = '/session'
const customResponses = new Set<{ pattern, response }>()
const defaultSessionId = 'foobar-123'
let sessionId = defaultSessionId
const genericElementId = 'some-elem-123'
const genericSubElementId = 'some-sub-elem-321'
const genericSubSubElementId = 'some-sub-sub-elem-231'
const genericShadowElementId = 'some-shadow-elem-123'
const genericSubShadowElementId = 'some-shadow-sub-elem-321'
/**
* Transform the specified property of each object in the collection by replacing 'mockFunction' with a predefined function (vi.fn()).
* This is intended to ensure that, when converting the request body to a string, functions are retained and not omitted.
* @param collection - An array of objects to process.
* @returns A new array with updated objects.
*/
const transformPropertyWithMockFunction = (collection: any[]) => {
return collection.map(item => {
for (const prop in item) {
if (item[prop] && item[prop] === 'mockFunction') {
item[prop] = vi.fn()
}
}
return item
})
}
const requestMock: any = vi.fn().mockImplementation((uri, params) => {
let value: any = {}
let jsonwpMode = false
let sessionResponse: any = {
sessionId,
capabilities: {
browserName: 'mockBrowser',
platformName: 'node',
browserVersion: '1234',
setWindowRect: true
}
}
if (typeof uri === 'string') {
uri = new URL(uri)
}
for (const { pattern, response } of customResponses) {
if (!(uri as URL).pathname.match(pattern)) {
continue
}
return Response.json(response)
}
let body: any = params?.body
try {
body = body && JSON.parse(body.toString())
} catch {
return Response.json({}, {
status: 422,
statusText: 'Unprocessable Entity'
})
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.jsonwpMode
) {
jsonwpMode = true
sessionResponse = {
sessionId,
browserName: 'mockBrowser'
}
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode
) {
sessionResponse.capabilities.deviceName = 'iNode'
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode &&
body.capabilities.alwaysMatch.nativeAppMode
) {
sessionResponse.capabilities.app = 'mockApp'
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode &&
body.capabilities.alwaysMatch.windowsAppMode
) {
sessionResponse.capabilities['appium:automationName'] = 'windows'
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode &&
body.capabilities.alwaysMatch.macAppMode
) {
sessionResponse.capabilities['appium:automationName'] = 'mac2'
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.keepBrowserName
) {
sessionResponse.capabilities.browserName = body.capabilities.alwaysMatch.browserName
}
if (body?.capabilities?.alwaysMatch?.browserName === 'bidi') {
sessionResponse.capabilities.webSocketUrl = 'ws://webdriver.io'
}
switch (uri.pathname) {
case path:
value = sessionResponse
if (body.capabilities.alwaysMatch.browserName && body.capabilities.alwaysMatch.browserName.includes('devtools')) {
value.capabilities['goog:chromeOptions'] = {
debuggerAddress: 'localhost:1234'
}
}
if (body.capabilities.alwaysMatch.platformName && body.capabilities.alwaysMatch.platformName.includes('iOS')) {
value.capabilities.platformName = 'iOS'
}
break
case `/session/${sessionId}/element`:
if (body && body.value === '#nonexisting') {
value = { elementId: null }
break
}
if (body && body.value === 'html') {
value = { [ELEMENT_KEY]: 'html-element' }
break
}
if (body && body.value === '#slowRerender') {
++requestMock.retryCnt
if (requestMock.retryCnt === 2) {
++requestMock.retryCnt
value = { elementId: null }
break
}
}
value = {
[ELEMENT_KEY]: genericElementId
}
break
case `${path}/${sessionId}/element/${genericElementId}/element`:
value = {
[ELEMENT_KEY]: genericSubElementId
}
break
case `${path}/${sessionId}/shadow/${genericShadowElementId}/element`:
value = {
[ELEMENT_KEY]: genericSubShadowElementId
}
break
case `${path}/${sessionId}/element/${genericElementId}/shadow`:
value = {
[SHADOW_ELEMENT_KEY]: genericShadowElementId
}
break
case `${path}/${sessionId}/element/${genericSubElementId}/element`:
value = {
[ELEMENT_KEY]: genericSubSubElementId
}
break
case `${path}/${sessionId}/element/html-element/rect`:
value = {
x: 0,
y: 0,
height: 1000,
width: 1000
}
break
case `${path}/${sessionId}/element/${genericElementId}/rect`:
value = {
x: 15,
y: 20,
height: 30,
width: 50
}
break
case `${path}/${sessionId}/element/${genericSubElementId}/rect`:
value = {
x: 100,
y: 200,
height: 120,
width: 150
}
break
case `${path}/${sessionId}/element/${genericElementId}/size`:
value = {
height: 30,
width: 50
}
break
case `${path}/${sessionId}/element/${genericElementId}/location`:
value = {
x: 15,
y: 20
}
break
case `${path}/${sessionId}/element/${genericElementId}/displayed`:
value = true
break
case `${path}/${sessionId}/elements`:
value = [
{ [ELEMENT_KEY]: genericElementId },
{ [ELEMENT_KEY]: 'some-elem-456' },
{ [ELEMENT_KEY]: 'some-elem-789' },
]
break
case `${path}/${sessionId}/element/${genericElementId}/css/width`:
value = '1250px'
break
case `${path}/${sessionId}/element/${genericElementId}/css/margin-top`:
case `${path}/${sessionId}/element/${genericElementId}/css/margin-right`:
case `${path}/${sessionId}/element/${genericElementId}/css/margin-bottom`:
case `${path}/${sessionId}/element/${genericElementId}/css/margin-left`:
value = '42px'
break
case `${path}/${sessionId}/element/${genericElementId}/css/padding-top`:
case `${path}/${sessionId}/element/${genericElementId}/css/padding-bottom`:
value = '4px'
break
case `${path}/${sessionId}/element/${genericElementId}/css/padding-right`:
case `${path}/${sessionId}/element/${genericElementId}/css/padding-left`:
value = '2px'
break
case `${path}/${sessionId}/element/${genericElementId}/property/tagName`:
value = 'BODY'
break
case `/session/${sessionId}/element/${genericElementId}/css/display`:
value = 'contents'
break
case `/session/${sessionId}/execute`:
case `/session/${sessionId}/execute/sync`: {
const script = Function(body.script)
const args = transformPropertyWithMockFunction(body.args.map((arg: any) => (arg && (arg.ELEMENT || arg[ELEMENT_KEY])) || arg))
let result: any = null
if (body.script.includes('resq')) {
if (body.script.includes('react$$')) {
result = [
{ [ELEMENT_KEY]: genericElementId },
{ [ELEMENT_KEY]: 'some-elem-456' },
{ [ELEMENT_KEY]: 'some-elem-789' },
]
} else if (body.script.includes('react$')) {
result = args[0] === 'myNonExistingComp'
? new Error('foobar')
: { [ELEMENT_KEY]: genericElementId }
} else {
result = null
}
} else if (body.script.includes('testLocatorStrategy')) {
result = { [ELEMENT_KEY]: genericElementId }
} else if (body.script.includes('testLocatorStrategiesMultiple')) {
result = [
{ [ELEMENT_KEY]: genericElementId },
{ [ELEMENT_KEY]: 'some-elem-456' },
{ [ELEMENT_KEY]: 'some-elem-789' },
]
} else if (body.script.includes('previousElementSibling')) {
result = body.args[0][ELEMENT_KEY] === genericSubElementId
? { [ELEMENT_KEY]: 'some-previous-elem' }
: {}
} else if (body.script.includes('parentElement')) {
result = body.args[0][ELEMENT_KEY] === genericSubElementId
? { [ELEMENT_KEY]: 'some-parent-elem' }
: {}
} else if (body.script.includes('nextElementSibling')) {
result = body.args[0][ELEMENT_KEY] === genericElementId
? { [ELEMENT_KEY]: 'some-next-elem' }
: {}
} else if (body.script.includes('scrollX')) {
result = [0, 0]
} else if (body.script.includes('function isFocused')) {
result = true
} else if (body.script.includes('mobile:')) {
result = true
} else if (body.script.includes('document.URL')) {
result = 'https://webdriver.io/?foo=bar'
} else if (body.script.includes('function checkVisibility')) {
result = true
} else {
result = script.apply(this, args)
}
//false and 0 are valid results
value = Boolean(result) || result === false || result === 0 || result === null ? result : {}
break
} case `/session/${sessionId}/execute/async`: {
const script = Function(body.script)
let result
script.call(this, ...body.args, (_result: any) => result = _result)
value = result ?? {}
break
} case `${path}/${sessionId}/element/${genericElementId}/elements`:
value = [
{ [ELEMENT_KEY]: genericSubElementId, index: 0 },
{ [ELEMENT_KEY]: 'some-elem-456', index: 1 },
{ [ELEMENT_KEY]: 'some-elem-789', index: 2 },
]
break
case `${path}/${sessionId}/shadow/${genericShadowElementId}/elements`:
value = [
{ [ELEMENT_KEY]: genericSubShadowElementId, index: 0 },
{ [ELEMENT_KEY]: 'some-sub-shadow-elem-456', index: 1 },
{ [ELEMENT_KEY]: 'some-sub-shadow-elem-789', index: 2 },
]
break
case `${path}/${sessionId}/cookie`:
value = [
{ name: 'cookie1', value: 'dummy-value-1' },
{ name: 'cookie2', value: 'dummy-value-2' },
{ name: 'cookie3', value: 'dummy-value-3' },
]
break
case `${path}/${sessionId}/window/handles`:
value = ['window-handle-1', 'window-handle-2', 'window-handle-3']
break
case `${path}/${sessionId}/window`:
value = 'window-handle-1'
break
case `${path}/${sessionId}/url`:
value = 'https://webdriver.io/?foo=bar'
break
case `${path}/${sessionId}/title`:
value = 'WebdriverIO · Next-gen browser and mobile automation test framework for Node.js | WebdriverIO'
break
case `${path}/${sessionId}/screenshot`:
case `${path}/${sessionId}/appium/stop_recording_screen`:
value = Buffer.from('some screenshot').toString('base64')
break
case `${path}/${sessionId}/print`:
value = Buffer.from('some pdf print').toString('base64')
break
case `${path}/${sessionId}/element/${genericElementId}/screenshot`:
value = Buffer.from('some element screenshot').toString('base64')
break
case '/grid/api/hub':
value = { some: 'config' }
break
case '/grid/api/testsession':
value = '<!DOCTYPE html><html lang="en"></html>'
break
case '/connectionRefused':
if (requestMock.retryCnt < 5) {
++requestMock.retryCnt
value = {
stacktrace: 'java.lang.RuntimeException: java.net.ConnectException: Connection refused',
stackTrace: [],
message: 'java.net.ConnectException: Connection refused: connect',
error: 'unknown error'
}
} else {
value = { foo: 'bar' }
}
}
if (uri.pathname.endsWith('timeout') && requestMock.retryCnt < 5) {
const timeoutError: any = new Error('Timeout')
timeoutError.name = 'TimeoutError'
timeoutError.code = 'ETIMEDOUT'
timeoutError.event = 'request'
++requestMock.retryCnt
return Promise.reject(timeoutError)
}
if (uri.pathname.startsWith(`/session/${sessionId}/element/`) && uri.pathname.includes('/attribute/')) {
value = `${uri.pathname.substring(uri.pathname.lastIndexOf('/') + 1)}-value`
}
if (uri.pathname.endsWith('sumoerror')) {
return Promise.reject(new Error('ups'))
}
/**
* Simulate a stale element
*/
if (uri.pathname === `/session/${sessionId}/element/${genericSubSubElementId}/click`) {
++requestMock.retryCnt
if (requestMock.retryCnt > 1) {
const response = { value: null }
return Response.json(response, {
status: 200,
headers: { foo: 'bar' }
})
}
// https://www.w3.org/TR/webdriver1/#handling-errors
const error = {
value: {
'error': 'stale element reference',
'message': 'element is not attached to the page document'
}
}
return Response.json(error, {
status: 404,
headers: { foo: 'bar' }
})
}
/**
* empty response
*/
if (uri.pathname === '/empty') {
return Response.json('', {
status: 500,
headers: { foo: 'bar' }
})
}
/**
* session error due to wrong path
*/
if (uri.pathname === '/wrong/path') {
return Response.json({}, {
status: 404,
headers: { foo: 'bar' }
})
}
/**
* simulate failing response
*/
if (uri.pathname === '/failing') {
++requestMock.retryCnt
/**
* success this request if you retry 3 times
*/
if (requestMock.retryCnt > 3) {
const response = { value: 'caught' }
return Response.json(response, {
status: 200,
headers: { foo: 'bar' }
})
}
return Response.json({}, {
status: 400,
headers: { foo: 'bar' }
})
}
/**
* overwrite if manual response is set
*/
let statusCode = 200
if (Array.isArray(manualMockResponse)) {
value = manualMockResponse.shift() || value
if (typeof value.statusCode === 'number') {
statusCode = value.statusCode
}
if (manualMockResponse.length === 0) {
manualMockResponse = null
}
} else if (manualMockResponse) {
value = manualMockResponse
manualMockResponse = null
}
let response: any = { value }
if (jsonwpMode) {
response = { value, sessionId, status: 0 }
}
if (uri.pathname.startsWith('/grid')) {
response = response.value
}
return Response.json(response, {
status: statusCode,
headers: { foo: 'bar' }
})
})
requestMock.retryCnt = 0
requestMock.setMockResponse = (value: any) => {
manualMockResponse = value
}
requestMock.customResponseFor = (pattern: RegExp, response: any) => {
const existingEntry = Array.from(customResponses.values())
.find((p) => p.pattern.toString() === pattern.toString())
if (existingEntry) {
customResponses.delete(existingEntry)
}
customResponses.add({ pattern, response })
}
requestMock.getSessionId = () => sessionId
requestMock.setSessionId = (newSessionId: any) => {
sessionId = newSessionId
}
requestMock.resetSessionId = () => {
sessionId = defaultSessionId
}
vi.stubGlobal('fetch', requestMock)