🏡 index : quine-global/webdriverio.git

import path from 'node:path'

import logger from '@wdio/logger'
import { describe, expect, it, vi, afterEach, beforeEach } from 'vitest'
import { executeHooksWithArgs } from '@wdio/utils'
import { ConfigParser } from '@wdio/config/node'
import { attach } from 'webdriverio'
import { _setGlobal } from '@wdio/globals'
import { setOptions, SnapshotService, SoftAssertionService } from 'expect-webdriverio'

import WDIORunner from '../src/index.js'

vi.mock('fs/promises', async (orig) => ({
    ...(await orig()) as any,
    default: { writeFile: vi.fn() }
}))
vi.mock('util')
vi.mock('expect-webdriverio', () => ({
    setOptions: vi.fn(),
    expect: vi.fn(),
    SoftAssertionService: vi.fn(),
    SnapshotService: {
        initiate: vi.fn().mockReturnValue({
            results: ['foobar']
        })
    }
}))
vi.mock('webdriverio', () => import(path.join(process.cwd(), '__mocks__', 'webdriverio')))
vi.mock('@wdio/utils', () => import(path.join(process.cwd(), '__mocks__', '@wdio/utils')))
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
vi.mock('@wdio/globals', () => ({
    _setGlobal: vi.fn()
}))

type BrowserObject = WebdriverIO.Browser
type MultiRemoteBrowserObject = WebdriverIO.MultiRemoteBrowser

describe('wdio-runner', () => {
    beforeEach(() => {
        vi.mocked(_setGlobal).mockClear()
        vi.mocked(setOptions).mockClear()
        process.send = vi.fn()
    })

    describe('endSession', () => {
        it('should work normally when called after framework run', async () => {
            const hook = vi.fn()
            const runner = new WDIORunner()
            runner['_shutdown'] = vi.fn()
            runner['_browser'] = {
                deleteSession: vi.fn(),
                sessionId: '123',
                config: { afterSession: [hook] }
            } as unknown as BrowserObject
            runner['_config'] = { logLevel: 'info', afterSession: [hook] } as any
            await runner.endSession()
            expect(executeHooksWithArgs).toBeCalledWith(
                'afterSession',
                [hook],
                [{ logLevel: 'info', afterSession: [hook] }, {}, undefined])
            expect(runner['_browser'].deleteSession).toBeCalledTimes(1)
            expect(!runner['_browser'].sessionId).toBe(true)
            expect(runner['_shutdown']).toBeCalledTimes(0)
        })

        it('should do nothing when triggered by run method without session', async () => {
            const hook = vi.fn()
            const runner = new WDIORunner()
            runner['_shutdown'] = vi.fn()
            await runner.endSession()
            expect(hook).toBeCalledTimes(0)
        })

        it('should work normally when called after framework run in multiremote', async () => {
            const hook = vi.fn()
            const runner = new WDIORunner()
            runner['_isMultiremote'] = true
            runner['_shutdown'] = vi.fn()
            runner['_browser'] = {
                deleteSession: vi.fn(),
                instances: ['foo', 'bar'],
                getInstance: vi.fn().mockReturnValue({
                    sessionId: '123',
                }),
                capabilities: {
                    foo: {
                        browserName: 'Chrome'
                    },
                    bar: {
                        browserName: 'Chrome'
                    }
                },
                config: { afterSession: [hook] }
            } as any
            runner['_config'] = { logLevel: 'error', afterSession: [hook] } as any
            await runner.endSession()
            expect(executeHooksWithArgs).toBeCalledWith(
                'afterSession',
                [hook],
                [{ logLevel: 'error', afterSession: [hook] }, { foo: undefined, bar: undefined }, undefined])
            expect(runner['_browser']!.deleteSession).toBeCalledTimes(1)
            expect(!(runner['_browser'] as unknown as MultiRemoteBrowserObject).getInstance('foo').sessionId).toBe(true)
            expect(!(runner['_browser'] as unknown as MultiRemoteBrowserObject).getInstance('bar').sessionId).toBe(true)
            expect(runner['_shutdown']).toBeCalledTimes(0)
        })

        it('should do nothing when triggered by run method without session in multiremote', async () => {
            const hook = vi.fn()
            const runner = new WDIORunner()
            runner['_isMultiremote'] = true
            runner['_shutdown'] = vi.fn()
            await runner.endSession()
            expect(hook).toBeCalledTimes(0)
        })
    })

    describe('run', () => {
        afterEach(() => {
            vi.spyOn(ConfigParser.prototype, 'initialize').mockRestore()
        })

        it('should fail if log file is corrupted', async () => {
            vi.spyOn(ConfigParser.prototype, 'initialize').mockRejectedValueOnce(new Error('ups'))
            const runner = new WDIORunner()
            runner['_shutdown'] = vi.fn()
            await runner.run({ args: {}, configFile: '/foo/bar' } as any)
            expect(runner['_shutdown']).toBeCalledWith(1, undefined, true)
        })

        it('should auto compile if args are given', async () => {
            const config: any = {
                reporters: [],
                before: [],
                beforeSession: [],
                framework: 'testWithFailures',
                runner: 'local'
            }

            const runner = new WDIORunner()
            runner['_shutdown'] = vi.fn()
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            await runner.run({ configFile: '/foo/bar', args: {}, caps: [] } as any)

            expect(runner['_shutdown']).toBeCalledWith(1, undefined, true)
        })

        it('should fail if init session fails', async () => {
            const runner = new WDIORunner()
            const beforeSession = vi.fn()
            const before = vi.fn()
            const caps = { browserName: '123' }
            const specs = ['foobar']
            const config:any = {
                reporters: [],
                before: [before],
                beforeSession: [beforeSession],
                framework: 'testWithFailures',
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            // @ts-expect-error mock private
            vi.spyOn(ConfigParser.prototype, 'addConfigFile').mockReturnValue()

            runner['_shutdown'] = vi.fn()
            const stubBrowser = {
                capabilities: { browserName: 'chrome' },
                options: {}
            }
            runner['_initSession'] = vi.fn().mockReturnValue(stubBrowser)
            await runner.run({
                args: { reporters: [] },
                cid: '0-0',
                retries: 2,
                caps,
                specs,
                configFile: '/foo/bar'
            })

            expect(runner['_shutdown']).toBeCalledWith(123, 2)
            expect(executeHooksWithArgs).toBeCalledWith('beforeSession', [beforeSession], [config, {
                browserName: '123'
            }, ['foobar'], '0-0'])
            expect(executeHooksWithArgs).toBeCalledWith('before', config.before, [caps, specs, stubBrowser])

            // session capabilities should be passed to reporter
            expect(runner['_reporter']?.caps).toEqual({ browserName: 'chrome' })
        })

        it('should return failures count', async () => {
            const runner = new WDIORunner()
            const config: any = {
                framework: 'testNoFailures',
                reporters: [],
                beforeSession: [],
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_initSession'] = vi.fn().mockReturnValue({ options: { capabilities: {} } })
            const failures = await runner.run({ args: {}, caps: {}, configFile: '/bar/foo' } as any)

            expect(failures).toBe(0)
        })

        it('should not call browser url if args watch', async () => {
            const runner = new WDIORunner()
            const config: any = {
                framework: 'testNoFailures',
                reporters: [],
                beforeSession: [],
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_browser'] = { url: vi.fn(url => url) } as unknown as BrowserObject
            runner['_startSession'] = vi.fn().mockReturnValue({ })
            runner['_initSession'] = vi.fn().mockReturnValue({ options: { capabilities: {} } })
            const failures = await runner.run({ args: { watch: true }, caps: {}, configFile: '/foo/bar' } as any)

            expect(failures).toBe(0)
            expect(runner['_browser']?.url).not.toBeCalled()
        })

        it('should attach snapshot service to service list', async () => {
            const runner = new WDIORunner()
            const config: any = {
                framework: 'testNoFailures',
                reporters: [],
                beforeSession: [],
                runner: 'local',
                updateSnapshots: 'do it',
                resolveSnapshotPath: 'resolve me'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            const addServiceSpy = vi.spyOn(ConfigParser.prototype, 'addService')
            runner['_browser'] = { url: vi.fn(url => url) } as unknown as BrowserObject
            runner['_startSession'] = vi.fn().mockReturnValue({ })
            runner['_initSession'] = vi.fn().mockReturnValue({ options: { capabilities: {} } })
            await runner.run({ args: { watch: true }, caps: {}, configFile: '/foo/bar' } as any)

            expect(addServiceSpy).toBeCalledWith({
                results: ['foobar']
            })
            expect(SnapshotService.initiate).toBeCalledWith({
                updateState: 'do it',
                resolveSnapshotPath: 'resolve me'
            })
            expect(process.send).toBeCalledWith({
                origin: 'worker',
                name: 'snapshot',
                content: ['foobar']
            })
        })

        it('should set failures to 1 in case of error', async () => {
            const runner = new WDIORunner()
            const config:any = {
                framework: 'testThrows',
                reporters: [],
                beforeSession: [],
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_initSession'] = vi.fn().mockReturnValue({ options: { capabilities: {} } })
            runner.emit = vi.fn()
            const failures = await runner.run({ args: {}, caps: {}, configFile: '/foo/bar' } as any)

            expect(failures).toBe(1)
            expect(vi.mocked(runner.emit).mock.calls[0])
                .toEqual(['error', new Error('framework testThrows failed')])
        })

        it('should return if sigintWasCalled', async () => {
            const runner = new WDIORunner()
            const caps = { browserName: '123' }
            const specs = ['foobar']
            const config: any = {
                framework: 'testThrows',
                reporters: [],
                beforeSession: [],
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_shutdown'] = vi.fn()
            runner.endSession = vi.fn()
            runner['_initSession'] = vi.fn().mockReturnValue({})
            runner['_sigintWasCalled'] = true
            await runner.run({
                args: { reporters: [] },
                cid: '0-0',
                caps,
                specs,
                configFile: '/foo/bar',
                retries: 0
            })

            expect(runner.endSession).toBeCalledTimes(1)
            expect(runner['_shutdown']).toBeCalledWith(0, 0, true)
        })

        it('should not initSession if there are no tests to run', async () => {
            const runner = new WDIORunner()
            const config: any = {
                framework: 'testNoTests',
                reporters: [],
                beforeSession: [],
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_shutdown'] = vi.fn().mockImplementation((arg) => arg)
            runner['_initSession'] = vi.fn()

            expect(await runner.run({ args: {}, caps: {}, configFile: '/foo/bar' } as any)).toBe(0)
            expect(runner['_shutdown']).toBeCalledWith(0, undefined, true)
            expect(runner['_initSession']).not.toBeCalled()
        })

        it('should shutdown if session was not created', async () => {
            const runner = new WDIORunner()
            const caps = { browserName: '123' }
            const specs = ['foobar']
            const config: any = {
                framework: 'testNoFailures',
                reporters: [],
                beforeSession: [],
                after: 'foobar',
                runner: 'local'
            }
            vi.spyOn(ConfigParser.prototype, 'getConfig').mockReturnValue(config)
            runner['_shutdown'] = vi.fn().mockReturnValue('_shutdown')
            runner.endSession = vi.fn()
            runner['_initSession'] = vi.fn().mockReturnValue(null)
            expect(await runner.run({
                args: { reporters: [] },
                cid: '0-0',
                caps,
                specs,
                configFile: '/foo/bar',
                retries: 0
            })).toBe('_shutdown')

            expect(runner['_shutdown']).toBeCalledWith(1, 0, true)
            expect(executeHooksWithArgs).toBeCalledWith(
                'after',
                'foobar',
                [1, caps, specs]
            )

            // user defined capabilities should be used until
            // browser session is started
            expect(runner['_reporter']?.caps).toEqual(caps)
        })
    })

    describe('_initSession', () => {
        it('should register browser to global scope', async () => {
            const runner = new WDIORunner()
            const browser = await runner['_initSession'](
                { hostname: 'foobar', waitforTimeout: 1, waitforInterval: 2 } as any,
                [{ browserName: 'chrome1' }] as any
            )

            expect(_setGlobal).toBeCalledWith('browser', browser, undefined)
            expect(_setGlobal).toBeCalledWith('driver', browser, undefined)
            expect(_setGlobal).toBeCalledWith('expect', expect.any(Function), undefined)
            expect(_setGlobal).toBeCalledWith('$', expect.any(Function), undefined)
            expect(_setGlobal).toBeCalledWith('$$', expect.any(Function), undefined)

            expect(setOptions).toBeCalledTimes(1)
            expect(setOptions).toBeCalledWith({
                afterAssertion: expect.any(Function),
                beforeAssertion: expect.any(Function),
                wait: 1,
                interval: 2
            })
        })

        it('should register before and after command listener', async () => {
            const reporter = { emit: vi.fn() }
            const runner = new WDIORunner()

            runner['_reporter'] = reporter as any
            const browser = await runner['_initSession'](
                { hostname: 'foobar' } as any,
                [{ browserName: 'chrome' }] as any
            )

            const beforeListener = vi.mocked(browser!.on).mock.calls[0]
            expect(beforeListener[0]).toBe('command')
            beforeListener[1]({ foo: 'bar' })
            expect(reporter.emit).toBeCalledWith(
                'client:beforeCommand',
                { foo: 'bar', sessionId: 'fakeid' })

            reporter.emit.mockClear()

            const afterListener = vi.mocked(browser!.on).mock.calls[1]
            expect(afterListener[0]).toBe('result')
            afterListener[1]({ bar: 'foo' })
            expect(reporter.emit).toBeCalledWith(
                'client:afterCommand',
                { bar: 'foo', sessionId: 'fakeid' })
        })

        it('should return null if initiating session fails', async () => {
            // @ts-ignore test scenario
            global.throwRemoteCall = true
            const runner = new WDIORunner()
            runner.emit = vi.fn()
            const browser = await runner['_initSession'](
                { hostname: 'foobar' } as any,
                [{ browserName: 'chrome' }] as any
            )

            expect(browser).toBe(undefined)
        })

        afterEach(() => {
            // @ts-ignore test scenario
            delete global.throwRemoteCall
        })
    })

    describe('_startSession', () => {
        it('can start a session', async () => {
            const runner = new WDIORunner()
            const browser = await runner['_startSession']({} as any, {} as any)
            expect(typeof browser?.deleteSession).toBe('function')
            expect(_setGlobal).toBeCalledTimes(3)
            expect(setOptions).toBeCalledTimes(1)
        })

        it('transfers custom commands from old instance to new one', async () => {
            const runner = new WDIORunner()
            runner['_browser'] = {
                customCommands: [[1, 2, 3]],
                overwrittenCommands: [[3, 2, 1]]
            } as any
            const browser = await runner['_startSession']({} as any, {} as any)
            expect(browser?.addCommand).toBeCalledWith(1, 2, 3)
            expect(browser?.overwriteCommand).toBeCalledWith(3, 2, 1)
        })
    })

    describe('_shutdown', () => {
        it('should emit exit', async () => {
            const runner = new WDIORunner()
            runner['_browser'] = {} as unknown as BrowserObject
            runner['_reporter'] = {
                waitForSync: vi.fn().mockReturnValue(Promise.resolve()),
                emit: vi.fn()
            } as any
            runner.emit = vi.fn()

            expect(await runner['_shutdown'](123, 123)).toBe(123)
            expect(runner['_reporter']!.waitForSync).toBeCalledTimes(1)
            expect(runner.emit).toBeCalledWith('exit', 1)
        })

        it('should emit exit when reporter unsync', async () => {
            const log = logger('wdio-runner')
            vi.spyOn(log, 'error').mockImplementation((string) => string)

            const runner = new WDIORunner()
            runner['_browser'] = {} as unknown as BrowserObject
            runner['_reporter'] = {
                waitForSync: vi.fn().mockReturnValue(Promise.reject('foo')),
                emit: vi.fn()
            } as any
            runner.emit = vi.fn()

            expect(await runner['_shutdown'](123, 123)).toBe(123)
            expect(runner['_reporter']!.waitForSync).toBeCalledTimes(1)
            expect(runner.emit).toBeCalledWith('exit', 1)
            expect(log.error).toHaveBeenCalledWith('foo')
        })

        it('should emit runner:start if the initialization failed', async () => {
            const runner = new WDIORunner()
            runner['_configParser'] = { getCapabilities: vi.fn().mockReturnValue([{ browserName: 'safari' }]) } as any
            runner['_browser'] = {} as unknown as BrowserObject
            runner['_caps'] = { 'browserName': 'safari' }
            runner['_reporter'] = {
                waitForSync: vi.fn().mockReturnValue(Promise.resolve()),
                emit: vi.fn()
            } as any
            const reporter = runner['_reporter'] as any
            runner.emit = vi.fn()

            const args = [
                ['runner:start', {
                    'capabilities': { 'browserName': 'safari' },
                    'cid': undefined,
                    'config': undefined,
                    'instanceOptions': {},
                    'isMultiremote': false,
                    'retry': 0,
                    'specs': undefined
                }],
                ['runner:end', { 'cid': undefined, 'failures': 123, 'retries': 123 }]
            ]
            expect(await runner['_shutdown'](123, 123, true)).toBe(123)
            expect(reporter.emit.mock.calls).toEqual(args)
            expect(runner.emit).toBeCalledWith('exit', 1)
        })
    })

    afterEach(() => {
        vi.mocked(executeHooksWithArgs).mockClear()
        vi.mocked(attach).mockClear()
        delete process.send
    })
})