441 lines
16 KiB
TypeScript
441 lines
16 KiB
TypeScript
import { describe, it, expect, afterEach, vi, test } from 'vitest'
|
|
|
|
import path from 'node:path'
|
|
// @ts-ignore no types needed
|
|
import debug from 'debug'
|
|
import which from 'which'
|
|
import { launch } from 'chrome-launcher'
|
|
import { canAccess } from '@wdio/utils'
|
|
|
|
import {
|
|
validate, getPrototype, findElement, findElements, getStaleElementError,
|
|
sanitizeError, transformExecuteArgs, transformExecuteResult, getPages,
|
|
uniq, findByWhich, patchDebug, sleep, launchChromeUsingWhich
|
|
} from '../src/utils.js'
|
|
|
|
vi.mock('which')
|
|
vi.mock('chrome-launcher')
|
|
vi.mock('@wdio/logger', async () => {
|
|
const pathModule = await vi.importActual('node:path') as typeof path
|
|
return import(pathModule.join(process.cwd(), '__mocks__', '@wdio/logger'))
|
|
})
|
|
vi.mock('@wdio/utils', async () => {
|
|
const pathModule = await vi.importActual('node:path') as typeof path
|
|
return import(pathModule.join(process.cwd(), '__mocks__', '@wdio/utils'))
|
|
})
|
|
vi.mock('pptrDebug', async () => {
|
|
const pathModule = await vi.importActual('node:path') as typeof path
|
|
return import(pathModule.join(process.cwd(), '__mocks__', 'pptrDebug'))
|
|
})
|
|
|
|
const PUPPETEER_LOG = ` puppeteer:protocol:RECV ◀ [
|
|
puppeteer:protocol:RECV ◀ '{"id":20,"result":{"result":{"type":"string","value":"complete"}},"sessionId":"B5BB76CBB624830A41E4159E01ABED39"}'
|
|
puppeteer:protocol:RECV ◀ ]`
|
|
|
|
/**
|
|
* some WebDriver commands are either not part of a recommended standard
|
|
* or not used enough by end users that it would make sense to implement
|
|
* parity to the devtools protocol
|
|
*/
|
|
const IGNORE_MISSING_COMMANDS = [
|
|
'addCredential', 'addVirtualAuthenticator', 'createMockSensor', 'deleteMockSensor',
|
|
'generateTestReport', 'getCredentials', 'getMockSensor', 'removeAllCredentials',
|
|
'removeCredential', 'removeVirtualAuthenticator', 'setPermissions', 'setTimeZone',
|
|
'setUserVerified', 'updateMockSensor'
|
|
]
|
|
|
|
const command = {
|
|
endpoint: '/session/:sessionId/element/:elementId/element',
|
|
method: 'POST',
|
|
command: 'findElementFromElement',
|
|
ref: 'https://w3c.github.io/webdriver/webdriver-spec.html#dfn-find-element-from-element',
|
|
variables: [{
|
|
'name': 'elementId',
|
|
'description': 'the id of an element returned in a previous call to Find Element(s)'
|
|
}],
|
|
parameters: [{
|
|
'name': 'using',
|
|
'type': 'string',
|
|
'description': 'a valid element location strategy',
|
|
'required': true
|
|
}, {
|
|
'name': 'value',
|
|
'type': 'string',
|
|
'description': 'the actual selector that will be used to find an element',
|
|
'required': true
|
|
}, {
|
|
'name': 'customParam',
|
|
'type': 'number',
|
|
'description': 'a random not required param',
|
|
'required': false
|
|
}]
|
|
}
|
|
|
|
let pageMock = {
|
|
waitForSelector: vi.fn(),
|
|
waitForXPath: vi.fn(),
|
|
$$eval: vi.fn(),
|
|
$$: vi.fn(),
|
|
$x: vi.fn(),
|
|
$: vi.fn()
|
|
}
|
|
|
|
vi.mock('fs', () => {
|
|
return {
|
|
default: {
|
|
existsSync: (pkgName: string) => pkgName === 'pptrDebug'
|
|
}
|
|
}
|
|
})
|
|
|
|
vi.mock('debug', () => ({
|
|
default: { log: vi.fn() }
|
|
}))
|
|
|
|
describe('validate', () => {
|
|
it('should fail if wrong arguments are passed in', () => {
|
|
expect(() => validate(command.command, command.parameters, command.variables, command.ref, []))
|
|
.toThrow(/Wrong parameters applied for findElementFromElement/)
|
|
})
|
|
|
|
it('should fail if arguments are malformed', () => {
|
|
expect(() => validate(command.command, command.parameters, command.variables, command.ref, ['123', 123, '123']))
|
|
.toThrow(/Malformed type for "using" parameter of command/)
|
|
})
|
|
|
|
it('should fail if not required param has wrong type', () => {
|
|
expect(() => validate(command.command, command.parameters, command.variables, command.ref, ['123', '123', '123', 'foobar']))
|
|
.toThrow(/Malformed type for "customParam" parameter of command/)
|
|
})
|
|
|
|
it('should do a proper request', () => {
|
|
const body = validate(command.command, command.parameters, command.variables, command.ref, ['123', 'css selector', '#body', undefined])
|
|
expect(body).toMatchSnapshot()
|
|
})
|
|
|
|
it('should do a proper request with non required params', () => {
|
|
const body = validate(command.command, command.parameters, command.variables, command.ref, ['123', 'css selector', '#body', 123])
|
|
expect(body).toMatchSnapshot()
|
|
})
|
|
})
|
|
|
|
test('getPrototype', () => {
|
|
let i = 0
|
|
const commands = getPrototype(() => ++i)
|
|
const filteredCommands = Object.entries(commands)
|
|
.reduce((cmds, [name, description]) => {
|
|
if (IGNORE_MISSING_COMMANDS.includes(name)) {
|
|
return cmds
|
|
}
|
|
cmds[name] = description
|
|
return cmds
|
|
}, {} as Record<string, { value: Function }>)
|
|
expect(filteredCommands).toMatchSnapshot()
|
|
})
|
|
|
|
describe('findElement utils', () => {
|
|
afterEach(() => {
|
|
pageMock.waitForSelector.mockClear()
|
|
pageMock.waitForXPath.mockClear()
|
|
pageMock.$$eval.mockClear()
|
|
pageMock.$.mockClear()
|
|
pageMock.$x.mockClear()
|
|
pageMock.$$.mockClear()
|
|
})
|
|
|
|
describe('findElement', () => {
|
|
it('tries to find element using css selector', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve(42))
|
|
|
|
expect(await findElement.call(scope as any, pageMock as any as any, 'css selector', 'barfoo'))
|
|
.toEqual({ 'element-6066-11e4-a52e-4f735466cecf': 'foobar' })
|
|
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.$).toBeCalledWith('barfoo')
|
|
expect(pageMock.$x).toBeCalledTimes(0)
|
|
})
|
|
|
|
it('tries to find element using xpath', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$x.mockReturnValue(Promise.resolve([42]))
|
|
|
|
expect(await findElement.call(scope as any, pageMock as any, 'xpath', '//img'))
|
|
.toEqual({ 'element-6066-11e4-a52e-4f735466cecf': 'foobar' })
|
|
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.$).toBeCalledTimes(0)
|
|
expect(pageMock.$x).toBeCalledWith('//img')
|
|
})
|
|
|
|
it('should fail if not found', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve(null))
|
|
|
|
const errorMessage = await findElement.call(scope as any, pageMock as any, 'css selector', 'barfoo')
|
|
expect((errorMessage as Error).message)
|
|
.toContain('Element with selector "barfoo" not found')
|
|
})
|
|
|
|
it('should not fail with the same error if Puppeteer can not find the element', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.reject(new Error('failed to find element')))
|
|
|
|
const errorMessage = await findElement.call(scope as any, pageMock as any, 'css selector', 'barfoo')
|
|
expect((errorMessage as Error).message)
|
|
.toContain('Element with selector "barfoo" not found')
|
|
})
|
|
|
|
it('sets implicit waits', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn().mockReturnValue(1234) },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve('foobar'))
|
|
await findElement.call(scope as any, pageMock as any, 'css selector', 'barfoo')
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(1)
|
|
expect(pageMock.waitForXPath).toBeCalledTimes(0)
|
|
})
|
|
|
|
it('sets implicit waits with xpath', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn().mockReturnValue(1234) },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve('foobar'))
|
|
await findElement.call(scope as any, pageMock as any, 'xpath', 'barfoo')
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.waitForXPath).toBeCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('findElements', () => {
|
|
it('should find elements', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$$.mockReturnValue(Promise.resolve([42, 11]))
|
|
expect(await findElements.call(scope as any, pageMock as any, 'css selector', 'barfoo')).toMatchSnapshot()
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.$$).toBeCalledWith('barfoo')
|
|
expect(pageMock.$x).toBeCalledTimes(0)
|
|
})
|
|
|
|
it('should find elements with xpath', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$x.mockReturnValue(Promise.resolve([42, 11]))
|
|
expect(await findElements.call(scope as any, pageMock as any, 'xpath', 'barfoo')).toMatchSnapshot()
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.$$).toBeCalledTimes(0)
|
|
expect(pageMock.$x).toBeCalledWith('barfoo')
|
|
})
|
|
|
|
it('should return immediately if no elements were found', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$$.mockReturnValue(Promise.resolve([]))
|
|
expect(await findElements.call(scope as any, pageMock as any, 'css selector', 'barfoo')).toEqual([])
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(scope.elementStore.set).toBeCalledTimes(0)
|
|
})
|
|
|
|
it('sets implicit waits', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn().mockReturnValue(1234) },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$$.mockReturnValue(Promise.resolve(['foobar']))
|
|
await findElements.call(scope as any, pageMock as any, 'css selector', 'barfoo')
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(1)
|
|
})
|
|
|
|
it('sets implicit waits with xpath', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn().mockReturnValue(1234) },
|
|
elementStore: { set: vi.fn() }
|
|
}
|
|
pageMock.$x.mockReturnValue(Promise.resolve(['foobar']))
|
|
await findElements.call(scope as any, pageMock as any, 'xpath', 'barfoo')
|
|
expect(pageMock.waitForSelector).toBeCalledTimes(0)
|
|
expect(pageMock.waitForXPath).toBeCalledTimes(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('sanitizeError', () => {
|
|
it('should set stale element error', () => {
|
|
const err = new Error('Node is detached from document bla bla')
|
|
expect(sanitizeError(err).stack)
|
|
.toContain('stale element reference: The element reference is stale')
|
|
})
|
|
|
|
it('should not contain puppeteer traces', () => {
|
|
const err = new Error('some error')
|
|
const newStack = err?.stack?.split('\n') || []
|
|
newStack.push('at /foo/bar/devtools/node_modules/puppeteer-core/bla.js')
|
|
err.stack = newStack.join('\n')
|
|
expect(sanitizeError(err).stack).not.toContain('devtools/node_modules/puppeteer-core')
|
|
})
|
|
})
|
|
|
|
test('transformExecuteArgs', async () => {
|
|
const scope = { elementStore: new Map() }
|
|
scope.elementStore.set('foobar', 'barfoo')
|
|
|
|
expect(await transformExecuteArgs.call(scope as any, [
|
|
'foo',
|
|
{ 'element-6066-11e4-a52e-4f735466cecf': 'foobar' },
|
|
true,
|
|
42
|
|
])).toEqual(['foo', 'barfoo', true, 42])
|
|
})
|
|
|
|
test('transformExecuteArgs throws stale element if element is not in store', async () => {
|
|
const scope = { elementStore: new Map() }
|
|
scope.elementStore.set('foobar', 'barfoo')
|
|
|
|
expect(transformExecuteArgs.call(scope as any, [
|
|
'foo',
|
|
{ 'element-6066-11e4-a52e-4f735466cecf': 'not-existing' },
|
|
true,
|
|
42
|
|
])).rejects.toThrow()
|
|
})
|
|
|
|
test('transformExecuteArgs should allow undefined params', async () => {
|
|
const scope = { elementStore: new Map() }
|
|
expect(await transformExecuteArgs.call(scope as any, undefined)).toEqual([])
|
|
})
|
|
|
|
describe('transformExecuteResult', () => {
|
|
test('multiple results', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve(42))
|
|
expect(await transformExecuteResult.call(scope as any, pageMock as any, [
|
|
'foobar',
|
|
'__executeElement',
|
|
42
|
|
])).toMatchSnapshot()
|
|
})
|
|
|
|
test('single result', async () => {
|
|
const scope = {
|
|
timeouts: { get: vi.fn() },
|
|
elementStore: { set: vi.fn().mockReturnValue('foobar') }
|
|
}
|
|
pageMock.$.mockReturnValue(Promise.resolve(42))
|
|
expect(await transformExecuteResult.call(scope as any, pageMock as any, 'foobar'))
|
|
.toEqual('foobar')
|
|
})
|
|
|
|
afterEach(() => {
|
|
pageMock.waitForSelector.mockClear()
|
|
pageMock.$.mockClear()
|
|
pageMock.$$.mockClear()
|
|
pageMock.$$eval.mockClear()
|
|
})
|
|
})
|
|
|
|
test('getPages', async () => {
|
|
const browser = {
|
|
pages: vi.fn()
|
|
.mockReturnValueOnce([])
|
|
.mockReturnValueOnce([])
|
|
.mockReturnValueOnce([{}])
|
|
}
|
|
await getPages(browser as any, 10)
|
|
expect(browser.pages).toBeCalledTimes(3)
|
|
})
|
|
|
|
test('getStaleElementError', () => {
|
|
const err = getStaleElementError('foobar')
|
|
expect(err instanceof Error).toBe(true)
|
|
expect(err.name).toContain('stale element reference')
|
|
})
|
|
|
|
test('uniq', () => {
|
|
const listA = [1, 2, 3]
|
|
expect(listA).toBe(listA)
|
|
expect(listA).not.toBe(uniq(listA as any))
|
|
})
|
|
|
|
test('findByWhich', () => {
|
|
vi.mocked(canAccess).mockImplementation(() => true)
|
|
expect(findByWhich(['firefox'], [{ regex: /firefox/, weight: 51 }]))
|
|
.toEqual([])
|
|
|
|
vi.mocked(which.sync).mockReturnValue('/path/to/other/firefox')
|
|
expect(findByWhich(['firefox'], [{ regex: /firefox/, weight: 51 }]))
|
|
.toEqual(['/path/to/other/firefox'])
|
|
|
|
vi.mocked(canAccess).mockImplementation(() => {
|
|
throw new Error('uups')
|
|
})
|
|
expect(findByWhich(['firefox'], [{ regex: /firefox/, weight: 51 }]))
|
|
.toEqual([])
|
|
})
|
|
|
|
test('patchDebug', async () => {
|
|
/**
|
|
* fails in Windows
|
|
*/
|
|
if (process.platform === 'win32') {
|
|
return
|
|
}
|
|
const logMock = { debug: vi.fn() }
|
|
await patchDebug(logMock as any)
|
|
debug.log(PUPPETEER_LOG)
|
|
expect(logMock.debug.mock.calls[0]).toMatchSnapshot('foobar')
|
|
})
|
|
|
|
test('sleep', async () => {
|
|
const start = Date.now()
|
|
await sleep(100)
|
|
expect(Date.now() - start).toBeGreaterThanOrEqual(90)
|
|
})
|
|
|
|
describe('launchChromeUsingWhich', () => {
|
|
it('should throw if error is not related to chrome binary not found', async () => {
|
|
await expect(() => launchChromeUsingWhich(new Error('ups'), {}))
|
|
.rejects.toThrow('ups')
|
|
})
|
|
|
|
it('should throw if user has specified path explicitly', async () => {
|
|
await expect(() => launchChromeUsingWhich(
|
|
new Error('No Chrome installations found.'),
|
|
{ chromePath: '/foo/bar' }
|
|
)).rejects.toThrow('No Chrome installations found.')
|
|
})
|
|
|
|
it('should use which properly to find path', async () => {
|
|
vi.mocked(which).mockImplementation(async (binary: string) => {
|
|
if (binary === 'google-chrome') {
|
|
return '/foo/bar/google-chrome'
|
|
}
|
|
throw new Error('not found')
|
|
})
|
|
await launchChromeUsingWhich(new Error('No Chrome installations found.'), {})
|
|
expect(launch).toBeCalledWith({ chromePath: '/foo/bar/google-chrome' })
|
|
})
|
|
})
|