diff --git a/package-lock.json b/package-lock.json index d91c018a3..192a22ba1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@types/glob": "^8.0.0", "@types/node": "^18.11.9", "@types/stack-utils": "^2.0.1", - "@types/vscode": "1.78.0", + "@types/vscode": "1.86.0", "@types/which": "^2.0.1", "@types/ws": "^8.5.3", "@typescript-eslint/eslint-plugin": "^5.44.0", @@ -40,7 +40,7 @@ "typescript": "^4.9.3" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.86.0" } }, "node_modules/@ampproject/remapping": { @@ -824,9 +824,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==", + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.86.0.tgz", + "integrity": "sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==", "dev": true }, "node_modules/@types/which": { @@ -5144,9 +5144,9 @@ "dev": true }, "@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==", + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.86.0.tgz", + "integrity": "sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==", "dev": true }, "@types/which": { diff --git a/package.json b/package.json index 3f54f1458..e404476bc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/microsoft/playwright-vscode/issues" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.86.0" }, "categories": [ "Testing" @@ -128,7 +128,7 @@ "@types/glob": "^8.0.0", "@types/node": "^18.11.9", "@types/stack-utils": "^2.0.1", - "@types/vscode": "1.78.0", + "@types/vscode": "1.86.0", "@types/which": "^2.0.1", "@types/ws": "^8.5.3", "@typescript-eslint/eslint-plugin": "^5.44.0", diff --git a/src/extension.ts b/src/extension.ts index 08ab72bed..78464278d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -292,17 +292,30 @@ export class Extension implements RunHooks { } const model = new TestModel(this._vscode, this._playwrightTest, workspaceFolderPath, configFileUri.fsPath, playwrightInfo, this._envProvider.bind(this)); - this._models.push(model); - this._testTree.addModel(model); const configError = await model.listFiles(); if (configError) { configErrors.set(configError.location?.file ?? configFilePath, configError); continue; } - for (const project of model.projects.values()) { - await this._createRunProfile(project); - this._workspaceObserver.addWatchFolder(project.testDir); + this._models.push(model); + this._testTree.addModel(model); + } + + // Update list of run profiles. + { + // rebuildModel can run concurrently, swapping run profiles needs to be sync. + const existingProfiles = this._runProfiles; + this._runProfiles = new Map(); + for (const model of this._models) { + for (const project of model.allProjects().values()) { + this._createRunProfile(project, existingProfiles); + this._workspaceObserver.addWatchFolder(project.testDir); + } + } + for (const [id, profile] of existingProfiles) { + if (!this._runProfiles.has(id)) + profile.dispose(); } } @@ -332,7 +345,7 @@ export class Extension implements RunHooks { if (error?.location) { const range = new this._vscode.Range( new this._vscode.Position(Math.max(error.location.line - 4, 0), 0), - new this._vscode.Position(error.location.line - 1, error.location.column - 1), + new this._vscode.Position(Math.max(error.location.line - 1, 0), Math.max(error.location.column - 1, 0)), ); this._vscode.window.activeTextEditor?.revealRange(range); } @@ -349,25 +362,26 @@ export class Extension implements RunHooks { })) as NodeJS.ProcessEnv; } - private async _createRunProfile(project: TestProject) { + private _createRunProfile(project: TestProject, existingProfiles: Map) { const configFile = project.model.config.configFile; const configName = path.basename(configFile); const folderName = path.basename(path.dirname(configFile)); const projectPrefix = project.name ? `${project.name} — ` : ''; const keyPrefix = configFile + ':' + project.name; - let runProfile = this._runProfiles.get(keyPrefix + ':run'); + let runProfile = existingProfiles.get(keyPrefix + ':run'); const projectTag = this._testTree.projectTag(project); const isDefault = false; const supportsContinuousRun = this._settingsModel.allowWatchingFiles.get(); - if (!runProfile) { + if (!runProfile) runProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Run, this._scheduleTestRunRequest.bind(this, configFile, project.name, false), isDefault, projectTag, supportsContinuousRun); - this._runProfiles.set(keyPrefix + ':run', runProfile); - } - let debugProfile = this._runProfiles.get(keyPrefix + ':debug'); - if (!debugProfile) { + this._runProfiles.set(keyPrefix + ':run', runProfile); + let debugProfile = existingProfiles.get(keyPrefix + ':debug'); + if (!debugProfile) debugProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Debug, this._scheduleTestRunRequest.bind(this, configFile, project.name, true), isDefault, projectTag, supportsContinuousRun); - this._runProfiles.set(keyPrefix + ':debug', debugProfile); - } + this._runProfiles.set(keyPrefix + ':debug', debugProfile); + + runProfile.onDidChangeDefault(enabled => project.model.setProjectEnabled(project, enabled)); + debugProfile.onDidChangeDefault(enabled => project.model.setProjectEnabled(project, enabled)); } private _scheduleTestRunRequest(configFile: string, projectName: string, isDebug: boolean, request: vscodeTypes.TestRunRequest, cancellationToken?: vscodeTypes.CancellationToken) { @@ -380,7 +394,7 @@ export class Extension implements RunHooks { const model = this._models.find(m => m.config.configFile === configFile); if (!model) return; - const project = model.projects.get(projectName); + const project = model.allProjects().get(projectName); if (!project) return; @@ -690,7 +704,10 @@ located next to Run / Debug Tests toolbar buttons.`); const allErrors = new Set(); const errorsByFile = new MultiMap(); for (const model of this._models.slice()) { - const errors = await model.listTests([...files]).catch(e => console.log(e)) || []; + const filteredFiles = model.narrowDownFilesToEnabledProjects(files); + if (!filteredFiles.size) + continue; + const errors = await model.listTests([...filteredFiles]).catch(e => console.log(e)) || []; for (const error of errors) { if (!error.location || !error.message) continue; @@ -728,7 +745,7 @@ located next to Run / Debug Tests toolbar buttons.`); diagnostics.push({ severity: this._vscode.DiagnosticSeverity.Error, source: 'playwright', - range: new this._vscode.Range(error.location!.line - 1, error.location!.column - 1, error.location!.line, 0), + range: new this._vscode.Range(Math.max(error.location!.line - 1, 0), Math.max(error.location!.column - 1, 0), error.location!.line, 0), message: error.message!, }); } diff --git a/src/reusedBrowser.ts b/src/reusedBrowser.ts index 59f373234..c272d73eb 100644 --- a/src/reusedBrowser.ts +++ b/src/reusedBrowser.ts @@ -15,7 +15,7 @@ */ import { TestConfig } from './playwrightTest'; -import { TestModel, TestProject } from './testModel'; +import type { TestModel } from './testModel'; import { createGuid } from './utils'; import * as vscodeTypes from './vscodeTypes'; import path from 'path'; @@ -324,7 +324,7 @@ export class ReusedBrowser implements vscodeTypes.Disposable { } private async _createFileForNewTest(model: TestModel) { - const project = model.projects.values().next().value as TestProject; + const project = model.enabledProjects()[0]; if (!project) return; let file; diff --git a/src/testModel.ts b/src/testModel.ts index 099c0fe4e..7d6b1c53d 100644 --- a/src/testModel.ts +++ b/src/testModel.ts @@ -60,17 +60,16 @@ export type TestProject = { name: string; testDir: string; model: TestModel; - isFirst: boolean; files: Map; + isEnabled: boolean; }; export class TestModel { private _vscode: vscodeTypes.VSCode; readonly config: TestConfig; - readonly projects = new Map(); + private _projects = new Map(); private _didUpdate: vscodeTypes.EventEmitter; readonly onUpdated: vscodeTypes.Event; - readonly allFiles = new Set(); private _playwrightTest: PlaywrightTest; private _fileToSources: Map = new Map(); private _sourceToFile: Map = new Map(); @@ -85,6 +84,28 @@ export class TestModel { this.onUpdated = this._didUpdate.event; } + setProjectEnabled(project: TestProject, enabled: boolean) { + project.isEnabled = enabled; + this._didUpdate.fire(); + } + + allProjects(): Map { + return this._projects; + } + + enabledProjects(): TestProject[] { + return [...this._projects.values()].filter(p => p.isEnabled); + } + + enabledFiles(): string[] { + const result: string[] = []; + for (const project of this.enabledProjects()) { + for (const file of project.files.keys()) + result.push(file); + } + return result; + } + async listFiles(): Promise { const report = await this._playwrightTest.listFiles(this.config); if (report.error) @@ -102,29 +123,28 @@ export class TestModel { const projectsToKeep = new Set(); for (const projectReport of report.projects) { projectsToKeep.add(projectReport.name); - let project = this.projects.get(projectReport.name); + let project = this._projects.get(projectReport.name); if (!project) - project = this._createProject(projectReport, projectReport === report.projects[0]); + project = this._createProject(projectReport); this._updateProject(project, projectReport); } - for (const projectName of this.projects.keys()) { + for (const projectName of this._projects.keys()) { if (!projectsToKeep.has(projectName)) - this.projects.delete(projectName); + this._projects.delete(projectName); } - this._recalculateAllFiles(); this._didUpdate.fire(); } - private _createProject(projectReport: ProjectConfigWithFiles, isFirst: boolean): TestProject { + private _createProject(projectReport: ProjectConfigWithFiles): TestProject { const project: TestProject = { model: this, ...projectReport, - isFirst, files: new Map(), + isEnabled: true, }; - this.projects.set(project.name, project); + this._projects.set(project.name, project); return project; } @@ -157,7 +177,7 @@ export class TestModel { change.deleted = this._mapFilesToSources(change.deleted); if (change.deleted.size) { - for (const project of this.projects.values()) { + for (const project of this._projects.values()) { for (const file of change.deleted) { if (project.files.has(file)) { project.files.delete(file); @@ -169,7 +189,7 @@ export class TestModel { if (change.created.size) { let hasMatchingFiles = false; - for (const project of this.projects.values()) { + for (const project of this._projects.values()) { for (const file of change.created) { if (file.startsWith(project.testDir)) hasMatchingFiles = true; @@ -179,12 +199,9 @@ export class TestModel { await this.listFiles(); } - if (change.created.size || change.deleted.size) - this._recalculateAllFiles(); - if (change.changed.size) { const filesToLoad = new Set(); - for (const project of this.projects.values()) { + for (const project of this._projects.values()) { for (const file of change.changed) { const testFile = project.files.get(file); if (!testFile || !testFile.entries()) @@ -200,17 +217,13 @@ export class TestModel { } async listTests(files: string[]): Promise { - const sourcesToLoad = files.filter(f => this.allFiles.has(f)); - if (!sourcesToLoad.length) - return []; - - const { entries, errors } = await this._playwrightTest.listTests(this.config, sourcesToLoad); - this._updateProjects(entries, sourcesToLoad); + const { entries, errors } = await this._playwrightTest.listTests(this.config, files); + this._updateProjects(entries, files); return errors; } private _updateProjects(projectEntries: Entry[], requestedFiles: string[]) { - for (const [projectName, project] of this.projects) { + for (const [projectName, project] of this._projects) { const projectEntry = projectEntries.find(e => e.title === projectName); const filesToDelete = new Set(requestedFiles); for (const fileEntry of projectEntry?.children || []) { @@ -232,7 +245,7 @@ export class TestModel { updateFromRunningProjects(projectEntries: Entry[]) { for (const projectEntry of projectEntries) { - const project = this.projects.get(projectEntry.title); + const project = this._projects.get(projectEntry.title); if (project) this._updateFromRunningProject(project, projectEntry); } @@ -252,14 +265,6 @@ export class TestModel { this._didUpdate.fire(); } - private _recalculateAllFiles() { - this.allFiles.clear(); - for (const project of this.projects.values()) { - for (const file of project.files.values()) - this.allFiles.add(file.file); - } - } - async runTests(projects: TestProject[], locations: string[] | null, testListener: TestListener, parametrizedTestTitle: string | undefined, token: vscodeTypes.CancellationToken) { locations = locations || []; await this._playwrightTest.runTests(this.config, projects.map(p => p.name), locations, testListener, parametrizedTestTitle, token); @@ -281,4 +286,15 @@ export class TestModel { } return result; } + + narrowDownFilesToEnabledProjects(fileNames: Set) { + const result = new Set(); + for (const project of this.enabledProjects()) { + for (const fileName of fileNames) { + if (project.files.has(fileName)) + result.add(fileName); + } + } + return result; + } } diff --git a/src/testTree.ts b/src/testTree.ts index 9622ca2e0..8baba1f6b 100644 --- a/src/testTree.ts +++ b/src/testTree.ts @@ -35,6 +35,7 @@ export class TestTree { private _testGeneration = ''; // Global test item map location => fileItem that are files. + private _rootItems = new Map(); private _folderItems = new Map(); private _fileItems = new Map(); @@ -79,6 +80,7 @@ export class TestTree { this._loadingItem.parent.children.delete(this._loadingItem.id); else if (this._testController.items.get(this._loadingItem.id)) this._testController.items.delete(this._loadingItem.id); + this._update(); } addModel(model: TestModel) { @@ -102,8 +104,7 @@ export class TestTree { private _update() { const allFiles = new Set(); for (const model of this._models) - model.allFiles.forEach(f => allFiles.add(f)); - + model.enabledFiles().forEach(f => allFiles.add(f)); for (const file of allFiles) { if (!this._belongsToWorkspace(file)) continue; @@ -111,7 +112,7 @@ export class TestTree { const signature: string[] = []; const entriesByTitle: EntriesByTitle = new MultiMap(); for (const model of this._models) { - for (const testProject of model.projects.values()) { + for (const testProject of model.enabledProjects()) { const testFile = testProject.files.get(file); if (!testFile || !testFile.entries()) continue; @@ -131,13 +132,28 @@ export class TestTree { } for (const [location, fileItem] of this._fileItems) { - if (!allFiles.has(location)) { - this._fileItems.delete(location); - fileItem.parent?.children.delete(fileItem.id); - } + if (!allFiles.has(location)) + this._removeEmptyFileIfNeeded(location, fileItem); } } + private _removeEmptyFileIfNeeded(location: string, fileItem: vscodeTypes.TestItem) { + this._fileItems.delete(location); + fileItem.parent?.children.delete(fileItem.id); + if (fileItem.parent && !fileItem.parent?.children.size) + this._removeEmptyFolderIfNeeded(path.dirname(location), fileItem.parent); + } + + private _removeEmptyFolderIfNeeded(location: string, folderItem: vscodeTypes.TestItem) { + if (this._rootItems.has(location)) + return; + this._folderItems.delete(location); + const children = folderItem.parent?.children || this._testController.items; + children.delete(folderItem.id); + if (folderItem.parent && !children.size) + this._removeEmptyFolderIfNeeded(path.dirname(location), folderItem.parent); + } + private _belongsToWorkspace(file: string) { for (const workspaceFolder of this._vscode.workspace.workspaceFolders || []) { if (file.startsWith(workspaceFolder.uri.fsPath)) @@ -202,12 +218,14 @@ export class TestTree { error: undefined, }; this._folderItems.set(uri.fsPath, testItem); + this._rootItems.set(uri.fsPath, testItem); return testItem; } private _createRootFolderItem(folder: string): vscodeTypes.TestItem { const folderItem = this._testController.createTestItem(this._id(folder), path.basename(folder), this._vscode.Uri.file(folder)); this._folderItems.set(folder, folderItem); + this._rootItems.set(folder, folderItem); return folderItem; } diff --git a/tests/global-errors.spec.ts b/tests/global-errors.spec.ts index 5d8e95fc4..07d932a3d 100644 --- a/tests/global-errors.spec.ts +++ b/tests/global-errors.spec.ts @@ -28,7 +28,7 @@ test('should report duplicate test title', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); diff --git a/tests/list-files.spec.ts b/tests/list-files.spec.ts index c14021ca9..bed216225 100644 --- a/tests/list-files.spec.ts +++ b/tests/list-files.spec.ts @@ -16,6 +16,7 @@ import { expect, test } from './utils'; import { Extension } from '../out/extension'; +import { TestRunProfileKind } from './mock/vscode'; test('should list files', async ({ activate }) => { const { vscode, testController } = await activate({ @@ -26,7 +27,7 @@ test('should list files', async ({ activate }) => { `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -44,7 +45,7 @@ test('should list files top level if no testDir', async ({ activate }, testInfo) `, }, { rootDir: testInfo.outputPath('myWorkspace') }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - test.spec.ts `); expect(vscode).toHaveExecLog(` @@ -64,7 +65,7 @@ test('should list only test files', async ({ activate }) => { `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -79,7 +80,7 @@ test('should list folders', async ({ activate }) => { 'tests/a/b/c/d/test-c.spec.ts': ``, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - a - b @@ -103,7 +104,7 @@ test('should pick new files', async ({ activate }) => { 'tests/test-1.spec.ts': `` }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts `); @@ -117,7 +118,7 @@ test('should pick new files', async ({ activate }) => { workspaceFolder.addFile('tests/test-2.spec.ts', '') ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -135,7 +136,7 @@ test('should not pick non-test files', async ({ activate }) => { 'tests/test-1.spec.ts': `` }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts `); @@ -146,7 +147,7 @@ test('should not pick non-test files', async ({ activate }) => { workspaceFolder.addFile('tests/test-2.spec.ts', ''), ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -158,7 +159,7 @@ test('should tolerate missing testDir', async ({ activate }) => { 'playwright.config.js': `module.exports = { testDir: 'tests' }`, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` `); await Promise.all([ @@ -166,7 +167,7 @@ test('should tolerate missing testDir', async ({ activate }) => { workspaceFolder.addFile('tests/test.spec.ts', '') ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -180,7 +181,7 @@ test('should remove deleted files', async ({ activate }) => { 'tests/test-3.spec.ts': ``, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -196,7 +197,7 @@ test('should remove deleted files', async ({ activate }) => { workspaceFolder.removeFile('tests/test-2.spec.ts') ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-3.spec.ts @@ -215,7 +216,7 @@ test('should do nothing for not loaded changed file', async ({ activate }) => { 'tests/test-3.spec.ts': ``, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -229,7 +230,7 @@ test('should do nothing for not loaded changed file', async ({ activate }) => { expect(changed).toBeFalsy(); }); -test('should support multiple configs', async ({ activate }) => { +test('should switch between configs', async ({ activate }) => { const { vscode, testController } = await activate({ 'tests1/playwright.config.js': `module.exports = { testDir: '.' }`, 'tests2/playwright.config.js': `module.exports = { testDir: '.' }`, @@ -242,13 +243,24 @@ test('should support multiple configs', async ({ activate }) => { test(two', async () => {}); `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests1 - test.spec.ts + `); + + expect(vscode).toHaveExecLog(` + tests1> playwright list-files -c playwright.config.js + tests2> playwright list-files -c playwright.config.js + `); + + const profiles = testController.runProfilesByKind(TestRunProfileKind.Run); + profiles[0].isDefault = false; + profiles[1].isDefault = true; + + await expect(testController).toHaveTestTree(` - tests2 - test.spec.ts `); - expect(vscode).toHaveExecLog(` tests1> playwright list-files -c playwright.config.js tests2> playwright list-files -c playwright.config.js @@ -273,7 +285,7 @@ test('should support multiple projects', async ({ activate }) => { test(two', async () => {}); `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts - test2.spec.ts @@ -284,7 +296,7 @@ test('should support multiple projects', async ({ activate }) => { `); }); -test('should support multiple projects with filter', async ({ activate }) => { +test('should switch between multiple projects with filter', async ({ activate }) => { const { vscode, testController } = await activate({ 'playwright.config.js': `module.exports = { testDir: './tests', @@ -306,9 +318,21 @@ test('should support multiple projects with filter', async ({ activate }) => { test(three', async () => {}); `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts + `); + + expect(vscode).toHaveExecLog(` + > playwright list-files -c playwright.config.js + `); + + const profiles = testController.runProfilesByKind(TestRunProfileKind.Run); + profiles[0].isDefault = false; + profiles[1].isDefault = true; + + await expect(testController).toHaveTestTree(` + - tests - test2.spec.ts `); @@ -325,7 +349,7 @@ test('should list files in relative folder', async ({ activate }) => { test('one', async () => {}); `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -334,7 +358,7 @@ test('should list files in relative folder', async ({ activate }) => { `); }); -test('should list files in multi-folder workspace', async ({ activate }, testInfo) => { +test('should list files in multi-folder workspace with project switching', async ({ activate }, testInfo) => { const { vscode, testController } = await activate({}, { workspaceFolders: [ [testInfo.outputPath('folder1'), { @@ -358,10 +382,19 @@ test('should list files in multi-folder workspace', async ({ activate }, testInf const context = { subscriptions: [] }; await extension.activate(context); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - folder1 - test.spec.ts - folder2 + `); + + const profiles = testController.runProfilesByKind(TestRunProfileKind.Run); + profiles[0].isDefault = false; + profiles[1].isDefault = true; + + await expect(testController).toHaveTestTree(` + - folder1 + - folder2 - test.spec.ts `); }); @@ -376,7 +409,7 @@ test('should ignore errors when listing files', async ({ activate }) => { `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); diff --git a/tests/list-tests.spec.ts b/tests/list-tests.spec.ts index 0530c4601..eed51972e 100644 --- a/tests/list-tests.spec.ts +++ b/tests/list-tests.spec.ts @@ -27,7 +27,7 @@ test('should list tests on expand', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -55,7 +55,7 @@ test('should list tests for visible editors', async ({ activate }) => { await vscode.openEditors('**/test*.spec.ts'); await new Promise(f => testController.onDidChangeTestItem(f)); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts - one [2:0] @@ -92,7 +92,7 @@ test('should list suits', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -134,7 +134,7 @@ test('should discover new tests', async ({ activate }) => { `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -176,7 +176,7 @@ test('should discover new tests with active editor', async ({ activate }) => { vscode.openEditors('**/test2.spec.ts'), ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts - test2.spec.ts @@ -200,7 +200,7 @@ test('should discover tests on add + change', async ({ activate }) => { workspaceFolder.addFile('test.spec.ts', ``) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - test.spec.ts `); @@ -214,7 +214,7 @@ test('should discover tests on add + change', async ({ activate }) => { `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - test.spec.ts - one [2:0] `); @@ -251,7 +251,7 @@ test('should discover new test at existing location', async ({ activate }) => { `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - two [2:0] @@ -281,7 +281,7 @@ test('should remove deleted tests', async ({ activate }) => { > playwright test -c playwright.config.js --list --reporter=null tests/test.spec.ts `); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -296,7 +296,7 @@ test('should remove deleted tests', async ({ activate }) => { `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -321,7 +321,7 @@ test('should forget tests after error before first test', async ({ activate }) = await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -338,7 +338,7 @@ test('should forget tests after error before first test', async ({ activate }) = `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -362,7 +362,7 @@ test('should regain tests after error is fixed', async ({ activate }) => { > playwright test -c playwright.config.js --list --reporter=null tests/test.spec.ts `); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -376,7 +376,7 @@ test('should regain tests after error is fixed', async ({ activate }) => { `) ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -404,9 +404,13 @@ test('should support multiple configs', async ({ activate }) => { `, }); + const runProfiles = testController.runProfilesByKind(vscode.TestRunProfileKind.Run); + runProfiles[0].isDefault = true; + runProfiles[1].isDefault = true; + await testController.expandTestItems(/test.spec/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests1 - test.spec.ts - one [2:0] @@ -442,9 +446,13 @@ test('should support multiple projects', async ({ activate }) => { `, }); + const runProfiles = testController.runProfilesByKind(vscode.TestRunProfileKind.Run); + runProfiles[0].isDefault = true; + runProfiles[1].isDefault = true; + await testController.expandTestItems(/test1.spec/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts - one [2:0] @@ -468,7 +476,7 @@ test('should list parametrized tests', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [3:0] @@ -491,7 +499,7 @@ test('should list tests in parametrized groups', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - level 1 [3:0] @@ -514,7 +522,7 @@ test('should not run config reporters', async ({ activate }, testInfo) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -524,7 +532,7 @@ test('should not run config reporters', async ({ activate }, testInfo) => { }); test('should list tests in multi-folder workspace', async ({ activate }, testInfo) => { - const { testController } = await activate({}, { + const { vscode, testController } = await activate({}, { workspaceFolders: [ [testInfo.outputPath('folder1'), { 'playwright.config.js': `module.exports = { testDir: './' }`, @@ -543,8 +551,12 @@ test('should list tests in multi-folder workspace', async ({ activate }, testInf ] }); + const runProfiles = testController.runProfilesByKind(vscode.TestRunProfileKind.Run); + runProfiles[0].isDefault = true; + runProfiles[1].isDefault = true; + await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - folder1 - test.spec.ts - one [2:0] @@ -555,7 +567,7 @@ test('should list tests in multi-folder workspace', async ({ activate }, testInf }); test('should merge items from different projects', async ({ activate }, testInfo) => { - const { testController } = await activate({ + const { vscode, testController } = await activate({ 'playwright.config.ts': `module.exports = { projects: [ { name: 'desktop', grepInvert: /mobile|tablet/ }, @@ -573,6 +585,11 @@ test('should merge items from different projects', async ({ activate }, testInfo });`, }); + const runProfiles = testController.runProfilesByKind(vscode.TestRunProfileKind.Run); + runProfiles[0].isDefault = true; + runProfiles[1].isDefault = true; + runProfiles[2].isDefault = true; + await testController.expandTestItems(/test.spec.ts/); await testController.expandTestItems(/group/); expect(testController.renderTestTree({ renderTags: true })).toBe(` diff --git a/tests/mock/vscode.ts b/tests/mock/vscode.ts index 1a39e7c2b..7dee29a72 100644 --- a/tests/mock/vscode.ts +++ b/tests/mock/vscode.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import glob from 'glob'; import path from 'path'; -import { Disposable, EventEmitter } from './events'; +import { Disposable, EventEmitter, Event } from './events'; import minimatch from 'minimatch'; import { spawn } from 'child_process'; import which from 'which'; @@ -245,14 +245,29 @@ class TestItem { } class TestRunProfile { + private _isDefault = true; + readonly didChangeDefault = new EventEmitter(); + readonly onDidChangeDefault: Event | undefined; + constructor( private testController: TestController, readonly label: string, readonly kind: TestRunProfileKind, readonly runHandler: (request: TestRunRequest, token: CancellationToken) => Promise, - readonly isDefault: boolean, + isDefault: boolean, private runProfiles: TestRunProfile[]) { runProfiles.push(this); + this.onDidChangeDefault = this.didChangeDefault.event; + this._isDefault = isDefault; + } + + get isDefault(): boolean | undefined { + return this._isDefault; + } + + set isDefault(value: boolean) { + this._isDefault = value; + this.didChangeDefault.fire(value); } async run(include?: TestItem[], exclude?: TestItem[]): Promise { @@ -416,12 +431,22 @@ export class TestController { this.items = new TestItem(this, id, label); } + runProfilesByKind(kind: TestRunProfileKind): TestRunProfile[] { + return this.runProfiles.filter(p => p.kind === kind); + } + createTestItem(id: string, label: string, uri?: Uri): TestItem { return new TestItem(this, id, label, uri); } createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Promise, isDefault?: boolean): TestRunProfile { - return new TestRunProfile(this, label, kind, runHandler, !!isDefault, this.runProfiles); + // Enable first profile by default, lazily. + const isFirst = !this.runProfilesByKind(kind).length; + const profile = new TestRunProfile(this, label, kind, runHandler, !!isDefault, this.runProfiles); + setTimeout(() => { + profile.isDefault = isFirst; + }, 0); + return profile; } createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun { @@ -767,8 +792,10 @@ export class VSCode { readonly l10n = new L10n(); lastWithProgressData = undefined; private _hoverProviders: Map = new Map(); + readonly version: string; - constructor(baseDir: string, browser: Browser) { + constructor(readonly versionNumber: number, baseDir: string, browser: Browser) { + this.version = String(versionNumber); this.context = { subscriptions: [], extensionUri: Uri.file(baseDir) }; this._browser = browser; @@ -939,7 +966,7 @@ export class VSCode { return; } initializedPage.evaluate((data: any) => { - const event = new Event('message'); + const event = new globalThis.Event('message'); (event as any).data = data; globalThis.dispatchEvent(event); }, data).catch(() => {}); diff --git a/tests/profile-discovery.spec.ts b/tests/profile-discovery.spec.ts index 18b54d0ad..d329c1bd2 100644 --- a/tests/profile-discovery.spec.ts +++ b/tests/profile-discovery.spec.ts @@ -33,11 +33,11 @@ test('should create run & debug profiles', async ({ activate }, testInfo) => { expect(runProfiles[0].label).toBe(profileTitle); expect(runProfiles[0].kind).toBe(vscode.TestRunProfileKind.Run); - expect(runProfiles[0].isDefault).toBeFalsy(); + expect(runProfiles[0].isDefault).toBeTruthy(); expect(runProfiles[1].label).toBe(profileTitle); expect(runProfiles[1].kind).toBe(vscode.TestRunProfileKind.Debug); - expect(runProfiles[1].isDefault).toBeFalsy(); + expect(runProfiles[1].isDefault).toBeTruthy(); expect(vscode).toHaveExecLog(` > playwright list-files -c playwright.config.js @@ -57,19 +57,19 @@ test('should create run & debug profile per project', async ({ activate }, testI expect(runProfiles[0].label).toBe('projectA — ' + configPath); expect(runProfiles[0].kind).toBe(vscode.TestRunProfileKind.Run); - expect(runProfiles[0].isDefault).toBeFalsy(); + expect(runProfiles[0].isDefault).toBeTruthy(); expect(runProfiles[1].label).toBe('projectA — ' + configPath); expect(runProfiles[1].kind).toBe(vscode.TestRunProfileKind.Debug); - expect(runProfiles[1].isDefault).toBeFalsy(); + expect(runProfiles[1].isDefault).toBeTruthy(); expect(runProfiles[2].label).toBe('projectB — ' + configPath); expect(runProfiles[2].kind).toBe(vscode.TestRunProfileKind.Run); - expect(runProfiles[1].isDefault).toBeFalsy(); + expect(runProfiles[2].isDefault).toBeFalsy(); expect(runProfiles[3].label).toBe('projectB — ' + configPath); expect(runProfiles[3].kind).toBe(vscode.TestRunProfileKind.Debug); - expect(runProfiles[1].isDefault).toBeFalsy(); + expect(runProfiles[3].isDefault).toBeFalsy(); expect(vscode).toHaveExecLog(` > playwright list-files -c playwright.config.js diff --git a/tests/run-tests.spec.ts b/tests/run-tests.spec.ts index 3c836e765..4921769b8 100644 --- a/tests/run-tests.spec.ts +++ b/tests/run-tests.spec.ts @@ -287,7 +287,8 @@ test('should only create test run if file belongs to context', async ({ activate `, }); - const profiles = testController.runProfiles.filter(p => p.kind === vscode.TestRunProfileKind.Run); + const profiles = testController.runProfilesByKind(vscode.TestRunProfileKind.Run); + profiles.forEach(p => p.isDefault = true); let testRuns: TestRun[] = []; testController.onDidCreateTestRun(run => testRuns.push(run)); @@ -347,36 +348,6 @@ test('should only create test run if folder belongs to context', async ({ activa `); }); -test('should only create test run if test belongs to context', async ({ activate }) => { - const { vscode, testController } = await activate({ - 'tests1/playwright.config.js': `module.exports = { testDir: '.' }`, - 'tests2/playwright.config.js': `module.exports = { testDir: '.' }`, - 'tests1/foo1/bar1/test1.spec.ts': ` - import { test } from '@playwright/test'; - test('one', async () => {}); - `, - 'tests2/foo2/bar2/test2.spec.ts': ` - import { test } from '@playwright/test'; - test('two', async () => {}); - `, - }); - - await testController.expandTestItems(/test2.spec.ts/); - const profiles = testController.runProfiles.filter(p => p.kind === vscode.TestRunProfileKind.Run); - const testRuns: TestRun[] = []; - testController.onDidCreateTestRun(run => testRuns.push(run)); - const items = testController.findTestItems(/two/); - await Promise.all(profiles.map(p => p.run(items))); - expect(testRuns).toHaveLength(1); - - expect(vscode).toHaveExecLog(` - tests1> playwright list-files -c playwright.config.js - tests2> playwright list-files -c playwright.config.js - tests2> playwright test -c playwright.config.js --list --reporter=null foo2/bar2/test2.spec.ts - tests2> playwright test -c playwright.config.js foo2/bar2/test2.spec.ts:3 - `); -}); - test('should run all projects at once', async ({ activate }) => { const { vscode, testController } = await activate({ 'playwright.config.js': `module.exports = { @@ -516,7 +487,7 @@ test('should not remove other tests when running focused test', async ({ activat const testItems = testController.findTestItems(/two/); await testController.run(testItems); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -701,14 +672,14 @@ test('should list tests in relative folder', async ({ activate }) => { foo/bar> playwright test -c playwright.config.js --list --reporter=null ../../tests/test.spec.ts `); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] `); }); -test('should specify project', async ({ activate }) => { +test('should filter selected project', async ({ activate }) => { const { vscode, testController } = await activate({ 'playwright.config.js': `module.exports = { projects: [ @@ -727,7 +698,7 @@ test('should specify project', async ({ activate }) => { }); const testItems = testController.findTestItems(/test.spec/); - expect(testItems.length).toBe(2); + expect(testItems.length).toBe(1); const testRun = await testController.run(testItems); expect(testRun.renderLog()).toBe(` tests1 > test.spec.ts > one [2:0] @@ -846,7 +817,7 @@ test('should discover tests after running one test', async ({ activate }) => { await testController.expandTestItems(/test2.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test1.spec.ts - one [2:0] @@ -917,38 +888,6 @@ test('should run tests for folders above root', async ({ activate }) => { `); }); -test('should show warning when tests do not belong to projects', async ({ activate }) => { - const { vscode, testController } = await activate({ - 'tests1/playwright.config.js': `module.exports = { testDir: '.' }`, - 'tests2/playwright.config.js': `module.exports = { testDir: '.' }`, - 'tests1/test1.spec.ts': ` - import { test } from '@playwright/test'; - test('one', async () => {}); - `, - 'tests2/test2.spec.ts': ` - import { test } from '@playwright/test'; - test('two', async () => {}); - `, - }); - - const profile = testController.runProfiles.find(p => p.kind === vscode.TestRunProfileKind.Run)!; - let testRuns: TestRun[] = []; - testController.onDidCreateTestRun(run => testRuns.push(run)); - - { - testRuns = []; - const items = testController.findTestItems(/test2.spec/); - await profile.run(items); - } - - expect(vscode).toHaveExecLog(` - tests1> playwright list-files -c playwright.config.js - tests2> playwright list-files -c playwright.config.js - `); - - expect(vscode.warnings[0]).toContain('Selected test is outside of the Default Profile (config)'); -}); - test('should produce output twice', async ({ activate }) => { const { testController } = await activate({ 'playwright.config.js': `module.exports = { diff --git a/tests/source-map.spec.ts b/tests/source-map.spec.ts index 5db7ac7ad..5904bf4d7 100644 --- a/tests/source-map.spec.ts +++ b/tests/source-map.spec.ts @@ -23,7 +23,7 @@ test('should list files', async ({ activate }) => { 'build/test.spec.js': testSpecJs('test.spec'), 'build/test.spec.js.map': testSpecJsMap('test.spec'), }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts `); @@ -41,7 +41,7 @@ test('should list tests on expand', async ({ activate }) => { }); await testController.expandTestItems(/test.spec.ts/); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -65,7 +65,7 @@ test('should list tests for visible editors', async ({ activate }) => { await vscode.openEditors('**/test.spec.ts'); await new Promise(f => testController.onDidChangeTestItem(f)); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - one [2:0] @@ -86,7 +86,7 @@ test('should pick new files', async ({ activate }) => { 'build/test-1.spec.js.map': testSpecJsMap('test-1.spec'), }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts `); @@ -102,7 +102,7 @@ test('should pick new files', async ({ activate }) => { workspaceFolder.addFile('build/test-2.spec.js.map', testSpecJsMap('test-2.spec')), ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -128,7 +128,7 @@ test('should remove deleted files', async ({ activate }) => { 'build/test-3.spec.js.map': testSpecJsMap('test-3.spec'), }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-2.spec.ts @@ -146,7 +146,7 @@ test('should remove deleted files', async ({ activate }) => { workspaceFolder.removeFile('build/test-2.spec.js.map'), ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test-1.spec.ts - test-3.spec.ts @@ -179,7 +179,7 @@ test('should discover new tests', async ({ activate }) => { workspaceFolder.changeFile('build/test.spec.js.map', testSpecJsMapAfter), ]); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - new [2:0] diff --git a/tests/track-configs.spec.ts b/tests/track-configs.spec.ts index 8d2c2f58f..c1b9c0e7f 100644 --- a/tests/track-configs.spec.ts +++ b/tests/track-configs.spec.ts @@ -14,11 +14,12 @@ * limitations under the License. */ +import { TestRunProfileKind } from './mock/vscode'; import { expect, test } from './utils'; test('should load first config', async ({ activate }) => { const { vscode, testController, workspaceFolder } = await activate({}); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` `); workspaceFolder.addFile('playwright.config.js', `module.exports = { testDir: 'tests' }`); @@ -27,11 +28,10 @@ test('should load first config', async ({ activate }) => { test('one', async () => {}); `); - const golden = ` + await expect(testController).toHaveTestTree(` - tests - test.spec.ts - `; - while (testController.renderTestTree() !== golden) await new Promise(f => setTimeout(f, 200)); + `); expect(vscode).toHaveExecLog(` > playwright list-files -c playwright.config.js @@ -46,7 +46,7 @@ test('should load second config', async ({ activate }) => { test('one', async () => {}); `, }); - expect(testController).toHaveTestTree(` + await expect(testController).toHaveTestTree(` - tests1 - test.spec.ts `); @@ -57,13 +57,16 @@ test('should load second config', async ({ activate }) => { test('one', async () => {}); `); - const golden = ` + await expect.poll(() => testController.runProfilesByKind(TestRunProfileKind.Run)).toHaveLength(2); + const profiles = testController.runProfilesByKind(TestRunProfileKind.Run); + profiles.forEach(p => p.isDefault = true); + + await expect(testController).toHaveTestTree(` - tests1 - test.spec.ts - tests2 - test.spec.ts - `; - while (testController.renderTestTree() !== golden) await new Promise(f => setTimeout(f, 200)); + `); expect(vscode).toHaveExecLog(` > playwright list-files -c playwright1.config.js @@ -85,7 +88,11 @@ test('should remove model for config', async ({ activate }) => { test('one', async () => {}); `, }); - expect(testController).toHaveTestTree(` + + const profiles = testController.runProfilesByKind(TestRunProfileKind.Run); + profiles.forEach(p => p.isDefault = true); + + await expect(testController).toHaveTestTree(` - tests1 - test.spec.ts - tests2 @@ -94,11 +101,10 @@ test('should remove model for config', async ({ activate }) => { workspaceFolder.removeFile('playwright1.config.js'); - const golden = ` + await expect(testController).toHaveTestTree(` - tests2 - test.spec.ts - `; - while (testController.renderTestTree() !== golden) await new Promise(f => setTimeout(f, 200)); + `); expect(vscode).toHaveExecLog(` > playwright list-files -c playwright1.config.js diff --git a/tests/utils.ts b/tests/utils.ts index 849630f9f..05e145662 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -33,15 +33,16 @@ type TestFixtures = { export type WorkerOptions = { useTestServer: boolean; showBrowser: boolean; + vsCodeVersion: number; }; // Make sure connect tests work with the locally-rolled version. process.env.PW_VERSION_OVERRIDE = require('@playwright/test/package.json').version; export const expect = baseExpect.extend({ - toHaveTestTree(testController: TestController, expectedTree: string) { + async toHaveTestTree(testController: TestController, expectedTree: string) { try { - expect(testController.renderTestTree()).toBe(expectedTree); + await expect.poll(() => testController.renderTestTree()).toBe(expectedTree); return { pass: true, message: () => '' }; } catch (e) { return { @@ -69,9 +70,10 @@ export const expect = baseExpect.extend({ export const test = baseTest.extend({ useTestServer: [false, { option: true, scope: 'worker' }], showBrowser: [false, { option: true, scope: 'worker' }], + vsCodeVersion: [1.86, { option: true, scope: 'worker' }], - vscode: async ({ browser }, use) => { - await use(new VSCode(path.resolve(__dirname, '..'), browser)); + vscode: async ({ browser, vsCodeVersion }, use) => { + await use(new VSCode(vsCodeVersion, path.resolve(__dirname, '..'), browser)); }, activate: async ({ vscode, showBrowser, useTestServer }, use, testInfo) => {