Skip to content

Commit fb82fab

Browse files
committed
feat: support windows platform
- Add configuration for nodeExecutable path In the windows environment, The node path can not be detected system level by some node managers (e.g., fnm). So we added this parameter to specify the path of Node executable. - use temporary configuration file when run wdio test on Windows platform Since the interpretation of the `reporters` parameter is not optimized for Windows, the original configuration file should be copied and tested in the Windows environment with the settings for Extension added.
1 parent a2df6fb commit fb82fab

28 files changed

Lines changed: 946 additions & 59 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ For example:
6464

6565
This extension contributes the following settings:
6666

67+
- `webdriverio.nodeExecutable`: The path to the Node.js executable. If not assigned, WebdriverIO try to resolve the node path from environment valuables of `PATH`.
6768
- `webdriverio.configFilePattern`: Glob pattern for WebdriverIO configuration file
6869
- `webdriverio.logLevel`: Set the logLevel
6970
- `webdriverio.showOutput`: Show WebdriverIO output in the test result when set `true` this option

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"configuration": {
5353
"title": "WebdriverIO",
5454
"properties": {
55+
"webdriverio.nodeExecutable": {
56+
"markdownDescription": "The path to the Node.js executable. If not assigned, WebdriverIO just passes down `'node'` to `child_process.spawn`.",
57+
"type": "string"
58+
},
5559
"webdriverio.configFilePattern": {
5660
"type": "array",
5761
"items": {

packages/vscode-wdio-api/src/debug.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as vscode from 'vscode'
55

66
import { TestRunner } from './run.js'
77
import { WdioExtensionWorker } from './worker.js'
8+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
89
import type { TestItemMetadata } from '@vscode-wdio/types/test'
910

1011
let debuggerId = 0
@@ -22,11 +23,15 @@ export class DebugRunner extends TestRunner {
2223
private _runController: AbortController | null = null
2324

2425
constructor(
26+
configManager: ExtensionConfigManagerInterface,
2527
workspaceFolder: vscode.WorkspaceFolder | undefined,
2628
token: vscode.CancellationToken,
2729
workerCwd: string,
28-
worker = new WdioExtensionDebugWorker(`#DEBUGGER${debuggerId++}`, workerCwd, workspaceFolder, token)
30+
_worker?: WdioExtensionDebugWorker
2931
) {
32+
const worker = _worker
33+
? _worker
34+
: new WdioExtensionDebugWorker(configManager, `#DEBUGGER${debuggerId++}`, workerCwd, workspaceFolder, token)
3035
super(worker)
3136

3237
worker.setDebugTerminationCallback(() => {
@@ -75,12 +80,13 @@ export class WdioExtensionDebugWorker extends WdioExtensionWorker {
7580
private _debugTerminationCallback: (() => void) | null = null
7681

7782
constructor(
83+
configManager: ExtensionConfigManagerInterface,
7884
cid: string = '#0',
7985
cwd: string,
8086
private _workspaceFolder: vscode.WorkspaceFolder | undefined,
8187
private _token: vscode.CancellationToken
8288
) {
83-
super(cid, cwd)
89+
super(configManager, cid, cwd)
8490
}
8591

8692
/**

packages/vscode-wdio-api/src/manager.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { dirname, normalize } from 'node:path'
22

33
import { log } from '@vscode-wdio/logger'
4+
import * as vscode from 'vscode'
45

56
import { WdioExtensionWorker } from './worker.js'
67
import type { ServerManagerInterface, WdioExtensionWorkerInterface } from '@vscode-wdio/types/api'
8+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
79

810
export class ServerManager implements ServerManagerInterface {
911
private _serverPool = new Map<string, WdioExtensionWorkerInterface>()
@@ -13,6 +15,28 @@ export class ServerManager implements ServerManagerInterface {
1315
private _operationLock = false
1416
private _operationQueue: (() => Promise<void>)[] = []
1517

18+
constructor(private readonly configManager: ExtensionConfigManagerInterface) {
19+
configManager.on('update:nodeExecutable', async (nodeExecutable: string) => {
20+
log.debug(`Restart worker using webdriverio.nodeExecutable: ${nodeExecutable}`)
21+
const cwds = Array.from(this._serverPool.keys())
22+
await Promise.all(
23+
cwds.map(async (cwd) => {
24+
const worker = this._serverPool.get(cwd)
25+
if (worker) {
26+
await this.stopWorker(cwd, worker)
27+
}
28+
})
29+
)
30+
try {
31+
await this.start(this.configManager.getWdioConfigPaths())
32+
} catch (error) {
33+
const errorMessage = `Failed to restart WebdriverIO worker process: ${error instanceof Error ? error.message : String(error)}`
34+
log.error(errorMessage)
35+
vscode.window.showErrorMessage(errorMessage)
36+
}
37+
})
38+
}
39+
1640
/**
1741
* Start worker process directory by directory which is located the wdio config file.
1842
* @param configPaths path to the configuration file for wdio (e.g. /path/to/wdio.config.js)
@@ -28,7 +52,7 @@ export class ServerManager implements ServerManagerInterface {
2852
})
2953

3054
const workerCwds = Array.from(duplicatedWorkerCwds)
31-
const ids = Array.from({ length: workerCwds.length }, (_, i) => i)
55+
const ids = Array.from({ length: workerCwds.length }, (_, i) => this.latestId + i)
3256
this.latestId = ids[ids.length - 1]
3357

3458
await Promise.all(
@@ -146,35 +170,35 @@ export class ServerManager implements ServerManagerInterface {
146170
}
147171
}
148172

149-
private async startWorker(id: number, configPaths: string): Promise<WdioExtensionWorkerInterface> {
173+
private async startWorker(id: number, workerCwd: string): Promise<WdioExtensionWorkerInterface> {
150174
// Return existing server if already created
151-
const existingServer = this._serverPool.get(configPaths)
175+
const existingServer = this._serverPool.get(workerCwd)
152176
if (existingServer) {
153177
return existingServer
154178
}
155179

156180
// Return pending operation if one is in progress
157-
const pendingOperation = this._pendingOperations.get(`start:${configPaths}`)
181+
const pendingOperation = this._pendingOperations.get(`start:${workerCwd}`)
158182
if (pendingOperation) {
159183
return pendingOperation as Promise<WdioExtensionWorkerInterface>
160184
}
161185

162186
// Start a new process and track it
163-
const serverPromise = this.createWorker(id, configPaths)
164-
this._pendingOperations.set(`start:${configPaths}`, serverPromise)
187+
const serverPromise = this.createWorker(id, workerCwd)
188+
this._pendingOperations.set(`start:${workerCwd}`, serverPromise)
165189

166190
try {
167191
const server = await serverPromise
168192
return server
169193
} finally {
170194
// Remove from pending list when completed
171-
this._pendingOperations.delete(`start:${configPaths}`)
195+
this._pendingOperations.delete(`start:${workerCwd}`)
172196
}
173197
}
174198

175199
private async createWorker(id: number, configPaths: string): Promise<WdioExtensionWorker> {
176200
const strId = `#${String(id)}`
177-
const server = new WdioExtensionWorker(strId, configPaths)
201+
const server = new WdioExtensionWorker(this.configManager, strId, configPaths)
178202
await server.start()
179203
await server.waitForStart()
180204
log.debug(`[server manager] server was registered: ${configPaths}`)

packages/vscode-wdio-api/src/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import * as fs from 'node:fs/promises'
2+
13
import { LOG_LEVEL, TEST_ID_SEPARATOR } from '@vscode-wdio/constants'
24
import { log } from '@vscode-wdio/logger'
5+
import which from 'which'
36
import { WebSocketServer } from 'ws'
47

58
import type { Server } from 'node:http'
9+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
610
import type { TestItemMetadataWithRepository } from '@vscode-wdio/types/test'
711
import type { NumericLogLevel } from '@vscode-wdio/types/utils'
812
import type * as vscode from 'vscode'
@@ -90,3 +94,33 @@ export function getCucumberSpec(testItem: vscode.TestItem, metadata: TestItemMet
9094
}
9195
return baseSpec
9296
}
97+
98+
export async function resolveNodePath(configManager: ExtensionConfigManagerInterface) {
99+
log.debug('Resolving the Node executable path')
100+
const configuredPath = configManager.globalConfig.nodeExecutable
101+
if (configuredPath && (await checkExistence(configuredPath))) {
102+
log.debug(`Resolved executable path: ${configuredPath}`)
103+
return configuredPath
104+
}
105+
106+
const foundPath = await which('node', { nothrow: true })
107+
if (foundPath && (await checkExistence(foundPath))) {
108+
log.debug(`Resolved executable path: ${foundPath}`)
109+
return foundPath
110+
}
111+
const msg = `Unable to find 'node' executable.\nMake sure to have Node.js installed and available in your PATH.\nCurrent PATH: '${process.env.PATH}'.`
112+
log.error(msg)
113+
throw new Error(msg)
114+
}
115+
116+
async function checkExistence(targetPath: string) {
117+
log.debug(`Access check: ${targetPath}`)
118+
try {
119+
await fs.access(targetPath, fs.constants.X_OK)
120+
return true
121+
} catch (error) {
122+
const msg = error instanceof Error ? error.message : String(error)
123+
log.debug(`Access check was failed: ${msg}`)
124+
return false
125+
}
126+
}

packages/vscode-wdio-api/src/worker.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import { log } from '@vscode-wdio/logger'
88
import { createBirpc } from 'birpc'
99
import getPort from 'get-port'
1010

11-
import { createWss, loggingFn } from './utils.js'
11+
import { createWss, loggingFn, resolveNodePath } from './utils.js'
1212

1313
import type { ExtensionApi, WdioExtensionWorkerInterface, WorkerApi } from '@vscode-wdio/types/api'
14+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
1415
import type * as vscode from 'vscode'
1516
import type { WebSocketServer } from 'ws'
1617

1718
const WORKER_PATH = resolve(__dirname, 'worker.cjs')
1819
export class WdioExtensionWorker extends EventEmitter implements WdioExtensionWorkerInterface {
20+
protected configManager: ExtensionConfigManagerInterface
1921
public cid: string
2022
protected cwd: string
2123
protected disposables: vscode.Disposable[] = []
@@ -26,10 +28,11 @@ export class WdioExtensionWorker extends EventEmitter implements WdioExtensionWo
2628
private _server: Server | null = null
2729
private _wss: WebSocketServer | null = null
2830

29-
constructor(cid: string = '#0', cwd: string) {
31+
constructor(configManager: ExtensionConfigManagerInterface, cid: string = '#0', cwd: string) {
3032
super()
3133
this.cid = cid
3234
this.cwd = cwd
35+
this.configManager = configManager
3336

3437
const psListener = () => {
3538
if (this._workerProcess && !this._workerProcess.killed) {
@@ -73,6 +76,7 @@ export class WdioExtensionWorker extends EventEmitter implements WdioExtensionWo
7376
return
7477
}
7578
try {
79+
const nodeExecutable = await resolveNodePath(this.configManager)
7680
// Find available port for WebSocket communication
7781
// Store server instances for proper cleanup
7882
const wsUrl = await this.getServer()
@@ -86,7 +90,7 @@ export class WdioExtensionWorker extends EventEmitter implements WdioExtensionWo
8690
}
8791

8892
// Start worker process
89-
this._workerProcess = spawn('node', [WORKER_PATH], {
93+
this._workerProcess = spawn(nodeExecutable, [WORKER_PATH], {
9094
cwd: this.cwd,
9195
env,
9296
stdio: 'pipe',

packages/vscode-wdio-api/tests/debug.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { DebugRunner, DebugSessionTerminatedError, WdioExtensionDebugWorker } fr
77
import * as runModule from '../src/run.js'
88
import * as workerModule from '../src/worker.js'
99

10+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
11+
1012
// Mock VSCode
1113
vi.mock('vscode', async () => {
1214
const mockModule = await import('../../../tests/__mocks__/vscode.cjs')
@@ -24,6 +26,8 @@ vi.mock('vscode', async () => {
2426
// Mock logger
2527
vi.mock('@vscode-wdio/logger', () => import('../../../tests/__mocks__/logger.js'))
2628

29+
const mockConfigManager = {} as unknown as ExtensionConfigManagerInterface
30+
2731
describe('DebugRunner', () => {
2832
let workspaceFolder: vscode.WorkspaceFolder
2933
let debugRunner: DebugRunner
@@ -73,7 +77,7 @@ describe('DebugRunner', () => {
7377
})
7478

7579
// Create debug runner instance
76-
debugRunner = new DebugRunner(workspaceFolder, mockToken, '/path/to/worker/cwd', mockWorker)
80+
debugRunner = new DebugRunner(mockConfigManager, workspaceFolder, mockToken, '/path/to/worker/cwd', mockWorker)
7781
})
7882

7983
afterEach(() => {
@@ -215,7 +219,13 @@ describe('WdioExtensionDebugWorker', () => {
215219
vi.spyOn(workerModule.WdioExtensionWorker.prototype, 'stop').mockResolvedValue(undefined)
216220

217221
// Create worker instance
218-
debugWorker = new WdioExtensionDebugWorker('#DEBUGGER1', '/path/to', workspaceFolder, mockToken)
222+
debugWorker = new WdioExtensionDebugWorker(
223+
mockConfigManager,
224+
'#DEBUGGER1',
225+
'/path/to',
226+
workspaceFolder,
227+
mockToken
228+
)
219229

220230
// Mock getServer method
221231
vi.spyOn(debugWorker as any, 'getServer').mockResolvedValue('ws://localhost:1234')

packages/vscode-wdio-api/tests/manager.test.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
44

55
import { ServerManager } from '../src/manager.js'
66
import { WdioExtensionWorker } from '../src/worker.js'
7+
import type { ExtensionConfigManagerInterface } from '@vscode-wdio/types/config'
8+
9+
vi.mock('vscode', () => import('../../../tests/__mocks__/vscode.cjs'))
710

811
// Mock the worker.js module
912
vi.mock('../src/worker.js', () => {
10-
const WdioExtensionWorker = vi.fn(function (cid, configPath) {
13+
const WdioExtensionWorker = vi.fn(function (_configManager, cid, configPath) {
1114
// @ts-ignore
1215
this.cid = cid
1316
// @ts-ignore
@@ -22,13 +25,26 @@ vi.mock('../src/worker.js', () => {
2225
// Mock the logger module
2326
vi.mock('@vscode-wdio/logger', () => import('../../../tests/__mocks__/logger.js'))
2427

28+
vi.mock('../src/utils.js', async (importActual) => {
29+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
30+
const actual = await importActual<typeof import('../src/utils.js')>()
31+
return {
32+
...actual,
33+
resolveNodePath: vi.fn(),
34+
}
35+
})
36+
37+
const mockConfigManager = {
38+
on: vi.fn(),
39+
} as unknown as ExtensionConfigManagerInterface
40+
2541
describe('ServerManager', () => {
2642
let serverManager: ServerManager
2743

2844
// Create a fresh instance of ServerManager before each test
2945
beforeEach(() => {
3046
vi.resetAllMocks()
31-
serverManager = new ServerManager()
47+
serverManager = new ServerManager(mockConfigManager)
3248
})
3349

3450
afterEach(() => {
@@ -45,8 +61,8 @@ describe('ServerManager', () => {
4561

4662
// Assert
4763
expect(WdioExtensionWorker).toHaveBeenCalledTimes(2)
48-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#0', normalize('/path/to'))
49-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#1', normalize('/another/path'))
64+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#0', normalize('/path/to'))
65+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', normalize('/another/path'))
5066

5167
// Check that start was called on each worker
5268
expect(vi.mocked(WdioExtensionWorker).mock.instances.length).toBe(2)
@@ -90,7 +106,7 @@ describe('ServerManager', () => {
90106

91107
// Assert
92108
expect(WdioExtensionWorker).toHaveBeenCalledTimes(1)
93-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#1', wdioDirName)
109+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', wdioDirName)
94110
expect(result).toBeDefined()
95111
expect(result.cid).toBe('#1')
96112
})
@@ -104,8 +120,8 @@ describe('ServerManager', () => {
104120

105121
// Assert
106122
expect(WdioExtensionWorker).toHaveBeenCalledTimes(2)
107-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#1', '/path/to')
108-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#2', '/another/path')
123+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', '/path/to')
124+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#2', '/another/path')
109125
})
110126
})
111127

@@ -149,7 +165,7 @@ describe('ServerManager', () => {
149165

150166
// Should create one new worker
151167
expect(WdioExtensionWorker).toHaveBeenCalledTimes(1)
152-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#2', normalize('/new/path'))
168+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#2', normalize('/new/path'))
153169
})
154170
})
155171

@@ -235,7 +251,7 @@ describe('ServerManager', () => {
235251

236252
// Assert
237253
expect(WdioExtensionWorker).toHaveBeenCalledTimes(1)
238-
expect(WdioExtensionWorker).toHaveBeenCalledWith('#42', configPath)
254+
expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#42', configPath)
239255
expect(result).toBeDefined()
240256
expect(result.start).toHaveBeenCalledTimes(1)
241257
expect(result.waitForStart).toHaveBeenCalledTimes(1)
@@ -256,7 +272,7 @@ describe('ServerManager', () => {
256272
createWorkerCalls++
257273
// Mock delay to ensure operations overlap
258274
await new Promise((resolve) => setTimeout(resolve, 50))
259-
return new WdioExtensionWorker(`#${id}`, path)
275+
return new WdioExtensionWorker(mockConfigManager, `#${id}`, path)
260276
}) as any)
261277

262278
// Execute two concurrent calls with same path

0 commit comments

Comments
 (0)