import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' import url from 'node:url' import path from 'node:path' import Mocha from 'mocha' import logger from '@wdio/logger' import { wrapGlobalTestMethod, executeHooksWithArgs } from '@wdio/utils' import MochaAdapterFactory, { MochaAdapter } from '../src/index.js' import { EVENTS } from '../src/constants.js' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) vi.mock('mocha') vi.mock('@wdio/utils') vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('expect-webdriverio') vi.mock('../src/utils', () => ({ loadModule: vi.fn() })) const wdioReporter = { write: vi.fn(), emit: vi.fn(), on: vi.fn() } as const const adapterFactory = (config: any) => new MochaAdapter( '0-2', { ...config }, ['/foo/bar.test.js'], { browserName: 'chrome' }, wdioReporter as any ) beforeEach(() => { wdioReporter.write.mockReset() wdioReporter.emit.mockReset() wdioReporter.on.mockReset() }) test('comes with a factory', async () => { expect(typeof MochaAdapterFactory.init).toBe('function') const instance = await MochaAdapterFactory.init!( '0-2', {}, ['/foo/bar.test.js'], { browserName: 'chrome' }, wdioReporter ) const result = await instance.run() expect(result).toBe(0) }) test('should properly set up mocha', async () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() await adapter.init() const result = await adapter.run() expect(result).toBe(0) expect(adapter['_mocha']!['loadFiles']).not.toBeCalled() expect(adapter['_mocha']!.loadFilesAsync).toBeCalled() expect(adapter['_mocha']!.reporter).toBeCalled() expect(adapter['_mocha']!.fullTrace).toBeCalled() expect(adapter['_mocha']!.run).toBeCalled() expect(vi.mocked(executeHooksWithArgs).mock.calls).toHaveLength(1) // @ts-ignore outdated types const runner = adapter['_mocha']!['runner'] expect(runner.on.mock.calls).toHaveLength(Object.keys(EVENTS).length) expect(runner.suite.beforeAll).toBeCalled() expect(runner.suite.beforeEach).not.toBeCalled() expect(runner.suite.afterEach).not.toBeCalled() expect(runner.suite.afterAll).toBeCalled() expect(adapter['_mocha']!.addFile).toBeCalledWith('/foo/bar.test.js') }) test('should properly load mocha hooks', async () => { const adapter = adapterFactory({ rootDir: __dirname, mochaOpts: { require: ['./__fixtures__/mochaHooks.js'] } }) await adapter.init() expect(adapter['_config'].mochaOpts).toEqual({ require: ['./__fixtures__/mochaHooks.js'], rootHooks: { beforeAll: [], beforeEach: [expect.any(Function)], afterAll: [], afterEach: [] } }) }) test('should return amount of errors', async () => { const adapter = adapterFactory({ mochaOpts: { mockFailureCount: 42 } }) await adapter.init() const result = await adapter.run() expect(result).toBe(42) }) test('should throw runtime error if spec is invalid', async () => { const runtimeError = new Error('Uuups') const adapter = adapterFactory({ mochaOpts: { mockRuntimeError: runtimeError } }) await adapter.init() await expect(adapter.run()).rejects.toEqual(runtimeError) }) test('should throw runtime error if spec could not be loaded', async () => { const runtimeError = new Error('Uuups') const adapter = adapterFactory({ mochaOpts: { mockFailureCount: 0 } }) await adapter.init() adapter['_specLoadError'] = runtimeError await expect(adapter.run()).rejects.toEqual(runtimeError) }) test('should pass error to hook if spec could not be loaded', async () => { const runtimeError = new Error('Uuups') const adapter = adapterFactory({ mochaOpts: { mockFailureCount: 0 } }) await adapter.init() adapter['_specLoadError'] = runtimeError await expect(adapter.run()).rejects.toEqual(runtimeError) expect(vi.mocked(executeHooksWithArgs).mock.calls[0][0]).toBe('after') expect(vi.mocked(executeHooksWithArgs).mock.calls[0][2]).toHaveLength(3) expect((vi.mocked(executeHooksWithArgs).mock.calls[0][2] as any)[0]).toBe(runtimeError) }) test('wrapHook if successful', async () => { 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] as any)[0].type).toBe('beforeAll') }) test('wrapHook if failing', async () => { 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).mock.calls[0][2] as any)[0].type) .toBe('beforeAll') expect(vi.mocked(logger('').error).mock.calls[0][0] .startsWith('Error in beforeAll hook: uuuups')).toBe(true) }) describe('prepareMessage', () => { test('should prepare a message', async () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() await adapter.init() await adapter.run() let result = adapter.prepareMessage('beforeSuite') expect(result.type).toBe('beforeSuite') adapter['_runner']!.test = { title: 'foobar', file: '/foo/bar.test.js' } as any result = adapter.prepareMessage('afterTest') expect(result.type).toBe('afterTest') expect(result.title).toBe('foobar') expect(result.file).toBe('/foo/bar.test.js') adapter['_suiteStartDate'] = Date.now() - 5000 result = adapter.prepareMessage('afterSuite') expect(result.type).toBe('afterSuite') expect(result.title).toBe('first suite') expect(result.duration).toBeDefined() expect(result.duration! >= 5000 && result.duration! <= 5020).toBeTruthy() }) test('should prepare a message when suites array is empty', async () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() await adapter.init() await adapter.run() adapter['_runner']!.suite.suites = [] adapter['_suiteStartDate'] = Date.now() - 5000 const result = adapter.prepareMessage('afterSuite') expect(result.type).toBe('afterSuite') expect(result.title).toBeUndefined() expect(result.duration).toBeUndefined() }) }) test('emit does not emit anything on root level', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() // @ts-ignore test invalid params adapter.emit(null, { root: true }) expect(wdioReporter.emit).not.toBeCalled() }) test('emit properly reports to reporter', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() adapter.getUID = () => '123' adapter.emit( 'suite:start', { title: 'foobar' }, new Error('uups') as any ) expect(wdioReporter.emit.mock.calls[0][0]).toBe('suite:start') expect(wdioReporter.emit.mock.calls[0][1].error.message).toBe('uups') expect(wdioReporter.emit.mock.calls[0][1].cid).toBe('0-2') expect(wdioReporter.emit.mock.calls[0][1].uid).toBe('123') }) test('emits "before all"-hook errors as hook:end', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() adapter.getUID = () => '123' adapter.emit( 'test:fail', { title: '"before all" hook' }, new Error('uups') as any ) expect(wdioReporter.emit.mock.calls[0][0]).toBe('hook:end') expect(wdioReporter.emit.mock.calls[0][1].error.message).toBe('uups') }) test('emits "before each"-hook errors as hook:end', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() adapter.getUID = () => '123' adapter.emit( 'test:fail', { title: '"before each" hook' }, new Error('uups') as any ) expect(wdioReporter.emit.mock.calls[0][0]).toBe('hook:end') expect(wdioReporter.emit.mock.calls[0][1].error.message).toBe('uups') }) test('getUID', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() // disabling indent eslint rule for better visibility /* eslint-disable @stylistic/indent */ expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-0-0') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-0-0') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-0-1') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-0-1') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-0-0') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-00-0') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-00-0') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-1-0') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-2-0') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-20-0') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-20-0') expect(adapter.getUID({ type: 'test:start' })).toBe('test-20-0') expect(adapter.getUID({ type: 'test:pass' })).toBe('test-20-0') expect(adapter.getUID({ type: 'test:end' })).toBe('test-20-0') expect(adapter.getUID({ type: 'test:start' })).toBe('test-20-1') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-20-1') expect(adapter.getUID({ type: 'test:end' })).toBe('test-20-1') expect(adapter.getUID({ type: 'test:pending' })).toBe('test-20-2') expect(adapter.getUID({ type: 'test:end' })).toBe('test-20-2') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-20-1') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-20-1') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-2-0') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-10-0') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-10-0') expect(adapter.getUID({ type: 'test:start' })).toBe('test-10-0') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-10-0') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-10-1') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-10-1') expect(adapter.getUID({ type: 'test:start' })).toBe('test-10-1') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-10-1') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-1-0') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-1-1') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-1-1') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-1-2') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-2-1') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-2-1') expect(adapter.getUID({ type: 'hook:start' })).toBe('hook-12-0') expect(adapter.getUID({ type: 'hook:end' })).toBe('hook-12-0') expect(adapter.getUID({ type: 'test:start' })).toBe('test-12-0') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-12-0') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-2-2') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-2-2') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-1-2') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-1-3') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-1-3') expect(adapter.getUID({ type: 'suite:start' })).toBe('suite-1-4') expect(adapter.getUID({ type: 'test:start' })).toBe('test-14-0') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-14-0') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-1-4') expect(adapter.getUID({ type: 'test:start' })).toBe('test-00-0') expect(adapter.getUID({ type: 'test:fail' })).toBe('test-00-0') expect(adapter.getUID({ type: 'suite:end' })).toBe('suite-0-0') expect(() => adapter.getUID({ type: 'test:nonexisting' })).toThrow() /* eslint-enable @stylistic/indent */ }) describe('loadFiles', () => { test('should set _hasTests to true if there are tests to run', async () => { const adapter = adapterFactory({}) adapter['_hasTests'] = false adapter['_mocha']! = { loadFilesAsync: vi.fn(), suite: 1 // mochaRunner.total } as any await adapter._loadFiles({}) expect(adapter['_hasTests']).toBe(true) }) test('should set _hasTests to false if there no tests to run', async () => { const adapter = adapterFactory({}) adapter['_hasTests'] = false adapter['_mocha']! = { loadFilesAsync: vi.fn(), options: { grep: 'regexp foo' }, suite: 0 // mochaRunner.total } as any await adapter._loadFiles({ grep: 'foo', invert: 'invert' as any }) expect(vi.mocked(Mocha.Runner).mock.results[0].value.grep).toBeCalledWith('regexp foo', 'invert') expect(adapter['_hasTests']).toBe(false) }) test('should propagate error', async () => { const adapter = adapterFactory({}) // @ts-ignore test scenario delete adapter['_hasTests'] adapter['_mocha']! = { loadFilesAsync: vi.fn().mockImplementation( () => Promise.reject(new Error('foo')) ), } as any await adapter._loadFiles({}) expect(adapter['_mocha']!.loadFilesAsync).toBeCalled() expect(adapter['_hasTests']).toBe(undefined) expect(adapter['_specLoadError']!.message) .toContain('Unable to load spec files') }) test('should decorate file with esmDecorator', async () => { const adapter = adapterFactory({}) adapter['_mocha']! = { loadFilesAsync: vi.fn() } as any await adapter._loadFiles({}) expect(adapter['_mocha']!.loadFilesAsync).toBeCalled() const loadFilesOptions: any = vi.mocked(adapter['_mocha']!.loadFilesAsync).mock.lastCall?.at(0) expect(loadFilesOptions).toBeDefined() const mockFilePath = '/foo/bar.test.js' const decoratedFilePath = loadFilesOptions.esmDecorator(mockFilePath) expect(decoratedFilePath).toContain(mockFilePath) expect(decoratedFilePath).toContain('?invalidateCache=') }) }) describe('hasTests', () => { test('should return true if feature is not enabled', () => { // @ts-ignore params not needed for test scenario const adapter = adapterFactory() adapter['_hasTests'] = true expect(adapter.hasTests()).toBe(true) adapter['_hasTests'] = false expect(adapter.hasTests()).toBe(false) }) }) afterEach(() => { vi.mocked(Mocha.Runner).mockClear() vi.mocked(wrapGlobalTestMethod).mockReset() vi.mocked(executeHooksWithArgs).mockReset() })