import path from 'node:path'
import { ELEMENT_KEY } from 'webdriver'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import type { ElementReference } from '@wdio/protocols'
import {
getElementFromResponse,
parseCSS,
checkUnicode,
findElement,
findElements,
verifyArgsAndStripIfElement,
getElementRect,
validateUrl
} from '../src/utils/index.js'
vi.mock('http', () => {
const req = { on: vi.fn(), end: vi.fn() }
let response = { statusCode: 200 }
return {
default: {
setResponse: (data: any) => {
response = data
},
request: vi.fn((url, cb) => {
cb(response)
return req
})
}
}
})
vi.mock('fs')
describe('utils', () => {
describe('getElementFromResponse', () => {
it('should return null if response is null', () => {
expect(getElementFromResponse(null)).toBe(null)
})
it('should return null if response is undfined', () => {
expect(getElementFromResponse()).toBe(null)
})
it('should find element from JSONWireProtocol response', () => {
expect(getElementFromResponse({ ELEMENT: 'foobar' })).toBe('foobar')
})
it('should find element from W3C response', () => {
expect(getElementFromResponse({ 'element-6066-11e4-a52e-4f735466cecf': 'barfoo' })).toBe('barfoo')
})
it('should throw otherwise', () => {
expect(getElementFromResponse({ invalid: 'response ' })).toBe(null)
})
})
describe('parseCSS', () => {
it('should parse colors properly', () => {
expect(parseCSS('rgba(0, 136, 204, 1)', 'color')).toEqual({
property: 'color',
value: 'rgba(0,136,204,1)',
parsed: {
hex: '#0088cc',
alpha: 1,
type: 'color',
rgba: 'rgba(0,136,204,1)'
}
})
expect(parseCSS('#0088cc', 'color')).toEqual({
property: 'color',
value: '#0088cc',
parsed: {}
})
})
it('should parse fonts properly', () => {
expect(parseCSS('helvetica', 'font-family')).toEqual({
property: 'font-family',
value: 'helvetica',
parsed: {
value: ['helvetica'],
type: 'font',
string: 'helvetica'
}
})
})
it('should parse number with unit values', () => {
expect(parseCSS('100px', 'width')).toEqual({
property: 'width',
value: '100px',
parsed: {
type: 'number',
string: '100px',
unit: 'px',
value: 100
}
})
expect(parseCSS('50%', 'width')).toEqual({
property: 'width',
value: '50%',
parsed: {
type: 'number',
string: '50%',
unit: '%',
value: 50
}
})
expect(parseCSS('42', 'foobar')).toEqual({
property: 'foobar',
value: 42,
parsed: {
type: 'number',
string: '42',
unit: '',
value: 42
}
})
})
})
describe('checkUnicode', () => {
it('should return array with unicode', () => {
const result = checkUnicode('Home')
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(1)
expect(result[0]).toEqual('\uE011')
})
it('should return an array without unicode', () => {
const result = checkUnicode('foo')
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(3)
expect(result[0]).toBe('f')
expect(result[1]).toBe('o')
expect(result[2]).toBe('o')
})
})
describe('findElement', () => {
const malformedElementResponse = { foo: 'bar' }
const elementResponse = { [ELEMENT_KEY]: 'foobar' }
const elementsResponse = [
{ [ELEMENT_KEY]: 'foobar' },
{ [ELEMENT_KEY]: 'barfoo' }
]
let scope: WebdriverIO.Element
beforeEach(() => {
scope = {
findElementsFromElement: vi.fn(),
findElementFromElement: vi.fn(),
findElements: vi.fn(),
findElement: vi.fn(),
execute: vi.fn(),
executeScript: vi.fn(),
on: vi.fn(),
removeAllListeners: vi.fn()
} as unknown as WebdriverIO.Element
})
it('fetches element using a selector string with browser scope', async () => {
await findElement.call(scope as any, '.elem')
expect(scope.findElement).toBeCalledWith('css selector', '.elem')
expect(scope.findElementFromElement).not.toBeCalled()
})
it('fetches element using a selector string with element scope', async () => {
scope.elementId = 'foobar'
await findElement.call(scope as any, '.elem')
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement)
.toBeCalledWith('foobar', 'css selector', '.elem')
})
it('fetches element using a function with browser scope', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
const elem = await findElement.call(scope as any, () => { return global.document.body }) as Element
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalled()
expect(elem[ELEMENT_KEY]).toBe('foobar')
})
it('fetches element using a function with element scope', async () => {
scope.elementId = 'foobar'
vi.mocked(scope.executeScript).mockResolvedValue(elementResponse)
const elem = await findElement.call(scope as any, () => { return global.document.body }) as Element
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.executeScript).toBeCalled()
expect(elem[ELEMENT_KEY]).toBe('foobar')
expect(vi.mocked(scope.executeScript).mock.calls[0][1]).toEqual([scope])
})
it('should return only one element if multiple are returned', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementsResponse)
const elem = await findElement.call(
scope as any,
(() => { return global.document.body as unknown as ElementReference }) as any
) as Element
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalled()
expect(elem[ELEMENT_KEY]).toBe('foobar')
})
it('throws if element response is malformed', async () => {
vi.mocked(scope.execute).mockResolvedValue(malformedElementResponse)
const res = await findElement.call(scope as any, () => { return global.document.body }) as Error
expect(res instanceof Error)
expect(res.message).toMatch('did not return an HTMLElement')
})
it('throws if selector is neither string nor function', async () => {
await expect(findElement.call(scope as any, null)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `object`'))
await expect(findElement.call(scope as any, 123)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `number`'))
await expect(findElement.call(scope as any, false)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `boolean`'))
await expect(findElement.call(scope as any)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `undefined`'))
})
it('should use execute if shadow selector is used', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
const elem = await findElement.call(scope as any, '>>>.foobar') as Element
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalledWith(
expect.any(Function),
false,
'.foobar',
undefined
)
expect(elem[ELEMENT_KEY]).toBe('foobar')
})
it('should use execute if shadow selector is used with element scope', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
scope.elementId = 'foobar'
const elem = await findElement.call(scope as any, '>>>.foobar') as Element
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalledWith(
expect.any(Function),
false,
'.foobar',
scope
)
expect(elem[ELEMENT_KEY]).toBe('foobar')
})
})
describe('findElements', () => {
const malformedElementResponse = { foo: 'bar' }
const elementResponse = { [ELEMENT_KEY]: 'foobar' }
const elementsResponse = [
{ [ELEMENT_KEY]: 'foobar' },
{ [ELEMENT_KEY]: 'barfoo' }
]
let scope: WebdriverIO.Element
beforeEach(() => {
scope = {
findElementsFromElement: vi.fn(),
findElementFromElement: vi.fn(),
findElements: vi.fn(),
findElement: vi.fn(),
execute: vi.fn(),
executeScript: vi.fn()
} as unknown as WebdriverIO.Element
})
it('fetches element using a selector string with browser scope', async () => {
await findElements.call(scope as any, '.elem')
expect(scope.findElements).toBeCalledWith('css selector', '.elem')
expect(scope.findElementsFromElement).not.toBeCalled()
})
it('fetches element using a selector string with element scope', async () => {
scope.elementId = 'foobar'
await findElements.call(scope as any, '.elem')
expect(scope.findElements).not.toBeCalled()
expect(scope.findElementsFromElement)
.toBeCalledWith('foobar', 'css selector', '.elem')
})
it('fetches element using a function with browser scope', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
const elem = await findElements.call(scope as any, () => { return global.document.body })
expect(scope.findElements).not.toBeCalled()
expect(scope.findElementsFromElement).not.toBeCalled()
expect(scope.execute).toBeCalled()
expect(elem).toHaveLength(1)
expect(elem[0][ELEMENT_KEY]).toBe('foobar')
})
it('fetches element using a function with element scope', async () => {
scope.elementId = 'foobar'
vi.mocked(scope.executeScript).mockResolvedValue(elementResponse)
const elem = await findElements.call(scope as any, () => { return global.document.body })
expect(scope.findElements).not.toBeCalled()
expect(scope.findElementsFromElement).not.toBeCalled()
expect(scope.executeScript).toBeCalled()
expect(elem).toHaveLength(1)
expect(elem[0][ELEMENT_KEY]).toBe('foobar')
expect(vi.mocked(scope.executeScript).mock.calls[0][1]).toEqual([scope])
})
it('should return multiple elements if multiple are returned', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementsResponse)
const elem = await findElements.call(scope as any, () => { return global.document.body })
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalled()
expect(elem).toEqual(elementsResponse)
})
it('should filter out malformed responses', async () => {
vi.mocked(scope.execute).mockResolvedValue([...elementsResponse, 'foobar'])
const elem = await findElements.call(scope as any, () => { return global.document.body })
expect(scope.findElement).not.toBeCalled()
expect(scope.findElementFromElement).not.toBeCalled()
expect(scope.execute).toBeCalled()
expect(elem).toEqual(elementsResponse)
})
it('throws if element response is malformed', async () => {
vi.mocked(scope.execute).mockResolvedValue(malformedElementResponse)
const res = await findElements.call(scope as any, () => { return global.document.body })
expect(res).toHaveLength(0)
})
it('throws if selector is neither string nor function', async () => {
await expect(findElements.call(scope, null)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `object`'))
await expect(findElements.call(scope, 123)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `number`'))
await expect(findElements.call(scope, false)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `boolean`'))
await expect(findElements.call(scope)).rejects.toEqual(
new Error('selector needs to be typeof `string` or `function`, but found: `undefined`'))
})
it('fetches element using a function with browser scope', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
const elem = await findElements.call(scope as any, '>>>.foobar')
expect(scope.findElements).not.toBeCalled()
expect(scope.findElementsFromElement).not.toBeCalled()
expect(scope.execute).toBeCalledWith(
expect.any(Function),
true,
'.foobar',
undefined
)
expect(elem).toHaveLength(1)
expect(elem[0][ELEMENT_KEY]).toBe('foobar')
})
it('fetches element using a function with element scope', async () => {
vi.mocked(scope.execute).mockResolvedValue(elementResponse)
scope.elementId = 'foobar'
const elem = await findElements.call(scope as any, '>>>.foobar')
expect(scope.findElements).not.toBeCalled()
expect(scope.findElementsFromElement).not.toBeCalled()
expect(scope.execute).toBeCalledWith(
expect.any(Function),
true,
'.foobar',
scope
)
expect(elem).toHaveLength(1)
expect(elem[0][ELEMENT_KEY]).toBe('foobar')
})
})
describe('verifyArgsAndStripIfElement', () => {
class Element {
elementId: string
constructor({ elementId, ...otherProps }: any) {
this.elementId = elementId
Object.keys(otherProps).forEach((key) => this[key] = otherProps[key])
}
}
it('returns the same value if it is not an element object', () => {
expect(verifyArgsAndStripIfElement([1, 'two', true, false, null, undefined])).toEqual([1, 'two', true, false, null, undefined])
})
it('strips down properties if value is element object', () => {
const fakeObj = new Element({
elementId: 'foo-bar',
someProp: 123,
anotherProp: 'abc'
})
expect(verifyArgsAndStripIfElement([fakeObj, 'abc', 123])).toMatchObject([
{ [ELEMENT_KEY]: 'foo-bar', ELEMENT: 'foo-bar' },
'abc',
123
])
})
it('should work even if parameter is not of type Array', () => {
const fakeObj = new Element({
elementId: 'foo-bar',
someProp: 123,
anotherProp: 'abc'
})
expect(verifyArgsAndStripIfElement(fakeObj)).toMatchObject(
{ [ELEMENT_KEY]: 'foo-bar', ELEMENT: 'foo-bar' }
)
expect(verifyArgsAndStripIfElement('foo')).toEqual('foo')
})
it('throws error if element object is missing element id', () => {
const fakeObj = new Element({
someProp: 123,
anotherProp: 'abc',
selector: 'div'
})
expect(() => verifyArgsAndStripIfElement(fakeObj)).toThrow('The element with selector "div" you are trying to pass into the execute method wasn\'t found')
})
})
describe('getElementRect', () => {
it('uses getBoundingClientRect if a key is missing', async () => {
const fakeScope = {
elementId: 123,
getElementRect: vi.fn(() => Promise.resolve({ x: 10, width: 300, height: 400 })),
execute: vi.fn(() => Promise.resolve({ x: 11, y: 22, width: 333, height: 444 }))
} as unknown as WebdriverIO.Element
expect(await getElementRect(fakeScope as any)).toEqual({ x: 10, y: 22, width: 300, height: 400 })
expect(fakeScope.getElementRect).toHaveBeenCalled()
expect(fakeScope.execute).toHaveBeenCalled()
})
it('does not use getBoundingClientRect if a value is 0', async () => {
const fakeScope = {
elementId: 123,
getElementRect: vi.fn(() => Promise.resolve({ x: 10, y: 0, width: 300, height: 400 })),
execute: vi.fn(() => Promise.reject(new Error('Method is not implemented')))
} as unknown as WebdriverIO.Element
expect(await getElementRect(fakeScope as any)).toEqual({ x: 10, y: 0, width: 300, height: 400 })
expect(fakeScope.getElementRect).toHaveBeenCalled()
expect(fakeScope.execute).not.toHaveBeenCalled()
})
})
describe('validateUrl', () => {
it('should ensure url is correct', () => {
expect(validateUrl('http://json.org')).toEqual('http://json.org/')
expect(validateUrl('www.json.org')).toEqual('http://www.json.org/')
expect(validateUrl('json.org')).toEqual('http://json.org/')
expect(validateUrl('about:blank')).toEqual('about:blank')
expect(validateUrl('IamInAHost')).toEqual('http://iaminahost/')
expect(validateUrl('data:text/html, <html contenteditable>'))
.toEqual('data:text/html, <html contenteditable>')
expect(() => validateUrl('_I.am.I:nvalid'))
.toThrowError('Invalid URL: _I.am.I:nvalid')
})
})
})