🏡 index : quine-global/webdriverio.git

import path from 'node:path'
import { expect, test, vi, it, describe, afterEach } from 'vitest'
import logger from '@wdio/logger'
import { wrapGlobalTestMethod, executeHooksWithArgs } from '@wdio/utils'
// @ts-expect-error mock
import { jasmine } from 'jasmine'
import type { EventEmitter } from 'node:events'

import JasmineAdapterFactory, { JasmineAdapter } from '../src/index.js'

vi.mock('jasmine')
vi.mock('expect-webdriverio', () => ({
    expect: {},
    matchers: new Map([['toHaveTitle', vi.fn()]]),
    getConfig: vi.fn()
}))
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
vi.mock('@wdio/utils', () => import(path.join(process.cwd(), '__mocks__', '@wdio/utils')))

const INTERFACES = {
    bdd: ['beforeAll', 'beforeEach', 'it', 'xit', 'fit', 'afterEach', 'afterAll']
}
const TEST_INTERFACES = ['it', 'fit', 'xit']
const BEFORE_HOOK_IDX = 1
const AFTER_HOOK_IDX = 3

globalThis.jasmine = {
    addMatchers: 'addMatchers',
    addAsyncMatchers: 'addAsyncMatchers'
} as any

const wdioReporter: EventEmitter = {
    write: vi.fn(),
    emit: vi.fn(),
    on: vi.fn()
} as any

const hookPayload = (type: string, error?: Error) => ({
    debugLogs: null,
    filename: '',
    id: '',
    description: `"${type} all" hook`,
    duration: null,
    ...(error ? { error } : {}),
    fullName: `"${type} all" hook`,
    properties: {},
    start: expect.any(Date),
    type: 'hook',
    passedExpectations: [],
    failedExpectations: [],
    deprecationWarnings: [],
    status: '',
    pendingReason: ''
})

const adapterFactory = (config = {}) => new JasmineAdapter(
    '0-2',
    { beforeHook: [], afterHook: [], beforeTest: 'beforeTest', afterTest: 'afterTest', ...config } as any,
    ['/foo/bar.test.js'],
    { browserName: 'chrome' } as any,
    wdioReporter
)

test('comes with a factory', async () => {
    expect(typeof JasmineAdapterFactory.init).toBe('function')
    const instance: JasmineAdapter = await JasmineAdapterFactory.init!(
        '0-2',
        { beforeHook: [], afterHook: [] },
        ['/foo/bar.test.js'],
        { browserName: 'chrome' },
        wdioReporter
    )
    instance.setupExpect(expect as any, new Map(), vi.fn())
    const result = await instance.run()
    expect(result).toBe(0)

    globalThis.jasmine.addAsyncMatchers = vi.fn()
    globalThis.jasmine.addMatchers({
        testMatcher: function testMatcher(/*matcherUtils*/) {
            return {
                compare: function compare(/*actual, expected*/) {
                    return { pass: true, message: 'Just good vibes.' }
                }
            }
        }
    })
    expect(jasmine.addAsyncMatchers).toBeCalledTimes(1)
    const testMatcher = vi.mocked(globalThis.jasmine.addAsyncMatchers).mock.calls[0][0].testMatcher
    const { compare, negativeCompare } = testMatcher({} as any)
    expect(compare.constructor.name).toBe('AsyncFunction')
    expect(negativeCompare?.constructor.name).toBe('AsyncFunction')
})

test('should properly set up jasmine', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    const result = await adapter.run()

    expect(result).toBe(0)
    expect(vi.mocked(adapter['_jrunner']!.addSpecFile).mock.calls[0][0]).toEqual('/foo/bar.test.js')
    // @ts-ignore outdated types
    expect(vi.mocked(adapter['_jrunner']!.jasmine.addReporter).mock.calls).toHaveLength(2)
    expect(vi.mocked(executeHooksWithArgs).mock.calls).toHaveLength(1)

    // @ts-expect-error
    expect(vi.mocked(adapter['_jrunner']!.env.beforeAll).mock.calls).toHaveLength(1)
    // @ts-expect-error
    expect(vi.mocked(adapter['_jrunner']!.env.beforeEach).mock.calls).toHaveLength(0)
    // @ts-expect-error
    expect(vi.mocked(adapter['_jrunner']!.env.afterEach).mock.calls).toHaveLength(0)
    // @ts-expect-error
    expect(vi.mocked(adapter['_jrunner']!.env.afterAll).mock.calls).toHaveLength(1)

    expect(vi.mocked(adapter['_jrunner']!.execute).mock.calls).toHaveLength(1)

    expect(adapter['_jrunner']!.configureDefaultReporter.name).toBe('noop')
    // @ts-ignore outdated types
    adapter['_jrunner']!.configureDefaultReporter()

    expect(globalThis.jasmine.addAsyncMatchers).toBeCalledTimes(1)
})

test('should propery wrap interfaces', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    await adapter.run()

    expect(vi.mocked(wrapGlobalTestMethod).mock.calls).toHaveLength(INTERFACES.bdd.length)

    INTERFACES.bdd.forEach((fnName, idx) => {
        const isTest = TEST_INTERFACES.includes(fnName)
        const hook = fnName.includes('All') ? [expect.any(Function)] : []

        expect(vi.mocked(wrapGlobalTestMethod).mock.calls[idx][5]).toBe(fnName)
        expect(vi.mocked(wrapGlobalTestMethod).mock.calls[idx][BEFORE_HOOK_IDX]).toEqual(isTest ? 'beforeTest' : hook)
        expect(vi.mocked(wrapGlobalTestMethod).mock.calls[idx][AFTER_HOOK_IDX]).toEqual(isTest ? 'afterTest' : hook)
    })
})

test('hookArgsFn: should return proper value', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    await adapter.run()

    const hookArgsFn = vi.mocked(wrapGlobalTestMethod).mock.calls[0][2]
    adapter['_lastTest'] = { title: 'foo' } as any
    expect(hookArgsFn('bar')).toEqual([{ title: 'foo' }, 'bar'])
    delete adapter['_lastTest']
    expect(hookArgsFn('bar')).toEqual([{}, 'bar'])
})

test('emitHookEvent: should emit events for beforeAll and afterAll hooks', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    await adapter.run()

    const allHooks = INTERFACES.bdd.filter(fnName => fnName.includes('All'))
    expect(allHooks).toHaveLength(2)

    adapter['_reporter'] = { emit: vi.fn() } as any
    allHooks.forEach((hookName) => {
        const hookIdx = INTERFACES.bdd.indexOf(hookName)
        adapter['_reporter'].startedSuite = true as any
        (vi.mocked(wrapGlobalTestMethod).mock.calls[hookIdx][BEFORE_HOOK_IDX] as Function[]).pop()!(null, null, undefined)
        adapter['_reporter'].startedSuite = false as any
        (vi.mocked(wrapGlobalTestMethod).mock.calls[hookIdx][AFTER_HOOK_IDX] as Function[]).pop()!(null, null, { error: new Error(hookName) })
    })

    expect(adapter['_reporter'].emit).toHaveBeenCalledTimes(4)

    expect(adapter['_reporter'].emit).toBeCalledWith('hook:start', hookPayload('before'))
    expect(adapter['_reporter'].emit).toBeCalledWith('hook:end', hookPayload('before', new Error('beforeAll')))
    expect(adapter['_reporter'].emit).toBeCalledWith('hook:start', hookPayload('after'))
    expect(adapter['_reporter'].emit).toBeCalledWith('hook:end', hookPayload('after', new Error('afterAll')))
})

test('should properly configure the jasmine environment', async () => {
    const stopOnSpecFailure = false
    const failSpecWithNoExpectations = false
    const stopSpecOnExpectationFailure = false
    const random = false
    const failFast = false
    const seed = false

    const adapter = adapterFactory({
        jasmineOpts: {
            stopOnSpecFailure,
            stopSpecOnExpectationFailure,
            random,
            failFast,
        }
    })
    await adapter.init()
    await adapter.run()
    // @ts-ignore outdated types
    expect(adapter['_jrunner']!.jasmine.getEnv().configure).toBeCalledWith({
        specFilter: expect.any(Function),
        failSpecWithNoExpectations,
        oneFailurePerSpec: stopSpecOnExpectationFailure,
        stopOnSpecFailure,
        random,
        seed,
        failFast,
    })
})

test('set custom ', async () => {
    const config = {
        jasmineOpts: { expectationResultHandler: vi.fn() },
        beforeHook: [],
        afterHook: []
    }
    const adapter = adapterFactory(config)
    await adapter.init()
    await adapter.run()
    adapter['_jrunner']!.jasmine.Spec.prototype.addExpectationResult('foobar')
    expect(config.jasmineOpts.expectationResultHandler).toBeCalledWith('foobar', undefined)
})

test('get data from beforeAll hook', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    await adapter.run()
    expect(adapter['_lastSpec']).toBeUndefined()

    // @ts-ignore outdated types
    adapter['_jrunner']!.jasmine.Suite.prototype.beforeAll.call({
        result: 'some result'
    }, 'foobar')
    expect(adapter['_lastSpec']).toBe('some result')
    // @ts-ignore outdated types
    expect(adapter['_jrunner']!.beforeAllHook).toBeCalledWith('foobar')
})

test('get data from execute hook', async () => {
    const adapter = adapterFactory()
    await adapter.init()
    await adapter.run()
    expect(adapter['_lastTest']).toBeUndefined()

    adapter['_jrunner']!.jasmine.Spec.prototype.execute.call({
        result: {
            text: 'some result'
        }
    }, 'barfoo')

    // @ts-ignore outdated types
    expect(adapter['_lastTest'].text).toBe('some result')
    // @ts-ignore outdated types
    expect(typeof adapter['_lastTest'].start).toBe('number')
    // @ts-ignore outdated types
    expect(adapter['_jrunner']!.executeHook).toBeCalledWith('barfoo')
})

test('customSpecFilter', () => {
    const specMock = {
        getFullName: () => 'my test @smoke',
        pend: vi.fn()
    } as any
    const config = {
        jasmineOpts: { grepMatch: '@smoke' }
    }
    const adapter = adapterFactory(config)

    expect(adapter.customSpecFilter(specMock)).toBe(true)
    expect(specMock.pend.mock.calls).toHaveLength(0)

    adapter['_jasmineOpts'].grep = '@random'
    expect(adapter.customSpecFilter(specMock)).toBe(false)
    expect(specMock.pend.mock.calls).toHaveLength(1)

    adapter['_jasmineOpts'].invertGrep = true
    expect(adapter.customSpecFilter(specMock)).toBe(true)
    expect(specMock.pend.mock.calls).toHaveLength(1)

    adapter['_jasmineOpts'].grep = '@smoke'
    adapter['_jasmineOpts'].invertGrep = true
    expect(adapter.customSpecFilter(specMock)).toBe(false)
    expect(specMock.pend.mock.calls).toHaveLength(2)
})

test('wrapHook if successful', async () => {
    vi.mocked(executeHooksWithArgs).mockClear()
    const config = { beforeAll: 'somehook' }
    const adapter = adapterFactory(config)
    const wrappedHook = adapter.wrapHook('beforeAll' as any)

    vi.mocked(executeHooksWithArgs).mockImplementation((...args: any[]) => Promise.resolve(args))
    await wrappedHook()
    expect(vi.mocked(executeHooksWithArgs).mock.calls[0][0]).toBe('beforeAll')
    expect(vi.mocked(executeHooksWithArgs).mock.calls[0][1]).toBe('somehook')
    expect((vi.mocked(executeHooksWithArgs).mock.calls[0][2]![0] as any).type).toBe('beforeAll')
})

test('wrapHook if failing', async () => {
    vi.mocked(executeHooksWithArgs).mockClear()
    const config = { beforeAll: 'somehook' }
    const adapter = adapterFactory(config)
    const wrappedHook = adapter.wrapHook('beforeAll' as any)

    vi.mocked(executeHooksWithArgs).mockImplementation(() => Promise.reject(new Error('uuuups')))
    await wrappedHook()
    expect(vi.mocked(executeHooksWithArgs).mock.calls[0][0]).toBe('beforeAll')
    expect(vi.mocked(executeHooksWithArgs).mock.calls[0][1]).toBe('somehook')
    expect(vi.mocked((executeHooksWithArgs as any).mock.calls[0][2]![0] as any).type).toBe('beforeAll')
    expect(vi.mocked(logger('').info).mock.calls[0][0].startsWith('Error in beforeAll hook: uuuups'))
        .toBe(true)
})

test('formatMessage', () => {
    const adapter = adapterFactory()

    let message = adapter.formatMessage({ type: 'foobar' })
    expect(message).toEqual({ type: 'foobar' })

    message = adapter.formatMessage({
        type: 'foobar',
        payload: {
            failedExpectations: [new Error('foobar')]
        }
    })
    expect(message.error?.message).toBe('foobar')

    adapter['_lastSpec'] = { description: 'lasttestdesc' } as any
    message = adapter.formatMessage({
        type: 'afterTest',
        payload: {
            id: 'spec1',
            description: 'foodesc',
            fullName: 'foo',
            file: '/some/test.js',
            failedExpectations: [],
            start: Date.now() - 2000
        }
    })
    expect(message.duration).toBeGreaterThan(1999)
    expect(message.duration).toBeLessThan(2005)
    expect(message.passed).toBe(true)
    expect(message.parent).toBe('lasttestdesc')
    expect(message.title).toBe('foodesc')

    message = adapter.formatMessage({
        type: 'afterTest',
        payload: {
            description: 'foodesc',
            fullName: 'foo',
            file: '/some/test.js',
            start: Date.now() - 2000,
            duration: 123
        }
    })
    expect(message.duration).toBe(123)
})

test('formatMessage should pass all failedExpectations as errors', () => {
    const adapter = adapterFactory()
    const message = adapter.formatMessage({
        type: 'foobar',
        payload: {
            failedExpectations: [new Error('foobar'), { message: 'I am also a failed expectation but not an exception' }]
        }
    })

    expect(message.errors?.length).toBe(2)
    expect(message.errors![1].message).toBe('I am also a failed expectation but not an exception')
})

test('getExpectationResultHandler returns origHandler if none is given', () => {
    const jasmine = { Spec: { prototype: { addExpectationResult: 'foobar' } } }
    const config = { jasmineOpts: {} }
    const adapter = adapterFactory(config)

    adapter.expectationResultHandler = vi.fn().mockImplementation(() => 'barfoo')
    const handler = adapter.getExpectationResultHandler(jasmine as any)
    expect(handler).toBe('foobar')
})

test('getExpectationResultHandler returns modified origHandler if expectationResultHandler is given', () => {
    const jasmine = { Spec: { prototype: { addExpectationResult: 'foobar' } } }
    const config = { jasmineOpts: { expectationResultHandler: vi.fn() } }
    const adapter = adapterFactory(config)

    adapter.expectationResultHandler = vi.fn().mockImplementation(() => 'barfoo')
    const handler = adapter.getExpectationResultHandler(jasmine as any)
    expect(handler).toBe('barfoo')
    expect(adapter.expectationResultHandler).toBeCalledWith('foobar')
})

test('expectationResultHandler', () => {
    const origHandler = vi.fn()
    const config = { jasmineOpts: { expectationResultHandler: vi.fn() } }
    const adapter = adapterFactory(config)

    const resultHandler = adapter.expectationResultHandler(origHandler)
    // @ts-ignore mock feature
    resultHandler(true, 'foobar')
    expect(config.jasmineOpts.expectationResultHandler).toBeCalledWith(true, 'foobar')
    expect(origHandler).toBeCalledWith(true, 'foobar')
})

test('expectationResultHandler failing', () => {
    const origHandler = vi.fn()
    const err = new Error('uuups')
    const config = { jasmineOpts: { expectationResultHandler: () => {
        throw err
    } } }
    const adapter = adapterFactory(config)

    const resultHandler = adapter.expectationResultHandler(origHandler)
    // @ts-ignore mock feature
    resultHandler(true, 'foobar')
    expect(origHandler).toBeCalledWith(
        false,
        {
            passed: false,
            message: 'expectationResultHandlerError: uuups',
            error: err
        }
    )
})

test('expectationResultHandler failing with failing test', () => {
    const origHandler = vi.fn()
    const config = { jasmineOpts: { expectationResultHandler: () => {
        throw new Error('uuups')
    } } }
    const adapter = adapterFactory(config)

    const resultHandler = adapter.expectationResultHandler(origHandler)
    // @ts-ignore mock feature
    resultHandler(false, 'foobar')
    expect(origHandler).toBeCalledWith(false, 'foobar')
})

test('prepareMessage', () => {
    const adapter = adapterFactory()
    adapter.formatMessage = (params) => params
    adapter['_jrunner'] = {} as any
    adapter['_jrunner']!.specFiles = ['/some/path.test.js']
    adapter['_lastSpec'] = { foo: 'bar' } as any
    adapter['_lastTest'] = { bar: 'foo' } as any

    const msg = adapter.prepareMessage('beforeSuite')
    expect(msg.type).toBe('beforeSuite')
    // @ts-ignore overwritten formatMessage
    expect(msg.payload.file).toBe('/some/path.test.js')
    // @ts-ignore overwritten formatMessage
    expect(msg.payload.foo).toBe('bar')

    const msgSpec = adapter.prepareMessage('beforeTest')
    expect(msgSpec.type).toBe('beforeTest')
    // @ts-ignore overwritten formatMessage
    expect(msgSpec.payload.file).toBe('/some/path.test.js')
    // @ts-ignore overwritten formatMessage
    expect(msgSpec.payload.bar).toBe('foo')
})

describe('_grep', () => {
    test('should increase totalTests counter if test is not filtered by grep', () => {
        const adapter = adapterFactory()
        expect(adapter['_totalTests']).toBe(0)

        adapter.customSpecFilter = vi.fn().mockImplementation(spec => !!spec.disable)

        adapter._grep({
            children: [
                { disable: false }, { disable: true },
                { children: [{ disable: true }] }]
        } as any)
        expect(adapter['_totalTests']).toBe(2)
    })
})

describe('loadFiles', () => {
    it('should set _hasTests to true if there are tests to run', async () => {
        const adapter = adapterFactory({})
        // @ts-ignore test scenario
        delete adapter['_hasTests']
        adapter['_jrunner'] = {} as any
        // @ts-ignore outdated types
        adapter['_jrunner']!.loadRequires = vi.fn()
        adapter['_jrunner']!.loadHelpers = vi.fn()
        adapter['_jrunner']!.loadSpecs = vi.fn()
        // @ts-ignore outdated types
        adapter['_jrunner']!.env = { topSuite () { return { children: [1] } } }

        await adapter._loadFiles()

        // @ts-ignore outdated types
        expect(adapter['_jrunner']!.loadRequires).toBeCalled()
        expect(adapter['_jrunner']!.loadHelpers).toBeCalled()
        expect(adapter['_jrunner']!.loadSpecs).toBeCalled()
        expect(adapter['_hasTests']).toBe(true)
    })

    it('should set _hasTests to false if there no tests to run', async () => {
        const adapter = adapterFactory()
        // @ts-ignore test scenario
        delete adapter['_hasTests']
        adapter['_jrunner'] = {} as any
        adapter['_jasmineOpts'].requires = ['r']
        adapter['_jasmineOpts'].helpers = ['h']
        // @ts-ignore outdated types
        adapter['_jrunner']!.addRequires = vi.fn()
        // @ts-ignore outdated types
        adapter['_jrunner']!.addMatchingHelperFiles = vi.fn()
        // @ts-ignore outdated types
        adapter['_jrunner']!.loadRequires = vi.fn()
        adapter['_jrunner']!.loadHelpers = vi.fn()
        adapter['_jrunner']!.loadSpecs = vi.fn()
        // @ts-ignore outdated types
        adapter['_jrunner']!.env = { topSuite() { return { children: [] } } }

        await adapter._loadFiles()

        // @ts-ignore outdated types
        expect(adapter['_jrunner']!.addRequires).toHaveBeenCalledWith(adapter['_jasmineOpts'].requires)
        // @ts-ignore outdated types
        expect(adapter['_jrunner']!.addMatchingHelperFiles).toHaveBeenCalledWith(adapter['_jasmineOpts'].helpers)
        expect(adapter['_hasTests']).toBe(false)
    })

    it('should not fail on exception', () => {
        const adapter = adapterFactory()
        adapter['_jrunner'] = {} as any
        // @ts-ignore test scenario
        delete adapter['_hasTests']

        // @ts-ignore outdated types
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        adapter['_jrunner']!.loadRequires = vi.fn().mockImplementation(() => { throw new Error('foo') }),

        adapter._loadFiles()
        // @ts-ignore outdated types
        expect(adapter['_jrunner']!.loadRequires).toBeCalled()
        expect(adapter['_hasTests']).toBe(undefined)
    })
})

describe('hasTests', () => {
    it('should return flag result', () => {
        const adapter = adapterFactory()
        adapter['_hasTests'] = true
        expect(adapter.hasTests()).toBe(true)
        adapter['_hasTests'] = false
        expect(adapter.hasTests()).toBe(false)
    })
})

afterEach(() => {
    vi.mocked(wrapGlobalTestMethod).mockClear()
    vi.mocked(executeHooksWithArgs).mockClear()
    vi.mocked(jasmine.addAsyncMatchers).mockClear()
})