🏡 index : quine-global/webdriverio.git

import path from 'node:path'
import { afterAll, beforeAll, expect, test, vi } from 'vitest'

// @ts-ignore mock exports instances, package doesn't
import { instances } from '@wdio/runner'

vi.mock('@wdio/runner', () => import(path.join(process.cwd(), '__mocks__', '@wdio/runner')))
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))

let exitHookCallback: Function | undefined
const exitHookMock = vi.fn((callback: Function) => {
    exitHookCallback = callback
    return () => {} // return unsubscribe function
})

const gracefulExitMock = vi.fn()

vi.mock('exit-hook', () => ({
    default: exitHookMock,
    asyncExitHook: exitHookMock,
    gracefulExit: gracefulExitMock
}))

vi.mock('../src/constants', () => ({
    SHUTDOWN_TIMEOUT: 10
}))

const sleep = (ms = 100) => new Promise(
    (resolve) => setTimeout(resolve, ms))

let runner: any
const origExit = process.exit.bind(process)

beforeAll(async () => {
    vi.spyOn(process, 'on')
    process.send = vi.fn()
    process.exit = vi.fn() as any

    const run = await import('../src/run.js')
    runner = run.runner
})

test('should register exitHook', () => {
    expect(exitHookMock).toHaveBeenCalled()
    expect(exitHookCallback).toBeDefined()
})

test('should have registered runner listener', () => {
    expect(instances[0].on).toHaveBeenCalledWith('exit', expect.any(Function))
    expect(instances[0].on).toHaveBeenCalledWith('error', expect.any(Function))
    instances[0].on.mock.calls[1][1]({ name: 'name', message: 'message', stack: 'stack' })
    expect(process.send).toHaveBeenCalledWith({
        origin: 'worker',
        name: 'error',
        content: { name: 'name', message: 'message', stack: 'stack' }
    })
})

test('should not call runner if message is undefined', () => {
    vi.mocked(process.on).mock.calls[0][1](false)
})

test('should call runner command on process message', async () => {
    expect(instances[0].run).toHaveBeenCalledTimes(0)
    vi.mocked(process.on).mock.calls[0][1]({
        command: 'run',
        foo: 'bar'
    })
    expect(instances[0].run).toHaveBeenCalledTimes(1)
    await new Promise((resolve) => setTimeout(resolve, 10))
    expect(process.send).toHaveBeenCalledWith({
        origin: 'worker',
        name: 'finishedCommand',
        content: { command: 'run', result: { foo: 'bar' } }
    })
})

test('should exit process if failing to execute', async () => {
    runner.errorMe = vi.fn().mockReturnValue(Promise.reject(new Error('Uups')))
    vi.mocked(process.on).mock.calls[0][1]({
        command: 'errorMe',
        foo: 'bar'
    })
    expect((await instances[0]).errorMe).toHaveBeenCalledTimes(1)
    await sleep()
    expect(process.exit).toBeCalledWith(1)

})

test('should call gracefulExit with exit code on exit', () => {
    const exitListener = instances[0].on.mock.calls.find(([event]: [string]) => event === 'exit')?.[1]
    expect(exitListener).toBeDefined()
    exitListener?.(5)
    expect(gracefulExitMock).toHaveBeenCalledWith(5)
})

test('should call gracefulExit(130) and set sigintWasCalled on SIGINT', () => {
    // Find SIGINT listener
    const sigintListener = vi.mocked(process.on).mock.calls.find(([event]) => event === 'SIGINT')?.[1]
    expect(sigintListener).toBeDefined()
    sigintListener?.()
    expect(runner.sigintWasCalled).toBe(true)
    expect(gracefulExitMock).toHaveBeenCalledWith(130)
})

test('should delay shutdown in exitHook if SIGINT was received', async () => {
    runner.sigintWasCalled = true
    const startTime = process.hrtime.bigint()
    await exitHookCallback?.()
    const endTime = process.hrtime.bigint()
    // Should wait at least SHUTDOWN_TIMEOUT (10ms in test due to mock)
    // Use nanosecond precision and allow for 9ms minimum to account for timing variance
    const elapsedMs = Number(endTime - startTime) / 1_000_000
    expect(elapsedMs).toBeGreaterThanOrEqual(9)
})

test('should not delay in exitHook if SIGINT was not received', async () => {
    runner.sigintWasCalled = false
    const startTime = process.hrtime.bigint()
    await exitHookCallback?.()
    const endTime = process.hrtime.bigint()
    // Should not wait (almost immediate) when sigintWasCalled = false
    // Allow up to 5ms for normal async overhead
    const elapsedMs = Number(endTime - startTime) / 1_000_000
    expect(elapsedMs).toBeLessThan(5)
})

afterAll(() => {
    vi.mocked(process.on).mockRestore()
    vi.mocked(process.send)!.mockRestore()
    gracefulExitMock.mockReset()
    process.exit = origExit
})