Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions __mocks__/@testplane/wdio-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { vi } from 'vitest'

export const logMock = {
error: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
trace: vi.fn(),
progress: vi.fn()
}
const mock = () => logMock
mock.setLevel = vi.fn()
mock.setLogLevelsConfig = vi.fn()
mock.waitForBuffer = vi.fn()
mock.clearLogger = vi.fn()
export default mock
32 changes: 30 additions & 2 deletions packages/wdio-utils/src/monad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ interface PropertiesObject {
[key: string | symbol]: PropertyDescriptor
}

function composeElementOverrides (previousCommand: Function | undefined, nextCommand: Function): Function {
if (!previousCommand) {
return nextCommand
}

const previousElementCommand = previousCommand

return function composedElementOverride (this: WebdriverIO.Element, originalCommand: Function, ...args: unknown[]) {
const element = this

function previousCommandAsOriginal (this: WebdriverIO.Element, ...previousArgs: unknown[]) {
const context = this || element

function originalForPrevious (this: WebdriverIO.Element, ...originalArgs: unknown[]) {
return originalCommand.apply(this || context, originalArgs)
}

return previousElementCommand.call(context, originalForPrevious, ...previousArgs)
}

return nextCommand.call(element, previousCommandAsOriginal, ...args)
}
}

function setElementOverride (overrides: Record<string, Function>, name: string, command: Function): void {
overrides[name] = composeElementOverrides(overrides[name], command)
}

export default function WebDriver (options: object, modifier?: Function, propertiesObject: PropertiesObject = {}) {
/**
* In order to allow named scopes for elements we have to propagate that
Expand Down Expand Up @@ -152,13 +180,13 @@ export default function WebDriver (options: object, modifier?: Function, propert
* add command to every multiremote instance
*/
Object.values(instances).forEach(instance => {
instance.__propertiesObject__.__elementOverrides__.value[name] = customCommand
setElementOverride(instance.__propertiesObject__.__elementOverrides__.value, name, customCommand)
})
} else {
/**
* regular mode
*/
this.__propertiesObject__.__elementOverrides__.value[name] = customCommand
setElementOverride(this.__propertiesObject__.__elementOverrides__.value, name, customCommand)
}
} else if (client[name]) {
const origCommand = client[name]
Expand Down
235 changes: 228 additions & 7 deletions packages/wdio-utils/tests/monad.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ beforeEach(() => {

const sessionId = 'c5fa4320-07d5-48f5-b7c2-922d4405e17f'

function createElement (elementOverrides: PropertyDescriptor, command: Function) {
return webdriverMonad({}, (element: any) => element, {
scope: { value: 'element' },
someFunc: { value: command },
__elementOverrides__: elementOverrides
})(sessionId)
}

describe('monad', () => {
it('should be able to initialize client with prototype with commands', () => {
const modifier = vi.fn()
Expand Down Expand Up @@ -93,6 +101,162 @@ describe('monad', () => {
expect(client.__propertiesObject__.__elementOverrides__.value.someFunc(2, 3)).toBe(5)
})

it('should compose sequential element command overrides and propagate arguments and return values', async () => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId)
const calls: string[] = []

client.overwriteCommand('someFunc', async (originalCommand: Function, arg: number) => {
calls.push(`first before: ${arg}`)
const result = await originalCommand(arg * 2)
calls.push('first after')
return result + 1
}, true)
client.overwriteCommand('someFunc', async (originalCommand: Function, arg: number) => {
calls.push(`second before: ${arg}`)
const result = await originalCommand(arg + 1)
calls.push('second after')
return result * 3
}, true)

const element = createElement(client.__propertiesObject__.__elementOverrides__, async (arg: number) => {
calls.push(`base: ${arg}`)
return arg
})

await expect(element.someFunc(4)).resolves.toBe(33)
expect(calls).toEqual(['second before: 4', 'first before: 5', 'base: 10', 'first after', 'second after'])
})

it.each([
{
name: 'plugin before guard',
register: (client: any, plugin: Function, guard: Function) => {
client.overwriteCommand('someFunc', plugin, true)
client.overwriteCommand('someFunc', guard, true)
},
expected: ['guard start', 'plugin', 'base', 'guard wait']
},
{
name: 'guard before plugin',
register: (client: any, plugin: Function, guard: Function) => {
client.overwriteCommand('someFunc', guard, true)
client.overwriteCommand('someFunc', plugin, true)
},
expected: ['plugin', 'guard start', 'base', 'guard wait']
}
])('should preserve a navigation guard with $name', async ({ register, expected }) => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId)
const calls: string[] = []
let resolveBase!: (value: string) => void
const baseResult = new Promise<string>((resolve) => {
resolveBase = resolve
})
const plugin = async (originalCommand: Function) => {
calls.push('plugin')
return originalCommand()
}
const guard = async (originalCommand: Function) => {
calls.push('guard start')
const result = await originalCommand()
calls.push('guard wait')
return result
}

register(client, plugin, guard)
const element = createElement(client.__propertiesObject__.__elementOverrides__, async () => {
calls.push('base')
return baseResult
})
let commandSettled = false
const commandResult = element.someFunc().then((result: string) => {
commandSettled = true
return result
})

await Promise.resolve()
expect(commandSettled).toBe(false)
expect(calls).toEqual(expected.slice(0, -1))

resolveBase('base result')
await expect(commandResult).resolves.toBe('base result')
expect(calls).toEqual(expected)
})

it('should propagate errors through sequential element command overrides', async () => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId)
const calls: string[] = []
const error = new Error('boom')

client.overwriteCommand('someFunc', async (originalCommand: Function) => {
try {
return await originalCommand()
} catch (err) {
calls.push('first caught')
throw err
}
}, true)
client.overwriteCommand('someFunc', async (originalCommand: Function) => {
try {
return await originalCommand()
} catch (err) {
calls.push('second caught')
throw err
}
}, true)

const element = createElement(client.__propertiesObject__.__elementOverrides__, async () => {
calls.push('base')
throw error
})

await expect(element.someFunc()).rejects.toBe(error)
expect(calls).toEqual(['base', 'first caught', 'second caught'])
})

it('should preserve explicit context rebinding through sequential element command overrides', () => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId)
const reboundElement = { name: 'rebound element' }
let firstContext: unknown
let secondContext: unknown
let baseContext: unknown

client.overwriteCommand('someFunc', function (this: unknown, originalCommand: Function, arg: number) {
firstContext = this
return originalCommand(arg)
}, true)
client.overwriteCommand('someFunc', function (this: unknown, originalCommand: Function, arg: number) {
secondContext = this
return originalCommand.call(reboundElement, arg)
}, true)

const element = createElement(client.__propertiesObject__.__elementOverrides__, function (this: unknown, arg: number) {
baseContext = this
return arg
})

expect(element.someFunc(123)).toBe(123)
expect(secondContext).toBe(element)
expect(firstContext).toBe(reboundElement)
expect(baseContext).toBe(reboundElement)
})

it('should compose three element command overrides in reverse registration order', () => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId)
const calls: string[] = []

for (const label of ['first', 'second', 'third']) {
client.overwriteCommand('someFunc', (originalCommand: Function) => {
calls.push(label)
return originalCommand()
}, true)
}

const element = createElement(client.__propertiesObject__.__elementOverrides__, () => calls.push('base'))

element.someFunc()
expect(calls).toEqual(['third', 'second', 'first', 'base'])
})

it('should add element commands to the __propertiesObject__ cache in multiremote', () => {
const monad = webdriverMonad({}, (client: any) => client, prototype)
const client = monad(sessionId)
Expand All @@ -105,25 +269,82 @@ describe('monad', () => {
expect(instances.foo.__propertiesObject__.myCustomElementCommand.value).toBe(func)
})

it('should add element commands for override to the __propertiesObject__.__elementOverrides__ cache in multiremote', () => {
it('should compose element command overrides in multiremote', () => {
const monad = webdriverMonad({}, (client: any) => client, { ...prototype })
const client = monad(sessionId)
const calls: string[] = []
const instances = {
foo: {
__propertiesObject__: {
__elementOverrides__: {
value: {
someFunc: (x: number, y: number) => x - y
}
value: {}
}
}
},
bar: {
__propertiesObject__: {
__elementOverrides__: {
value: {}
}
}
}
}

const func = function (x: number, y: number) { return x + y }
client.overwriteCommand('someFunc', function (originalCommand: Function, x: number, y: number) {
calls.push('first')
return originalCommand(x * 2, y) + 1
}, true, undefined, instances)
client.overwriteCommand('someFunc', function (originalCommand: Function, x: number, y: number) {
calls.push('second')
return originalCommand(x, y * 3) * 2
}, true, undefined, instances)
const fooElement = createElement(instances.foo.__propertiesObject__.__elementOverrides__, (x: number, y: number) => {
calls.push('foo base')
return x + y
})
const barElement = createElement(instances.bar.__propertiesObject__.__elementOverrides__, (x: number, y: number) => {
calls.push('bar base')
return x + y
})

expect(fooElement.someFunc(2, 3)).toBe(28)
expect(barElement.someFunc(2, 3)).toBe(28)
expect(calls).toEqual(['second', 'first', 'foo base', 'second', 'first', 'bar base'])
})

it('should invoke command wrappers once for each composed element override', () => {
const calls: string[] = []
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId, (commandName: string, commandFn: Function) => {
return function (this: unknown, ...args: unknown[]) {
calls.push(`wrapper: ${commandName}`)
return commandFn.apply(this, args)
}
})

client.overwriteCommand('someFunc', (originalCommand: Function) => originalCommand(), true)
client.overwriteCommand('someFunc', (originalCommand: Function) => originalCommand(), true)

const element = createElement(client.__propertiesObject__.__elementOverrides__, () => calls.push('base'))

element.someFunc()
expect(calls).toEqual(['wrapper: someFunc', 'wrapper: someFunc', 'base'])
})

it('should keep composing sequential browser command overrides', () => {
const client = webdriverMonad({}, (browser: any) => browser, { ...prototype })(sessionId, (commandName: string, commandFn: Function) => commandFn)
const calls: string[] = []

client.overwriteCommand('someFunc', (originalCommand: Function, arg: number) => {
calls.push('first')
return originalCommand(arg + 1)
})
client.overwriteCommand('someFunc', (originalCommand: Function, arg: number) => {
calls.push('second')
return originalCommand(arg + 1)
})

client.overwriteCommand('someFunc', func, true, undefined, instances)
expect(instances.foo.__propertiesObject__.__elementOverrides__.value.someFunc(4, 5)).toBe(9)
expect(client.someFunc(1)).toBe('result-3')
expect(calls).toEqual(['second', 'first'])
})

it('allows to use custom command wrapper', () => {
Expand Down
Loading