Skip to content

Commit f4b9b5c

Browse files
committed
Backend related unit test cases
1 parent fcf09fa commit f4b9b5c

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { start } from '../src/index.js'
3+
import * as utils from '../src/utils.js'
4+
5+
vi.mock('../src/utils.js', () => ({
6+
getDevtoolsApp: vi.fn()
7+
}))
8+
9+
vi.mock('ws')
10+
11+
describe('backend index', () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks()
14+
})
15+
16+
afterEach(() => {
17+
// Clean up any running servers
18+
})
19+
20+
describe('start', () => {
21+
it('should handle start errors', async () => {
22+
vi.mocked(utils.getDevtoolsApp).mockRejectedValue(
23+
new Error('Package not found')
24+
)
25+
await expect(start()).rejects.toThrow('Package not found')
26+
})
27+
})
28+
29+
describe('API endpoints', () => {
30+
it('should handle test run and stop requests with validation', async () => {
31+
vi.mocked(utils.getDevtoolsApp).mockResolvedValue('/mock/app/path')
32+
const server = await start({ port: 0 })
33+
const { testRunner } = await import('../src/runner.js')
34+
const runSpy = vi.spyOn(testRunner, 'run').mockResolvedValue()
35+
const stopSpy = vi.spyOn(testRunner, 'stop')
36+
37+
// Test invalid payload - missing uid
38+
const invalidResponse = await server?.inject({
39+
method: 'POST',
40+
url: '/api/tests/run',
41+
payload: { entryType: 'test' }
42+
})
43+
expect(invalidResponse?.statusCode).toBe(400)
44+
expect(JSON.parse(invalidResponse?.body || '{}')).toEqual({
45+
error: 'Invalid run payload'
46+
})
47+
expect(runSpy).not.toHaveBeenCalled()
48+
49+
// Test valid run request with all parameters
50+
const runPayload = {
51+
uid: 'test-123',
52+
entryType: 'test',
53+
specFile: '/test.spec.ts'
54+
}
55+
const runResponse = await server?.inject({
56+
method: 'POST',
57+
url: '/api/tests/run',
58+
payload: runPayload
59+
})
60+
expect(runResponse?.statusCode).toBe(200)
61+
expect(JSON.parse(runResponse?.body || '{}')).toEqual({ ok: true })
62+
expect(runSpy).toHaveBeenCalledWith(
63+
expect.objectContaining({
64+
uid: 'test-123',
65+
entryType: 'test',
66+
specFile: '/test.spec.ts',
67+
devtoolsHost: expect.any(String),
68+
devtoolsPort: expect.any(Number)
69+
})
70+
)
71+
72+
// Test stop request
73+
const stopResponse = await server?.inject({
74+
method: 'POST',
75+
url: '/api/tests/stop'
76+
})
77+
expect(stopResponse?.statusCode).toBe(200)
78+
expect(JSON.parse(stopResponse?.body || '{}')).toEqual({ ok: true })
79+
expect(stopSpy).toHaveBeenCalled()
80+
81+
await server?.close()
82+
})
83+
84+
it('should handle test run errors gracefully', async () => {
85+
vi.mocked(utils.getDevtoolsApp).mockResolvedValue('/mock/app/path')
86+
const server = await start({ port: 0 })
87+
const { testRunner } = await import('../src/runner.js')
88+
vi.spyOn(testRunner, 'run').mockRejectedValue(
89+
new Error('Test execution failed')
90+
)
91+
92+
const response = await server?.inject({
93+
method: 'POST',
94+
url: '/api/tests/run',
95+
payload: {
96+
uid: 'test-456',
97+
entryType: 'test',
98+
specFile: '/test.spec.ts'
99+
}
100+
})
101+
102+
expect(response?.statusCode).toBe(500)
103+
expect(JSON.parse(response?.body || '{}')).toEqual({
104+
error: 'Test execution failed'
105+
})
106+
107+
await server?.close()
108+
})
109+
})
110+
})
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { spawn } from 'node:child_process'
3+
import fs from 'node:fs'
4+
import path from 'node:path'
5+
import type { RunnerRequestBody } from '../src/types.js'
6+
7+
vi.mock('node:child_process')
8+
vi.mock('tree-kill')
9+
vi.mock('node:fs', () => ({
10+
default: {
11+
existsSync: vi.fn().mockReturnValue(true),
12+
readFileSync: vi.fn()
13+
}
14+
}))
15+
16+
// Mock the module resolution to prevent resolveWdioBin from failing during import
17+
vi.mock('node:module', () => ({
18+
createRequire: () => ({
19+
resolve: () => '/mock/wdio/cli/index.js'
20+
})
21+
}))
22+
23+
// Now import after mocks are set up
24+
const { testRunner } = await import('../src/runner.js')
25+
26+
describe('TestRunner', () => {
27+
const mockConfigPath = '/test/wdio.conf.ts'
28+
const mockSpecFile = '/test/specs/test.spec.ts'
29+
const mockChild = {
30+
once: vi.fn((event: string, callback: (err?: Error) => void) => {
31+
if (event === 'spawn') {
32+
setTimeout(() => callback(), 0)
33+
}
34+
}),
35+
pid: 12345
36+
} as any
37+
38+
const createMockChild = (spawnCallback = true, errorCallback = false) =>
39+
({
40+
once: vi.fn((event, callback) => {
41+
if (event === 'spawn' && spawnCallback) {
42+
callback()
43+
}
44+
if (event === 'error' && errorCallback) {
45+
callback(new Error('Spawn failed'))
46+
}
47+
}),
48+
pid: 12345
49+
}) as any
50+
51+
beforeEach(() => {
52+
vi.clearAllMocks()
53+
vi.mocked(fs.existsSync).mockReturnValue(true)
54+
process.env.DEVTOOLS_RUNNER_CWD = ''
55+
process.env.DEVTOOLS_WDIO_CONFIG = ''
56+
process.env.DEVTOOLS_WDIO_BIN = ''
57+
})
58+
59+
afterEach(() => {
60+
testRunner.stop()
61+
})
62+
63+
describe('framework filters', () => {
64+
beforeEach(() => {
65+
vi.mocked(spawn).mockReturnValue(mockChild)
66+
})
67+
68+
it('should apply correct filters for cucumber, mocha, and jasmine frameworks', async () => {
69+
const frameworks = [
70+
{ name: 'cucumber', flag: '--cucumberOpts.name' },
71+
{ name: 'mocha', flag: '--mochaOpts.grep' },
72+
{ name: 'jasmine', flag: '--jasmineOpts.grep' }
73+
]
74+
75+
for (let i = 0; i < frameworks.length; i++) {
76+
const { name, flag } = frameworks[i]
77+
await testRunner.run({
78+
uid: `test-${i + 1}`,
79+
entryType: 'test',
80+
framework: name as any,
81+
fullTitle: `${name} test`,
82+
specFile: mockSpecFile,
83+
configFile: mockConfigPath
84+
})
85+
expect(vi.mocked(spawn).mock.calls[i][1]).toEqual(
86+
expect.arrayContaining([flag])
87+
)
88+
testRunner.stop()
89+
}
90+
})
91+
})
92+
93+
describe('run and stop', () => {
94+
it('should prevent concurrent runs and handle environment variables', async () => {
95+
vi.mocked(spawn).mockReturnValue(mockChild)
96+
const payload: RunnerRequestBody = {
97+
uid: 'test-1',
98+
entryType: 'test',
99+
configFile: mockConfigPath,
100+
devtoolsHost: 'localhost',
101+
devtoolsPort: 3000
102+
}
103+
104+
const firstRun = testRunner.run(payload)
105+
await new Promise((resolve) => setTimeout(resolve, 10))
106+
107+
await expect(testRunner.run(payload)).rejects.toThrow(
108+
'A test run is already in progress'
109+
)
110+
111+
const env = vi.mocked(spawn).mock.calls[0][2]?.env as Record<
112+
string,
113+
string
114+
>
115+
expect(env.DEVTOOLS_APP_HOST).toBe('localhost')
116+
expect(env.DEVTOOLS_APP_PORT).toBe('3000')
117+
expect(env.DEVTOOLS_APP_REUSE).toBe('1')
118+
119+
testRunner.stop()
120+
await firstRun.catch(() => {})
121+
})
122+
123+
it('should handle spawn errors', async () => {
124+
const errorChild = createMockChild(false, true)
125+
vi.mocked(spawn).mockReturnValue(errorChild)
126+
127+
await expect(
128+
testRunner.run({
129+
uid: 'test-1',
130+
entryType: 'test',
131+
configFile: mockConfigPath
132+
})
133+
).rejects.toThrow('Spawn failed')
134+
})
135+
})
136+
137+
describe('configuration', () => {
138+
it('should find config and use environment variables', async () => {
139+
const specFile = '/project/test/specs/test.spec.ts'
140+
const configInTestDir = '/project/test/wdio.conf.ts'
141+
const envConfig = '/custom/wdio.conf.ts'
142+
143+
const mockChild = {
144+
once: vi.fn((event, callback) => {
145+
if (event === 'spawn') {
146+
callback()
147+
}
148+
}),
149+
pid: 12345
150+
} as any
151+
152+
vi.mocked(spawn).mockReturnValue(mockChild)
153+
154+
// Test with spec file location
155+
vi.mocked(fs.existsSync).mockImplementation(
156+
(path) => path === configInTestDir
157+
)
158+
await testRunner.run({ uid: 'test-1', entryType: 'test', specFile })
159+
expect(vi.mocked(spawn).mock.calls[0][1]).toContain(configInTestDir)
160+
testRunner.stop()
161+
162+
// Test with env variable
163+
process.env.DEVTOOLS_WDIO_CONFIG = envConfig
164+
vi.mocked(fs.existsSync).mockImplementation((path) => path === envConfig)
165+
await testRunner.run({ uid: 'test-2', entryType: 'test' })
166+
expect(vi.mocked(spawn).mock.calls[1][1]).toContain(envConfig)
167+
testRunner.stop()
168+
})
169+
170+
it('should throw error if config cannot be found', async () => {
171+
vi.mocked(fs.existsSync).mockReturnValue(false)
172+
173+
const payload: RunnerRequestBody = {
174+
uid: 'test-1',
175+
entryType: 'test'
176+
}
177+
178+
const mockChild = {
179+
once: vi.fn(),
180+
pid: 12345
181+
} as any
182+
183+
vi.mocked(spawn).mockReturnValue(mockChild)
184+
185+
await expect(testRunner.run(payload)).rejects.toThrow(
186+
'Cannot locate WDIO config'
187+
)
188+
})
189+
})
190+
191+
describe('spec file normalization', () => {
192+
it('should handle file:// URLs', async () => {
193+
const payload: RunnerRequestBody = {
194+
uid: 'test-1',
195+
entryType: 'test',
196+
specFile: 'file:///project/test.spec.ts',
197+
configFile: mockConfigPath
198+
}
199+
200+
vi.mocked(spawn).mockReturnValue(createMockChild())
201+
await testRunner.run(payload)
202+
203+
const args = vi.mocked(spawn).mock.calls[0][1] as string[]
204+
expect(args.some((arg) => arg.includes('/project/test.spec.ts'))).toBe(
205+
true
206+
)
207+
})
208+
209+
it('should extract spec from callSource', async () => {
210+
vi.mocked(spawn).mockReturnValue(createMockChild())
211+
await testRunner.run({
212+
uid: 'test-1',
213+
entryType: 'test',
214+
callSource: '/project/test.spec.ts:10:5',
215+
configFile: mockConfigPath
216+
})
217+
expect(spawn).toHaveBeenCalled()
218+
})
219+
220+
it('should resolve relative paths', async () => {
221+
vi.mocked(spawn).mockReturnValue(createMockChild())
222+
await testRunner.run({
223+
uid: 'test-1',
224+
entryType: 'test',
225+
specFile: 'test/test.spec.ts',
226+
configFile: mockConfigPath
227+
})
228+
229+
const args = vi.mocked(spawn).mock.calls[0][1] as string[]
230+
expect(
231+
args.some((arg) => arg.startsWith('/') || path.isAbsolute(arg))
232+
).toBe(true)
233+
})
234+
})
235+
236+
describe('line number resolution', () => {
237+
it('should use lineNumber from payload', async () => {
238+
vi.mocked(spawn).mockReturnValue(createMockChild())
239+
await testRunner.run({
240+
uid: 'test-1',
241+
entryType: 'test',
242+
specFile: mockSpecFile,
243+
lineNumber: 42,
244+
configFile: mockConfigPath
245+
})
246+
247+
const args = vi.mocked(spawn).mock.calls[0][1] as string[]
248+
expect(args.some((arg) => arg.includes(':42'))).toBe(true)
249+
})
250+
251+
it('should extract line number from callSource', async () => {
252+
vi.mocked(spawn).mockReturnValue(createMockChild())
253+
await testRunner.run({
254+
uid: 'test-1',
255+
entryType: 'test',
256+
specFile: mockSpecFile,
257+
callSource: '/project/test.spec.ts:25:10',
258+
configFile: mockConfigPath
259+
})
260+
261+
const args = vi.mocked(spawn).mock.calls[0][1] as string[]
262+
expect(args.some((arg) => arg.includes(':25'))).toBe(true)
263+
})
264+
})
265+
266+
describe('runAll mode', () => {
267+
it('should not use spec filter when runAll is true', async () => {
268+
vi.mocked(spawn).mockReturnValue(createMockChild())
269+
await testRunner.run({
270+
uid: 'run-all',
271+
entryType: 'suite',
272+
runAll: true,
273+
configFile: mockConfigPath
274+
})
275+
276+
const args = vi.mocked(spawn).mock.calls[0][1] as string[]
277+
expect(args).not.toContain('--spec')
278+
})
279+
})
280+
})

0 commit comments

Comments
 (0)