import path from 'node:path' import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { executeHooksWithArgs, executeAsync, wrapCommand } from '../src/shim.js' const globalAny: any = global vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) beforeEach(() => { globalAny.browser = {} as WebdriverIO.Browser }) afterEach(() => { delete globalAny.browser }) describe('executeHooksWithArgs', () => { it('multiple hooks, multiple args', async () => { const hookHoge = () => { return 'hoge' } const hookFuga = () => { return 'fuga' } const argHoge = { hoge: 'hoge' } const argFuga = { fuga: 'fuga' } const res = await executeHooksWithArgs('hookName', [hookHoge, hookFuga], [argHoge, argFuga]) expect(res).toEqual(['hoge', 'fuga']) }) it('one hook, one arg', async () => { const hook = () => { return 'hoge' } const arg = { hoge: 'hoge' } // @ts-ignore test with invalid param const res = await executeHooksWithArgs('hookName', hook, arg) expect(res).toHaveLength(1) expect(res).toContain('hoge') }) it('with error', async () => { const hook = () => { throw new Error('Hoge') } const res = await executeHooksWithArgs('hookName', hook, []) expect(res).toHaveLength(1) expect(res).toEqual([new Error('Hoge')]) }) it('return promise with error', async () => { const hook = () => { return new Promise(() => { throw new Error('Hoge') }) } const res = await executeHooksWithArgs('hookName', hook, []) expect(res).toHaveLength(1) expect(res).toEqual([new Error('Hoge')]) }) it('async functions', async () => { const hookHoge = () => { return new Promise(reject => setTimeout(reject, 5, new Error('Hoge'))) } const hookFuga = async () => new Promise(resolve => setTimeout(resolve, 10, 'fuga')) const res = await executeHooksWithArgs('hookName', [hookHoge, hookFuga], []) expect(res).toEqual([new Error('Hoge'), 'fuga']) }) }) describe('executeAsync', () => { it('should pass with default values and fn returning synchronous value', async () => { const result = await executeAsync.call({}, async () => 'foo', { limit: 0, attempts: 0 }) expect(result).toEqual('foo') }) it('should pass when optional arguments are passed', async () => { const result = await executeAsync.call({}, async (arg: unknown) => arg, { limit: 1, attempts: 0 }, ['foo']) expect(result).toEqual('foo') }) it('should reject if fn throws error directly', async () => { let error const fn = () => { throw new Error('foo') } try { await executeAsync.call({}, fn, { limit: 0, attempts: 0 }) } catch (err: any) { error = err } expect(error.message).toEqual('foo') }) it('should repeat if fn throws error directly and repeatTest provided', async () => { let counter = 3 const scope = { wdioRetries: undefined } const repeatTest = { limit: counter, attempts: 0 } const result = await executeAsync.call(scope, () => { if (counter > 0) { counter-- throw new Error('foo') } return Promise.resolve(true) }, repeatTest) expect(result).toEqual(true) expect(counter).toEqual(0) expect(repeatTest).toEqual({ limit: 3, attempts: 3 }) expect(scope.wdioRetries).toEqual(3) }) it('should repeat if fn rejects and repeatTest provided', async () => { let counter = 3 const scope = { wdioRetries: undefined } const repeatTest = { limit: counter, attempts: 0 } const result = await executeAsync.call(scope, () => { if (counter > 0) { counter-- return Promise.reject(new Error('foo')) } return Promise.resolve(true) }, repeatTest) expect(result).toEqual(true) expect(counter).toEqual(0) expect(repeatTest).toEqual({ limit: 3, attempts: 3 }) expect(scope.wdioRetries).toEqual(3) }) }) describe('wrapCommand', () => { it('should not run a command hook in command hook', async () => { const rawCommand = vi.fn().mockReturnValue(Promise.resolve('Yayy!')) const commandA = wrapCommand('foobar', rawCommand) const commandB = wrapCommand('barfoo', rawCommand) const scope: any = { options: { beforeCommand: vi.fn(), afterCommand: vi.fn().mockImplementation( () => commandB.call(scope, 123)) } } expect(await commandA.call(scope, true, false, '!!')).toBe('Yayy!') expect(scope.options!.beforeCommand).toBeCalledTimes(1) expect(scope.options!.afterCommand).toBeCalledTimes(1) expect(rawCommand).toBeCalledTimes(2) }) it('throws an error if command fails', async () => { const rawCommand = vi.fn().mockReturnValue( Promise.reject(new Error('Uppsi!'))) const commandA = wrapCommand('foobar', rawCommand) const commandB = wrapCommand('barfoo', rawCommand) const scope: any = { options: { beforeCommand: vi.fn(), afterCommand: vi.fn().mockImplementation( () => commandB.call(scope, 123)) } } const error = await commandA.call(scope, true, false, '!!') .catch((err: Error) => err) expect((error as Error).message).toBe('Uppsi!') expect(scope.options!.beforeCommand).toBeCalledTimes(1) expect(scope.options!.afterCommand).toBeCalledTimes(1) expect(rawCommand).toBeCalledTimes(2) }) it('allows to chain element promises', async () => { const rawCommand = vi.fn() const scope: any = { options: { beforeCommand: vi.fn(), afterCommand: vi.fn() }, getTagName: vi.fn().mockResolvedValue('Yayy'), $: rawCommand } rawCommand.mockReturnValue(Promise.resolve(scope)) const commandA = wrapCommand('$', rawCommand) expect(await commandA.call(scope, 'bar') .$('foo') .getTagName() ).toBe('Yayy') expect(scope.$).toBeCalledTimes(2) expect(scope.$).toBeCalledWith('bar') expect(scope.$).toBeCalledWith('foo') expect(scope.getTagName).toBeCalledTimes(1) }) it('allows to chain element promises for custom command', async () => { const rawCommand = vi.fn() const scope: any = { options: { beforeCommand: vi.fn(), afterCommand: vi.fn() }, getTagName: vi.fn().mockResolvedValue('Yayy'), user$: rawCommand } rawCommand.mockReturnValue(Promise.resolve(scope)) const commandB = wrapCommand('user$', rawCommand) expect(await commandB.call(scope, 'bar') .user$('foo') .getTagName()).toBe('Yayy') expect(scope.user$).toBeCalledTimes(2) expect(scope.user$).toBeCalledWith('bar') expect(scope.user$).toBeCalledWith('foo') expect(scope.getTagName).toBeCalledTimes(1) }) it('allows to access indexed element', async () => { const rawCommand$ = vi.fn() const rawCommand$$ = vi.fn() const scope: (i: number) => any = (i) => ({ options: { beforeCommand: vi.fn(), afterCommand: vi.fn() }, getTagName: vi.fn().mockResolvedValue('Yayy' + i), $: rawCommand$, $$: rawCommand$$ }) rawCommand$.mockResolvedValue(scope(0)) rawCommand$$.mockReturnValue([ Promise.resolve(scope(0)), Promise.resolve(scope(1)), Promise.resolve(scope(2)) ]) const commandA = wrapCommand('$', rawCommand$) expect(await commandA.call(scope(0)) .$('foo') .$$('bar')[2] .getTagName() ).toBe('Yayy2') expect(await commandA.call(scope(0)) .$('foo') .$$('bar')[2] .$('barfoo') .getTagName() ).toBe('Yayy0') expect(rawCommand$$).toBeCalledTimes(2) expect(rawCommand$$).toBeCalledWith('bar') }) it('offers array methods on elements', async () => { const rawCommand$ = vi.fn() const rawCommand$$ = vi.fn() const scope: (i: number) => any = (i) => ({ options: { beforeCommand: vi.fn(), afterCommand: vi.fn() }, getTagName: vi.fn().mockResolvedValue('Yayy' + i), $: rawCommand$, $$: rawCommand$$ }) rawCommand$.mockResolvedValue(scope(0)) rawCommand$$.mockReturnValue([ Promise.resolve(scope(0)), Promise.resolve(scope(1)), Promise.resolve(scope(2)) ]) const commandA = wrapCommand('$', rawCommand$) expect(await commandA.call(scope(0)) .$('foo') .$$('bar') .map((el: any) => el.getTagName()) ).toEqual(['Yayy0', 'Yayy1', 'Yayy2']) }) it('can access element properties', async () => { const scope: any = { options: { beforeCommand: vi.fn(), afterCommand: vi.fn() }, selector: 'foobar' } const rawCommand = vi.fn().mockReturnValue(Promise.resolve(scope)) const commandA = wrapCommand('$', rawCommand) expect(await commandA.call(scope).selector).toBe('foobar') }) it('can iterate over elements asynchronously', async () => { const options = { beforeCommand: vi.fn(), afterCommand: vi.fn() } const scope: any = [{ selector: 'foobarA', options }, { selector: 'foobarB', options }, { selector: 'foobarC', options }] scope.options = options const rawCommand = vi.fn().mockReturnValue(Promise.resolve(scope)) const commandA = wrapCommand('$$', rawCommand).bind(scope) as unknown as (sel: string) => Promise[] const expectedResults = ['foobarA', 'foobarB', 'foobarC'] let i = 0 for await (const elem of commandA('selector')) { expect(expectedResults[i++]).toBe(elem.selector) } }) it('throws an error if iterating through a non array', async () => { expect.assertions(1) const options = { beforeCommand: vi.fn(), afterCommand: vi.fn() } const scope: any = { selector: 'foobarA', options } scope.options = options const rawCommand = vi.fn().mockReturnValue(Promise.resolve(scope)) const commandA = wrapCommand('$', rawCommand).bind(scope) as unknown as (sel: string) => Promise[] try { for await (const elem of commandA('selector')) { console.log(elem) } } catch (err: any) { expect(err.message).toBe('Can not iterate over non array') } }) })