diff --git a/media/settingsView.css b/media/settingsView.css index a32e4dcf5..2a9d3ba4e 100644 --- a/media/settingsView.css +++ b/media/settingsView.css @@ -93,3 +93,11 @@ div.separator { body[data-vscode-theme-kind=vscode-dark] div.separator { border-color: rgba(204, 204, 204, 0.2); } + +.list#projects > div .actions { + display: none; +} + +.list#projects > div:hover .actions { + display: flex; +} diff --git a/src/backend.ts b/src/backend.ts index e09e09c19..e7d51ab9e 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -69,6 +69,8 @@ export class BackendServer { fulfill(client); }); serverProcess.on('exit', () => { + if (this._options.dumpIO) + console.log('[server exit]'); fulfill(null); client._onCloseEvent.fire(); }); diff --git a/src/extension.ts b/src/extension.ts index d35c53836..cafd2dbb3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -193,19 +193,21 @@ export class Extension implements RunHooks { vscode.commands.registerCommand('pw.extension.command.closeBrowsers', () => { this._reusedBrowser.closeAllBrowsers(); }), - vscode.commands.registerCommand('pw.extension.command.recordNew', async () => { - if (!this._models.hasEnabledModels()) { + vscode.commands.registerCommand('pw.extension.command.recordNew', async (projectName?: string) => { + const selectedProject = await this._getRecorderProject(projectName); + if (!selectedProject) { vscode.window.showWarningMessage(messageNoPlaywrightTestsFound); return; } - await this._reusedBrowser.record(this._models, true); + await this._reusedBrowser.record(selectedProject, true); }), vscode.commands.registerCommand('pw.extension.command.recordAtCursor', async () => { - if (!this._models.hasEnabledModels()) { + const selectedProject = await this._getRecorderProject(); + if (!selectedProject) { vscode.window.showWarningMessage(messageNoPlaywrightTestsFound); return; } - await this._reusedBrowser.record(this._models, false); + await this._reusedBrowser.record(selectedProject, false); }), vscode.commands.registerCommand('pw.extension.command.toggleModels', async () => { this._settingsView.toggleModels(); @@ -255,6 +257,29 @@ export class Extension implements RunHooks { context.subscriptions.push(this); } + private async _getRecorderProject(externalSelectedProjectName?: string): Promise { + const selectedModel = this._models.selectedModel(); + if (!selectedModel) { + await this._vscode.window.showWarningMessage(this._vscode.l10n.t('No Playwright config selected.')); + return; + } + const enabledProjects = selectedModel.enabledProjects(); + if (!enabledProjects.length) + return selectedModel.projects()[0]; + if (externalSelectedProjectName) { + const selectedProject = selectedModel.projects().find(project => project.name === externalSelectedProjectName); + if (selectedProject) + return selectedProject; + } + if (enabledProjects.length === 1) + return enabledProjects[0]; + const selectedProject = await this._vscode.window.showQuickPick(enabledProjects.map(project => project.name), { + placeHolder: this._vscode.l10n.t('Select a project to run'), + canPickMany: false, + }); + return selectedProject ? enabledProjects.find(project => project.name === selectedProject) : undefined; + } + private async _rebuildModels(showWarnings: boolean): Promise { this._models.clear(); this._testTree.startedLoading(); diff --git a/src/listTests.d.ts b/src/listTests.d.ts index 5e5280e72..e1c0be312 100644 --- a/src/listTests.d.ts +++ b/src/listTests.d.ts @@ -16,12 +16,19 @@ import type { TestError } from './reporter'; -// This matches the structs in packages/playwright-test/src/runner/runner.ts. +// This matches the structs in packages/playwright/src/runner/runner.ts + +export type ProjectUse = { + testIdAttribute?: string; + browserName?: string; + contextOptions: Record; + launchOptions: Record; +}; export type ProjectConfigWithFiles = { name: string; testDir: string; - use: { testIdAttribute?: string }; + use: ProjectUse; files: string[]; }; diff --git a/src/playwrightTest.ts b/src/playwrightTest.ts index 11dd6ed65..45f931e17 100644 --- a/src/playwrightTest.ts +++ b/src/playwrightTest.ts @@ -30,7 +30,6 @@ export type TestConfig = { configFile: string; cli: string; version: number; - testIdAttributeName?: string; }; const pathSeparator = process.platform === 'win32' ? ';' : ':'; diff --git a/src/reusedBrowser.ts b/src/reusedBrowser.ts index c395d93ca..61239a434 100644 --- a/src/reusedBrowser.ts +++ b/src/reusedBrowser.ts @@ -15,7 +15,7 @@ */ import { TestConfig } from './playwrightTest'; -import type { TestModel, TestModelCollection } from './testModel'; +import type { TestModel, TestModelCollection, TestProject } from './testModel'; import { createGuid } from './utils'; import * as vscodeTypes from './vscodeTypes'; import path from 'path'; @@ -44,6 +44,7 @@ export class ReusedBrowser implements vscodeTypes.Disposable { private _editOperations = Promise.resolve(); private _pausedOnPagePause = false; private _settingsModel: SettingsModel; + private _autoCloseTimer?: NodeJS.Timeout; constructor(vscode: vscodeTypes.VSCode, settingsModel: SettingsModel, envProvider: () => NodeJS.ProcessEnv) { this._vscode = vscode; @@ -182,13 +183,19 @@ export class ReusedBrowser implements vscodeTypes.Disposable { } private _pageCountChanged(pageCount: number) { + if (this._autoCloseTimer) + clearTimeout(this._autoCloseTimer); this._pageCount = pageCount; this._onPageCountChangedEvent.fire(pageCount); if (this._isRunningTests) return; if (pageCount) return; - this._stop(); + const heartBeat = () => { + if (!this._pageCount) + this._stop(); + }; + this._autoCloseTimer = setTimeout(heartBeat, 5000); } browserServerWSEndpoint() { @@ -242,9 +249,8 @@ export class ReusedBrowser implements vscodeTypes.Disposable { return !this._isRunningTests && !!this._pageCount; } - async record(models: TestModelCollection, recordNew: boolean) { - const selectedModel = models.selectedModel(); - if (!selectedModel || !this._checkVersion(selectedModel.config)) + async record(project: TestProject, recordNew: boolean) { + if (!this._checkVersion(project.model.config)) return; if (!this.canRecord()) { this._vscode.window.showWarningMessage( @@ -256,7 +262,7 @@ export class ReusedBrowser implements vscodeTypes.Disposable { location: this._vscode.ProgressLocation.Notification, title: 'Playwright codegen', cancellable: true - }, async (progress, token) => this._doRecord(progress, selectedModel, recordNew, token)); + }, async (progress, token) => this._doRecord(progress, project, recordNew, token)); } highlight(selector: string) { @@ -290,7 +296,8 @@ export class ReusedBrowser implements vscodeTypes.Disposable { return true; } - private async _doRecord(progress: vscodeTypes.Progress<{ message?: string; increment?: number }>, model: TestModel, recordNew: boolean, token: vscodeTypes.CancellationToken) { + private async _doRecord(progress: vscodeTypes.Progress<{ message?: string; increment?: number }>, selectedProject: TestProject, recordNew: boolean, token: vscodeTypes.CancellationToken) { + const model = selectedProject.model; const startBackend = this._startBackendIfNeeded(model.config); let editor: vscodeTypes.TextEditor | undefined; if (recordNew) @@ -309,7 +316,13 @@ export class ReusedBrowser implements vscodeTypes.Disposable { } try { - await this._backend?.setMode({ mode: 'recording', testIdAttributeName: model.config.testIdAttributeName }); + await this._backend?.setMode({ + mode: 'recording', + testIdAttributeName: selectedProject.useOptions.testIdAttribute, + browserName: selectedProject.useOptions.browserName, + launchOptions: selectedProject.useOptions.launchOptions, + contextOptions: selectedProject.useOptions.contextOptions, + }); } catch (e) { showExceptionAsUserError(this._vscode, model, e as Error); this._stop(); @@ -446,7 +459,7 @@ export class Backend extends BackendClient { await this.send('navigate', params); } - async setMode(params: { mode: 'none' | 'inspecting' | 'recording', testIdAttributeName?: string }) { + async setMode(params: { mode: 'none' | 'inspecting' | 'recording', testIdAttributeName?: string, browserName?: string, launchOptions?: Record, contextOptions?: Record }) { await this.send('setRecorderMode', params); } diff --git a/src/settingsView.ts b/src/settingsView.ts index d5ae25ed9..17cfd529c 100644 --- a/src/settingsView.ts +++ b/src/settingsView.ts @@ -71,7 +71,7 @@ export class SettingsView extends DisposableBase implements vscodeTypes.WebviewV webviewView.webview.html = htmlForWebview(this._vscode, this._extensionUri, webviewView.webview); this._disposables.push(webviewView.webview.onDidReceiveMessage(data => { if (data.method === 'execute') { - this._vscode.commands.executeCommand(data.params.command); + this._vscode.commands.executeCommand(data.params.command, ...(data.params.args ?? [])); } else if (data.method === 'toggle') { this._vscode.commands.executeCommand(`pw.extension.toggle.${data.params.setting}`); } else if (data.method === 'setProjectEnabled') { @@ -109,37 +109,20 @@ export class SettingsView extends DisposableBase implements vscodeTypes.WebviewV private _updateActions() { const actions = [ - { - command: 'pw.extension.command.inspect', - svg: ``, - text: this._vscode.l10n.t('Pick locator'), - }, - { - command: 'pw.extension.command.recordNew', - svg: ``, - text: this._vscode.l10n.t('Record new'), - disabled: !this._reusedBrowser.canRecord(), - }, - { - command: 'pw.extension.command.recordAtCursor', - svg: ``, - text: this._vscode.l10n.t('Record at cursor'), - disabled: !this._reusedBrowser.canRecord(), - }, { command: 'testing.showMostRecentOutput', - svg: ``, + icon: 'showMostRecentOutput', text: this._vscode.l10n.t('Reveal test output'), }, { command: 'pw.extension.command.closeBrowsers', - svg: ``, + icon: 'closeBrowsers', text: this._vscode.l10n.t('Close all browsers'), disabled: !this._reusedBrowser.canClose(), }, { command: 'pw.extension.command.toggleModels', - svg: ``, + icon: 'toggleModels', title: this._vscode.l10n.t('Toggle Playwright Configs'), location: 'configToolbar', }, @@ -243,6 +226,15 @@ function htmlForWebview(vscode: vscodeTypes.VSCode, extensionUri: vscodeTypes.Ur