diff --git a/e2e/assertions/index.ts b/e2e/assertions/index.ts index 8d9d398..27fcb34 100644 --- a/e2e/assertions/index.ts +++ b/e2e/assertions/index.ts @@ -1,6 +1,6 @@ import type { MatcherContext } from 'expect' import type { BottomBarPanel, TreeItem, Workbench } from 'wdio-vscode-service' -import type { STATUS } from '../helpers/index.ts' +import type { STATUS } from '../helpers/index.js' export interface ExpectedTreeItem { text: string diff --git a/e2e/helpers/constants.ts b/e2e/helpers/constants.ts index 11c279a..7501858 100644 --- a/e2e/helpers/constants.ts +++ b/e2e/helpers/constants.ts @@ -1,5 +1,11 @@ -import * as cucumber from './cucumber.ts' -import { STATUS } from './index.ts' +import * as cucumber from './cucumber.js' + +export const STATUS = { + NOT_YET_RUN: 'Not yet run', + PASSED: 'Passed', + FAILED: 'Failed', + SKIPPED: 'Skipped', +} as const function createExpectedNotRun(targetFramework: 'mocha' | 'jasmine') { return { diff --git a/e2e/helpers/cucumber.ts b/e2e/helpers/cucumber.ts index 754d300..4221844 100644 --- a/e2e/helpers/cucumber.ts +++ b/e2e/helpers/cucumber.ts @@ -1,4 +1,4 @@ -import { STATUS } from './index.ts' +import { STATUS } from './constants.js' export function createExpectedNotRun() { return { diff --git a/e2e/helpers/index.ts b/e2e/helpers/index.ts index 1fbb62e..8743b29 100644 --- a/e2e/helpers/index.ts +++ b/e2e/helpers/index.ts @@ -2,12 +2,7 @@ import { DefaultTreeSection } from 'wdio-vscode-service' import type { StatusStrings } from 'assertions/index.ts' import type { TreeItem, Workbench, ViewControl, ViewContent, ViewItemAction, ViewTitlePart } from 'wdio-vscode-service' -export const STATUS = { - NOT_YET_RUN: 'Not yet run', - PASSED: 'Passed', - FAILED: 'Failed', - SKIPPED: 'Skipped', -} as const +export { STATUS } from './constants.js' export async function openTestingView(workbench: Workbench) { const activityBar = workbench.getActivityBar() diff --git a/e2e/tests/basic.spec.ts b/e2e/tests/basic.spec.ts index ab958fc..657749f 100644 --- a/e2e/tests/basic.spec.ts +++ b/e2e/tests/basic.spec.ts @@ -1,5 +1,5 @@ import { browser, expect } from '@wdio/globals' -import { createExpected } from 'helpers/constants.ts' +import { createExpected } from 'helpers/constants.js' import { STATUS, @@ -11,7 +11,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, ViewControl, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/basicCucumber.spec.ts b/e2e/tests/basicCucumber.spec.ts index 60e5227..fba5837 100644 --- a/e2e/tests/basicCucumber.spec.ts +++ b/e2e/tests/basicCucumber.spec.ts @@ -1,6 +1,6 @@ import { browser, expect } from '@wdio/globals' -import { createCucumberExpected } from '../helpers/cucumber.ts' +import { createCucumberExpected } from '../helpers/cucumber.js' import { STATUS, clearAllTestResults, @@ -11,7 +11,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, ViewControl, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/basicWorkspace.spec.ts b/e2e/tests/basicWorkspace.spec.ts index d92db92..768ccc4 100644 --- a/e2e/tests/basicWorkspace.spec.ts +++ b/e2e/tests/basicWorkspace.spec.ts @@ -1,5 +1,5 @@ import { browser, expect } from '@wdio/globals' -import { createWorkspaceExpected } from 'helpers/constants.ts' +import { createWorkspaceExpected } from 'helpers/constants.js' import { STATUS, @@ -11,7 +11,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, ViewControl, Workbench } from 'wdio-vscode-service' const expected = createWorkspaceExpected() diff --git a/e2e/tests/envEnable.spec.ts b/e2e/tests/envEnable.spec.ts index ca6bc68..f51f8e2 100644 --- a/e2e/tests/envEnable.spec.ts +++ b/e2e/tests/envEnable.spec.ts @@ -14,7 +14,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/updateConfig.spec.ts b/e2e/tests/updateConfig.spec.ts index 027372a..ac6ec69 100644 --- a/e2e/tests/updateConfig.spec.ts +++ b/e2e/tests/updateConfig.spec.ts @@ -13,7 +13,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/updateErrorConfig.spec.ts b/e2e/tests/updateErrorConfig.spec.ts index 891f24c..943b307 100644 --- a/e2e/tests/updateErrorConfig.spec.ts +++ b/e2e/tests/updateErrorConfig.spec.ts @@ -13,7 +13,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/updateErrorSpec.spec.ts b/e2e/tests/updateErrorSpec.spec.ts index 0308c0c..fb5bab2 100644 --- a/e2e/tests/updateErrorSpec.spec.ts +++ b/e2e/tests/updateErrorSpec.spec.ts @@ -13,7 +13,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/updateSettings.spec.ts b/e2e/tests/updateSettings.spec.ts new file mode 100644 index 0000000..a663a94 --- /dev/null +++ b/e2e/tests/updateSettings.spec.ts @@ -0,0 +1,162 @@ +import { browser, expect } from '@wdio/globals' +import { sleep } from 'wdio-vscode-service' + +import { + STATUS, + clearAllTestResults, + clickTreeItemButton, + collapseAllTests, + getTestingSection, + openTestingView, + waitForResolved, + waitForTestStatus, +} from '../helpers/index.js' + +import type { SideBarView, TextEditor, Workbench } from 'wdio-vscode-service' + +describe('VS Code Extension Testing (Update config)', function () { + let workbench: Workbench + let sideBarView: SideBarView + let orgSettings: string + + before(async function () { + workbench = await browser.getWorkbench() + const tab = await getSettingTextEditor(workbench) + orgSettings = await tab.getText() + await workbench.getEditorView().closeAllEditors() + }) + + beforeEach(async function () { + workbench = await browser.getWorkbench() + await openTestingView(workbench) + sideBarView = workbench.getSideBar() + + const testingSection = await getTestingSection(sideBarView.getContent()) + await collapseAllTests(testingSection) + + await browser.waitUntil(async () => (await testingSection.getVisibleItems()).length === 1) + }) + + afterEach(async function () { + await clearAllTestResults(workbench) + }) + + after(async function () { + const tab = await getSettingTextEditor(workbench) + await tab.clearText() + await tab.setText(JSON.stringify(JSON.parse(orgSettings), null, 2)) + await tab.save() + + await workbench.getEditorView().closeAllEditors() + }) + + it('should be resolved the defined tests after settings changed', async function () { + const testingSection = await getTestingSection(sideBarView.getContent()) + const items = await testingSection.getVisibleItems() + + await waitForResolved(browser, items[0]) + + await expect(items).toMatchTreeStructure([ + { + text: 'wdio.conf.ts', + status: STATUS.NOT_YET_RUN, + children: [ + { + text: 'before.spec.ts', + status: STATUS.NOT_YET_RUN, + children: [ + { + text: 'Before Tests', + status: STATUS.NOT_YET_RUN, + children: [{ text: 'TEST BEFORE 1', status: STATUS.NOT_YET_RUN }], + }, + ], + }, + { + text: 'sample.spec.ts', + status: STATUS.NOT_YET_RUN, + children: [ + { + text: 'Sample 1', + status: STATUS.NOT_YET_RUN, + children: [{ text: 'TEST SAMPLE 1', status: STATUS.NOT_YET_RUN }], + }, + ], + }, + ], + }, + ]) + + // Emulate the changing configuration + const settings = JSON.parse(orgSettings) + settings['webdriverio.configFilePattern'] = ['**/webdriverio.conf.ts'] + + const tab = await getSettingTextEditor(workbench) + await tab.clearText() + await tab.setText(JSON.stringify(settings, null, 2)) + await tab.save() + + await workbench.getEditorView().closeAllEditors() + await sleep(1500) + + await waitForResolved(browser, items[0]) + + await expect(items).toMatchTreeStructure([ + { + text: 'webdriverio.conf.ts', + status: STATUS.NOT_YET_RUN, + children: [ + { + text: 'after.test.ts', + status: STATUS.NOT_YET_RUN, + children: [ + { + text: 'After Tests', + status: STATUS.NOT_YET_RUN, + children: [{ text: 'TEST AFTER 1', status: STATUS.NOT_YET_RUN }], + }, + ], + }, + ], + }, + ]) + }) + + it('should run tests successfully after changing the settings', async function () { + const testingSection = await getTestingSection(sideBarView.getContent()) + const items = await testingSection.getVisibleItems() + + await waitForResolved(browser, items[0]) + + await clickTreeItemButton(browser, items[0], 'Run Test') + + await waitForTestStatus(browser, items[0], STATUS.PASSED) + + await expect(items).toMatchTreeStructure([ + { + text: 'webdriverio.conf.ts', + status: STATUS.PASSED, + children: [ + { + text: 'after.test.ts', + status: STATUS.PASSED, + children: [ + { + text: 'After Tests', + status: STATUS.PASSED, + children: [{ text: 'TEST AFTER 1', status: STATUS.PASSED }], + }, + ], + }, + ], + }, + ]) + }) +}) + +async function getSettingTextEditor(workbench: Workbench) { + await workbench.executeCommand('Preferences: Open User Settings (JSON)') + await sleep(1500) + const editorView = workbench.getEditorView() + return (await editorView.openEditor('settings.json')) as TextEditor +} diff --git a/e2e/tests/updateSpec.spec.ts b/e2e/tests/updateSpec.spec.ts index 6e2be7e..3c4defe 100644 --- a/e2e/tests/updateSpec.spec.ts +++ b/e2e/tests/updateSpec.spec.ts @@ -13,7 +13,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, Workbench } from 'wdio-vscode-service' diff --git a/e2e/tests/workerIdleTimeout.spec.ts b/e2e/tests/workerIdleTimeout.spec.ts index 267a10f..db1dd86 100644 --- a/e2e/tests/workerIdleTimeout.spec.ts +++ b/e2e/tests/workerIdleTimeout.spec.ts @@ -1,6 +1,6 @@ import { browser, expect } from '@wdio/globals' -import { createCucumberExpected } from '../helpers/cucumber.ts' +import { createCucumberExpected } from '../helpers/cucumber.js' import { STATUS, clearAllTestResults, @@ -10,7 +10,7 @@ import { openTestingView, waitForResolved, waitForTestStatus, -} from '../helpers/index.ts' +} from '../helpers/index.js' import type { SideBarView, ViewControl, Workbench } from 'wdio-vscode-service' @@ -56,7 +56,7 @@ describe(`VS Code Extension Testing with ${targetFramework}`, function () { it('should shutdown the work process after idle timeout was reached', async function () { await new Promise((resolve) => setTimeout(resolve, 2000)) - await expect(workbench).hasExpectedLog(/Worker#0 process shutdown gracefully/) + await expect(workbench).hasExpectedLog(/Worker#1 process shutdown gracefully/) const bottomBar = workbench.getBottomBar() const outputView = await bottomBar.openOutputView() @@ -75,7 +75,7 @@ describe(`VS Code Extension Testing with ${targetFramework}`, function () { await waitForTestStatus(browser, items[0], STATUS.PASSED) // assert that start work process - await expect(workbench).hasExpectedLog(/\[#1\] Worker process started successfully/) + await expect(workbench).hasExpectedLog(/\[#2\] Worker process started successfully/) // assert that run test successfully await expect(items).toMatchTreeStructure(expected.runAll) diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 026249a..764885e 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -6,12 +6,12 @@ "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework", "wdio-vscode-service"], "skipLibCheck": true, "noEmit": true, - "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, + "experimentalDecorators": true, "noFallthroughCasesInSwitch": true } } diff --git a/e2e/wdioSmoke.conf.ts b/e2e/wdioSmoke.conf.ts index a1633ec..6b77819 100644 --- a/e2e/wdioSmoke.conf.ts +++ b/e2e/wdioSmoke.conf.ts @@ -1,4 +1,4 @@ -import { createBaseConfig } from './wdio.conf.ts' +import { createBaseConfig } from './wdio.conf.js' type TestTargets = 'config' | 'timeout' | 'env' @@ -13,6 +13,7 @@ function defineSmokePrams(target: TestTargets) { './tests/updateSpec.spec.ts', './tests/updateErrorSpec.spec.ts', './tests/updateErrorConfig.spec.ts', + './tests/updateSettings.spec.ts', ], workspace: '../samples/smoke/update-config', settings: {}, diff --git a/eslint.config.mjs b/eslint.config.mjs index 75aa2de..b70177e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -91,4 +91,13 @@ export default wdioEslint.config([ files: ['e2e/**/*.spec.ts'], ...mochaPlugin.configs.recommended, }, + { + /** + * for extension of wdio-vscode-service + */ + files: ['e2e/pageobjects/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-declaration-merging': 'off', + }, + }, ]) diff --git a/packages/vscode-wdio-server/src/manager.ts b/packages/vscode-wdio-server/src/manager.ts index 2af26f8..768faa2 100644 --- a/packages/vscode-wdio-server/src/manager.ts +++ b/packages/vscode-wdio-server/src/manager.ts @@ -1,11 +1,10 @@ import { dirname, normalize } from 'node:path' import { log } from '@vscode-wdio/logger' -import * as vscode from 'vscode' -import { WdioExtensionWorker } from './worker.js' +import { WdioExtensionWorkerFactory } from './worker.js' import type { IExtensionConfigManager } from '@vscode-wdio/types/config' -import type { IWorkerManager, IWdioExtensionWorker } from '@vscode-wdio/types/server' +import type { IWorkerManager, IWdioExtensionWorker, IWdioExtensionWorkerFactory } from '@vscode-wdio/types/server' export class WdioWorkerManager implements IWorkerManager { private _workerPool = new Map() @@ -15,9 +14,12 @@ export class WdioWorkerManager implements IWorkerManager { private _operationLock = false private _operationQueue: (() => Promise)[] = [] - constructor(private readonly configManager: IExtensionConfigManager) { + constructor( + configManager: IExtensionConfigManager, + private workerFactory: IWdioExtensionWorkerFactory = new WdioExtensionWorkerFactory(configManager) + ) { configManager.on('update:nodeExecutable', async (nodeExecutable: string | undefined) => { - log.debug(`Restart worker using webdriverio.nodeExecutable: ${nodeExecutable}`) + log.debug(`Stop all worker using webdriverio.nodeExecutable: ${nodeExecutable}`) const cwds = Array.from(this._workerPool.keys()) await Promise.all( cwds.map(async (cwd) => { @@ -27,13 +29,6 @@ export class WdioWorkerManager implements IWorkerManager { } }) ) - try { - await this.start(this.configManager.getWdioConfigPaths()) - } catch (error) { - const errorMessage = `Failed to restart WebdriverIO worker process: ${error instanceof Error ? error.message : String(error)}` - log.error(errorMessage) - vscode.window.showErrorMessage(errorMessage) - } }) // Listen for worker idle timeout configuration changes @@ -43,32 +38,6 @@ export class WdioWorkerManager implements IWorkerManager { }) } - /** - * Start worker process directory by directory which is located the wdio config file. - * @param configPaths path to the configuration file for wdio (e.g. /path/to/wdio.config.js) - */ - public async start(configPaths: string[]) { - // Add to queue and then execute the process - return this.queueOperation(async () => { - const duplicatedWorkerCwds = new Set() - configPaths.forEach((configPath) => { - const normalizedConfigPath = normalize(configPath) - const wdioDirName = dirname(normalizedConfigPath) - duplicatedWorkerCwds.add(wdioDirName) - }) - - const workerCwds = Array.from(duplicatedWorkerCwds) - const ids = Array.from({ length: workerCwds.length }, (_, i) => this.latestId + i) - this.latestId = ids[ids.length - 1] - - await Promise.all( - workerCwds.map(async (workerCwd, index) => { - await this.startWorker(ids[index], workerCwd) - }) - ) - }) - } - /** * * @param configPaths path to the configuration file for wdio (e.g. /path/to/wdio.config.js) @@ -84,7 +53,7 @@ export class WdioWorkerManager implements IWorkerManager { return server } this.latestId++ - return this.startWorker(this.latestId, dirname(configPaths)) + return this.startWorker(this.latestId, dirname(normalizedConfigPath)) }) } @@ -160,12 +129,11 @@ export class WdioWorkerManager implements IWorkerManager { */ private async updateWorkerIdleTimeout(worker: IWdioExtensionWorker, idleTimeout: number): Promise { try { - worker.idleMonitor.updateTimeout(idleTimeout) + worker.updateIdleTimeout(idleTimeout) log.debug(`[server manager] successfully updated idle timeout for worker ${worker.cid}`) } catch (error) { const errorMessage = `Failed to update idle timeout for worker ${worker.cid}: ${error instanceof Error ? error.message : String(error)}` log.error(errorMessage) - // Don't throw error to prevent stopping other workers from being updated } } @@ -175,14 +143,15 @@ export class WdioWorkerManager implements IWorkerManager { * @param workerCwd Worker's current working directory */ public async handleWorkerIdleTimeout(workerCwd: string): Promise { - log.debug(`[server manager] received idle timeout notification for worker: ${workerCwd}`) + const normalizedConfigPath = normalize(workerCwd) + log.debug(`[server manager] received idle timeout notification for worker: ${normalizedConfigPath}`) - const worker = this._workerPool.get(workerCwd) + const worker = this._workerPool.get(normalizedConfigPath) if (worker) { - await this.stopWorker(workerCwd, worker) - log.debug(`[server manager] worker stopped due to idle timeout: ${workerCwd}`) + await this.stopWorker(normalizedConfigPath, worker) + log.debug(`[server manager] worker stopped due to idle timeout: ${normalizedConfigPath}`) } else { - log.warn(`[server manager] received idle timeout for unknown worker: ${workerCwd}`) + log.warn(`[server manager] received idle timeout for unknown worker: ${normalizedConfigPath}`) } } @@ -255,9 +224,9 @@ export class WdioWorkerManager implements IWorkerManager { } } - private async createWorker(id: number, configPaths: string): Promise { + private async createWorker(id: number, configPaths: string): Promise { const strId = `#${String(id)}` - const worker = new WdioExtensionWorker(this.configManager, strId, configPaths) + const worker = this.workerFactory.generate(strId, configPaths) // Set up idle timeout notification handler worker.on('idleTimeout', () => { @@ -267,16 +236,6 @@ export class WdioWorkerManager implements IWorkerManager { await worker.start() await worker.waitForStart() - // Send initial idle timeout configuration - const idleTimeout = this.configManager.globalConfig.workerIdleTimeout - if ( - idleTimeout !== undefined && - 'updateIdleTimeout' in worker && - typeof worker.updateIdleTimeout === 'function' - ) { - worker.updateIdleTimeout(idleTimeout) - } - log.debug(`[server manager] server was registered: ${configPaths}`) this._workerPool.set(configPaths, worker) return worker diff --git a/packages/vscode-wdio-server/src/run.ts b/packages/vscode-wdio-server/src/run.ts index 75c5994..aa093d4 100644 --- a/packages/vscode-wdio-server/src/run.ts +++ b/packages/vscode-wdio-server/src/run.ts @@ -23,7 +23,7 @@ export class TestRunner implements vscode.Disposable { protected workspaceFolder: vscode.WorkspaceFolder, protected worker: IWdioExtensionWorker ) { - worker.idleMonitor.pauseTimer() + worker.pauseIdleTimer() } public get stdout() { @@ -165,6 +165,6 @@ export class TestRunner implements vscode.Disposable { } async dispose() { - this.worker.idleMonitor.resumeTimer() + this.worker.resumeIdleTimer() } } diff --git a/packages/vscode-wdio-server/src/worker.ts b/packages/vscode-wdio-server/src/worker.ts index 107efa4..79a31f6 100644 --- a/packages/vscode-wdio-server/src/worker.ts +++ b/packages/vscode-wdio-server/src/worker.ts @@ -18,17 +18,26 @@ import type { IWdioExtensionWorker, WorkerApi, IWorkerIdleMonitor, + IWdioExtensionWorkerFactory, } from '@vscode-wdio/types/server' import type * as vscode from 'vscode' import type { WebSocketServer } from 'ws' const WORKER_PATH = resolve(__dirname, 'worker.cjs') +export class WdioExtensionWorkerFactory implements IWdioExtensionWorkerFactory { + constructor(private configManager: IExtensionConfigManager) {} + + generate(id: string, cwd: string): IWdioExtensionWorker { + return new WdioExtensionWorker(this.configManager, id, cwd) + } +} + export class WdioExtensionWorker extends TypedEventEmitter implements IWdioExtensionWorker { protected configManager: IExtensionConfigManager public cid: string protected cwd: string - public idleMonitor: IWorkerIdleMonitor + protected idleMonitor: IWorkerIdleMonitor protected disposables: vscode.Disposable[] = [] private _workerProcess: ChildProcess | null = null private _workerRpc: WorkerApi | null = null @@ -345,6 +354,12 @@ export class WdioExtensionWorker extends TypedEventEmitter { diff --git a/packages/vscode-wdio-server/tests/debug.test.ts b/packages/vscode-wdio-server/tests/debug.test.ts index 61f4435..8df6e3e 100644 --- a/packages/vscode-wdio-server/tests/debug.test.ts +++ b/packages/vscode-wdio-server/tests/debug.test.ts @@ -68,6 +68,7 @@ describe('DebugRunner', () => { pauseTimer: vi.fn(), resumeTimer: vi.fn(), }, + pauseIdleTimer: vi.fn(), } as unknown as WdioExtensionDebugWorker mockWorkerResult = { diff --git a/packages/vscode-wdio-server/tests/manager.test.ts b/packages/vscode-wdio-server/tests/manager.test.ts index e083f3e..69f1a30 100644 --- a/packages/vscode-wdio-server/tests/manager.test.ts +++ b/packages/vscode-wdio-server/tests/manager.test.ts @@ -3,26 +3,10 @@ import { dirname, join, normalize } from 'node:path' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { WdioWorkerManager } from '../src/manager.js' -import { WdioExtensionWorker } from '../src/worker.js' -import type { IExtensionConfigManager } from '@vscode-wdio/types/config' +import type { IExtensionConfigManager, IWdioExtensionWorker, IWdioExtensionWorkerFactory } from '@vscode-wdio/types' vi.mock('vscode', () => import('../../../tests/__mocks__/vscode.cjs')) -// Mock the worker.js module -vi.mock('../src/worker.js', () => { - const WdioExtensionWorker = vi.fn(function (_configManager, cid, configPath) { - // @ts-ignore - this.cid = cid - // @ts-ignore - this.configPath = configPath - }) - WdioExtensionWorker.prototype.start = vi.fn().mockResolvedValue(undefined) - WdioExtensionWorker.prototype.waitForStart = vi.fn().mockResolvedValue(undefined) - WdioExtensionWorker.prototype.stop = vi.fn().mockResolvedValue(undefined) - WdioExtensionWorker.prototype.on = vi.fn() - return { WdioExtensionWorker } -}) - // Mock the logger module vi.mock('@vscode-wdio/logger', () => import('../../../tests/__mocks__/logger.js')) @@ -35,51 +19,56 @@ vi.mock('../src/utils.js', async (importActual) => { } }) -const mockConfigManager = { - globalConfig: { - workerIdleTimeout: 600, - }, - on: vi.fn(), -} as unknown as IExtensionConfigManager +// Mock the worker.js module +class MockWorkerFactory implements IWdioExtensionWorkerFactory { + WdioExtensionWorker + generatedWorkers: IWdioExtensionWorker[] = [] + + constructor(private configManager: IExtensionConfigManager) { + this.WdioExtensionWorker = vi.fn(function (_configManager, cid, configPath) { + // @ts-ignore + this.cid = cid + // @ts-ignore + this.configPath = configPath + }) + this.WdioExtensionWorker.prototype.start = vi.fn().mockResolvedValue(undefined) + this.WdioExtensionWorker.prototype.waitForStart = vi.fn().mockResolvedValue(undefined) + this.WdioExtensionWorker.prototype.stop = vi.fn().mockResolvedValue(undefined) + this.WdioExtensionWorker.prototype.on = vi.fn() + this.WdioExtensionWorker.prototype.isConnected = vi.fn().mockReturnValue(true) + this.WdioExtensionWorker.prototype.updateIdleTimeout = vi.fn() + } + generate(id: string, cwd: string): IWdioExtensionWorker { + const worker = new this.WdioExtensionWorker(this.configManager, id, cwd) as unknown as IWdioExtensionWorker + this.generatedWorkers.push(worker) + return worker + } +} describe('ServerManager', () => { let workerManager: WdioWorkerManager + let workerFactory: MockWorkerFactory + let mockConfigManager: IExtensionConfigManager // Create a fresh instance of ServerManager before each test beforeEach(() => { vi.resetAllMocks() - workerManager = new WdioWorkerManager(mockConfigManager) + + mockConfigManager = { + globalConfig: { + workerIdleTimeout: 600, + }, + on: vi.fn(), + } as unknown as IExtensionConfigManager + + workerFactory = new MockWorkerFactory(mockConfigManager) + workerManager = new WdioWorkerManager(mockConfigManager, workerFactory) }) afterEach(() => { vi.resetAllMocks() }) - describe('start', () => { - it('should start workers for each unique directory', async () => { - // Setup - const configPaths = ['/path/to/wdio.config.js', '/path/to/wdio.config.ts', '/another/path/wdio.config.js'] - - // Execute - await workerManager.start(configPaths) - - // Assert - expect(WdioExtensionWorker).toHaveBeenCalledTimes(2) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#0', normalize('/path/to')) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', normalize('/another/path')) - - // Check that start was called on each worker - expect(vi.mocked(WdioExtensionWorker).mock.instances.length).toBe(2) - expect(WdioExtensionWorker.prototype.start).toHaveBeenCalledTimes(2) - }) - - it('should handle empty config paths array', async () => { - await workerManager.start([]) - - expect(WdioExtensionWorker).not.toHaveBeenCalled() - }) - }) - describe('getConnection', () => { it('should return existing worker if it exists', async () => { // Setup @@ -87,15 +76,13 @@ describe('ServerManager', () => { // First call to create server const result = await workerManager.getConnection(configPath) - - // Reset mocks before second call - vi.clearAllMocks() + const spyFactory = vi.spyOn(workerFactory, 'generate') // Execute - second call should use existing server const cachedResult = await workerManager.getConnection(configPath) // Assert - WdioExtensionWorker constructor should not be called again - expect(WdioExtensionWorker).not.toHaveBeenCalled() + expect(spyFactory).not.toHaveBeenCalled() expect(cachedResult).toBeDefined() expect(result.cid).toBe(cachedResult.cid) }) @@ -103,73 +90,71 @@ describe('ServerManager', () => { it('should create a new worker if it does not exist', async () => { // Setup const configPath = '/path/to/wdio.config.js' - const wdioDirName = dirname(configPath) + const wdioDirName = dirname(normalize(configPath)) + const spyFactory = vi.spyOn(workerFactory, 'generate') // Execute const result = await workerManager.getConnection(configPath) // Assert - expect(WdioExtensionWorker).toHaveBeenCalledTimes(1) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', wdioDirName) + expect(spyFactory).toHaveBeenCalledTimes(1) + expect(spyFactory).toHaveBeenCalledWith('#1', wdioDirName) expect(result).toBeDefined() expect(result.cid).toBe('#1') }) it('should increment the id for each new worker', async () => { // Setup - create first worker + const spyFactory = vi.spyOn(workerFactory, 'generate') await workerManager.getConnection('/path/to/wdio.config.js') // Execute - create second worker with different path await workerManager.getConnection('/another/path/wdio.config.js') // Assert - expect(WdioExtensionWorker).toHaveBeenCalledTimes(2) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#1', '/path/to') - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#2', '/another/path') + expect(spyFactory).toHaveBeenCalledTimes(2) + expect(spyFactory).toHaveBeenCalledWith('#1', normalize('/path/to')) + expect(spyFactory).toHaveBeenCalledWith('#2', normalize('/another/path')) }) }) describe('dispose', () => { it('should stop all workers', async () => { - // Setup - create multiple workers - const configPaths = ['/path/to/wdio.config.js', '/another/path/wdio.config.js'] - - await workerManager.start(configPaths) - - // Execute - await workerManager.dispose() - - // Assert - expect(vi.mocked(WdioExtensionWorker).mock.instances.length).toBe(2) - expect(WdioExtensionWorker.prototype.stop).toHaveBeenCalledTimes(2) - }) - - it('should handle empty server pool', async () => { + // Setup - create worker + const worker1 = await workerManager.getConnection('/path/to/wdio.config.js') + const worker2 = await workerManager.getConnection('/another/path/wdio.config.js') + const spyWorkerStop1 = vi.spyOn(worker1, 'stop') + const spyWorkerStop2 = vi.spyOn(worker2, 'stop') // Execute await workerManager.dispose() - // Assert - should not throw an error - expect(WdioExtensionWorker).not.toHaveBeenCalled() + // Assert - All worker was called `stop` + expect(spyWorkerStop1).toHaveBeenCalledTimes(1) + expect(spyWorkerStop2).toHaveBeenCalledTimes(1) }) }) describe('reorganize', () => { it('should stop unnecessary workers and start new ones', async () => { - // Setup - create initial workers - await workerManager.start(['/path/to/wdio.config.js', '/another/path/wdio.config.js']) + const oldWorker1 = await workerManager.getConnection('/path/to/wdio.config.js') + const oldWorker2 = await workerManager.getConnection('/another/path/wdio.config.js') - vi.clearAllMocks() + const spyWorkerStop1 = vi.spyOn(oldWorker1, 'stop') + const spyWorkerStop2 = vi.spyOn(oldWorker2, 'stop') + const spyFactory = vi.spyOn(workerFactory, 'generate') // Execute - reorganize with different paths await workerManager.reorganize(['/path/to/wdio.config.js', '/new/path/wdio.config.js']) // Assert // Should stop one worker - expect(WdioExtensionWorker.prototype.stop).toHaveBeenCalledTimes(1) + expect(workerFactory.generatedWorkers.length).toBe(3) + expect(spyWorkerStop1).not.toHaveBeenCalled() + expect(spyWorkerStop2).toHaveBeenCalledTimes(1) // Should create one new worker - expect(WdioExtensionWorker).toHaveBeenCalledTimes(1) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#2', normalize('/new/path')) + expect(spyFactory).toHaveBeenCalledTimes(1) + expect(spyFactory).toHaveBeenCalledWith('#3', normalize('/new/path')) }) }) @@ -202,9 +187,9 @@ describe('ServerManager', () => { // Assert operations were queued and processed in order expect(operations.length).toBe(3) - expect(operations[0]).toBe('start:/path/1') - expect(operations[1]).toBe('start:/path/2') - expect(operations[2]).toBe('start:/path/3') + expect(operations[0]).toBe(`start:${normalize('/path/1')}`) + expect(operations[1]).toBe(`start:${normalize('/path/2')}`) + expect(operations[2]).toBe(`start:${normalize('/path/3')}`) }) it('should avoid duplicate operations for the same path', async () => { @@ -246,6 +231,7 @@ describe('ServerManager', () => { // Setup const id = 42 const configPath = '/path/to/wdio.config.js' + const spyFactory = vi.spyOn(workerFactory, 'generate') // Access private method using any cast const startWorker = (workerManager as any).startWorker.bind(workerManager) @@ -254,8 +240,8 @@ describe('ServerManager', () => { const result = await startWorker(id, configPath) // Assert - expect(WdioExtensionWorker).toHaveBeenCalledTimes(1) - expect(WdioExtensionWorker).toHaveBeenCalledWith(mockConfigManager, '#42', configPath) + expect(spyFactory).toHaveBeenCalledTimes(1) + expect(spyFactory).toHaveBeenCalledWith('#42', configPath) expect(result).toBeDefined() expect(result.start).toHaveBeenCalledTimes(1) expect(result.waitForStart).toHaveBeenCalledTimes(1) @@ -270,15 +256,6 @@ describe('ServerManager', () => { // Access private method using any cast const startWorker = (workerManager as any).startWorker.bind(workerManager) - // Add tracking to see if createWorker is called multiple times - let createWorkerCalls = 0 - vi.spyOn(workerManager as any, 'createWorker').mockImplementation((async (id: number, path: string) => { - createWorkerCalls++ - // Mock delay to ensure operations overlap - await new Promise((resolve) => setTimeout(resolve, 50)) - return new WdioExtensionWorker(mockConfigManager, `#${id}`, path) - }) as any) - // Execute two concurrent calls with same path const promise1 = startWorker(id1, configPath) const promise2 = startWorker(id2, configPath) @@ -287,7 +264,7 @@ describe('ServerManager', () => { const [result1, result2] = await Promise.all([promise1, promise2]) // Assert - expect(createWorkerCalls).toBe(1) // Only one actual worker creation + // expect(createWorkerCalls).toBe(1) // Only one actual worker creation expect(result1).toBe(result2) // Should be the same worker instance }) }) @@ -301,7 +278,7 @@ describe('ServerManager', () => { const stopWorker = (workerManager as any).stopWorker.bind(workerManager) // Get the worker from the server pool - const worker = (workerManager as any)._workerPool.get('/path/to') + const worker = (workerManager as any)._workerPool.get(normalize('/path/to')) // Execute await stopWorker('/path/to', worker) @@ -340,4 +317,55 @@ describe('ServerManager', () => { expect(executeStopWorkerCalls).toBe(1) // Only one actual stop execution }) }) + + describe('Worker idle timeout', () => { + it('should return the same promise for concurrent stop calls', async () => { + // Setup - create worker + const worker1 = await workerManager.getConnection('/path/to/wdio.config.js') + const worker2 = await workerManager.getConnection('/another/path/wdio.config.js') + + const spyWorkerTimeout1 = vi.spyOn(worker1, 'updateIdleTimeout') + const spyWorkerTimeout2 = vi.spyOn(worker2, 'updateIdleTimeout') + + const updateHandler = vi.mocked(mockConfigManager.on).mock.calls[1][1] + + // Execute + updateHandler(987) + // Assert - All worker was called `stop` + expect(spyWorkerTimeout1).toHaveBeenCalledTimes(1) + expect(spyWorkerTimeout1).toHaveBeenCalledWith(987) + expect(spyWorkerTimeout2).toHaveBeenCalledTimes(1) + expect(spyWorkerTimeout2).toHaveBeenCalledWith(987) + }) + + it('should stop worker when timeout occurred', async () => { + // Setup - create worker + const worker1 = await workerManager.getConnection('/path/to/wdio.config.js') + const spyWorkerStop1 = vi.spyOn(worker1, 'stop') + + await workerManager.handleWorkerIdleTimeout('/path/to') + + expect(spyWorkerStop1).toHaveBeenCalledTimes(1) + }) + }) + + describe('Node path change', () => { + it('should stop all worker when node path update', async () => { + // Setup - create worker + const worker1 = await workerManager.getConnection('/path/to/wdio.config.js') + const worker2 = await workerManager.getConnection('/another/path/wdio.config.js') + + const spyWorkerStop1 = vi.spyOn(worker1, 'stop') + const spyWorkerStop2 = vi.spyOn(worker2, 'stop') + + const updateHandler = vi.mocked(mockConfigManager.on).mock.calls[0][1] + + // Execute + updateHandler('/path/to/node') + + // Assert - All worker was called `stop` + expect(spyWorkerStop1).toHaveBeenCalledTimes(1) + expect(spyWorkerStop2).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/packages/vscode-wdio-server/tests/run.test.ts b/packages/vscode-wdio-server/tests/run.test.ts index 2a476f9..5a82e3e 100644 --- a/packages/vscode-wdio-server/tests/run.test.ts +++ b/packages/vscode-wdio-server/tests/run.test.ts @@ -56,6 +56,7 @@ describe('TestRunner', () => { pauseTimer: vi.fn(), resumeTimer: vi.fn(), }, + pauseIdleTimer: vi.fn(), } as unknown as IWdioExtensionWorker vi.mocked(getEnvOptions).mockResolvedValue({ diff --git a/packages/vscode-wdio-test/src/manager.ts b/packages/vscode-wdio-test/src/manager.ts index 0ff52f0..707451f 100644 --- a/packages/vscode-wdio-test/src/manager.ts +++ b/packages/vscode-wdio-test/src/manager.ts @@ -54,14 +54,7 @@ export class RepositoryManager extends MetadataRepository implements IRepository await configManager .initialize() .then(async () => await this.initialize()) - .then( - async () => - await Promise.all( - this.repos.map(async (repo) => { - return await repo.discoverAllTests() - }) - ) - ) + .then(async () => await Promise.all(this.repos.map(async (repo) => await repo.discoverAllTests()))) .then(() => this.registerToTestController()) .then(() => this.workerManager.reorganize(configManager.getWdioConfigPaths())) }) @@ -193,11 +186,9 @@ export class RepositoryManager extends MetadataRepository implements IRepository workspaceTestItem.children.add(configItem) this._wdioConfigTestItems.push(configItem) - const worker = await this.workerManager.getConnection(wdioConfigPath) const repo = new TestRepository( this.configManager, this.controller, - worker, wdioConfigPath, configItem, this.workerManager, diff --git a/packages/vscode-wdio-test/src/repository.ts b/packages/vscode-wdio-test/src/repository.ts index b8e792b..e570c86 100644 --- a/packages/vscode-wdio-test/src/repository.ts +++ b/packages/vscode-wdio-test/src/repository.ts @@ -17,13 +17,10 @@ import type * as vscode from 'vscode' class WorkerProxy extends MetadataRepository { private _worker: IWdioExtensionWorker | undefined constructor( - private readonly _wdioConfigPath: string, - worker: IWdioExtensionWorker, - private workerManager: IWorkerManager + private workerManager: IWorkerManager, + private readonly _wdioConfigPath: string ) { super() - this._worker = worker - this.setListener() } async getWorker() { @@ -55,13 +52,12 @@ export class TestRepository extends WorkerProxy implements ITestRepository { constructor( public readonly configManager: IExtensionConfigManager, public readonly controller: vscode.TestController, - _worker: IWdioExtensionWorker, public readonly wdioConfigPath: string, private _wdioConfigTestItem: vscode.TestItem, workerManager: IWorkerManager, private _workspaceFolder: vscode.WorkspaceFolder ) { - super(wdioConfigPath, _worker, workerManager) + super(workerManager, wdioConfigPath) } public get specPatterns() { diff --git a/packages/vscode-wdio-test/tests/manager.test.ts b/packages/vscode-wdio-test/tests/manager.test.ts index 4378c33..1d15a1b 100644 --- a/packages/vscode-wdio-test/tests/manager.test.ts +++ b/packages/vscode-wdio-test/tests/manager.test.ts @@ -114,8 +114,6 @@ describe('RepositoryManager', () => { expect((repositoryManager as any)._workspaceTestItems.length).toBe(1) expect((repositoryManager as any)._wdioConfigTestItems.length).toBe(1) - - expect(workerManager.getConnection).toHaveBeenCalledWith(fakeWorkspaces[0].wdioConfigFiles[0]) }) }) diff --git a/packages/vscode-wdio-test/tests/repository.test.ts b/packages/vscode-wdio-test/tests/repository.test.ts index 44b1e18..b6fe347 100644 --- a/packages/vscode-wdio-test/tests/repository.test.ts +++ b/packages/vscode-wdio-test/tests/repository.test.ts @@ -88,13 +88,14 @@ describe('TestRepository', () => { } } - workerManager = vi.fn() as unknown as IWorkerManager + workerManager = { + getConnection: vi.fn(() => mockWorker), + } as unknown as IWorkerManager // Create repository with mocked dependencies testRepository = new MockTestRepository( mockConfigManager, testController, - mockWorker, mockWdioConfigPath, wdioConfigTestItem, workerManager, @@ -157,7 +158,6 @@ describe('TestRepository', () => { const repo = new TestRepository( mockConfigManager, testController, - mockWorker, mockWdioConfigPath, wdioConfigTestItem, workerManager, diff --git a/packages/vscode-wdio-types/src/server.ts b/packages/vscode-wdio-types/src/server.ts index 191e204..f868f87 100644 --- a/packages/vscode-wdio-types/src/server.ts +++ b/packages/vscode-wdio-types/src/server.ts @@ -95,11 +95,13 @@ export interface TestResultData { export interface IWdioExtensionWorker extends ITypedEventEmitter { cid: string rpc: WorkerApi - idleMonitor: IWorkerIdleMonitor start(): Promise waitForStart(): Promise stop(): Promise isConnected(): boolean + updateIdleTimeout(timeout: number): void + pauseIdleTimer(): void + resumeIdleTimer(): void ensureConnected(): Promise } @@ -110,8 +112,11 @@ export interface WdioExtensionWorkerEvents { shutdown: undefined } +export interface IWdioExtensionWorkerFactory { + generate(id: string, cwd: string): IWdioExtensionWorker +} + export interface IWorkerManager extends vscode.Disposable { - start(configPaths: string[]): Promise getConnection(configPaths: string): Promise reorganize(configPaths: string[]): Promise } @@ -148,11 +153,6 @@ export interface IWorkerIdleMonitor { */ resumeTimer(): void - /** - * Check if monitoring is currently active - */ - isActive(): boolean - /** * Add event listener for idle timeout events * @param event Event name ('idleTimeout') diff --git a/packages/vscode-webdriverio/src/extension.ts b/packages/vscode-webdriverio/src/extension.ts index fc515ca..fe0e112 100644 --- a/packages/vscode-webdriverio/src/extension.ts +++ b/packages/vscode-webdriverio/src/extension.ts @@ -32,10 +32,6 @@ class WdioExtension implements vscode.Disposable { log.info('WebdriverIO Runner extension is now active') await this.configManager.initialize() - const configPaths = this.configManager.getWdioConfigPaths() - - // Start worker process asynchronously - const starting = this.workerManager.start(configPaths) // Create Manages and watchers const repositoryManager = new RepositoryManager(this.controller, this.configManager, this.workerManager) @@ -59,13 +55,6 @@ class WdioExtension implements vscode.Disposable { // Initialize try { - try { - await starting - } catch (error) { - const errorMessage = `Failed to start WebdriverIO worker process: ${error instanceof Error ? error.message : String(error)}` - log.error(errorMessage) - vscode.window.showErrorMessage(errorMessage) - } await repositoryManager.initialize() return Promise.all( repositoryManager.repos.map(async (repo) => { diff --git a/packages/vscode-webdriverio/tests/extension.test.ts b/packages/vscode-webdriverio/tests/extension.test.ts index 450550e..b256228 100644 --- a/packages/vscode-webdriverio/tests/extension.test.ts +++ b/packages/vscode-webdriverio/tests/extension.test.ts @@ -1,9 +1,7 @@ import { ExtensionConfigManager } from '@vscode-wdio/config' import { log } from '@vscode-wdio/logger' -import { WdioWorkerManager } from '@vscode-wdio/server' import { RepositoryManager } from '@vscode-wdio/test' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import * as vscode from 'vscode' import { activate, deactivate } from '../src/extension.js' @@ -25,19 +23,13 @@ vi.mock('vscode', async () => { }) vi.mock('@vscode-wdio/logger', () => import('../../../tests/__mocks__/logger.js')) -vi.mock('@vscode-wdio/server', () => { - const WdioWorkerManager = vi.fn() - WdioWorkerManager.prototype.start = vi.fn(() => Promise.resolve()) - WdioWorkerManager.prototype.dispose = vi.fn(() => Promise.resolve()) - - return { WdioWorkerManager } -}) vi.mock('@vscode-wdio/config', () => { const ExtensionConfigManager = vi.fn() ExtensionConfigManager.prototype.initialize = vi.fn(() => Promise.resolve()) ExtensionConfigManager.prototype.dispose = vi.fn(() => Promise.resolve()) ExtensionConfigManager.prototype.getWdioConfigPaths = vi.fn(() => ['/test/wdio.conf.ts']) ExtensionConfigManager.prototype.listener = vi.fn() + ExtensionConfigManager.prototype.on = vi.fn() const ConfigFileWatcher = vi.fn() ConfigFileWatcher.prototype.enable = vi.fn() @@ -95,41 +87,12 @@ describe('extension', () => { // vi.mocked(TestRunner).mock.instances[0].run // expect(vi.mocked(ExtensionConfigManager).mock.instances[0].initialize).toHaveBeenCalled() - expect(vi.mocked(WdioWorkerManager).mock.instances[0].start).toHaveBeenCalled() expect(vi.mocked(RepositoryManager).mock.instances[0].initialize).toHaveBeenCalled() expect(vi.mocked(RepositoryManager).mock.instances[0].registerToTestController).toHaveBeenCalled() // Verify context subscriptions expect(fakeContext.subscriptions.length).toBeGreaterThan(0) }) - - it('should handle server start error gracefully', async () => { - // Arrange - const errorMessage = 'Failed to start server' - vi.mocked(WdioWorkerManager.prototype.start).mockRejectedValueOnce(new Error(errorMessage)) - - // Act - await activate(fakeContext) - - // Assert - expect(log.error).toHaveBeenCalledWith(`Failed to start WebdriverIO worker process: ${errorMessage}`) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - `Failed to start WebdriverIO worker process: ${errorMessage}` - ) - }) - - it('should continue activation even when workerManager.start rejects', async () => { - // Arrange - vi.mocked(WdioWorkerManager.prototype.start).mockRejectedValueOnce(new Error('Server failed')) - - // Act - await activate(fakeContext) - - // Assert - expect(vi.mocked(RepositoryManager).mock.instances[0].initialize).toHaveBeenCalled() - expect(vi.mocked(RepositoryManager).mock.instances[0].registerToTestController).toHaveBeenCalled() - expect(log.error).toHaveBeenCalledWith(expect.stringContaining('Server failed')) - }) }) describe('deactivate', () => { diff --git a/samples/smoke/update-config/webdriverio.conf.ts b/samples/smoke/update-config/webdriverio.conf.ts new file mode 100644 index 0000000..d826b87 --- /dev/null +++ b/samples/smoke/update-config/webdriverio.conf.ts @@ -0,0 +1,7 @@ +import '@wdio/types' +import { config as baseConfig } from './wdio.conf.ts' + +export const config = { + ...baseConfig, + specs: ['tests/*.test.ts'], +}