├── .eslintignore ├── .vscode ├── extensions.json ├── tasks.json └── launch.json ├── images ├── example-test.png ├── test-explorer.png ├── playwright-logo.png └── playwright-logo.svg ├── .gitignore ├── .vscodeignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ └── ci.yml ├── tsconfig.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── utils ├── copyright.js └── roll-locally.js ├── src ├── debugSessionName.ts ├── disposableBase.ts ├── traceViewer.ts ├── babelBundle.ts ├── listTests.d.ts ├── oopReporter.ts ├── vscodeTypes.ts ├── terminalLinkProvider.ts ├── playwrightFinder.ts ├── debugTransform.ts ├── multimap.ts ├── methodNames.ts ├── common.ts ├── upstream │ ├── events.ts │ └── testServerInterface.ts ├── locatorsView.script.ts ├── transport.ts ├── workspaceObserver.ts ├── spawnTraceViewer.ts ├── ansi2html.ts ├── backend.ts ├── installer.ts ├── settingsView.script.ts ├── settingsModel.ts ├── babelHighlightUtil.ts └── locatorsView.ts ├── tests-integration ├── globalSetup.ts ├── playwright.config.ts └── tests │ ├── basic.test.ts │ └── baseTest.ts ├── SUPPORT.md ├── media ├── traceViewer.css └── common.css ├── PlaywrightVSCode.signproj ├── playwright.config.ts ├── package.nls.zh-CN.json ├── l10n ├── bundle.l10n.zh-CN.json ├── bundle.l10n.it.json ├── bundle.l10n.de.json └── bundle.l10n.fr.json ├── package.nls.json ├── tests ├── run-annotations.spec.ts ├── global-errors.spec.ts ├── pnp.spec.ts ├── problems.spec.ts ├── update-snapshots.spec.ts ├── highlight-locators.spec.ts ├── decorations.spec.ts ├── auto-close.spec.ts ├── project-setup.spec.ts ├── codegen.spec.ts ├── pick-selector.spec.ts ├── trace-viewer.spec.ts └── project-tree.spec.ts ├── package.nls.it.json ├── package.nls.de.json ├── package.nls.fr.json ├── .azure-pipelines └── publish.yml ├── SECURITY.md ├── .eslintrc.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | vscode.d.ts 2 | vscode.proposed.d.ts 3 | /out/ 4 | /test-results/ 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bierner.lit-html" 4 | ] 5 | } -------------------------------------------------------------------------------- /images/example-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-vscode/HEAD/images/example-test.png -------------------------------------------------------------------------------- /images/test-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-vscode/HEAD/images/test-explorer.png -------------------------------------------------------------------------------- /images/playwright-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-vscode/HEAD/images/playwright-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | .DS_Store 6 | test-results/ 7 | playwright-report/ 8 | vscode.d.ts 9 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !out/*.js 3 | !l10n/*.json 4 | !package.nls.json 5 | !package.nls.*.json 6 | !images 7 | !media/* 8 | !LICENSE 9 | !README.md 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: File an issue 4 | url: https://github.com/microsoft/playwright/issues 5 | about: We track issues in the microsoft/playwright repository 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "lib": ["esnext", "DOM"], 6 | "strict": true, 7 | "rootDir": ".", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "noEmit": true, 11 | "allowJs": true 12 | }, 13 | "include": ["**/*.ts", "**/*.js", ".eslintrc.js"], 14 | "exclude": ["node_modules", "test-results"] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | ## Releasing 3 | 4 | 1. Briefly review changes since last release with the team and get approval. Double-check tests are healthy. 5 | 2. Run `npm version patch --no-git-tag-version` and post a PR with it. Example: https://github.com/microsoft/playwright-vscode/pull/695 6 | 3. Get it approved + merged. 7 | 4. Draft a new release under https://github.com/microsoft/playwright-vscode/releases, use the merged commit as target. 8 | 5. Have somebody review the release notes, then hit "Release". An Azure Pipeline will release the extension to the marketplace. Rerun if it fails, that's ok. 9 | -------------------------------------------------------------------------------- /utils/copyright.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /src/debugSessionName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const debugSessionName = 'Playwright Test'; 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension in Playwright folder", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}", 15 | "${workspaceFolder}/../playwright/examples/todomvc" 16 | ], 17 | "outFiles": [ 18 | "${workspaceFolder}/out/**/*.js" 19 | ], 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests-integration/globalSetup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { downloadAndUnzipVSCode } from '@vscode/test-electron/out/download'; 17 | 18 | export default async () => { 19 | await downloadAndUnzipVSCode('insiders'); 20 | }; 21 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template. 6 | 7 | For help and questions about using this project, please see the [docs site for Playwright][docs]. 8 | 9 | Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for Playwright Test for VS Code is limited to the resources listed above. 14 | 15 | [gh-issues]: https://github.com/microsoft/playwright/issues/ 16 | [docs]: https://playwright.dev/ 17 | [discord-server]: https://aka.ms/playwright/discord 18 | -------------------------------------------------------------------------------- /media/traceViewer.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | html, body { 18 | height: 100%; 19 | min-height: 100%; 20 | padding: 0; 21 | margin: 0; 22 | overflow-y: hidden; 23 | } 24 | 25 | iframe { 26 | width: 100%; 27 | height: 100%; 28 | border: none; 29 | } 30 | -------------------------------------------------------------------------------- /src/disposableBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vscodeTypes from './vscodeTypes'; 18 | 19 | export class DisposableBase implements vscodeTypes.Disposable { 20 | protected _disposables: vscodeTypes.Disposable[] = []; 21 | 22 | dispose() { 23 | for (const d of this._disposables) 24 | d.dispose(); 25 | this._disposables = []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/traceViewer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export type TraceViewer = { 18 | currentFile(): string | undefined; 19 | willRunTests(): Promise; 20 | open(file?: string): Promise; 21 | reveal?(): Promise; 22 | close(): void; 23 | infoForTest(): Promise<{ 24 | type: string; 25 | serverUrlPrefix?: string; 26 | testConfigFile: string; 27 | traceFile?: string; 28 | visible: boolean; 29 | } | undefined>; 30 | }; 31 | -------------------------------------------------------------------------------- /src/babelBundle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { types as t } from '@babel/core'; 18 | import { parse } from '@babel/core'; 19 | export { parse }; 20 | export type ParseResult = ReturnType; 21 | import traverseFunc from '@babel/traverse'; 22 | export const traverse = traverseFunc; 23 | export type { SourceLocation } from '@babel/types'; 24 | export { declare } from '@babel/helper-plugin-utils'; 25 | 26 | export const babelPluginProposalDecorators = require('@babel/plugin-proposal-decorators'); 27 | export const babelPresetTypescript = require('@babel/preset-typescript'); 28 | -------------------------------------------------------------------------------- /PlaywrightVSCode.signproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 9 | 10 | 11 | VSCodePublisher 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/listTests.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type { TestError } from './upstream/reporter'; 18 | 19 | // This matches the structs in packages/playwright-test/src/runner/runner.ts. 20 | 21 | export type ProjectConfigWithFiles = { 22 | name: string; 23 | testDir: string; 24 | use: { 25 | // Legacy attribute, this is now part of FullProject['use']. 26 | // Remove once https://github.com/microsoft/playwright/commit/1af4e367f4a46323f3b5a013527b944fe3176203 is widely available. 27 | testIdAttribute?: string; 28 | }; 29 | files: string[]; 30 | }; 31 | 32 | export type ConfigListFilesReport = { 33 | projects: ProjectConfigWithFiles[]; 34 | error?: TestError; 35 | }; 36 | 37 | export type ConfigFindRelatedTestFilesReport = { 38 | testFiles: string[]; 39 | errors?: TestError[]; 40 | }; 41 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { defineConfig } from '@playwright/test'; 17 | import { WorkerOptions } from './tests/utils'; 18 | 19 | export default defineConfig({ 20 | testDir: './tests', 21 | outputDir: './test-results/inner', 22 | fullyParallel: true, 23 | forbidOnly: !!process.env.CI, 24 | workers: process.env.CI ? 1 : undefined, 25 | reporter: process.env.CI ? [ 26 | ['line'], 27 | ['blob'], 28 | ] : [ 29 | ['line'] 30 | ], 31 | tag: process.env.PW_TAG, // Set when running vscode extension tests in playwright repo CI. 32 | projects: [ 33 | { 34 | name: 'default', 35 | }, 36 | { 37 | name: 'default-reuse', 38 | use: { 39 | showBrowser: true, 40 | } 41 | }, 42 | { 43 | name: 'default-trace', 44 | use: { 45 | showTrace: true, 46 | } 47 | }, 48 | ] 49 | }); 50 | -------------------------------------------------------------------------------- /src/oopReporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { TeleReporterEmitter } from './upstream/teleEmitter'; 18 | import { FullResult } from './upstream/reporter'; 19 | 20 | class TeleReporter extends TeleReporterEmitter { 21 | private _hasSender: boolean; 22 | 23 | constructor(options: any) { 24 | let messageSink: (message: any) => void; 25 | if (options?._send) { 26 | messageSink = options._send; 27 | } else { 28 | messageSink = message => { 29 | console.log(message); 30 | }; 31 | } 32 | super(messageSink, { omitBuffers: false, omitOutput: true }); 33 | this._hasSender = !!options?._send; 34 | } 35 | 36 | async onEnd(result: FullResult) { 37 | await super.onEnd(result); 38 | // Embedder is responsible for terminating the connection. 39 | if (!this._hasSender) 40 | await new Promise(() => {}); 41 | } 42 | } 43 | 44 | export default TeleReporter; 45 | -------------------------------------------------------------------------------- /tests-integration/playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { defineConfig } from '@playwright/test'; 17 | import { TestOptions } from './tests/baseTest'; 18 | 19 | export default defineConfig({ 20 | reporter: process.env.CI ? 'html' : 'list', 21 | timeout: 120_000, 22 | workers: 1, 23 | expect: { 24 | timeout: 30_000, 25 | }, 26 | globalSetup: './globalSetup', 27 | projects: [ 28 | { 29 | name: 'npm', 30 | use: { 31 | packageManager: 'npm', 32 | } 33 | }, 34 | { 35 | name: 'pnpm', 36 | use: { 37 | packageManager: 'pnpm', 38 | } 39 | }, 40 | { 41 | name: 'pnpm-pnp', 42 | use: { 43 | packageManager: 'pnpm-pnp', 44 | } 45 | }, 46 | { 47 | name: 'yarn-berry', 48 | use: { 49 | packageManager: 'yarn-berry', 50 | } 51 | }, 52 | { 53 | name: 'yarn-classic', 54 | use: { 55 | packageManager: 'yarn-classic', 56 | } 57 | } 58 | ] 59 | }); 60 | -------------------------------------------------------------------------------- /src/vscodeTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export type { 18 | CancellationToken, 19 | CancellationTokenSource, 20 | ColorThemeKind, 21 | DebugSession, 22 | DecorationOptions, 23 | Diagnostic, 24 | DiagnosticCollection, 25 | Disposable, 26 | Event, 27 | EventEmitter, 28 | ExtensionContext, 29 | FileSystemWatcher, 30 | InputBox, 31 | Location, 32 | Position, 33 | Progress, 34 | QuickPickItem, 35 | Range, 36 | TestItem, 37 | TestItemCollection, 38 | TestMessage, 39 | TestMessageStackFrame, 40 | TestRun, 41 | TestRunProfile, 42 | TestRunRequest, 43 | TextEditor, 44 | TextEditorDecorationType, 45 | TextDocument, 46 | TestController, 47 | TestTag, 48 | TreeDataProvider, 49 | TreeItem, 50 | TreeView, 51 | Uri, 52 | Webview, 53 | WebviewPanel, 54 | WebviewView, 55 | WebviewViewProvider, 56 | WebviewViewResolveContext, 57 | WorkspaceConfiguration, 58 | TerminalLink, 59 | } from 'vscode'; 60 | 61 | export type VSCode = typeof import('vscode') & { 62 | isUnderTest?: boolean; 63 | }; 64 | -------------------------------------------------------------------------------- /src/terminalLinkProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as vscodeTypes from './vscodeTypes'; 17 | 18 | export function registerTerminalLinkProvider(vscode: vscodeTypes.VSCode): vscodeTypes.Disposable { 19 | return vscode.window.registerTerminalLinkProvider({ 20 | provideTerminalLinks: (context, token) => { 21 | // The end is either two spaces (box is expanded) or the right box character (end of box is reached). 22 | const supportedCommands = /((npx|pnpm exec|yarn) playwright (show-report|show-trace|install).*?)( | ║|$)/; 23 | const match = context.line.match(supportedCommands); 24 | if (!match) 25 | return []; 26 | const command = match[1]; 27 | return [ 28 | { 29 | command, 30 | startIndex: match.index!, 31 | length: command.length, 32 | tooltip: `Run ${command}`, 33 | } 34 | ]; 35 | }, 36 | handleTerminalLink: (link: vscodeTypes.TerminalLink & { command: string }) => { 37 | const terminal = vscode.window.activeTerminal; 38 | if (terminal) 39 | terminal.sendText(link.command); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/playwrightFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | 19 | const packages = [ 20 | '@playwright/test', 21 | 'playwright', 22 | '@playwright/experimental-ct-react', 23 | '@playwright/experimental-ct-react17', 24 | '@playwright/experimental-ct-vue', 25 | '@playwright/experimental-ct-vue2', 26 | '@playwright/experimental-ct-solid', 27 | '@playwright/experimental-ct-svelte', 28 | ]; 29 | 30 | for (const packageName of packages) { 31 | let packageJSONPath; 32 | try { 33 | packageJSONPath = require.resolve(path.join(packageName, 'package.json'), { paths: [process.cwd()] }); 34 | } catch (e) { 35 | continue; 36 | } 37 | try { 38 | const packageJSON = require(packageJSONPath); 39 | const { version } = packageJSON; 40 | const v = parseFloat(version.replace(/-(next|beta)$/, '')); 41 | const cli = path.join(packageJSONPath, '../cli.js'); 42 | console.log(JSON.stringify({ version: v, cli }, null, 2)); 43 | process.exit(0); 44 | } catch (e) { 45 | console.log(JSON.stringify({ error: String(e) }, null, 2)); 46 | process.exit(0); 47 | } 48 | } 49 | 50 | console.log(JSON.stringify({ error: 'Playwright installation not found for ' + process.cwd() })); 51 | -------------------------------------------------------------------------------- /package.nls.zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "在 Visual Studio Code 中运行 Playwright Test 测试用例.", 3 | "contributes.command.pw.extension.install": "安装 Playwright", 4 | "contributes.command.pw.extension.installBrowsers": "安装 Playwright 浏览器", 5 | "contributes.command.pw.extension.command.inspect": "拾取选择器", 6 | "contributes.command.pw.extension.command.clearCache": "清除缓存", 7 | "contributes.command.pw.extension.command.closeBrowsers": "关闭全部 Playwright 浏览器", 8 | "contributes.command.pw.extension.command.recordNew": "录制新用例", 9 | "contributes.command.pw.extension.command.recordAtCursor": "在当前光标处开始录制", 10 | "contributes.command.pw.extension.command.runGlobalSetup": "运行全局设置", 11 | "contributes.command.pw.extension.command.runGlobalTeardown": "运行全局拆卸", 12 | "contributes.command.pw.extension.command.startDevServer": "启动开发服务器", 13 | "contributes.command.pw.extension.command.stopDevServer": "停止开发服务器", 14 | "contributes.command.pw.extension.command.toggleModels": "选择 Playwright 配置", 15 | "contributes.command.pw.extension.toggle.reuseBrowser": "是否复用 Playwright 浏览器", 16 | "contributes.command.pw.extension.toggle.showTrace": "是否显示 Playwright 浏览器的跟踪", 17 | "contributes.command.pw.extension.toggle.runGlobalSetupOnEachRun": "切换 Playwright 全局设置的重用", 18 | "contributes.command.pw.extension.toggle.pickLocatorCopyToClipboard": "切换 Playwright 定位器选择器自动复制到剪贴板", 19 | "configuration.playwright.env": "Playwright Test 运行环境变量", 20 | "configuration.playwright.reuseBrowser": "在测试用例间显示并复用 Playwright 浏览器", 21 | "configuration.playwright.showTrace": "显示 Playwright 浏览器的跟踪", 22 | "configuration.playwright.runGlobalSetupOnEachRun": "不要重复使用全局设置", 23 | "configuration.playwright.pickLocatorCopyToClipboard": "自动将选中的定位器复制到剪贴板", 24 | "configuration.playwright.updateSnapshots": "更新快照", 25 | "configuration.playwright.updateSourceMethod": "更新源方法", 26 | "views.test.pw.extension.locatorsView": "定位器", 27 | "views.test.pw.extension.settingsView": "Playwright" 28 | } 29 | -------------------------------------------------------------------------------- /tests-integration/tests/basic.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { test, expect } from './baseTest'; 17 | import child_process from 'node:child_process'; 18 | 19 | test('should be able to execute the first test of the example project', async ({ workbox }) => { 20 | await workbox.getByRole('treeitem', { name: 'tests', exact: true }).locator('a').click(); 21 | await workbox.getByRole('treeitem', { name: 'example.spec.ts' }).locator('a').click(); 22 | await expect(workbox.locator('.testing-run-glyph'), 'there are two tests in the file').toHaveCount(2); 23 | await workbox.locator('.testing-run-glyph').first().click(); 24 | const passedLocator = workbox.locator('.monaco-editor').locator('.codicon-testing-passed-icon'); 25 | await expect(passedLocator).toHaveCount(1); 26 | }); 27 | 28 | test('is proper yarn classic', async ({ packageManager, createTempDir }) => { 29 | test.skip(packageManager !== 'yarn-classic'); 30 | const result = child_process.execSync('yarn --version', { cwd: await createTempDir() }); 31 | expect(result.toString()).toMatch(/^1\./); 32 | }); 33 | 34 | test('is proper yarn berry', async ({ packageManager, createTempDir }) => { 35 | test.skip(packageManager !== 'yarn-berry'); 36 | const result = child_process.execSync('yarn --version', { cwd: await createTempDir() }); 37 | expect(result.toString()).toMatch(/^4\./); 38 | }); 39 | -------------------------------------------------------------------------------- /l10n/bundle.l10n.zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept to copy locator into clipboard": "确认保存到剪贴板", 3 | "Automatically copy picked locator to clipboard": "自动将选中的定位器复制到剪贴板", 4 | "Can't close browsers while running tests": "运行测试时不能关闭浏览器", 5 | "Can't record while running tests": "运行测试时不能录制", 6 | "Clear cache": "清除缓存", 7 | "Close all browsers": "关闭全部 Playwright 浏览器", 8 | "Copy on pick": "选择时复制", 9 | "Locator": "定位器", 10 | "No Playwright tests found.": "没有找到 Playwright Test", 11 | "Pick locator": "拾取选择器", 12 | "Playwright Test v{0} or newer is required": "需要 v{0} 或以上版本的 Playwright Test", 13 | "Playwright v{0}+ is required for {1} to work, v{2} found": "{1} 需要 Playwright v{0} 或以上的版本,当前版本 v{2}", 14 | "Please install Playwright Test via running `npm i --save-dev @playwright/test`": "请通过运行 `npm i --save-dev @playwright/test` 安装 Playwright Test", 15 | "Record at cursor": "在当前光标处开始录制", 16 | "Record new": "录制新用例", 17 | "Reveal test output": "显示测试输出", 18 | "Run global setup": "运行全局设置", 19 | "Run global teardown": "运行全局拆卸", 20 | "Select Playwright Config": "选择 Playwright 配置", 21 | "Show browser mode does not work in remote vscode": "显示浏览器模式在远程vscode中不起作用", 22 | "Show browser": "显示浏览器", 23 | "Run global setup on each run": "每次运行全局设置", 24 | "Show trace viewer": "显示 Trace Viewer", 25 | "Start dev server": "启动 dev 服务器", 26 | "Stop dev server": "停止 dev 服务器", 27 | "this feature": "该功能", 28 | "Toggle Playwright Configs": "切换 Playwright 配置", 29 | "Update snapshots": "更新快照", 30 | "Update method" : "更新方法", 31 | "Select All": "全选", 32 | "Unselect All": "取消全选", 33 | "When enabled, Playwright will reuse the browser instance between tests. This will disable parallel execution.": "启用后,Playwright 将在测试之间重复使用浏览器实例。这将禁用并行执行。", 34 | "one worker": "一个 worker", 35 | "Project is disabled in the Playwright sidebar": "在 Playwright 侧边栏中禁用项目", 36 | "Enable project": "启用项目", 37 | "Cancel": "取消", 38 | "SETTINGS": "设置", 39 | "TOOLS": "工具", 40 | "SETUP": "设置", 41 | "PROJECTS": "项目", 42 | "CONFIGS": "配置" 43 | } 44 | -------------------------------------------------------------------------------- /src/debugTransform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the 'License'); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an 'AS IS' BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { declare, t } from './babelBundle'; 18 | 19 | export default declare(api => { 20 | api.assertVersion(7); 21 | 22 | return { 23 | name: 'playwright-debug-transform', 24 | visitor: { 25 | ExpressionStatement(path) { 26 | const expression = path.node.expression; 27 | const isAwaitExpression = t.isAwaitExpression(expression); 28 | const isCallExpression = t.isCallExpression(expression); 29 | if (!isAwaitExpression && !isCallExpression) 30 | return; 31 | // Prevent re-enterability without calling path.skip. 32 | if (path.parentPath.isBlockStatement() && path.parentPath.parentPath.isTryStatement()) 33 | return; 34 | if (isAwaitExpression && !t.isCallExpression(expression.argument)) 35 | return; 36 | path.replaceWith(t.tryStatement( 37 | t.blockStatement([ 38 | path.node 39 | ]), 40 | t.catchClause( 41 | t.identifier('__playwright_error__'), 42 | t.blockStatement([ 43 | t.debuggerStatement(), 44 | t.throwStatement(t.identifier('__playwright_error__')) 45 | ]) 46 | ) 47 | )); 48 | 49 | // Patch source map. 50 | path.node.start = expression.start; 51 | path.node.end = expression.end; 52 | path.node.loc = expression.loc; 53 | path.node.range = expression.range; 54 | } 55 | } 56 | }; 57 | }); -------------------------------------------------------------------------------- /src/multimap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class MultiMap { 18 | private _map: Map; 19 | 20 | constructor() { 21 | this._map = new Map(); 22 | } 23 | 24 | set(key: K, value: V) { 25 | let values = this._map.get(key); 26 | if (!values) { 27 | values = []; 28 | this._map.set(key, values); 29 | } 30 | values.push(value); 31 | } 32 | 33 | get(key: K): V[] { 34 | return this._map.get(key) || []; 35 | } 36 | 37 | has(key: K): boolean { 38 | return this._map.has(key); 39 | } 40 | 41 | delete(key: K, value: V) { 42 | const values = this._map.get(key); 43 | if (!values) 44 | return; 45 | if (values.includes(value)) 46 | this._map.set(key, values.filter(v => value !== v)); 47 | } 48 | 49 | deleteAll(key: K) { 50 | this._map.delete(key); 51 | } 52 | 53 | hasValue(key: K, value: V): boolean { 54 | const values = this._map.get(key); 55 | if (!values) 56 | return false; 57 | return values.includes(value); 58 | } 59 | 60 | get size(): number { 61 | return this._map.size; 62 | } 63 | 64 | [Symbol.iterator](): Iterator<[K, V[]]> { 65 | return this._map[Symbol.iterator](); 66 | } 67 | 68 | keys(): IterableIterator { 69 | return this._map.keys(); 70 | } 71 | 72 | values(): Iterable { 73 | const result: V[] = []; 74 | for (const key of this.keys()) 75 | result.push(...this.get(key)); 76 | return result; 77 | } 78 | 79 | clear() { 80 | this._map.clear(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/methodNames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const asyncMatchers = [ 18 | 'toBeChecked', 19 | 'toBeDisabled', 20 | 'toBeEditable', 21 | 'toBeEmpty', 22 | 'toBeEnabled', 23 | 'toBeFocused', 24 | 'toBeHidden', 25 | 'toContainText', 26 | 'toHaveAttribute', 27 | 'toHaveClass', 28 | 'toHaveCount', 29 | 'toHaveCSS', 30 | 'toHaveId', 31 | 'toHaveJSProperty', 32 | 'toHaveText', 33 | 'toHaveValue', 34 | 'toBeVisible', 35 | ]; 36 | 37 | export const pageMethods = [ 38 | 'check', 39 | 'click', 40 | 'dblclick', 41 | 'dragAndDrop', 42 | 'fill', 43 | 'focus', 44 | 'getAttribute', 45 | 'hover', 46 | 'innerHTML', 47 | 'innerText', 48 | 'inputValue', 49 | 'isChecked', 50 | 'isDisabled', 51 | 'isEditable', 52 | 'isEnabled', 53 | 'isHidden', 54 | 'isVisible', 55 | 'press', 56 | 'selectOption', 57 | 'setChecked', 58 | 'setInputFiles', 59 | 'tap', 60 | 'textContent', 61 | 'type', 62 | 'uncheck' 63 | ]; 64 | 65 | export const locatorMethods = [ 66 | 'locator', 67 | 'getByAltText', 68 | 'getByLabel', 69 | 'getByPlaceholder', 70 | 'getByRole', 71 | 'getByTestId', 72 | 'getByText', 73 | 'getByTitle', 74 | 'first', 75 | 'last', 76 | 'and', 77 | 'or', 78 | 'nth', 79 | 'filter', 80 | ]; 81 | 82 | export const locatorMethodRegex = /\.\s*(check|click|fill|type|locator|getBy[\w]+|first|last|nth|filter)\(/; 83 | 84 | export function replaceActionWithLocator(expression: string) { 85 | return expression.replace(/\.\s*(?:check|click|fill|type)\(([^,]+)(?:,\s*{.*})\)/, '.locator($1)'); 86 | } 87 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Run Playwright Test tests in Visual Studio Code.", 3 | "contributes.command.pw.extension.install": "Install Playwright", 4 | "contributes.command.pw.extension.installBrowsers": "Install Playwright Browsers", 5 | "contributes.command.pw.extension.command.inspect": "Pick locator", 6 | "contributes.command.pw.extension.command.clearCache": "Clear cache", 7 | "contributes.command.pw.extension.command.closeBrowsers": "Close all Playwright browsers", 8 | "contributes.command.pw.extension.command.recordNew": "Record new", 9 | "contributes.command.pw.extension.command.recordAtCursor": "Record at cursor", 10 | "contributes.command.pw.extension.command.runGlobalSetup": "Run global setup", 11 | "contributes.command.pw.extension.command.runGlobalTeardown": "Run global teardown", 12 | "contributes.command.pw.extension.command.startDevServer": "Start dev server", 13 | "contributes.command.pw.extension.command.stopDevServer": "Stop dev server", 14 | "contributes.command.pw.extension.command.toggleModels": "Toggle Playwright configs", 15 | "contributes.command.pw.extension.toggle.reuseBrowser": "Toggle Playwright browser reuse", 16 | "contributes.command.pw.extension.toggle.showTrace": "Toggle Playwright Trace Viewer", 17 | "contributes.command.pw.extension.toggle.runGlobalSetupOnEachRun": "Toggle Playwright global setup reuse", 18 | "contributes.command.pw.extension.toggle.pickLocatorCopyToClipboard": "Toggle Playwright locator picker automatically copying to clipboard", 19 | "configuration.playwright.env": "Environment variables to pass to Playwright Test.", 20 | "configuration.playwright.reuseBrowser": "Show & reuse browser between tests.", 21 | "configuration.playwright.showTrace": "Show Trace Viewer.", 22 | "configuration.playwright.runGlobalSetupOnEachRun": "Run global setup on each run.", 23 | "configuration.playwright.pickLocatorCopyToClipboard": "Automatically copy picked locator to clipboard.", 24 | "configuration.playwright.updateSnapshots": "Update snapshots", 25 | "configuration.playwright.updateSourceMethod": "Update source method", 26 | "views.test.pw.extension.locatorsView": "Locators", 27 | "views.test.pw.extension.settingsView": "Playwright" 28 | } 29 | -------------------------------------------------------------------------------- /tests/run-annotations.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect, test } from './utils'; 18 | 19 | test('should mark test as skipped', async ({ activate }) => { 20 | const { vscode, testController } = await activate({ 21 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 22 | 'tests/test.spec.ts': ` 23 | import { test } from '@playwright/test'; 24 | test('pass', async () => {}); 25 | test('skipped', async () => { 26 | test.skip(true, 'Test skipped'); 27 | }); 28 | test('fixme', async () => { 29 | test.fixme(true, 'Test to be fixed'); 30 | }); 31 | test('fails', async () => { 32 | test.fail(true, 'Test should fail'); 33 | expect(1).toBe(2); 34 | }); 35 | `, 36 | }); 37 | 38 | const testRun = await testController.run(); 39 | expect(testRun.renderLog()).toBe(` 40 | tests > test.spec.ts > pass [2:0] 41 | enqueued 42 | started 43 | passed 44 | tests > test.spec.ts > skipped [3:0] 45 | enqueued 46 | started 47 | skipped 48 | tests > test.spec.ts > fixme [6:0] 49 | enqueued 50 | started 51 | skipped 52 | tests > test.spec.ts > fails [9:0] 53 | enqueued 54 | started 55 | passed 56 | `); 57 | 58 | await expect(vscode).toHaveConnectionLog([ 59 | { method: 'listFiles', params: {} }, 60 | { method: 'runGlobalSetup', params: {} }, 61 | { 62 | method: 'runTests', 63 | params: expect.objectContaining({ 64 | locations: [], 65 | testIds: undefined 66 | }) 67 | }, 68 | ]); 69 | }); 70 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | declare global { 18 | function acquireVsCodeApi(): { postMessage(msg: any): void }; 19 | } 20 | 21 | export const vscode = acquireVsCodeApi(); 22 | 23 | export interface Config { 24 | configFile: string; 25 | } 26 | 27 | export interface ProjectEntry { 28 | name: string; 29 | enabled: boolean; 30 | } 31 | 32 | export interface ActionDescriptor { 33 | command: string; 34 | text: string; 35 | svg: string; 36 | title?: string; 37 | location?: string; 38 | hidden?: boolean; 39 | disabled?: boolean; 40 | } 41 | 42 | export function createAction(action: ActionDescriptor, options?: { omitText?: boolean }): HTMLElement | null { 43 | const actionElement = document.createElement('div'); 44 | actionElement.classList.add('action'); 45 | if (action.hidden) 46 | return null; 47 | if (action.disabled) 48 | actionElement.setAttribute('disabled', 'true'); 49 | const label = document.createElement('label'); 50 | if (!action.disabled) { 51 | label.addEventListener('click', () => { 52 | vscode.postMessage({ method: 'execute', params: { command: label.getAttribute('command') } }); 53 | }); 54 | } 55 | label.setAttribute('role', 'button'); 56 | if (action.disabled) 57 | label.setAttribute('aria-disabled', 'true'); 58 | label.setAttribute('command', action.command); 59 | const svg = /** @type {HTMLElement} */(document.createElement('svg')); 60 | label.appendChild(svg); 61 | svg.outerHTML = action.svg; 62 | if (!options?.omitText && action.text) 63 | label.appendChild(document.createTextNode(action.text)); 64 | label.title = action.title || action.text; 65 | actionElement.appendChild(label); 66 | return actionElement; 67 | } 68 | -------------------------------------------------------------------------------- /package.nls.it.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Esegui Playwright Test da Visual Studio Code.", 3 | "contributes.command.pw.extension.install": "Installa Playwright", 4 | "contributes.command.pw.extension.installBrowsers": "Installa i browser di Playwright", 5 | "contributes.command.pw.extension.command.inspect": "Seleziona il locator", 6 | "contributes.command.pw.extension.command.clearCache": "Pulisci cache", 7 | "contributes.command.pw.extension.command.closeBrowsers": "Chiudi tutti i browser di Playwright", 8 | "contributes.command.pw.extension.command.recordNew": "Nuova registrazione", 9 | "contributes.command.pw.extension.command.recordAtCursor": "Registra dal cursore", 10 | "contributes.command.pw.extension.command.runGlobalSetup": "Esegui il setup globale", 11 | "contributes.command.pw.extension.command.runGlobalTeardown": "Esegui il teardown globale", 12 | "contributes.command.pw.extension.command.startDevServer": "Inizia il dev server", 13 | "contributes.command.pw.extension.command.stopDevServer": "Ferma il dev server", 14 | "contributes.command.pw.extension.command.toggleModels": "Attiva/disattiva Playwright configs", 15 | "contributes.command.pw.extension.toggle.reuseBrowser": "Attiva/disattiva il riuso del browser Playwright", 16 | "contributes.command.pw.extension.toggle.showTrace": "Attiva/disattiva il Trace Viewer di Playwright", 17 | "contributes.command.pw.extension.toggle.runGlobalSetupOnEachRun": "Attiva/disattiva il riuso del Playwright global setup", 18 | "contributes.command.pw.extension.toggle.pickLocatorCopyToClipboard": "Attiva/disattiva la copia automatica del selettore Playwright negli appunti", 19 | "configuration.playwright.env": "Variabili d'ambiente da passare a Playwright Test.", 20 | "configuration.playwright.reuseBrowser": "Mostra & riusa il browser tra i test", 21 | "configuration.playwright.showTrace": "Mostra il Trace Viewer.", 22 | "configuration.playwright.runGlobalSetupOnEachRun": "Esegui il global setup ad ogni run.", 23 | "configuration.playwright.pickLocatorCopyToClipboard": "Copia automaticamente il locator selezionato negli appunti.", 24 | "configuration.playwright.updateSnapshots": "Aggiorna gli snapshot", 25 | "configuration.playwright.updateSourceMethod": "Aggiorna il metodo sorgente", 26 | "views.test.pw.extension.locatorsView": "Localizzatori", 27 | "views.test.pw.extension.settingsView": "Playwright" 28 | } -------------------------------------------------------------------------------- /package.nls.de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Führen Sie Playwright Test Tests in Visual Studio Code aus.", 3 | "contributes.command.pw.extension.install": "Playwright installieren", 4 | "contributes.command.pw.extension.installBrowsers": "Playwright Browser installieren", 5 | "contributes.command.pw.extension.command.inspect": "Locator auswählen", 6 | "contributes.command.pw.extension.command.clearCache": "Cache leeren", 7 | "contributes.command.pw.extension.command.closeBrowsers": "Alle Playwright Browser schließen", 8 | "contributes.command.pw.extension.command.recordNew": "Neu aufzeichnen", 9 | "contributes.command.pw.extension.command.recordAtCursor": "An Cursor aufzeichnen", 10 | "contributes.command.pw.extension.command.runGlobalSetup": "Globale Einrichtung ausführen", 11 | "contributes.command.pw.extension.command.runGlobalTeardown": "Globalen Abbau ausführen", 12 | "contributes.command.pw.extension.command.startDevServer": "Entwicklungsserver starten", 13 | "contributes.command.pw.extension.command.stopDevServer": "Entwicklungsserver stoppen", 14 | "contributes.command.pw.extension.command.toggleModels": "Playwright Konfigurationen umschalten", 15 | "contributes.command.pw.extension.toggle.reuseBrowser": "Wiederverwendung des Playwright Browsers umschalten", 16 | "contributes.command.pw.extension.toggle.showTrace": "Playwright Trace Viewer umschalten", 17 | "contributes.command.pw.extension.toggle.runGlobalSetupOnEachRun": "Wiederverwendung des globalen Setups umschalten", 18 | "contributes.command.pw.extension.toggle.pickLocatorCopyToClipboard": "Schalter für das automatische Kopieren des Playwright Locator-Pickers in die Zwischenablage umschalten", 19 | "configuration.playwright.env": "Umgebungsvariablen, die an Playwright Test übergeben werden.", 20 | "configuration.playwright.reuseBrowser": "Browser zwischen Tests anzeigen & wiederverwenden.", 21 | "configuration.playwright.showTrace": "Trace Viewer anzeigen.", 22 | "configuration.playwright.runGlobalSetupOnEachRun": "Globales Setup nicht wiederverwenden.", 23 | "configuration.playwright.pickLocatorCopyToClipboard": "Markierten Locator automatisch in die Zwischenablage kopieren.", 24 | "configuration.playwright.updateSnapshots": "Snapshots aktualisieren", 25 | "configuration.playwright.updateSourceMethod": "Quellmethode aktualisieren", 26 | "views.test.pw.extension.locatorsView": "Locator", 27 | "views.test.pw.extension.settingsView": "Playwright" 28 | } -------------------------------------------------------------------------------- /src/upstream/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export namespace Disposable { 18 | export function disposeAll(disposables: Disposable[]): void { 19 | for (const disposable of disposables.splice(0)) 20 | disposable.dispose(); 21 | } 22 | } 23 | 24 | export type Disposable = { 25 | dispose(): void; 26 | }; 27 | 28 | export interface Event { 29 | (listener: (e: T) => any, disposables?: Disposable[]): Disposable; 30 | } 31 | 32 | export class EventEmitter { 33 | public event: Event; 34 | 35 | private _deliveryQueue?: {listener: (e: T) => void, event: T}[]; 36 | private _listeners = new Set<(e: T) => void>(); 37 | 38 | constructor() { 39 | this.event = (listener: (e: T) => any, disposables?: Disposable[]) => { 40 | this._listeners.add(listener); 41 | let disposed = false; 42 | const self = this; 43 | const result: Disposable = { 44 | dispose() { 45 | if (!disposed) { 46 | disposed = true; 47 | self._listeners.delete(listener); 48 | } 49 | } 50 | }; 51 | if (disposables) 52 | disposables.push(result); 53 | return result; 54 | }; 55 | } 56 | 57 | fire(event: T): void { 58 | const dispatch = !this._deliveryQueue; 59 | if (!this._deliveryQueue) 60 | this._deliveryQueue = []; 61 | for (const listener of this._listeners) 62 | this._deliveryQueue.push({ listener, event }); 63 | if (!dispatch) 64 | return; 65 | for (let index = 0; index < this._deliveryQueue.length; index++) { 66 | const { listener, event } = this._deliveryQueue[index]; 67 | listener.call(null, event); 68 | } 69 | this._deliveryQueue = undefined; 70 | } 71 | 72 | dispose() { 73 | this._listeners.clear(); 74 | if (this._deliveryQueue) 75 | this._deliveryQueue = []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /package.nls.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Exécutez des tests Playwright Test dans Visual Studio Code.", 3 | "contributes.command.pw.extension.install": "Installer Playwright", 4 | "contributes.command.pw.extension.installBrowsers": "Installer les navigateurs Playwright", 5 | "contributes.command.pw.extension.command.inspect": "Trouver locator", 6 | "contributes.command.pw.extension.command.clearCache": "Effacer le cache", 7 | "contributes.command.pw.extension.command.closeBrowsers": "Fermer tous les navigateurs", 8 | "contributes.command.pw.extension.command.recordNew": "Enregistrer nouveau", 9 | "contributes.command.pw.extension.command.recordAtCursor": "Enregistrer au niveau du curseur", 10 | "contributes.command.pw.extension.command.runGlobalSetup": "Exécuter la configuration globale", 11 | "contributes.command.pw.extension.command.runGlobalTeardown": "Exécuter le nettoyage global", 12 | "contributes.command.pw.extension.command.startDevServer": "Démarrer le serveur de développement", 13 | "contributes.command.pw.extension.command.stopDevServer": "Arrêter le serveur de développement", 14 | "contributes.command.pw.extension.command.toggleModels": "Sélectionner les configurations Playwright", 15 | "contributes.command.pw.extension.toggle.reuseBrowser": "Activer/désactiver la réutilisation du navigateur", 16 | "contributes.command.pw.extension.toggle.showTrace": "Activer/désactiver Playwright Trace Viewer", 17 | "contributes.command.pw.extension.toggle.runGlobalSetupOnEachRun": "Activer/désactiver la réutilisation de la setup globale", 18 | "contributes.command.pw.extension.toggle.pickLocatorCopyToClipboard": "Basculer la copie automatique du sélecteur Playwright dans le presse-papiers", 19 | "configuration.playwright.env": "Variables d'environnement à transmettre à Playwright Test.", 20 | "configuration.playwright.reuseBrowser": "Afficher et réutiliser le navigateur entre les tests.", 21 | "configuration.playwright.showTrace": "Afficher les traces avec Trace Viewer.", 22 | "configuration.playwright.runGlobalSetupOnEachRun": "Ne pas réutiliser la setup globale.", 23 | "configuration.playwright.pickLocatorCopyToClipboard": "Copier automatiquement le locator sélectionné dans le presse-papiers.", 24 | "configuration.playwright.updateSnapshots": "Mettre à jour les captures d'écran", 25 | "configuration.playwright.updateSourceMethod": "Mettre à jour la méthode source", 26 | "views.test.pw.extension.locatorsView": "Localisateurs", 27 | "views.test.pw.extension.settingsView": "Playwright" 28 | } -------------------------------------------------------------------------------- /tests/global-errors.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect, test } from './utils'; 18 | 19 | test('should report duplicate test title', async ({ activate }) => { 20 | const { vscode, testController } = await activate({ 21 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 22 | 'tests/test.spec.ts': ` 23 | import { test } from '@playwright/test'; 24 | test('one', async () => {}); 25 | test('two', async () => {}); 26 | test('one', async () => {}); 27 | `, 28 | }); 29 | 30 | await testController.expandTestItems(/test.spec.ts/); 31 | await expect(testController).toHaveTestTree(` 32 | - tests 33 | - test.spec.ts 34 | `); 35 | await expect.poll(() => vscode.languages.getDiagnostics()).toEqual([ 36 | { 37 | message: 'Error: duplicate test title \"one\", first declared in test.spec.ts:3', 38 | range: { start: { line: 4, character: 10 }, end: { line: 5, character: 0 } }, 39 | severity: 'Error', 40 | source: 'playwright', 41 | } 42 | ]); 43 | }); 44 | 45 | test('should report error in global setup (explicit)', async ({ activate }) => { 46 | const { vscode, testController } = await activate({ 47 | 'playwright.config.js': `module.exports = { 48 | testDir: 'tests', 49 | globalSetup: 'globalSetup.ts', 50 | }`, 51 | 'globalSetup.ts': ` 52 | import { expect } from '@playwright/test'; 53 | async function globalSetup(config) { 54 | expect(true).toBe(false); 55 | } 56 | export default globalSetup;`, 57 | 'tests/test.spec.ts': ` 58 | import { test } from '@playwright/test'; 59 | test('should pass', async () => {}); 60 | `, 61 | }); 62 | 63 | const testRun = await testController.run(); 64 | await expect(testRun).toHaveOutput(/Running global setup if any…/); 65 | await expect(testRun).toHaveOutput(/Error: expect\(received\)\.toBe\(expected\)/); 66 | 67 | await expect(vscode).toHaveConnectionLog([ 68 | { method: 'listFiles', params: {} }, 69 | { method: 'runGlobalSetup', params: {} }, 70 | ]); 71 | }); 72 | -------------------------------------------------------------------------------- /l10n/bundle.l10n.it.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept to copy locator into clipboard": "Accetta di copiare il locator nella clipboard", 3 | "Automatically copy picked locator to clipboard": "Copia automaticamente il localizzatore selezionato negli appunti", 4 | "Can't close browsers while running tests": "Non e' possibile chiudere il browser mentre si eseguono i test", 5 | "Can't record while running tests": "Non e' possibile registrare mentre si eseguono i test", 6 | "Clear cache": "Pulire la cache", 7 | "Close all browsers": "Chiudere tutti i browser", 8 | "Copy on pick": "Copia al momento della selezione", 9 | "Locator": "Locator", 10 | "No Playwright tests found.": "0 Playwright test trovati.", 11 | "Pick locator": "Seleziona il locator", 12 | "Playwright Test v{0} or newer is required": "Playwright Test v{0} o una piu' recenbte e' richiesta", 13 | "Playwright v{0}+ is required for {1} to work, v{2} found": "Playwright Test v{0}+ e' richiesta per far funzionare {1}, v{2} trovata", 14 | "Please install Playwright Test via running `npm i --save-dev @playwright/test`": "Per favore installa Playwright Test attraverso il comando `npm i --save-dev @playwright/test`", 15 | "Record at cursor": "Registra fino al cursore", 16 | "Record new": "Nuova registrazione", 17 | "Reveal test output": "Rivela i test output", 18 | "Run global setup": "Esegui global setup", 19 | "Run global teardown": "Esegui global teardown", 20 | "Select Playwright Config": "Seleziona Playwright Config", 21 | "Show browser mode does not work in remote vscode": "Browser mode non funziona sul remote vscode", 22 | "Show browser": "Mostra browser", 23 | "Run global setup on each run": "Esegui global setup ad ogni run", 24 | "Show trace viewer": "Mostra trace viewer", 25 | "Start dev server": "Inizia dev server", 26 | "Stop dev server": "Ferma dev server", 27 | "this feature": "questo feature", 28 | "Toggle Playwright Configs": "Attiva/disattiva Playwright Configs", 29 | "Update snapshots": "Aggiorna snapshots", 30 | "Update method" : "Aggiorna metodo", 31 | "Select All": "Seleziona tutto", 32 | "Unselect All": "Deseleziona tutto", 33 | "When enabled, Playwright will reuse the browser instance between tests. This will disable parallel execution.": "Quando attivata, Playwright riusa l'instanza browser tra i test. Questo disattiva l'esecuzione parallela.", 34 | "one worker": "un worker", 35 | "Project is disabled in the Playwright sidebar": "Progetto disabilitato nella barra laterale di Playwright", 36 | "Enable project": "Abilita progetto", 37 | "Cancel": "Annulla", 38 | "SETTINGS": "IMPOSTAZIONI", 39 | "TOOLS": "STRUMENTI", 40 | "SETUP": "CONFIGURAZIONE", 41 | "PROJECTS": "PROGETTI", 42 | "CONFIGS": "CONFIGURAZIONI" 43 | } 44 | -------------------------------------------------------------------------------- /l10n/bundle.l10n.de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept to copy locator into clipboard": "Akzeptieren, um Locator in die Zwischenablage zu kopieren", 3 | "Automatically copy picked locator to clipboard": "Wählen Sie den Locator automatisch in die Zwischenablage", 4 | "Can't close browsers while running tests": "Browser können nicht geschlossen werden, während Tests ausgeführt werden", 5 | "Can't record while running tests": "Aufzeichnung während der Ausführung von Tests nicht möglich", 6 | "Clear cache": "Cache leeren", 7 | "Close all browsers": "Alle Browser schließen", 8 | "Copy on pick": "Beim Auswählen kopieren", 9 | "Locator": "Locator", 10 | "No Playwright tests found.": "Keine Playwright-Tests gefunden.", 11 | "Pick locator": "Locator auswählen", 12 | "Playwright Test v{0} or newer is required": "Playwright Test v{0} oder neuer ist erforderlich", 13 | "Playwright v{0}+ is required for {1} to work, v{2} found": "Playwright v{0}+ ist erforderlich, damit {1} funktioniert, v{2} gefunden", 14 | "Please install Playwright Test via running `npm i --save-dev @playwright/test`": "Bitte installieren Sie Playwright Test, indem Sie `npm i --save-dev @playwright/test` ausführen", 15 | "Record at cursor": "An Cursor aufzeichnen", 16 | "Record new": "Neu aufzeichnen", 17 | "Reveal test output": "Testergebnisse anzeigen", 18 | "Run global setup": "Global setup ausführen", 19 | "Run global teardown": "Global teardown ausführen", 20 | "Select Playwright Config": "Playwright-Konfiguration auswählen", 21 | "Show browser mode does not work in remote vscode": "Der Browser kann nicht mit Remote VSCode angezeigt werden", 22 | "Show browser": "Browser anzeigen", 23 | "Run global setup on each run": "Globales Setup jedesmal ausführen", 24 | "Show trace viewer": "Trace Viewer anzeigen", 25 | "Start dev server": "Entwicklungsserver starten", 26 | "Stop dev server": "Entwicklungsserver stoppen", 27 | "this feature": "diese Funktion", 28 | "Toggle Playwright Configs": "Playwright-Konfigurationen umschalten", 29 | "Update snapshots": "Snapshots aktualisieren", 30 | "Update method" : "Methode aktualisieren:", 31 | "When enabled, Playwright will reuse the browser instance between tests. This will disable parallel execution.": "Wenn aktiviert, wird Playwright die Browserinstanz zwischen den Tests wiederverwenden. Dies deaktiviert die parallele Ausführung.", 32 | "SETTINGS": "EINSTELLUNGEN", 33 | "PROJECTS": "PROJEKTE", 34 | "TOOLS": "WERKZEUGE", 35 | "SETUP": "KONFIGURATION", 36 | "CONFIGS": "KONFIGURATIONEN", 37 | "Select All": "Alle auswählen", 38 | "Unselect All": "Alle abwählen", 39 | "one worker": "ein worker", 40 | "Project is disabled in the Playwright sidebar": "Projekt ist im Playwright-Sidebar deaktiviert", 41 | "Enable project": "Projekt aktivieren", 42 | "Cancel": "Abbrechen" 43 | } 44 | -------------------------------------------------------------------------------- /.azure-pipelines/publish.yml: -------------------------------------------------------------------------------- 1 | pr: none 2 | 3 | trigger: 4 | tags: 5 | include: 6 | - '*' 7 | 8 | variables: 9 | # Required for Microbuild 10 | - name: TeamName 11 | value: Playwright 12 | 13 | resources: 14 | repositories: 15 | - repository: MicroBuildTemplate 16 | type: git 17 | name: 1ESPipelineTemplates/MicroBuildTemplate 18 | ref: refs/tags/release 19 | 20 | # VSCode extension signing: https://aka.ms/vsm-ms-publisher-sign 21 | extends: 22 | template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate 23 | parameters: 24 | pool: 25 | # https://aka.ms/MicroBuild 26 | name: VSEngSS-MicroBuild2022-1ES 27 | os: windows 28 | stages: 29 | - stage: Stage 30 | jobs: 31 | - job: HostJob 32 | templateContext: 33 | mb: 34 | signing: 35 | enabled: true 36 | signType: real 37 | signWithProd: true 38 | steps: 39 | - task: UseNode@1 40 | inputs: 41 | version: '20.x' 42 | displayName: 'Install Node.js' 43 | - script: npm ci 44 | displayName: 'Install dependencies' 45 | - task: PowerShell@2 46 | displayName: 'Package the extension' 47 | inputs: 48 | targetType: 'inline' 49 | script: | 50 | if ("$(Build.Reason)" -eq "Manual") { 51 | npm run package -- --out extension.vsix --pre-release 52 | } else { 53 | npm run package -- --out extension.vsix 54 | } 55 | - script: npx vsce generate-manifest -i extension.vsix -o extension.manifest 56 | displayName: 'Generate the manifest' 57 | - task: DotNetCoreCLI@2 58 | displayName: Sign 59 | inputs: 60 | command: 'build' 61 | projects: 'PlaywrightVSCode.signproj' 62 | - task: AzureCLI@2 63 | displayName: 'Publishing with Managed Identity' 64 | inputs: 65 | azureSubscription: 'Playwright-VSMarketplacePublishing' 66 | scriptType: "pscore" 67 | scriptLocation: 'inlineScript' 68 | inlineScript: | 69 | $aadToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv 70 | npx vsce verify-pat --pat $aadToken ms-playwright 71 | if ("$(Build.Reason)" -eq "Manual") { 72 | npx vsce publish --pre-release --pat $aadToken --packagePath extension.vsix --manifestPath extension.manifest --signaturePath extension.signature.p7s 73 | } else { 74 | npx vsce publish --pat $aadToken --packagePath extension.vsix --manifestPath extension.manifest --signaturePath extension.signature.p7s 75 | } 76 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /l10n/bundle.l10n.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept to copy locator into clipboard": "Accepter pour copier le locator dans le presse-papiers", 3 | "Automatically copy picked locator to clipboard": "Copier automatiquement le localisateur sélectionné dans le presse-papiers", 4 | "Can't close browsers while running tests": "Impossible de fermer les navigateurs pendant l'exécution des tests", 5 | "Can't record while running tests": "Impossible d'enregistrer pendant l'exécution des tests", 6 | "Clear cache": "Vider le cache", 7 | "Close all browsers": "Fermer tous les navigateurs", 8 | "Copy on pick": "Copier lors de la sélection", 9 | "Locator": "Locator", 10 | "No Playwright tests found.": "Aucun test Playwright trouvé.", 11 | "Pick locator": "Trouver locator", 12 | "Playwright Test v{0} or newer is required": "Playwright Test v{0} ou plus est nécessaire", 13 | "Playwright v{0}+ is required for {1} to work, v{2} found": "Playwright v{0}+ est requis pour que {1} fonctionne, v{2} trouvée", 14 | "Please install Playwright Test via running `npm i --save-dev @playwright/test`": "Veuillez installer Playwright Test en exécutant `npm i --save-dev @playwright/test`", 15 | "Record at cursor": "Enregistrer au niveau du curseur", 16 | "Record new": "Enregistrer nouveau", 17 | "Reveal test output": "Afficher les résultats des tests", 18 | "Run global setup": "Exécuter la configuration globale", 19 | "Run global teardown": "Exécuter le nettoyage global", 20 | "Select Playwright Config": "Sélectionner la configuration Playwright", 21 | "Show browser mode does not work in the Web mode": "Le mode affichage de navigateur ne peut être utilisé en mode Web", 22 | "Show browser": "Afficher le navigateur", 23 | "Run global setup on each run": "Exécuter la configuration globale chaque course", 24 | "Show trace viewer": "Afficher le Trace Viewer", 25 | "Start dev server": "Démarrer le serveur de développement", 26 | "Stop dev server": "Arrêter le serveur de développement", 27 | "this feature": "cette fonctionnalité", 28 | "Toggle Playwright Configs": "Basculer les configurations Playwright", 29 | "Update snapshots": "Mettre à jour les captures", 30 | "Update method" : "Mettre à jour la méthode", 31 | "Select All": "Tout sélectionner", 32 | "Unselect All": "Tout désélectionner", 33 | "When enabled, Playwright will reuse the browser instance between tests. This will disable parallel execution.": "Si cette option est activée, Playwright réutilisera l'instance du navigateur entre les tests. Cela désactivera l'exécution parallèle.", 34 | "one worker": "un worker", 35 | "Project is disabled in the Playwright sidebar" : "Le projet est désactivé dans la barre latérale Playwright", 36 | "Enable project": "Activer le projet", 37 | "Cancel": "Annuler", 38 | "SETTINGS": "PARAMÈTRES", 39 | "TOOLS": "OUTILS", 40 | "SETUP": "CONFIGURATION", 41 | "PROJECTS": "PROJETS", 42 | "CONFIGS": "CONFIGURATIONS" 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | FORCE_COLOR: 1 11 | VALIDATE_L10N: 1 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{ matrix.os }} 16 | name: Run tests on ${{ matrix.os }}, Node ${{ matrix.node-version }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ ubuntu-latest, windows-latest, macos-latest ] 21 | node-version: [ 20 ] 22 | include: 23 | - os: ubuntu-latest 24 | node-version: 18 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - run: npm ci 32 | - run: npx playwright install chromium 33 | - run: npm run lint 34 | - run: npm run build 35 | - run: npm run test --workers=1 36 | - run: npx vsce package 37 | if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' 38 | - uses: actions/upload-artifact@v4 39 | if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' 40 | with: 41 | name: vsc-extension 42 | path: "*.vsix" 43 | retention-days: 30 44 | test-e2e: 45 | runs-on: ${{ matrix.os }} 46 | name: Run e2e tests 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: [ ubuntu-latest, windows-latest ] 51 | env: 52 | DEBUG: pw:browser 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Use Node.js 56 | uses: actions/setup-node@v4 57 | with: 58 | node-version: 20 59 | - uses: pnpm/action-setup@v4 60 | with: 61 | version: 10 62 | - run: npm i -g yarn@1 63 | - run: npm ci 64 | - run: npx playwright install --with-deps chromium 65 | - run: npm run build 66 | - run: xvfb-run npx playwright test --grep-invert yarn-berry --reporter=blob 67 | working-directory: ./tests-integration 68 | if: matrix.os == 'ubuntu-latest' 69 | - run: npx playwright test --grep-invert yarn-berry --reporter=blob 70 | working-directory: ./tests-integration 71 | if: matrix.os != 'ubuntu-latest' 72 | - run: corepack enable 73 | - run: corepack prepare yarn@4 --activate 74 | - run: xvfb-run npx playwright test --grep yarn-berry --reporter=blob 75 | working-directory: ./tests-integration 76 | if: matrix.os == 'ubuntu-latest' 77 | env: 78 | PWTEST_BLOB_DO_NOT_REMOVE: 1 79 | - run: npx playwright test --grep yarn-berry --reporter=blob 80 | working-directory: ./tests-integration 81 | if: matrix.os != 'ubuntu-latest' 82 | env: 83 | PWTEST_BLOB_DO_NOT_REMOVE: 1 84 | - run: npx playwright merge-reports blob-report --reporter=html 85 | if: always() 86 | - uses: actions/upload-artifact@v4 87 | if: always() 88 | with: 89 | name: playwright-report-${{ matrix.os }} 90 | path: playwright-report/ 91 | -------------------------------------------------------------------------------- /src/locatorsView.script.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { createAction, vscode } from './common'; 18 | 19 | // @ts-check 20 | const locatorInput = document.getElementById('locator') as HTMLInputElement; 21 | const ariaTextArea = document.getElementById('ariaSnapshot') as HTMLTextAreaElement; 22 | const copyToClipboardCheckbox = document.getElementById('copyToClipboardCheckbox') as HTMLInputElement; 23 | 24 | locatorInput.addEventListener('input', () => { 25 | vscode.postMessage({ method: 'locatorChanged', params: { locator: locatorInput.value } }); 26 | }); 27 | 28 | ariaTextArea.addEventListener('input', () => { 29 | vscode.postMessage({ method: 'ariaSnapshotChanged', params: { ariaSnapshot: ariaTextArea.value } }); 30 | }); 31 | 32 | copyToClipboardCheckbox.addEventListener('change', () => { 33 | vscode.postMessage({ method: 'toggle', params: { setting: 'pickLocatorCopyToClipboard' } }); 34 | }); 35 | 36 | window.addEventListener('message', event => { 37 | const locatorError = document.getElementById('locatorError')!; 38 | const ariaSnapshotError = document.getElementById('ariaSnapshotError')!; 39 | const ariaSection = document.getElementById('ariaSection')!; 40 | const actionsElement = document.getElementById('actions')!; 41 | const actions2Element = document.getElementById('actions-2')!; 42 | 43 | const { method, params } = event.data; 44 | if (method === 'update') { 45 | locatorInput.value = params.locator.locator; 46 | locatorError.textContent = params.locator.error || ''; 47 | locatorError.style.display = params.locator.error ? 'inherit' : 'none'; 48 | ariaTextArea.value = params.ariaSnapshot.yaml; 49 | ariaSnapshotError.textContent = params.ariaSnapshot.error || ''; 50 | ariaSnapshotError.style.display = params.ariaSnapshot.error ? 'inherit' : 'none'; 51 | ariaSection.style.display = params.hideAria ? 'none' : 'flex'; 52 | } else if (method === 'actions') { 53 | actionsElement.textContent = ''; 54 | actions2Element.textContent = ''; 55 | for (const action of params.actions) { 56 | const actionElement = createAction(action, { omitText: true }); 57 | if (actionElement) 58 | (action.location === 'actions-2' ? actions2Element : actionsElement).appendChild(actionElement); 59 | } 60 | } else if (method === 'settings') { 61 | if ('pickLocatorCopyToClipboard' in params.settings) 62 | copyToClipboardCheckbox.checked = params.settings.pickLocatorCopyToClipboard; 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /tests/pnp.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { enableConfigs, expect, test } from './utils'; 18 | import fs from 'node:fs/promises'; 19 | import path from 'node:path'; 20 | 21 | test('should pick up .pnp.cjs file closest to config', async ({ activate }, testInfo) => { 22 | const pnpCjsOutfile = testInfo.outputPath('pnp-cjs.txt'); 23 | const esmLoaderInfile = testInfo.outputPath('esm-loader-in.txt'); 24 | const esmLoaderOutfile = testInfo.outputPath('esm-loader-out.txt'); 25 | await fs.writeFile(esmLoaderInfile, 'untouched'); 26 | 27 | const { vscode, testController, workspaceFolder } = await activate({ 28 | '.pnp.cjs': ` 29 | const fs = require("node:fs"); 30 | fs.writeFileSync(${JSON.stringify(pnpCjsOutfile)}, "root"); 31 | `, 32 | '.pnp.loader.mjs': ` 33 | import fs from 'node:fs'; 34 | fs.copyFileSync(${JSON.stringify(esmLoaderInfile)}, ${JSON.stringify(esmLoaderOutfile)}); 35 | `, 36 | 'apps/tests/tests/root.spec.ts': ` 37 | import { test } from '@playwright/test'; 38 | test('should pass', async () => {}); 39 | `, 40 | 41 | 'foo/playwright.config.js': `module.exports = { testDir: 'tests' }`, 42 | 'foo/.pnp.cjs': ` 43 | const fs = require("node:fs"); 44 | fs.writeFileSync(${JSON.stringify(pnpCjsOutfile)}, "foo"); 45 | `, 46 | 'foo/tests/foo.spec.ts': ` 47 | import { test } from '@playwright/test'; 48 | test('should pass', async () => {}); 49 | `, 50 | }); 51 | 52 | await expect(testController).toHaveTestTree(` 53 | - foo 54 | - tests 55 | - foo.spec.ts 56 | `); 57 | 58 | let testRun = await testController.run(); 59 | expect(testRun.renderLog()).toContain('passed'); 60 | expect(await fs.readFile(pnpCjsOutfile, 'utf-8')).toBe('foo'); 61 | 62 | await workspaceFolder.addFile('apps/tests/playwright.config.js', `module.exports = { testDir: 'tests' }`); 63 | await enableConfigs(vscode, [`apps${path.sep}tests${path.sep}playwright.config.js`, `foo${path.sep}playwright.config.js`]); 64 | await expect(testController).toHaveTestTree(` 65 | - apps 66 | - tests 67 | - tests 68 | - root.spec.ts 69 | - foo 70 | - tests 71 | - foo.spec.ts 72 | `); 73 | 74 | await fs.writeFile(esmLoaderInfile, 'root'); 75 | testRun = await testController.run(testController.findTestItems(/root/)); 76 | expect(testRun.renderLog()).toContain('passed'); 77 | expect(await fs.readFile(pnpCjsOutfile, 'utf-8')).toBe('root'); 78 | expect(await fs.readFile(esmLoaderOutfile, 'utf-8')).toBe('root'); 79 | }); -------------------------------------------------------------------------------- /tests/problems.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect, test } from './utils'; 18 | 19 | test('should list tests on expand', async ({ activate }) => { 20 | const { vscode, testController } = await activate({ 21 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 22 | 'tests/test.spec.ts': ` 23 | import { test } from '@playwright/test'; 24 | test('one', async ({ page }) => { 25 | const 26 | await page.goto('http://example.com'); 27 | }); 28 | `, 29 | }); 30 | 31 | await testController.expandTestItems(/test.spec.ts/); 32 | await expect(testController).toHaveTestTree(` 33 | - tests 34 | - test.spec.ts 35 | `); 36 | expect(vscode.diagnosticsCollections.length).toBe(1); 37 | expect([...vscode.diagnosticsCollections[0]._entries]).toEqual([ 38 | [ 39 | expect.stringContaining('test.spec.ts'), 40 | [ 41 | { 42 | message: expect.stringMatching(/^SyntaxError: tests[/\\]test.spec.ts: Unexpected reserved word 'await'. \(5:8\)$/), 43 | range: { 44 | end: { character: 0, line: 5 }, 45 | start: { character: 7, line: 4 } 46 | }, 47 | severity: 'Error', 48 | source: 'playwright' 49 | } 50 | ] 51 | ] 52 | ]); 53 | }); 54 | 55 | test('should update diagnostics on file change', async ({ activate }) => { 56 | const { vscode, testController, workspaceFolder } = await activate({ 57 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 58 | 'tests/test.spec.ts': ` 59 | import { test } from '@playwright/test'; 60 | test('one', async ({ page }) => { 61 | const 62 | await page.goto('http://example.com'); 63 | }); 64 | `, 65 | }); 66 | 67 | await testController.expandTestItems(/test.spec.ts/); 68 | await expect(testController).toHaveTestTree(` 69 | - tests 70 | - test.spec.ts 71 | `); 72 | expect(vscode.diagnosticsCollections.length).toBe(1); 73 | expect([...vscode.diagnosticsCollections[0]._entries]).toEqual([ 74 | [ 75 | expect.stringContaining('test.spec.ts'), 76 | [ 77 | expect.objectContaining({ 78 | message: expect.stringContaining('SyntaxError'), 79 | source: 'playwright', 80 | }) 81 | ] 82 | ] 83 | ]); 84 | 85 | await workspaceFolder.changeFile('tests/test.spec.ts', ` 86 | import { test } from '@playwright/test'; 87 | test('one', async ({ page }) => { 88 | await page.goto('http://example.com'); 89 | }); 90 | `); 91 | await expect.poll(() => vscode.diagnosticsCollections[0]._entries.size).toBe(0); 92 | }); 93 | -------------------------------------------------------------------------------- /utils/roll-locally.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const { execSync } = require('child_process'); 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | 21 | const packageNames = ['playwright-core', 'playwright', 'playwright-test']; 22 | const upstreamFiles = new Map([ 23 | ['src/upstream/events.ts', 'packages/playwright/src/isomorphic/events.ts'], 24 | ['src/upstream/testServerConnection.ts', 'packages/playwright/src/isomorphic/testServerConnection.ts'], 25 | ['src/upstream/testServerInterface.ts', 'packages/playwright/src/isomorphic/testServerInterface.ts'], 26 | ['src/upstream/testTree.ts', 'packages/playwright/src/isomorphic/testTree.ts'], 27 | 28 | // TODO: Roll these manually, on demand, upon changes into versioned folders. 29 | // ['src/upstream/teleEmitter.ts', 'packages/playwright/src/reporters/teleEmitter.ts'], 30 | // ['src/upstream/teleReceiver.ts', 'packages/playwright/src/isomorphic/teleReceiver.ts'], 31 | ]); 32 | 33 | void (async () => { 34 | const playwrightWorkspace = path.resolve(__dirname, '../../playwright'); 35 | await fs.promises.rm(path.join(__dirname, '../test-results'), { recursive: true, force: true }); 36 | for (const packageName of packageNames) { 37 | console.log('Packaging ' + packageName); 38 | console.log(execSync(`node ./utils/pack_package ${packageName} ` + path.join(__dirname, `../out/${packageName}.tgz`), { cwd: playwrightWorkspace }).toString()); 39 | } 40 | const nodeModules = path.join(__dirname, '../test-results', 'node_modules'); 41 | await fs.promises.mkdir(nodeModules, { recursive: true }); 42 | for (const packageName of packageNames) { 43 | const packagePath = path.join(__dirname, '../out', packageName + '.tgz'); 44 | if (fs.existsSync(packagePath)) { 45 | console.log('Extracting ' + packageName); 46 | console.log(execSync(`npm install ${packagePath}`, { cwd: nodeModules }).toString()); 47 | } 48 | } 49 | 50 | // Copy reused source files 51 | for (const [to, from] of upstreamFiles) { 52 | const fromPath = path.join(playwrightWorkspace, from); 53 | const toPath = path.join(__dirname, '..', to); 54 | const fromContent = await fs.promises.readFile(fromPath, 'utf8'); 55 | const toContent = await fs.promises.readFile(toPath, 'utf8'); 56 | const [fromPrefix, fromSuffix] = fromContent.split(upstreamBoundary); 57 | const [toPrefix] = toContent.split(downstreamBoundary); 58 | if (fromSuffix) 59 | await fs.promises.writeFile(toPath, toPrefix + downstreamBoundary + fromSuffix, 'utf8'); 60 | else 61 | await fs.promises.writeFile(toPath, fromPrefix, 'utf8'); 62 | } 63 | })(); 64 | 65 | const upstreamBoundary = '// -- Reuse boundary -- Everything below this line is reused in the vscode extension.'; 66 | const downstreamBoundary = '// -- Reuse boundary -- Everything below this line is taken from playwright core.'; 67 | -------------------------------------------------------------------------------- /src/transport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import WebSocket from 'ws'; 18 | 19 | export type ProtocolRequest = { 20 | id: number; 21 | method: string; 22 | params: any; 23 | }; 24 | 25 | export type ProtocolResponse = { 26 | id?: number; 27 | method?: string; 28 | error?: { message: string; data: any; }; 29 | params?: any; 30 | result?: any; 31 | }; 32 | 33 | export interface ConnectionTransport { 34 | send(s: ProtocolRequest): void; 35 | close(): void; // Note: calling close is expected to issue onclose at some point. 36 | isClosed(): boolean, 37 | onmessage?: (message: ProtocolResponse) => void, 38 | onclose?: () => void, 39 | } 40 | 41 | export class WebSocketTransport implements ConnectionTransport { 42 | private _ws: WebSocket; 43 | 44 | onmessage?: (message: ProtocolResponse) => void; 45 | onclose?: () => void; 46 | readonly wsEndpoint: string; 47 | 48 | static async connect(url: string, headers: Record = {}): Promise { 49 | const transport = new WebSocketTransport(url, headers); 50 | await new Promise((fulfill, reject) => { 51 | transport._ws.addEventListener('open', async () => { 52 | fulfill(transport); 53 | }); 54 | transport._ws.addEventListener('error', event => { 55 | reject(new Error('WebSocket error: ' + event.message)); 56 | transport._ws.close(); 57 | }); 58 | }); 59 | return transport; 60 | } 61 | 62 | constructor(url: string, headers: Record = {}) { 63 | this.wsEndpoint = url; 64 | this._ws = new WebSocket(url, [], { 65 | perMessageDeflate: false, 66 | maxPayload: 256 * 1024 * 1024, // 256Mb, 67 | handshakeTimeout: 30000, 68 | headers 69 | }); 70 | 71 | this._ws.addEventListener('message', event => { 72 | try { 73 | if (this.onmessage) 74 | this.onmessage.call(null, JSON.parse(event.data.toString())); 75 | } catch (e) { 76 | this._ws.close(); 77 | } 78 | }); 79 | 80 | this._ws.addEventListener('close', event => { 81 | if (this.onclose) 82 | this.onclose.call(null); 83 | }); 84 | // Prevent Error: read ECONNRESET. 85 | this._ws.addEventListener('error', () => {}); 86 | } 87 | 88 | isClosed() { 89 | return this._ws.readyState === WebSocket.CLOSING || this._ws.readyState === WebSocket.CLOSED; 90 | } 91 | 92 | send(message: ProtocolRequest) { 93 | this._ws.send(JSON.stringify(message)); 94 | } 95 | 96 | close() { 97 | this._ws.close(); 98 | } 99 | 100 | async closeAndWait() { 101 | const promise = new Promise(f => this._ws.once('close', f)); 102 | this.close(); 103 | await promise; // Make sure to await the actual disconnect. 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/workspaceObserver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vscodeTypes from './vscodeTypes'; 18 | import { uriToPath } from './utils'; 19 | 20 | export type WorkspaceChange = { 21 | created: Set; 22 | changed: Set; 23 | deleted: Set; 24 | }; 25 | 26 | export class WorkspaceObserver { 27 | private _vscode: vscodeTypes.VSCode; 28 | private _handler: (change: WorkspaceChange) => void; 29 | private _pendingChange: WorkspaceChange | undefined; 30 | private _timeout: NodeJS.Timeout | undefined; 31 | private _watchers = new Map(); 32 | private _isUnderTest: boolean; 33 | 34 | constructor(vscode: vscodeTypes.VSCode, handler: (change: WorkspaceChange) => void, isUnderTest: boolean) { 35 | this._vscode = vscode; 36 | this._handler = handler; 37 | this._isUnderTest = isUnderTest; 38 | } 39 | 40 | setPatterns(patterns: Set) { 41 | for (const pattern of patterns) { 42 | if (this._watchers.has(pattern)) 43 | continue; 44 | 45 | const watcher = this._vscode.workspace.createFileSystemWatcher(pattern); 46 | const disposables: vscodeTypes.Disposable[] = [ 47 | watcher.onDidCreate(uri => { 48 | if (uri.scheme === 'file' && this._isRelevant(uri)) 49 | this._change().created.add(uriToPath(uri)); 50 | }), 51 | watcher.onDidChange(uri => { 52 | if (uri.scheme === 'file' && this._isRelevant(uri)) 53 | this._change().changed.add(uriToPath(uri)); 54 | }), 55 | watcher.onDidDelete(uri => { 56 | if (uri.scheme === 'file' && this._isRelevant(uri)) 57 | this._change().deleted.add(uriToPath(uri)); 58 | }), 59 | watcher, 60 | ]; 61 | this._watchers.set(pattern, disposables); 62 | } 63 | 64 | for (const [pattern, disposables] of this._watchers) { 65 | if (!patterns.has(pattern)) { 66 | disposables.forEach(d => d.dispose()); 67 | this._watchers.delete(pattern); 68 | } 69 | } 70 | } 71 | 72 | private _isRelevant(uri: vscodeTypes.Uri): boolean { 73 | const path = uriToPath(uri); 74 | // TODO: parse .gitignore 75 | if (path.includes('node_modules')) 76 | return false; 77 | if (!this._isUnderTest && path.includes('test-results')) 78 | return false; 79 | return true; 80 | } 81 | 82 | private _change(): WorkspaceChange { 83 | if (!this._pendingChange) { 84 | this._pendingChange = { 85 | created: new Set(), 86 | changed: new Set(), 87 | deleted: new Set() 88 | }; 89 | } 90 | if (this._timeout) 91 | clearTimeout(this._timeout); 92 | this._timeout = setTimeout(() => this._reportChange(), 50); 93 | return this._pendingChange; 94 | } 95 | 96 | private _reportChange() { 97 | delete this._timeout; 98 | this._handler(this._pendingChange!); 99 | this._pendingChange = undefined; 100 | } 101 | 102 | dispose() { 103 | if (this._timeout) 104 | clearTimeout(this._timeout); 105 | for (const disposables of this._watchers.values()) 106 | disposables.forEach(d => d.dispose()); 107 | this._watchers.clear(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/spawnTraceViewer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ChildProcess, spawn } from 'child_process'; 18 | import type { TestConfig } from './playwrightTestServer'; 19 | import { findNode } from './utils'; 20 | import * as vscodeTypes from './vscodeTypes'; 21 | import { TraceViewer } from './traceViewer'; 22 | 23 | export class SpawnTraceViewer implements TraceViewer { 24 | private _vscode: vscodeTypes.VSCode; 25 | private _envProvider: (configFile: string) => NodeJS.ProcessEnv; 26 | private _traceViewerProcess: ChildProcess | undefined; 27 | private _currentFile?: string; 28 | private _config: TestConfig; 29 | private _serverUrlPrefixForTest?: string; 30 | 31 | constructor(vscode: vscodeTypes.VSCode, envProvider: (configFile: string) => NodeJS.ProcessEnv, config: TestConfig) { 32 | this._vscode = vscode; 33 | this._envProvider = envProvider; 34 | this._config = config; 35 | } 36 | 37 | currentFile() { 38 | return this._currentFile; 39 | } 40 | 41 | async willRunTests() { 42 | await this._startIfNeeded(); 43 | } 44 | 45 | async open(file?: string) { 46 | this._currentFile = file; 47 | if (!file && !this._traceViewerProcess) 48 | return; 49 | await this._startIfNeeded(); 50 | this._traceViewerProcess?.stdin?.write(file + '\n'); 51 | } 52 | 53 | private async _startIfNeeded() { 54 | const node = await findNode(this._vscode, this._config.workspaceFolder); 55 | if (this._traceViewerProcess) 56 | return; 57 | const allArgs = [this._config.cli, 'show-trace', `--stdin`]; 58 | if (this._vscode.env.remoteName) { 59 | allArgs.push('--host', '0.0.0.0'); 60 | allArgs.push('--port', '0'); 61 | } 62 | const traceViewerProcess = spawn(node, allArgs, { 63 | cwd: this._config.workspaceFolder, 64 | stdio: 'pipe', 65 | detached: true, 66 | env: { 67 | ...process.env, 68 | ...this._envProvider(this._config.configFile), 69 | }, 70 | }); 71 | this._traceViewerProcess = traceViewerProcess; 72 | 73 | const pipeLog = (data: Buffer) => { 74 | if (!this._vscode.isUnderTest) 75 | console.log(data.toString()); 76 | }; 77 | traceViewerProcess.stdout?.on('data', pipeLog); 78 | traceViewerProcess.stderr?.on('data', pipeLog); 79 | traceViewerProcess.on('exit', () => { 80 | this._traceViewerProcess = undefined; 81 | this._currentFile = undefined; 82 | }); 83 | traceViewerProcess.on('error', error => { 84 | void this._vscode.window.showErrorMessage(error.message); 85 | this.close(); 86 | }); 87 | if (this._vscode.isUnderTest) { 88 | traceViewerProcess.stdout?.on('data', data => { 89 | const match = data.toString().match(/Listening on (.*)/); 90 | if (match) 91 | this._serverUrlPrefixForTest = match[1]; 92 | }); 93 | } 94 | } 95 | 96 | close() { 97 | this._traceViewerProcess?.stdin?.end(); 98 | this._traceViewerProcess = undefined; 99 | this._currentFile = undefined; 100 | this._serverUrlPrefixForTest = undefined; 101 | } 102 | 103 | async infoForTest() { 104 | return { 105 | type: 'spawn', 106 | serverUrlPrefix: this._serverUrlPrefixForTest, 107 | testConfigFile: this._config.configFile, 108 | traceFile: this._currentFile, 109 | visible: !!this._serverUrlPrefixForTest 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /media/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | margin: 0; 19 | padding: 0; 20 | user-select: none; 21 | line-height: 22px; 22 | } 23 | 24 | body[data-vscode-theme-kind=vscode-dark] div.separator { 25 | border-color: rgba(204, 204, 204, 0.2); 26 | } 27 | 28 | .hbox { 29 | display: flex; 30 | flex: auto; 31 | position: relative; 32 | } 33 | 34 | .vbox { 35 | display: flex; 36 | flex-direction: column; 37 | flex: auto; 38 | position: relative; 39 | } 40 | 41 | svg { 42 | width: 16px; 43 | height: 16px; 44 | fill: var(--vscode-editor-foreground); 45 | pointer-events: none; 46 | } 47 | 48 | input, textarea { 49 | background-color: var(--vscode-input-background); 50 | color: var(--vscode-input-foreground); 51 | border-width: 1px; 52 | border-style: solid; 53 | border-color: var(--vscode-panelInput-border, transparent); 54 | border-radius: 2px; 55 | padding: 2px 6px; 56 | } 57 | 58 | input { 59 | font-family: system-ui, Ubuntu, "Droid Sans", sans-serif, "Droid Sans Mono", "monospace", monospace; 60 | font-size: 13px; 61 | line-height: 20px; 62 | letter-spacing: 0px; 63 | } 64 | 65 | .combobox select { 66 | margin-left: 6px; 67 | flex: auto; 68 | } 69 | 70 | input:focus, textarea:focus { 71 | opacity: 1; 72 | outline-color: var(--vscode-focusBorder); 73 | outline-offset: -1px; 74 | outline-style: solid; 75 | outline-width: 1px; 76 | } 77 | 78 | textarea { 79 | font-size: 12px; 80 | } 81 | 82 | .section-header { 83 | font-size: 11px; 84 | margin: 8px 12px 0 12px; 85 | font-weight: 700; 86 | color: var(--vscode-editor-inlineValuesForeground); 87 | 88 | display: flex; 89 | justify-content: space-between; 90 | align-items: center; 91 | } 92 | 93 | .section-toolbar > a { 94 | border-radius: 5px; 95 | font-size: 16px; 96 | cursor: pointer; 97 | } 98 | 99 | .section-toolbar > a:hover { 100 | background-color: var(--vscode-toolbar-hoverBackground); 101 | } 102 | 103 | .action, .combobox { 104 | padding: 0 12px; 105 | cursor: pointer; 106 | display: flex; 107 | } 108 | 109 | .action[disabled], .inactive { 110 | opacity: 0.5; 111 | cursor: default; 112 | background: none !important; 113 | } 114 | 115 | input[type=checkbox] { 116 | margin: 0; 117 | width: 16px; 118 | cursor: pointer; 119 | } 120 | 121 | select { 122 | /* important for dark theme */ 123 | background: var(--vscode-sideBar-background); 124 | color: inherit; 125 | outline: none !important; 126 | border: none !important; 127 | height: 22px; 128 | padding: 2px 0; 129 | cursor: pointer; 130 | min-width: 10px; 131 | } 132 | 133 | label { 134 | display: flex; 135 | align-items: center; 136 | cursor: inherit; 137 | flex: auto; 138 | } 139 | 140 | .action:hover, .combobox:hover { 141 | background-color: #e8e8e8; 142 | } 143 | 144 | body[data-vscode-theme-kind=vscode-dark] .action:hover, 145 | body[data-vscode-theme-kind=vscode-dark] .combobox:hover { 146 | background-color: #2a2d2e; 147 | } 148 | 149 | /* Settings view */ 150 | 151 | .settings-view .action > label { 152 | gap: 4px; 153 | } 154 | 155 | .settings-view .hbox > label { 156 | padding-left: 12px; 157 | } 158 | 159 | .settings-view select.models { 160 | padding: 2px 0; 161 | width: 100%; 162 | } 163 | 164 | /* Locators view */ 165 | 166 | .locators-view .section { 167 | margin: 0 10px 10px; 168 | display: flex; 169 | flex-direction: column; 170 | } 171 | 172 | .locators-view .section > label { 173 | margin-bottom: 5px; 174 | } 175 | 176 | .locators-view label { 177 | flex: none; 178 | } 179 | 180 | .locators-view p.error { 181 | color: var(--vscode-errorForeground); 182 | } 183 | 184 | .locators-view .actions { 185 | flex: none; 186 | display: flex; 187 | padding: 2px; 188 | } 189 | 190 | .locators-view .action { 191 | padding: 0; 192 | } 193 | 194 | .locators-view #locator { 195 | flex: auto; 196 | } 197 | -------------------------------------------------------------------------------- /tests/update-snapshots.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fs from 'fs'; 18 | import { expect, test } from './utils'; 19 | 20 | for (const mode of ['3-way', 'overwrite', 'patch'] as const) { 21 | test('should update missing snapshots ' + mode, async ({ activate }, testInfo) => { 22 | const { vscode, testController } = await activate({ 23 | 'playwright.config.ts': ` 24 | import { defineConfig } from '@playwright/test'; 25 | export default defineConfig({}); 26 | `, 27 | 'test.spec.ts': ` 28 | import { test, expect } from '@playwright/test'; 29 | test('should pass', async ({ page }) => { 30 | await page.setContent(''); 31 | await expect(page.locator('body')).toMatchAriaSnapshot(''); 32 | }); 33 | `, 34 | }); 35 | 36 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 37 | await webView.getByRole('combobox', { name: 'Update method' }).selectOption(mode); 38 | await webView.getByRole('combobox', { name: 'Update snapshots' }).selectOption('missing'); 39 | 40 | await testController.run(); 41 | 42 | let expectation; 43 | if (mode === '3-way') { 44 | expectation = `<<<<<<< HEAD 45 | await expect(page.locator('body')).toMatchAriaSnapshot(''); 46 | ======= 47 | await expect(page.locator('body')).toMatchAriaSnapshot(\` 48 | - button "Click me" 49 | \`); 50 | >>>>>>> SNAPSHOT`; 51 | } else if (mode === 'overwrite') { 52 | expectation = ` 53 | await page.setContent(''); 54 | await expect(page.locator('body')).toMatchAriaSnapshot(\` 55 | - button "Click me" 56 | \`);`; 57 | } else { 58 | expectation = ` 59 | await page.setContent(''); 60 | await expect(page.locator('body')).toMatchAriaSnapshot('');`; 61 | } 62 | 63 | await expect.poll(() => { 64 | return fs.promises.readFile(testInfo.outputPath('test.spec.ts'), 'utf8'); 65 | }).toContain(expectation); 66 | }); 67 | } 68 | 69 | for (const mode of ['3-way' , 'overwrite', 'patch'] as const) { 70 | test('should update all snapshots ' + mode, async ({ activate }, testInfo) => { 71 | const { vscode, testController } = await activate({ 72 | 'playwright.config.ts': ` 73 | import { defineConfig } from '@playwright/test'; 74 | export default defineConfig({}); 75 | `, 76 | 'test.spec.ts': ` 77 | import { test, expect } from '@playwright/test'; 78 | test('should pass', async ({ page }) => { 79 | await page.setContent(''); 80 | await expect(page.locator('body')).toMatchAriaSnapshot(\` 81 | - button 82 | \`); 83 | }); 84 | `, 85 | }); 86 | 87 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 88 | await webView.getByRole('combobox', { name: 'Update method' }).selectOption(mode); 89 | await webView.getByRole('combobox', { name: 'Update snapshots' }).selectOption('all'); 90 | 91 | await testController.run(); 92 | 93 | let expectation; 94 | if (mode === '3-way') { 95 | expectation = `<<<<<<< HEAD 96 | - button 97 | ======= 98 | - button "Click me" 99 | >>>>>>> SNAPSHOT`; 100 | } else if (mode === 'overwrite') { 101 | expectation = ` 102 | await page.setContent(''); 103 | await expect(page.locator('body')).toMatchAriaSnapshot(\` 104 | - button "Click me" 105 | \`);`; 106 | } else { 107 | expectation = ` 108 | await expect(page.locator('body')).toMatchAriaSnapshot(\` 109 | - button 110 | \`);`; 111 | } 112 | 113 | await expect.poll(() => { 114 | return fs.promises.readFile(testInfo.outputPath('test.spec.ts'), 'utf8'); 115 | }).toContain(expectation); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /tests/highlight-locators.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { chromium } from '@playwright/test'; 18 | import { expect, test } from './utils'; 19 | 20 | test.beforeEach(({ showBrowser }) => { 21 | // Locator highlighting is only relevant when the browser stays open. 22 | test.skip(!showBrowser); 23 | }); 24 | 25 | test('should work', async ({ activate }) => { 26 | const cdpPort = 9234 + test.info().workerIndex * 2; 27 | const { vscode, testController } = await activate({ 28 | 'playwright.config.js': `module.exports = { 29 | use: { 30 | launchOptions: { 31 | args: ['--remote-debugging-port=${cdpPort}'] 32 | } 33 | } 34 | }`, 35 | 'test.spec.ts': ` 36 | import { test } from '@playwright/test'; 37 | test('one', async ({ page }) => { 38 | await page.goto('about:blank'); 39 | await page.setContent(\` 40 | 41 | 42 | \`); 43 | await page.getByRole('button', { name: 'one' }).click(); // line 8 44 | await page.getByRole('button', { name: 'two' }).click(); // line 9 45 | page.getByRole('button', { name: 'not there!' }); // line 10 46 | await page 47 | .getByRole( 48 | 'button', 49 | { name: 'one' } 50 | ).click(); 51 | }); 52 | 53 | class MyPom { 54 | constructor(page) { 55 | this.myElementOne1 = page.getByRole('button', { name: 'one' }); // line 20 56 | this.myElementTwo1 = this._page.getByRole('button', { name: 'two' }); // line 21 57 | this.myElementOne2 = this.page.getByRole('button', { name: 'one' }); // line 22 58 | } 59 | 60 | @step // decorators require a babel plugin 61 | myMethod() {} 62 | } 63 | 64 | function step(target: Function, context: ClassMethodDecoratorContext) { 65 | return function replacementMethod(...args: any) { 66 | const name = this.constructor.name + '.' + (context.name as string); 67 | return test.step(name, async () => { 68 | return await target.call(this, ...args); 69 | }); 70 | }; 71 | } 72 | `, 73 | }); 74 | 75 | const testItems = testController.findTestItems(/test.spec.ts/); 76 | expect(testItems.length).toBe(1); 77 | await vscode.openEditors('test.spec.ts'); 78 | await testController.run(testItems); 79 | const browser = await chromium.connectOverCDP(`http://localhost:${cdpPort}`); 80 | { 81 | expect(browser.contexts()).toHaveLength(1); 82 | expect(browser.contexts()[0].pages()).toHaveLength(1); 83 | } 84 | const page = browser.contexts()[0].pages()[0]; 85 | const boxOne = await page.getByRole('button', { name: 'one' }).boundingBox(); 86 | const boxTwo = await page.getByRole('button', { name: 'two' }).boundingBox(); 87 | 88 | for (const language of ['javascript', 'typescript']) { 89 | for (const [[line, column], expectedBox] of [ 90 | [[9, 26], boxTwo], 91 | [[8, 26], boxOne], 92 | [[10, 26], null], 93 | [[13, 15], boxOne], 94 | [[20, 30], boxOne], 95 | [[21, 30], boxTwo], 96 | [[22, 30], boxOne], 97 | ] as const) { 98 | await test.step(`should highlight ${language} ${line}:${column}`, async () => { 99 | // Clear highlight. 100 | vscode.languages.emitHoverEvent(language, vscode.window.activeTextEditor.document, new vscode.Position(0, 0)); 101 | await expect(page.locator('x-pw-highlight')).toBeHidden(); 102 | 103 | vscode.languages.emitHoverEvent(language, vscode.window.activeTextEditor.document, new vscode.Position(line, column)); 104 | if (!expectedBox) { 105 | await expect(page.locator('x-pw-highlight')).toBeHidden(); 106 | } else { 107 | await expect(page.locator('x-pw-highlight')).toBeVisible(); 108 | expect(await page.locator('x-pw-highlight').boundingBox()).toEqual(expectedBox); 109 | } 110 | }); 111 | } 112 | } 113 | await browser.close(); 114 | }); 115 | -------------------------------------------------------------------------------- /src/upstream/testServerInterface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type * as reporterTypes from './reporter'; 18 | import type { Event } from '../vscodeTypes'; 19 | import { JsonEvent } from './teleReceiver'; 20 | 21 | // -- Reuse boundary -- Everything below this line is taken from playwright core. 22 | 23 | export type ReportEntry = JsonEvent; 24 | 25 | export interface TestServerInterface { 26 | initialize(params: { 27 | serializer?: string, 28 | closeOnDisconnect?: boolean, 29 | interceptStdio?: boolean, 30 | watchTestDirs?: boolean, 31 | populateDependenciesOnList?: boolean, 32 | }): Promise; 33 | 34 | ping(params: {}): Promise; 35 | 36 | watch(params: { 37 | fileNames: string[]; 38 | }): Promise; 39 | 40 | open(params: { location: reporterTypes.Location }): Promise; 41 | 42 | resizeTerminal(params: { cols: number, rows: number }): Promise; 43 | 44 | checkBrowsers(params: {}): Promise<{ hasBrowsers: boolean }>; 45 | 46 | installBrowsers(params: {}): Promise; 47 | 48 | runGlobalSetup(params: {}): Promise<{ 49 | report: ReportEntry[], 50 | env: [string, string | null][], 51 | status: reporterTypes.FullResult['status'] 52 | }>; 53 | 54 | runGlobalTeardown(params: {}): Promise<{ 55 | report: ReportEntry[], 56 | status: reporterTypes.FullResult['status'] 57 | }>; 58 | 59 | startDevServer(params: {}): Promise<{ 60 | report: ReportEntry[]; 61 | status: reporterTypes.FullResult['status'] 62 | }>; 63 | 64 | stopDevServer(params: {}): Promise<{ 65 | report: ReportEntry[]; 66 | status: reporterTypes.FullResult['status'] 67 | }>; 68 | 69 | clearCache(params: {}): Promise; 70 | 71 | listFiles(params: { 72 | projects?: string[]; 73 | }): Promise<{ 74 | report: ReportEntry[]; 75 | status: reporterTypes.FullResult['status'] 76 | }>; 77 | 78 | /** 79 | * Returns list of teleReporter events. 80 | */ 81 | listTests(params: { 82 | projects?: string[]; 83 | locations?: string[]; 84 | grep?: string; 85 | grepInvert?: string; 86 | }): Promise<{ 87 | report: ReportEntry[], 88 | status: reporterTypes.FullResult['status'] 89 | }>; 90 | 91 | runTests(params: { 92 | locations?: string[]; 93 | grep?: string; 94 | grepInvert?: string; 95 | testIds?: string[]; 96 | headed?: boolean; 97 | workers?: number | string; 98 | updateSnapshots?: 'all' | 'changed' | 'missing' | 'none'; 99 | updateSourceMethod?: 'overwrite' | 'patch' | '3way'; 100 | reporters?: string[], 101 | trace?: 'on' | 'off'; 102 | video?: 'on' | 'off'; 103 | projects?: string[]; 104 | reuseContext?: boolean; 105 | connectWsEndpoint?: string; 106 | timeout?: number; 107 | pauseOnError?: boolean; 108 | pauseAtEnd?: boolean; 109 | }): Promise<{ 110 | status: reporterTypes.FullResult['status']; 111 | }>; 112 | 113 | findRelatedTestFiles(params: { 114 | files: string[]; 115 | }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>; 116 | 117 | stopTests(params: {}): Promise; 118 | 119 | closeGracefully(params: {}): Promise; 120 | } 121 | 122 | export interface TestServerInterfaceEvents { 123 | onReport: Event; 124 | onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>; 125 | onTestFilesChanged: Event<{ testFiles: string[] }>; 126 | onLoadTraceRequested: Event<{ traceUrl: string }>; 127 | onTestPaused: Event<{ errors: reporterTypes.TestError[] }>; 128 | } 129 | 130 | export interface TestServerInterfaceEventEmitters { 131 | dispatchEvent(event: 'report', params: ReportEntry): void; 132 | dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void; 133 | dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void; 134 | dispatchEvent(event: 'loadTraceRequested', params: { traceUrl: string }): void; 135 | dispatchEvent(event: 'testPaused', params: { errors: reporterTypes.TestError[] }): void; 136 | } 137 | -------------------------------------------------------------------------------- /tests/decorations.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { escapedPathSep, expect, test } from './utils'; 18 | 19 | test('should highlight steps while running', async ({ activate }) => { 20 | const { vscode, testController } = await activate({ 21 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 22 | 'tests/test.spec.ts': ` 23 | import { test, expect } from '@playwright/test'; 24 | test('pass', async () => { 25 | expect(1).toBe(1); 26 | expect(2).toBe(2); 27 | expect(3).toBe(3); 28 | }); 29 | `, 30 | }); 31 | 32 | await vscode.openEditors('**/test.spec.ts'); 33 | await new Promise(f => testController.onDidChangeTestItem(f)); 34 | 35 | await testController.run(); 36 | expect(vscode.window.activeTextEditor.renderDecorations(' ')).toBe(` 37 | -------------------------------------------------------------- 38 | 39 | -------------------------------------------------------------- 40 | [3:18 - 3:18]: decorator activeStep 41 | -------------------------------------------------------------- 42 | 43 | -------------------------------------------------------------- 44 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 45 | -------------------------------------------------------------- 46 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 47 | [4:18 - 4:18]: decorator activeStep 48 | -------------------------------------------------------------- 49 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 50 | -------------------------------------------------------------- 51 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 52 | [4:18 - 4:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 53 | -------------------------------------------------------------- 54 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 55 | [4:18 - 4:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 56 | [5:18 - 5:18]: decorator activeStep 57 | -------------------------------------------------------------- 58 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 59 | [4:18 - 4:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 60 | -------------------------------------------------------------- 61 | [3:18 - 3:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 62 | [4:18 - 4:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 63 | [5:18 - 5:18]: decorator completedStep {"after":{"contentText":" — Xms"}} 64 | `); 65 | 66 | await expect(vscode).toHaveConnectionLog([ 67 | { method: 'listFiles', params: {} }, 68 | { 69 | method: 'listTests', 70 | params: expect.objectContaining({ 71 | locations: [expect.stringContaining(`tests${escapedPathSep}test\\.spec\\.ts`)] 72 | }) 73 | }, 74 | { method: 'runGlobalSetup', params: {} }, 75 | { 76 | method: 'runTests', 77 | params: expect.objectContaining({ 78 | locations: [], 79 | testIds: undefined 80 | }) 81 | }, 82 | ]); 83 | }); 84 | 85 | test('should limit highlights', async ({ activate }) => { 86 | const { vscode, testController } = await activate({ 87 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 88 | 'tests/test.spec.ts': ` 89 | import { test, expect } from '@playwright/test'; 90 | test('pass', async () => { 91 | for (let i = 0; i < 2000; i++) { 92 | expect(i).toBe(i); 93 | } 94 | }); 95 | `, 96 | }); 97 | 98 | await vscode.openEditors('**/test.spec.ts'); 99 | await new Promise(f => testController.onDidChangeTestItem(f)); 100 | 101 | await testController.run(); 102 | 103 | const decorationsLog = vscode.window.activeTextEditor.renderDecorations(' '); 104 | const lastState = decorationsLog.substring(decorationsLog.lastIndexOf('------')); 105 | expect(lastState).toContain(`[4:20 - 4:20]: decorator completedStep {"after":{"contentText":" — Xms (ran 2000×)"}}`); 106 | }); 107 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | module.exports = { 17 | 'parser': '@typescript-eslint/parser', 18 | 'plugins': ['@typescript-eslint', 'notice'], 19 | 'parserOptions': { 20 | ecmaVersion: 9, 21 | sourceType: 'module', 22 | project: './tsconfig.json', 23 | }, 24 | 'extends': [ 25 | 'plugin:react-hooks/recommended' 26 | ], 27 | 28 | /** 29 | * ESLint rules 30 | * 31 | * All available rules: http://eslint.org/docs/rules/ 32 | * 33 | * Rules take the following form: 34 | * "rule-name", [severity, { opts }] 35 | * Severity: 2 == error, 1 == warning, 0 == off. 36 | */ 37 | 'rules': { 38 | '@typescript-eslint/no-floating-promises': 2, 39 | '@typescript-eslint/no-unused-vars': [2, { args: 'none' }], 40 | 41 | 'no-restricted-properties': [2, { 42 | 'property': 'fsPath', 43 | 'message': 'Please use uriToPath(uri) instead.', 44 | }], 45 | 46 | /** 47 | * Enforced rules 48 | */ 49 | // syntax preferences 50 | 'object-curly-spacing': ['error', 'always'], 51 | 'quotes': [2, 'single', { 52 | 'avoidEscape': true, 53 | 'allowTemplateLiterals': true 54 | }], 55 | 'no-extra-semi': 2, 56 | '@typescript-eslint/semi': [2], 57 | 'comma-style': [2, 'last'], 58 | 'wrap-iife': [2, 'inside'], 59 | 'spaced-comment': [2, 'always', { 60 | 'markers': ['*'] 61 | }], 62 | 'eqeqeq': [2], 63 | 'accessor-pairs': [2, { 64 | 'getWithoutSet': false, 65 | 'setWithoutGet': false 66 | }], 67 | 'brace-style': [2, '1tbs', { 'allowSingleLine': true }], 68 | 'curly': [2, 'multi-or-nest', 'consistent'], 69 | 'new-parens': 2, 70 | 'arrow-parens': [2, 'as-needed'], 71 | 'prefer-const': 2, 72 | 'quote-props': [2, 'consistent'], 73 | 74 | // anti-patterns 75 | 'no-var': 2, 76 | 'no-with': 2, 77 | 'no-multi-str': 2, 78 | 'no-caller': 2, 79 | 'no-implied-eval': 2, 80 | 'no-labels': 2, 81 | 'no-new-object': 2, 82 | 'no-octal-escape': 2, 83 | 'no-self-compare': 2, 84 | 'no-shadow-restricted-names': 2, 85 | 'no-cond-assign': 2, 86 | 'no-debugger': 2, 87 | 'no-dupe-keys': 2, 88 | 'no-duplicate-case': 2, 89 | 'no-empty-character-class': 2, 90 | 'no-unreachable': 2, 91 | 'no-unsafe-negation': 2, 92 | 'radix': 2, 93 | 'valid-typeof': 2, 94 | 'no-implicit-globals': [2], 95 | 'no-unused-expressions': [2, { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], 96 | 97 | // es2015 features 98 | 'require-yield': 2, 99 | 'template-curly-spacing': [2, 'never'], 100 | 101 | // spacing details 102 | 'space-infix-ops': 2, 103 | 'space-in-parens': [2, 'never'], 104 | 'space-before-function-paren': [2, { 105 | 'anonymous': 'never', 106 | 'named': 'never', 107 | 'asyncArrow': 'always' 108 | }], 109 | 'no-whitespace-before-property': 2, 110 | 'keyword-spacing': [2, { 111 | 'overrides': { 112 | 'if': { 'after': true }, 113 | 'else': { 'after': true }, 114 | 'for': { 'after': true }, 115 | 'while': { 'after': true }, 116 | 'do': { 'after': true }, 117 | 'switch': { 'after': true }, 118 | 'return': { 'after': true } 119 | } 120 | }], 121 | 'arrow-spacing': [2, { 122 | 'after': true, 123 | 'before': true 124 | }], 125 | '@typescript-eslint/func-call-spacing': 2, 126 | '@typescript-eslint/type-annotation-spacing': 2, 127 | 128 | // file whitespace 129 | 'no-multiple-empty-lines': [2, { 'max': 2 }], 130 | 'no-mixed-spaces-and-tabs': 2, 131 | 'no-trailing-spaces': 2, 132 | 'linebreak-style': [ process.platform === 'win32' ? 0 : 2, 'unix' ], 133 | 'indent': [2, 2, { 'SwitchCase': 1, 'CallExpression': { 'arguments': 2 }, 'MemberExpression': 2 }], 134 | 'key-spacing': [2, { 135 | 'beforeColon': false 136 | }], 137 | 138 | // copyright 139 | 'notice/notice': [2, { 140 | 'mustMatch': 'Copyright', 141 | 'templateFile': require('path').join(__dirname, 'utils', 'copyright.js'), 142 | }], 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /tests/auto-close.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { connectToSharedBrowser, expect, test, waitForPage } from './utils'; 18 | 19 | test.skip(({ showBrowser }) => !showBrowser); 20 | 21 | test('should reuse browsers', async ({ activate }) => { 22 | const { vscode, testController } = await activate({ 23 | 'playwright.config.js': `module.exports = {}`, 24 | 'test.spec.ts': ` 25 | import { test } from '@playwright/test'; 26 | test('pass', async ({ page }) => {}); 27 | ` 28 | }); 29 | 30 | const reusedBrowser = await vscode.extensions[0].reusedBrowserForTest(); 31 | const events: number[] = []; 32 | reusedBrowser.onPageCountChanged((count: number) => events.push(count)); 33 | await testController.run(); 34 | await expect.poll(() => events).toEqual([1]); 35 | expect(reusedBrowser._backend).toBeTruthy(); 36 | }); 37 | 38 | test('should be closed with Close all Browsers button', async ({ activate }) => { 39 | const { vscode, testController } = await activate({ 40 | 'playwright.config.js': `module.exports = {}`, 41 | 'test.spec.ts': ` 42 | import { test } from '@playwright/test'; 43 | test('pass', async ({ page }) => {}); 44 | ` 45 | }); 46 | 47 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 48 | const closeAllBrowsers = webView.getByRole('button', { name: 'Close all browsers' }); 49 | await expect(closeAllBrowsers).toBeDisabled(); 50 | const reusedBrowser = await vscode.extensions[0].reusedBrowserForTest(); 51 | await testController.run(); 52 | expect(reusedBrowser._backend).toBeTruthy(); 53 | await expect(closeAllBrowsers).toBeEnabled(); 54 | await closeAllBrowsers.click(); 55 | await expect.poll(() => reusedBrowser._backend).toBeFalsy(); 56 | }); 57 | 58 | test('should auto-close after test', async ({ activate }) => { 59 | const { vscode, testController } = await activate({ 60 | 'playwright.config.js': `module.exports = {}`, 61 | 'test.spec.ts': ` 62 | import { test } from '@playwright/test'; 63 | test('pass', async ({ page }) => { await page.close(); }); 64 | ` 65 | }); 66 | 67 | await testController.run(); 68 | const reusedBrowser = await vscode.extensions[0].reusedBrowserForTest(); 69 | await expect.poll(() => !!reusedBrowser._backend).toBeFalsy(); 70 | }); 71 | 72 | test('should auto-close after pick', async ({ activate }) => { 73 | const { vscode } = await activate({ 74 | 'playwright.config.js': `module.exports = {}`, 75 | }); 76 | 77 | await vscode.commands.executeCommand('pw.extension.command.inspect'); 78 | 79 | // It is important that we await for command above to have context for reuse set up. 80 | const browser = await connectToSharedBrowser(vscode); 81 | const page = await waitForPage(browser); 82 | await page.close(); 83 | 84 | const reusedBrowser = await vscode.extensions[0].reusedBrowserForTest(); 85 | await expect.poll(() => !!reusedBrowser._backend).toBeFalsy(); 86 | }); 87 | 88 | test('should enact "Show Browser" setting change after test finishes', async ({ activate, createLatch }) => { 89 | const latch = createLatch(); 90 | 91 | const { vscode, testController } = await activate({ 92 | 'playwright.config.js': `module.exports = {}`, 93 | 'test.spec.ts': ` 94 | import { test } from '@playwright/test'; 95 | test('should pass', async ({ page }) => { 96 | await page.setContent('foo'); 97 | ${latch.blockingCode} 98 | }); 99 | ` 100 | }); 101 | 102 | const runPromise = testController.run(); 103 | 104 | const reusedBrowser = vscode.extensions[0].reusedBrowserForTest(); 105 | await expect.poll(() => !!reusedBrowser._backend, 'wait until test started').toBeTruthy(); 106 | 107 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 108 | await webView.getByRole('checkbox', { name: 'Show Browser' }).uncheck(); 109 | await expect.poll(() => !!reusedBrowser._backend, 'contrary to setting change, browser stays open during test run').toBeTruthy(); 110 | latch.open(); 111 | await runPromise; 112 | 113 | await expect.poll(() => !!reusedBrowser._backend, 'after test run, setting change is honored').toBeFalsy(); 114 | }); -------------------------------------------------------------------------------- /images/playwright-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests-integration/tests/baseTest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { test as base, type Page, _electron } from '@playwright/test'; 17 | import { downloadAndUnzipVSCode } from '@vscode/test-electron/out/download'; 18 | export { expect } from '@playwright/test'; 19 | import path from 'path'; 20 | import os from 'os'; 21 | import fs from 'fs'; 22 | import { spawnSync } from 'child_process'; 23 | 24 | export type TestOptions = { 25 | vscodeVersion: string; 26 | packageManager: 'npm' | 'pnpm' | 'pnpm-pnp' | 'yarn-berry' | 'yarn-classic'; 27 | }; 28 | 29 | type TestFixtures = TestOptions & { 30 | workbox: Page, 31 | createProject: () => Promise, 32 | createTempDir: () => Promise, 33 | }; 34 | 35 | export const test = base.extend({ 36 | vscodeVersion: ['insiders', { option: true }], 37 | packageManager: ['npm', { option: true }], 38 | workbox: async ({ vscodeVersion, createProject, createTempDir }, use) => { 39 | const defaultCachePath = await createTempDir(); 40 | const vscodePath = await downloadAndUnzipVSCode(vscodeVersion); 41 | const electronApp = await _electron.launch({ 42 | executablePath: vscodePath, 43 | args: [ 44 | // Stolen from https://github.com/microsoft/vscode-test/blob/0ec222ef170e102244569064a12898fb203e5bb7/lib/runTest.ts#L126-L160 45 | // https://github.com/microsoft/vscode/issues/84238 46 | '--no-sandbox', 47 | // https://github.com/microsoft/vscode-test/issues/221 48 | '--disable-gpu-sandbox', 49 | // https://github.com/microsoft/vscode-test/issues/120 50 | '--disable-updates', 51 | '--skip-welcome', 52 | '--skip-release-notes', 53 | '--disable-workspace-trust', 54 | `--extensionDevelopmentPath=${path.join(__dirname, '..', '..')}`, 55 | `--extensions-dir=${path.join(defaultCachePath, 'extensions')}`, 56 | `--user-data-dir=${path.join(defaultCachePath, 'user-data')}`, 57 | await createProject(), 58 | ], 59 | }); 60 | const workbox = await electronApp.firstWindow(); 61 | await workbox.context().tracing.start({ screenshots: true, snapshots: true, title: test.info().title }); 62 | await use(workbox); 63 | const tracePath = test.info().outputPath('trace.zip'); 64 | await workbox.context().tracing.stop({ path: tracePath }); 65 | test.info().attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' }); 66 | await electronApp.close(); 67 | const logPath = path.join(defaultCachePath, 'user-data'); 68 | if (fs.existsSync(logPath)) { 69 | const logOutputPath = test.info().outputPath('vscode-logs'); 70 | await fs.promises.cp(logPath, logOutputPath, { recursive: true }); 71 | } 72 | }, 73 | createProject: async ({ createTempDir, packageManager }, use) => { 74 | await use(async () => { 75 | // We want to be outside of the project directory to avoid already installed dependencies. 76 | const projectPath = await createTempDir(); 77 | if (fs.existsSync(projectPath)) 78 | await fs.promises.rm(projectPath, { recursive: true }); 79 | console.log(`Creating project in ${projectPath}`); 80 | await fs.promises.mkdir(projectPath); 81 | 82 | let command = 'npm init playwright@latest --'; 83 | if (packageManager === 'pnpm' || packageManager === 'pnpm-pnp') 84 | command = 'pnpm create playwright@latest'; 85 | else if (packageManager === 'yarn-classic') 86 | command = 'yarn create playwright'; 87 | else if (packageManager === 'yarn-berry') 88 | command = 'yarn create playwright'; 89 | if (packageManager === 'pnpm-pnp') 90 | await fs.promises.writeFile(path.join(projectPath, '.npmrc'), 'node-linker=pnp'); 91 | 92 | spawnSync(`${command} --quiet --browser=chromium --gha --install-deps`, { 93 | cwd: projectPath, 94 | stdio: 'inherit', 95 | shell: true, 96 | }); 97 | return projectPath; 98 | }); 99 | }, 100 | createTempDir: async ({ }, use) => { 101 | const tempDirs: string[] = []; 102 | await use(async () => { 103 | const tempDir = await fs.promises.realpath(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pwtest-'))); 104 | tempDirs.push(tempDir); 105 | return tempDir; 106 | }); 107 | for (const tempDir of tempDirs) 108 | await fs.promises.rm(tempDir, { recursive: true }); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /src/ansi2html.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Microsoft Corporation. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export function ansi2html(text: string, defaultColors?: { bg: string, fg: string }): string { 18 | const regex = /(\x1b\[(\d+(;\d+)*)m)|([^\x1b]+)/g; 19 | const tokens: string[] = []; 20 | let match; 21 | let style: any = {}; 22 | 23 | let reverse = false; 24 | let fg: string | undefined = defaultColors?.fg; 25 | let bg: string | undefined = defaultColors?.bg; 26 | 27 | while ((match = regex.exec(text)) !== null) { 28 | const [, , codeStr, , text] = match; 29 | if (codeStr) { 30 | const code = +codeStr; 31 | switch (code) { 32 | case 0: style = {}; break; 33 | case 1: style['font-weight'] = 'bold'; break; 34 | case 2: style['opacity'] = '0.8'; break; 35 | case 3: style['font-style'] = 'italic'; break; 36 | case 4: style['text-decoration'] = 'underline'; break; 37 | case 7: 38 | reverse = true; 39 | break; 40 | case 8: style.display = 'none'; break; 41 | case 9: style['text-decoration'] = 'line-through'; break; 42 | case 22: 43 | delete style['font-weight']; 44 | delete style['font-style']; 45 | delete style['opacity']; 46 | delete style['text-decoration']; 47 | break; 48 | case 23: 49 | delete style['font-weight']; 50 | delete style['font-style']; 51 | delete style['opacity']; 52 | break; 53 | case 24: 54 | delete style['text-decoration']; 55 | break; 56 | case 27: 57 | reverse = false; 58 | break; 59 | case 30: 60 | case 31: 61 | case 32: 62 | case 33: 63 | case 34: 64 | case 35: 65 | case 36: 66 | case 37: 67 | fg = ansiColors[code - 30]; 68 | break; 69 | case 39: 70 | fg = defaultColors?.fg; 71 | break; 72 | case 40: 73 | case 41: 74 | case 42: 75 | case 43: 76 | case 44: 77 | case 45: 78 | case 46: 79 | case 47: 80 | bg = ansiColors[code - 40]; 81 | break; 82 | case 49: 83 | bg = defaultColors?.bg; 84 | break; 85 | case 53: style['text-decoration'] = 'overline'; break; 86 | case 90: 87 | case 91: 88 | case 92: 89 | case 93: 90 | case 94: 91 | case 95: 92 | case 96: 93 | case 97: 94 | fg = brightAnsiColors[code - 90]; 95 | break; 96 | case 100: 97 | case 101: 98 | case 102: 99 | case 103: 100 | case 104: 101 | case 105: 102 | case 106: 103 | case 107: 104 | bg = brightAnsiColors[code - 100]; 105 | break; 106 | } 107 | } else if (text) { 108 | let token = escapeHTML(text); 109 | const isBold = style['font-weight'] === 'bold'; 110 | if (isBold) 111 | token = `${token}`; 112 | const isItalic = style['font-style'] === 'italic'; 113 | if (isItalic) 114 | token = `${token}`; 115 | const hasOpacity = style['opacity'] === '0.8'; 116 | if (hasOpacity) 117 | token = `${token}`; 118 | const color = reverse ? (bg || '#000') : fg; 119 | if (color) 120 | token = `${token}`; 121 | const backgroundColor = reverse ? fg : bg; 122 | if (backgroundColor) 123 | token = `${token}`; 124 | tokens.push(token); 125 | } 126 | } 127 | return tokens.join(''); 128 | } 129 | 130 | const ansiColors: Record = { 131 | 0: '#000', 132 | 1: '#f14c4c', 133 | 2: '#73c991', 134 | 3: '#ffcc66', 135 | 4: '#44a8f2', 136 | 5: '#b084eb', 137 | 6: '#afdab6', 138 | 7: '#fff', 139 | }; 140 | 141 | const brightAnsiColors: Record = { 142 | 0: '#808080', 143 | 1: '#f14c4c', 144 | 2: '#73c991', 145 | 3: '#ffcc66', 146 | 4: '#44a8f2', 147 | 5: '#b084eb', 148 | 6: '#afdab6', 149 | 7: '#fff', 150 | }; 151 | 152 | function escapeHTML(text: string): string { 153 | return text.replace(/[&"<> \n]/g, c => ({ 154 | ' ': ' ', 155 | '\n': '\n
\n', 156 | '&': '&', 157 | '"': '"', 158 | '<': '<', 159 | '>': '>' 160 | }[c]!)); 161 | } 162 | -------------------------------------------------------------------------------- /tests/project-setup.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { enableProjects, expect, test } from './utils'; 18 | 19 | const testsWithSetup = { 20 | 'playwright.config.ts': ` 21 | import { defineConfig } from '@playwright/test'; 22 | export default defineConfig({ 23 | projects: [ 24 | { name: 'setup', teardown: 'teardown', testMatch: 'setup.ts' }, 25 | { name: 'test', testMatch: 'test.ts', dependencies: ['setup'] }, 26 | { name: 'teardown', testMatch: 'teardown.ts' }, 27 | ] 28 | }); 29 | `, 30 | 'setup.ts': ` 31 | import { test, expect } from '@playwright/test'; 32 | test('setup', async ({}) => { 33 | console.log('from-setup'); 34 | }); 35 | `, 36 | 'test.ts': ` 37 | import { test, expect } from '@playwright/test'; 38 | test('test', async ({}) => { 39 | console.log('from-test'); 40 | }); 41 | `, 42 | 'teardown.ts': ` 43 | import { test, expect } from '@playwright/test'; 44 | test('teardown', async ({}) => { 45 | console.log('from-teardown'); 46 | }); 47 | `, 48 | }; 49 | 50 | test('should run setup and teardown projects (1)', async ({ activate }) => { 51 | const { vscode, testController } = await activate(testsWithSetup); 52 | await enableProjects(vscode, ['setup', 'teardown', 'test']); 53 | const testRun = await testController.run(); 54 | 55 | await expect(testController).toHaveTestTree(` 56 | - setup.ts 57 | - ✅ setup [2:0] 58 | - teardown.ts 59 | - ✅ teardown [2:0] 60 | - test.ts 61 | - ✅ test [2:0] 62 | `); 63 | 64 | const output = testRun.renderLog({ output: true }); 65 | expect(output).toContain('from-setup'); 66 | expect(output).toContain('from-test'); 67 | expect(output).toContain('from-teardown'); 68 | 69 | // Ensure the rendered order of the projects is correct. 70 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 71 | await expect(webView.getByTestId('projects').locator('div').locator('label')).toHaveText([ 72 | 'setup', 73 | 'test', 74 | 'teardown', 75 | ]); 76 | }); 77 | 78 | test('should run setup and teardown projects (2)', async ({ activate }) => { 79 | const { vscode, testController } = await activate(testsWithSetup); 80 | await enableProjects(vscode, ['teardown', 'test']); 81 | const testRun = await testController.run(); 82 | 83 | await expect(testController).toHaveTestTree(` 84 | - teardown.ts 85 | - ✅ teardown [2:0] 86 | - test.ts 87 | - ✅ test [2:0] 88 | - [playwright.config.ts [setup] — disabled] 89 | `); 90 | 91 | const output = testRun.renderLog({ output: true }); 92 | expect(output).not.toContain('from-setup'); 93 | expect(output).toContain('from-test'); 94 | expect(output).toContain('from-teardown'); 95 | }); 96 | 97 | test('should run setup and teardown projects (3)', async ({ activate }) => { 98 | const { vscode, testController } = await activate(testsWithSetup); 99 | await enableProjects(vscode, ['test']); 100 | const testRun = await testController.run(); 101 | 102 | await expect(testController).toHaveTestTree(` 103 | - test.ts 104 | - ✅ test [2:0] 105 | - [playwright.config.ts [setup] — disabled] 106 | - [playwright.config.ts [teardown] — disabled] 107 | `); 108 | 109 | const output = testRun.renderLog({ output: true }); 110 | expect(output).not.toContain('from-setup'); 111 | expect(output).toContain('from-test'); 112 | expect(output).not.toContain('from-teardown'); 113 | }); 114 | 115 | test('should run part of the setup only', async ({ activate }) => { 116 | const { vscode, testController } = await activate(testsWithSetup); 117 | await enableProjects(vscode, ['setup', 'teardown', 'test']); 118 | 119 | await testController.expandTestItems(/setup.ts/); 120 | const testItems = testController.findTestItems(/setup/); 121 | await testController.run(testItems); 122 | 123 | await expect(testController).toHaveTestTree(` 124 | - setup.ts 125 | - ✅ setup [2:0] 126 | - teardown.ts 127 | - ✅ teardown [2:0] 128 | - test.ts 129 | `); 130 | }); 131 | 132 | test('should run setup and teardown for test', async ({ activate }) => { 133 | const { vscode, testController } = await activate(testsWithSetup); 134 | await enableProjects(vscode, ['setup', 'teardown', 'test']); 135 | 136 | await testController.expandTestItems(/test.ts/); 137 | const testItems = testController.findTestItems(/test/); 138 | await testController.run(testItems); 139 | 140 | await expect(testController).toHaveTestTree(` 141 | - setup.ts 142 | - ✅ setup [2:0] 143 | - teardown.ts 144 | - ✅ teardown [2:0] 145 | - test.ts 146 | - ✅ test [2:0] 147 | `); 148 | }); 149 | -------------------------------------------------------------------------------- /src/backend.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { spawn } from 'child_process'; 18 | import { addNpmRunPath, findNode } from './utils'; 19 | import * as vscodeTypes from './vscodeTypes'; 20 | import EventEmitter from 'events'; 21 | import { WebSocketTransport } from './transport'; 22 | 23 | export type BackendServerOptions = { 24 | args: string[]; 25 | cwd: string; 26 | envProvider: () => NodeJS.ProcessEnv; 27 | dumpIO?: boolean; 28 | errors: string[]; 29 | }; 30 | 31 | export class BackendServer { 32 | private _vscode: vscodeTypes.VSCode; 33 | private _options: BackendServerOptions; 34 | private _clientFactory: () => T; 35 | 36 | constructor(vscode: vscodeTypes.VSCode, clientFactory: () => T, options: BackendServerOptions) { 37 | this._vscode = vscode; 38 | this._clientFactory = clientFactory; 39 | this._options = options; 40 | } 41 | 42 | async startAndConnect(): Promise { 43 | const client = this._clientFactory(); 44 | const wsEndpoint = await startBackend(this._vscode, { 45 | ...this._options, 46 | onError: error => client._onErrorEvent.fire(error), 47 | onClose: () => client._onCloseEvent.fire(), 48 | }); 49 | if (!wsEndpoint) 50 | return null; 51 | await client._connect(wsEndpoint); 52 | return client; 53 | } 54 | } 55 | 56 | export class BackendClient extends EventEmitter { 57 | private static _lastId = 0; 58 | private _callbacks = new Map void, reject: (e: Error) => void }>(); 59 | private _transport!: WebSocketTransport; 60 | wsEndpoint!: string; 61 | 62 | readonly onClose: vscodeTypes.Event; 63 | readonly _onCloseEvent: vscodeTypes.EventEmitter; 64 | readonly onError: vscodeTypes.Event; 65 | readonly _onErrorEvent: vscodeTypes.EventEmitter; 66 | 67 | constructor(protected vscode: vscodeTypes.VSCode) { 68 | super(); 69 | this._onCloseEvent = new vscode.EventEmitter(); 70 | this.onClose = this._onCloseEvent.event; 71 | this._onErrorEvent = new vscode.EventEmitter(); 72 | this.onError = this._onErrorEvent.event; 73 | } 74 | 75 | rewriteWsEndpoint(wsEndpoint: string): string { 76 | return wsEndpoint; 77 | } 78 | 79 | async _connect(wsEndpoint: string) { 80 | this.wsEndpoint = wsEndpoint; 81 | this._transport = await WebSocketTransport.connect(this.rewriteWsEndpoint(wsEndpoint)); 82 | this._transport.onmessage = (message: any) => { 83 | if (!message.id) { 84 | this.emit(message.method, message.params); 85 | return; 86 | } 87 | const pair = this._callbacks.get(message.id); 88 | if (!pair) 89 | return; 90 | this._callbacks.delete(message.id); 91 | if (message.error) { 92 | const error = new Error(message.error.error?.message || message.error.value); 93 | error.stack = message.error.error?.stack; 94 | pair.reject(error); 95 | } else { 96 | pair.fulfill(message.result); 97 | } 98 | }; 99 | await this.initialize(); 100 | } 101 | 102 | async initialize() { } 103 | 104 | requestGracefulTermination() { } 105 | 106 | send(method: string, params: any = {}): Promise { 107 | return new Promise((fulfill, reject) => { 108 | const id = ++BackendClient._lastId; 109 | const command = { id, guid: 'DebugController', method, params, metadata: {} }; 110 | this._transport.send(command as any); 111 | this._callbacks.set(id, { fulfill, reject }); 112 | }); 113 | } 114 | 115 | close() { 116 | this._transport.close(); 117 | } 118 | } 119 | 120 | export async function startBackend(vscode: vscodeTypes.VSCode, options: BackendServerOptions & { onError: (error: Error) => void, onClose: () => void }): Promise { 121 | const node = await findNode(vscode, options.cwd); 122 | const serverProcess = spawn(node, options.args, { 123 | cwd: options.cwd, 124 | stdio: 'pipe', 125 | env: { 126 | ...addNpmRunPath(process.env, options.cwd), 127 | ...options.envProvider(), 128 | }, 129 | }); 130 | serverProcess.stderr?.on('data', data => { 131 | if (options.dumpIO) 132 | process.stderr.write('[server err] ' + data.toString()); 133 | options.errors.push(data.toString()); 134 | }); 135 | serverProcess.on('error', options.onError); 136 | serverProcess.on('close', options.onClose); 137 | return new Promise(fulfill => { 138 | serverProcess.stdout?.on('data', async data => { 139 | if (options.dumpIO) 140 | console.log('[server out]', data.toString()); 141 | const match = data.toString().match(/Listening on (.*)/); 142 | if (!match) 143 | return; 144 | const wse = match[1]; 145 | fulfill(wse); 146 | }); 147 | serverProcess.on('exit', () => fulfill(null)); 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playwright Test for VS Code 2 | 3 | This extension integrates Playwright into your VS Code workflow. Here is what it can do: 4 | 5 | - [Playwright Test for VS Code](#playwright-test-for-vs-code) 6 | - [Requirements](#requirements) 7 | - [Install Playwright](#install-playwright) 8 | - [Run tests with a single click](#run-tests-with-a-single-click) 9 | - [Run multiple tests](#run-multiple-tests) 10 | - [Run tests in watch mode](#run-tests-in-watch-mode) 11 | - [Show browsers](#show-browsers) 12 | - [Show trace viewer](#show-trace-viewer) 13 | - [Pick locators](#pick-locators) 14 | - [Debug step-by-step, explore locators](#debug-step-by-step-explore-locators) 15 | - [Tune locators](#tune-locators) 16 | - [Record new tests](#record-new-tests) 17 | - [Record at cursor](#record-at-cursor) 18 | 19 | 20 | ![Playwright VS Code Extension](https://github.com/microsoft/playwright-vscode/assets/13063165/400a3f11-a1e8-4fe7-8ae6-b0460142de35) 21 | 22 | ### Requirements 23 | 24 | This extension works with [Playwright] version v1.38+ or newer. 25 | 26 | 27 | ## Install Playwright 28 | 29 | If you don't have the Playwright NPM package installed in your project, or if you are starting with a new testing project, the "Install Playwright" action from the command panel will help you get started. 30 | 31 | ![Install Playwright](https://github.com/microsoft/playwright-vscode/assets/13063165/716281a0-4206-4f53-ad27-4a6c8fe1c323) 32 | 33 | Pick the browsers you'd like to use by default, don't worry, you'll be able to change them later to add or configure the browsers used. You can also choose to add a GitHub Action so that you can easily run tests on Continuous Integration on every pull request or push. 34 | 35 | ![Choose browsers](https://github.com/microsoft/playwright-vscode/assets/13063165/138a65cb-96f1-41bc-8f3d-0aaff7835920) 36 | 37 | The extension automatically detects if you have [Playwright] installed and loads the browsers, known as [Playwright] projects, into Visual Studio Code. By default it will select the first project as a run profile. Inside the test explorer in VS Code you can change this behavior to run a single test in multiple or different browsers. 38 | 39 | ![select project](https://github.com/microsoft/playwright-vscode/assets/13063165/414f375d-865f-4882-9ca0-070b4a76ce50) 40 | 41 | ## Run tests with a single click 42 | 43 | Click the green triangle next to the test you want to run. You can also run the test from the testing sidebar by clicking the grey triangle next to the test name. 44 | 45 | ![run-tests](https://github.com/microsoft/playwright-vscode/assets/13063165/08eff858-b2ce-4a8d-8eb3-97feba478e68) 46 | 47 | ## Run multiple tests 48 | 49 | You can use the Testing sidebar to run a single test or a group of tests with a single click. While tests are running, the execution line is highlighted. Once the line has completed, the duration of each step of the test is shown. 50 | 51 | ![run-multiple-tests](https://github.com/microsoft/playwright-vscode/assets/13063165/542fb6c4-15ee-4f54-b542-215569c83fbf) 52 | 53 | ## Run tests in watch mode 54 | 55 | Click the "eye" icon to run tests in watch mode. This will re-run the watched tests when you save your changes. 56 | 57 | ![watch-mode](https://github.com/microsoft/playwright-vscode/assets/13063165/fdfb3348-23b2-4127-b4c1-3103dbde7d8a) 58 | 59 | ## Show browsers 60 | 61 | Check the "show browsers" checkbox to run tests with the browser open so that you can visually see what is happening while your test is running. Click on "close all browsers" to close the browsers. 62 | 63 | ![show-browser](https://github.com/microsoft/playwright-vscode/assets/13063165/3e1ab5bb-8ed2-4032-b6ef-81fc4a38bf8f) 64 | 65 | ## Show trace viewer 66 | 67 | Check the "show trace viewer" checkbox to see a full trace of your test. 68 | 69 | ![trace-viewer](https://github.com/microsoft/playwright-vscode/assets/13063165/959cb45c-7104-4607-b465-bf74099142c5) 70 | 71 | ## Pick locators 72 | 73 | Click the "pick locator" button and hover over the browser to see the locators available. Clicking an element will store it in the locators box in VS Code. Pressing enter will save it to the clip board so you can easily paste it into your code or press the escape key to cancel. 74 | 75 | ![pick-locator](https://github.com/microsoft/playwright-vscode/assets/13063165/3bcb9d63-3d78-4e1a-a176-79cb12b39202) 76 | 77 | ## Debug step-by-step, explore locators 78 | 79 | Right click and start breakpoint debugging. Set a breakpoint and hover over a value. When your cursor is on some Playwright action or a locator, the corresponding element (or elements) are highlighted in the browser. 80 | 81 | ![debug](https://github.com/microsoft/playwright-vscode/assets/13063165/7db9e6d4-f1b3-4794-9f61-270f78e930d8) 82 | 83 | ## Tune locators 84 | 85 | You can edit the source code to fine-tune locators while on a breakpoint. Test out different locators and see them highlighted in the browser. 86 | 87 | ![tune-locators](https://github.com/microsoft/playwright-vscode/assets/13063165/00d7cd44-e9b0-472d-9f1f-f8882802d73a) 88 | 89 | ## Record new tests 90 | 91 | Record new tests by clicking on the "record tests" button in the testing sidebar. This will open a browser window where you can navigate to a URL and perform actions on the page which will be recorded to a new test file in VS Code. 92 | 93 | ![record-test](https://github.com/microsoft/playwright-vscode/assets/13063165/841dbc65-35d7-40eb-8df2-5906b7aad4c6) 94 | 95 | ## Record at cursor 96 | 97 | This generates actions into the existing test at the current cursor position. You can run the test, position the cursor at the end of the test and continue generating the test. 98 | 99 | [Playwright]: https://playwright.dev "Playwright" 100 | -------------------------------------------------------------------------------- /src/installer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from 'path'; 18 | import fs from 'fs'; 19 | import os from 'os'; 20 | import * as vscodeTypes from './vscodeTypes'; 21 | import { TestModel } from './testModel'; 22 | import { uriToPath } from './utils'; 23 | 24 | export async function installPlaywright(vscode: vscodeTypes.VSCode) { 25 | const [workspaceFolder] = vscode.workspace.workspaceFolders || []; 26 | if (!workspaceFolder) { 27 | await vscode.window.showErrorMessage('Please open a folder in VS Code to initialize Playwright. Either an empty folder or a folder with an existing package.json.'); 28 | return; 29 | } 30 | const options: vscodeTypes.QuickPickItem[] = []; 31 | options.push({ label: 'Select browsers to install', kind: vscode.QuickPickItemKind.Separator }); 32 | options.push(chromiumItem, firefoxItem, webkitItem); 33 | options.push({ label: '', kind: vscode.QuickPickItemKind.Separator }); 34 | options.push(useJavaScriptItem); 35 | options.push(addActionItem); 36 | if (process.platform === 'linux') { 37 | updateInstallDepsPicked(); 38 | options.push(installDepsItem); 39 | } 40 | const result = await vscode.window.showQuickPick(options, { 41 | title: 'Install Playwright', 42 | canPickMany: true, 43 | }); 44 | if (result === undefined) 45 | return; 46 | 47 | const terminal = vscode.window.createTerminal({ 48 | name: 'Install Playwright', 49 | cwd: uriToPath(workspaceFolder.uri), 50 | env: process.env, 51 | }); 52 | 53 | terminal.show(); 54 | 55 | const args: string[] = []; 56 | if (result.includes(chromiumItem)) 57 | args.push('--browser=chromium'); 58 | if (result.includes(firefoxItem)) 59 | args.push('--browser=firefox'); 60 | if (result.includes(webkitItem)) 61 | args.push('--browser=webkit'); 62 | if (!result.includes(chromiumItem) && !result.includes(firefoxItem) && !result.includes(webkitItem)) 63 | args.push('--no-browsers'); 64 | if (result.includes(useJavaScriptItem)) 65 | args.push('--lang=js'); 66 | if (result.includes(addActionItem)) 67 | args.push('--gha'); 68 | if (result.includes(installDepsItem)) 69 | args.push('--install-deps'); 70 | 71 | terminal.sendText(`npm init playwright@latest --yes "--" . ${quote('--quiet')} ${args.map(quote).join(' ')}`, true); 72 | } 73 | 74 | function quote(s: string): string { 75 | return `'${s}'`; 76 | } 77 | 78 | export async function installBrowsers(vscode: vscodeTypes.VSCode, model: TestModel) { 79 | const options: vscodeTypes.QuickPickItem[] = []; 80 | options.push({ label: 'Select browsers to install', kind: vscode.QuickPickItemKind.Separator }); 81 | options.push(chromiumItem, firefoxItem, webkitItem); 82 | options.push({ label: '', kind: vscode.QuickPickItemKind.Separator }); 83 | if (process.platform === 'linux') { 84 | updateInstallDepsPicked(); 85 | options.push(installDepsItem); 86 | } 87 | const result = await vscode.window.showQuickPick(options, { 88 | title: `Install browsers for Playwright v${model.config.version}:`, 89 | canPickMany: true, 90 | }); 91 | if (!result?.length) 92 | return; 93 | 94 | const terminal = vscode.window.createTerminal({ 95 | name: 'Install Playwright', 96 | cwd: model.config.workspaceFolder, 97 | env: process.env, 98 | }); 99 | 100 | terminal.show(); 101 | 102 | const args: string[] = []; 103 | const installCommand = result.includes(installDepsItem) ? 'install --with-deps' : 'install'; 104 | if (result.includes(chromiumItem)) 105 | args.push('chromium'); 106 | if (result.includes(firefoxItem)) 107 | args.push('firefox'); 108 | if (result.includes(webkitItem)) 109 | args.push('webkit'); 110 | 111 | if (args.length) 112 | terminal.sendText(`npx playwright ${installCommand} ${args.join(' ')}`, true); 113 | else if (result.includes(installDepsItem)) 114 | terminal.sendText(`npx playwright install-deps`, true); 115 | } 116 | 117 | const chromiumItem: vscodeTypes.QuickPickItem = { 118 | label: 'Chromium', 119 | picked: true, 120 | description: '— powers Google Chrome, Microsoft Edge, etc\u2026', 121 | }; 122 | const firefoxItem: vscodeTypes.QuickPickItem = { 123 | label: 'Firefox', 124 | picked: true, 125 | description: '— powers Mozilla Firefox', 126 | }; 127 | const webkitItem: vscodeTypes.QuickPickItem = { 128 | label: 'WebKit', 129 | picked: true, 130 | description: '— powers Apple Safari', 131 | }; 132 | const addActionItem: vscodeTypes.QuickPickItem = { 133 | label: 'Add GitHub Actions workflow', 134 | picked: true, 135 | description: '— adds GitHub Actions recipe' 136 | }; 137 | const useJavaScriptItem: vscodeTypes.QuickPickItem = { 138 | label: 'Use JavaScript', 139 | picked: false, 140 | description: '— use JavaScript (TypeScript is the default)' 141 | }; 142 | const installDepsItem: vscodeTypes.QuickPickItem = { 143 | label: 'Install Linux dependencies', 144 | picked: false, 145 | }; 146 | 147 | function updateInstallDepsPicked() { 148 | installDepsItem.picked = process.platform === 'linux' && !fs.existsSync(path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'ms-playwright')); 149 | } 150 | -------------------------------------------------------------------------------- /src/settingsView.script.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Config, createAction, ProjectEntry, vscode } from './common'; 18 | 19 | let selectConfig: Config; 20 | 21 | const selectAllButton = document.getElementById('selectAll') as HTMLAnchorElement; 22 | const unselectAllButton = document.getElementById('unselectAll') as HTMLAnchorElement; 23 | const toggleModels = document.getElementById('toggleModels') as HTMLAnchorElement; 24 | 25 | function updateProjects(projects: ProjectEntry[]) { 26 | const projectSelector = document.getElementById('project-selector')!; 27 | if (projects.length < 2) { 28 | projectSelector.style.display = 'none'; 29 | return; 30 | } 31 | projectSelector.style.display = 'flex'; 32 | 33 | const projectsElement = document.getElementById('projects') as HTMLElement; 34 | projectsElement.textContent = ''; 35 | for (const project of projects) { 36 | const { name, enabled } = project; 37 | const div = document.createElement('div'); 38 | div.classList.add('action'); 39 | const label = document.createElement('label'); 40 | const input = document.createElement('input'); 41 | input.type = 'checkbox'; 42 | input.checked = enabled; 43 | input.addEventListener('change', () => { 44 | vscode.postMessage({ method: 'setProjectEnabled', params: { configFile: selectConfig.configFile, projectName: name, enabled: input.checked } }); 45 | }); 46 | label.appendChild(input); 47 | label.appendChild(document.createTextNode(name || '')); 48 | div.appendChild(label); 49 | projectsElement.appendChild(div); 50 | } 51 | 52 | const allEnabled = projects.every(p => p.enabled); 53 | selectAllButton.hidden = allEnabled; 54 | unselectAllButton.hidden = !allEnabled; 55 | } 56 | 57 | function setAllProjectsEnabled(enabled: boolean) { 58 | vscode.postMessage({ method: 'setAllProjectsEnabled', params: { configFile: selectConfig.configFile, enabled } }); 59 | } 60 | selectAllButton.addEventListener('click', () => setAllProjectsEnabled(true)); 61 | unselectAllButton.addEventListener('click', () => setAllProjectsEnabled(false)); 62 | toggleModels.addEventListener('click', () => (vscode.postMessage({ method: 'execute', params: { command: 'pw.extension.command.toggleModels' } }))); 63 | 64 | for (const input of Array.from(document.querySelectorAll('input[type=checkbox]'))) { 65 | input.addEventListener('change', event => { 66 | vscode.postMessage({ method: 'toggle', params: { setting: input.getAttribute('setting') } }); 67 | }); 68 | } 69 | for (const select of Array.from(document.querySelectorAll('select[setting]'))) { 70 | select.addEventListener('change', event => { 71 | vscode.postMessage({ method: 'set', params: { setting: select.getAttribute('setting'), value: select.value } }); 72 | }); 73 | } 74 | 75 | window.addEventListener('message', event => { 76 | const actionsElement = document.getElementById('actions')!; 77 | const rareActionsElement = document.getElementById('rareActions')!; 78 | const modelSelector = document.getElementById('model-selector')!; 79 | 80 | const { method, params } = event.data; 81 | if (method === 'settings') { 82 | for (const [key, value] of Object.entries(params.settings as Record)) { 83 | const input = document.querySelector('input[setting=' + key + ']') as HTMLInputElement; 84 | if (input) { 85 | if (typeof value === 'boolean') 86 | input.checked = value; 87 | else 88 | input.value = value; 89 | } 90 | const select = document.querySelector('select[setting=' + key + ']') as HTMLSelectElement; 91 | if (select) 92 | select.value = value as string; 93 | } 94 | } else if (method === 'actions') { 95 | actionsElement.textContent = ''; 96 | rareActionsElement.textContent = ''; 97 | for (const action of params.actions) { 98 | const actionElement = createAction(action); 99 | if (!actionElement) 100 | continue; 101 | if (action.location === 'rareActions') 102 | rareActionsElement.appendChild(actionElement); 103 | else 104 | actionsElement.appendChild(actionElement); 105 | } 106 | } else if (method === 'models') { 107 | const { configs, showModelSelector } = params; 108 | const select = document.getElementById('models') as HTMLSelectElement; 109 | select.textContent = ''; 110 | const configsMap = new Map(); 111 | for (const config of configs) { 112 | configsMap.set(config.configFile, config); 113 | const option = document.createElement('option'); 114 | option.value = config.configFile; 115 | option.textContent = config.label; 116 | select.appendChild(option); 117 | if (config.selected) { 118 | selectConfig = config; 119 | select.value = config.configFile; 120 | updateProjects(config.projects); 121 | } 122 | } 123 | select.addEventListener('change', event => { 124 | vscode.postMessage({ method: 'selectModel', params: { configFile: select.value } }); 125 | updateProjects(configsMap.get(select.value).projects); 126 | }); 127 | modelSelector.style.display = showModelSelector ? 'flex' : 'none'; 128 | } 129 | }); 130 | -------------------------------------------------------------------------------- /tests/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { connectToSharedBrowser, enableProjects, expect, test, waitForPage } from './utils'; 18 | import fs from 'node:fs'; 19 | 20 | test('should generate code', async ({ activate }) => { 21 | test.slow(); 22 | 23 | const globalSetupFile = test.info().outputPath('globalSetup.txt'); 24 | const { vscode } = await activate({ 25 | 'playwright.config.js': `module.exports = { 26 | projects: [ 27 | { 28 | name: 'default', 29 | }, 30 | { 31 | name: 'germany', 32 | use: { 33 | locale: 'de-DE', 34 | }, 35 | }, 36 | ], 37 | globalSetup: './globalSetup.js', 38 | }`, 39 | 'globalSetup.js': ` 40 | import fs from 'fs'; 41 | module.exports = async () => { 42 | fs.writeFileSync(${JSON.stringify(globalSetupFile)}, 'global setup was called'); 43 | } 44 | `, 45 | }); 46 | 47 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 48 | await webView.getByRole('checkbox', { name: 'default' }).setChecked(false); 49 | await webView.getByRole('checkbox', { name: 'germany' }).setChecked(true); 50 | await webView.getByText('Record new').click(); 51 | await expect.poll(() => vscode.lastWithProgressData, { timeout: 0 }).toEqual({ message: 'recording\u2026' }); 52 | 53 | expect(fs.readFileSync(globalSetupFile, 'utf-8')).toBe('global setup was called'); 54 | 55 | const browser = await connectToSharedBrowser(vscode); 56 | const page = await waitForPage(browser, { locale: 'de-DE' }); 57 | await page.locator('body').click(); 58 | expect(await page.evaluate(() => navigator.language)).toBe('de-DE'); 59 | await expect.poll(() => { 60 | return vscode.window.visibleTextEditors[0]?.edits; 61 | }).toEqual([{ 62 | from: `import { test, expect } from '@playwright/test'; 63 | 64 | test('test', async ({ page }) => { 65 | // Recording... 66 | });`, 67 | range: '[3:2 - 3:17]', 68 | to: `import { test, expect } from '@playwright/test'; 69 | 70 | test('test', async ({ page }) => { 71 | await page.locator('body').click(); 72 | });` 73 | }]); 74 | }); 75 | 76 | test('running test should stop the recording', async ({ activate, showBrowser }) => { 77 | test.skip(!showBrowser); 78 | 79 | const { vscode, testController } = await activate({ 80 | 'playwright.config.js': `module.exports = {}`, 81 | 'tests/test.spec.ts': ` 82 | import { test } from '@playwright/test'; 83 | test('one', () => {}); 84 | `, 85 | }); 86 | 87 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 88 | await webView.getByText('Record new').click(); 89 | await expect.poll(() => vscode.lastWithProgressData, { timeout: 0 }).toEqual({ message: 'recording\u2026' }); 90 | 91 | const testRun = await testController.run(); 92 | await expect(testRun).toHaveOutput('passed'); 93 | 94 | await expect.poll(() => vscode.lastWithProgressData, { timeout: 0 }).toEqual('finished'); 95 | }); 96 | 97 | test('Record at Cursor should respect custom testId', async ({ activate, showBrowser }) => { 98 | test.skip(!showBrowser); 99 | 100 | const { vscode, testController } = await activate({ 101 | 'playwright.config.js': `module.exports = { 102 | projects: [ 103 | { name: 'main', use: { testIdAttribute: 'data-testerid', testDir: 'tests' } }, 104 | { name: 'unused', use: { testIdAttribute: 'unused', testDir: 'nonExistant' } }, 105 | ] 106 | };`, 107 | 'tests/test.spec.ts': ` 108 | import { test } from '@playwright/test'; 109 | test('should pass', async ({ page }) => { 110 | await page.setContent(''); 111 | 112 | }); 113 | `, 114 | }); 115 | 116 | await enableProjects(vscode, ['main']); 117 | 118 | await testController.expandTestItems(/test.spec/); 119 | await expect(await testController.run()).toHaveOutput('1 passed'); 120 | 121 | await vscode.openEditors('**/test.spec.ts'); 122 | const editor = vscode.window.activeTextEditor; 123 | expect(editor.document.uri.path).toContain('test.spec.ts'); 124 | editor.selection = new vscode.Selection(4, 0, 4, 0); 125 | 126 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 127 | await webView.getByText('Record at cursor').click(); 128 | await expect.poll(() => vscode.lastWithProgressData, { timeout: 0 }).toEqual({ message: 'recording\u2026' }); 129 | 130 | const browser = await connectToSharedBrowser(vscode); 131 | const page = await waitForPage(browser); 132 | await page.getByRole('button', { name: 'click me' }).click(); 133 | await expect.poll(() => editor.edits).toEqual([ 134 | { 135 | range: '[4:0 - 4:0]', 136 | from: ` 137 | import { test } from '@playwright/test'; 138 | test('should pass', async ({ page }) => { 139 | await page.setContent(''); 140 | 141 | }); 142 | `, 143 | to: ` 144 | import { test } from '@playwright/test'; 145 | test('should pass', async ({ page }) => { 146 | await page.setContent(''); 147 | await page.getByTestId('foo').click(); 148 | }); 149 | `, 150 | } 151 | ]); 152 | 153 | vscode.lastWithProgressToken!.cancel(); 154 | }); 155 | -------------------------------------------------------------------------------- /tests/pick-selector.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { selectors } from '@playwright/test'; 18 | import { connectToSharedBrowser, expect, test, waitForPage, waitForRecorderMode } from './utils'; 19 | 20 | test('should pick locator and dismiss the toolbar', async ({ activate }) => { 21 | const { vscode } = await activate({ 22 | 'playwright.config.js': `module.exports = {}`, 23 | }); 24 | 25 | const settingsView = vscode.webViews.get('pw.extension.settingsView')!; 26 | await settingsView.getByText('Pick locator').click(); 27 | await waitForRecorderMode(vscode, 'inspecting'); 28 | 29 | const browser = await connectToSharedBrowser(vscode); 30 | const page = await waitForPage(browser); 31 | await page.setContent(` 32 |

Hello

33 |

World

34 | `); 35 | await page.locator('h1').first().click(); 36 | 37 | const locatorsView = vscode.webViews.get('pw.extension.locatorsView')!; 38 | await expect(locatorsView.locator('body')).toMatchAriaSnapshot(` 39 | - text: Locator 40 | - textbox "Locator": "getByRole('heading', { name: 'Hello' })" 41 | `); 42 | 43 | await expect(locatorsView.locator('body')).toMatchAriaSnapshot(` 44 | - text: Locator 45 | - textbox "Locator": "getByRole('heading', { name: 'Hello' })" 46 | - text: Aria 47 | - textbox "Aria": "- heading \\"Hello\\" [level=1]" 48 | `); 49 | 50 | await page.click('x-pw-tool-item.pick-locator'); 51 | await expect(page.locator('x-pw-tool-item.pick-locator')).toBeHidden(); 52 | await waitForRecorderMode(vscode, 'none'); 53 | }); 54 | 55 | test('should highlight locator on edit', async ({ activate }) => { 56 | const { vscode } = await activate({ 57 | 'playwright.config.js': `module.exports = {}`, 58 | }); 59 | 60 | const settingsView = vscode.webViews.get('pw.extension.settingsView')!; 61 | await settingsView.getByText('Pick locator').click(); 62 | await waitForRecorderMode(vscode, 'inspecting'); 63 | 64 | const browser = await connectToSharedBrowser(vscode); 65 | const page = await waitForPage(browser); 66 | await page.setContent(` 67 |

Hello

68 | 69 | `); 70 | const box = await page.getByRole('heading', { name: 'Hello' }).boundingBox(); 71 | 72 | const locatorsView = vscode.webViews.get('pw.extension.locatorsView')!; 73 | await locatorsView.getByRole('textbox', { name: 'Locator' }).fill('h1'); 74 | 75 | await expect(page.locator('x-pw-highlight')).toBeVisible(); 76 | expect(await page.locator('x-pw-highlight').boundingBox()).toEqual(box); 77 | }); 78 | 79 | test('should copy locator to clipboard', async ({ activate }) => { 80 | const { vscode } = await activate({ 81 | 'playwright.config.js': `module.exports = {}`, 82 | }); 83 | 84 | const locatorsView = vscode.webViews.get('pw.extension.locatorsView')!; 85 | await locatorsView.getByRole('checkbox', { name: 'Copy on pick' }).check(); 86 | await locatorsView.getByRole('button', { name: 'Pick locator' }).first().click(); 87 | await waitForRecorderMode(vscode, 'inspecting'); 88 | 89 | const browser = await connectToSharedBrowser(vscode); 90 | const page = await waitForPage(browser); 91 | await page.setContent(` 92 |

Hello

93 |

World

94 | `); 95 | await page.locator('h1').first().click(); 96 | 97 | await expect.poll(() => vscode.env.clipboard.readText()).toBe(`getByRole('heading', { name: 'Hello' })`); 98 | }); 99 | 100 | test('should pick locator and use the testIdAttribute from the config', async ({ activate }) => { 101 | const { vscode } = await activate({ 102 | 'playwright.config.js': `module.exports = { use: { testIdAttribute: 'data-testerid' } }`, 103 | }); 104 | 105 | const settingsView = vscode.webViews.get('pw.extension.settingsView')!; 106 | await settingsView.getByText('Pick locator').click(); 107 | await waitForRecorderMode(vscode, 'inspecting'); 108 | 109 | const browser = await connectToSharedBrowser(vscode); 110 | // TODO: Get rid of 'selectors.setTestIdAttribute' once launchServer multiclient is stable and migrate to it. 111 | // This is a workaround for waitForPage which internally uses Browser._newContextForReuse 112 | // which ends up overriding the testIdAttribute back to 'data-testid'. 113 | selectors.setTestIdAttribute('data-testerid'); 114 | const page = await waitForPage(browser); 115 | await page.setContent(` 116 |
Hello
117 | `); 118 | await page.locator('div').click(); 119 | 120 | const locatorsView = vscode.webViews.get('pw.extension.locatorsView')!; 121 | await expect(locatorsView.locator('body')).toMatchAriaSnapshot(` 122 | - text: Locator 123 | - textbox "Locator": "getByTestId('hello')" 124 | `); 125 | // TODO: remove as per TODO above. 126 | selectors.setTestIdAttribute('data-testid'); 127 | }); 128 | 129 | test('running test should dismiss the toolbar', async ({ activate, showBrowser }) => { 130 | test.skip(!showBrowser); 131 | 132 | const { vscode, testController } = await activate({ 133 | 'playwright.config.js': `module.exports = {}`, 134 | 'tests/test.spec.ts': ` 135 | import { test } from '@playwright/test'; 136 | test('one', () => {}); 137 | `, 138 | }); 139 | 140 | const settingsView = vscode.webViews.get('pw.extension.settingsView')!; 141 | await settingsView.getByRole('button', { name: 'Pick locator' }).click(); 142 | await waitForRecorderMode(vscode, 'inspecting'); 143 | 144 | const testRun = await testController.run(); 145 | await expect(testRun).toHaveOutput('passed'); 146 | 147 | await waitForRecorderMode(vscode, 'none'); 148 | }); 149 | -------------------------------------------------------------------------------- /src/settingsModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DisposableBase } from './disposableBase'; 18 | import * as vscodeTypes from './vscodeTypes'; 19 | 20 | export type ProjectSettings = { 21 | name: string; 22 | enabled: boolean; 23 | }; 24 | 25 | export type ConfigSettings = { 26 | relativeConfigFile: string; 27 | projects: ProjectSettings[]; 28 | enabled: boolean; 29 | selected: boolean; 30 | }; 31 | 32 | export type WorkspaceSettings = { 33 | configs?: ConfigSettings[]; 34 | }; 35 | 36 | export const workspaceStateKey = 'pw.workspace-settings'; 37 | 38 | export class SettingsModel extends DisposableBase { 39 | private _vscode: vscodeTypes.VSCode; 40 | private _settings = new Map>(); 41 | private _context: vscodeTypes.ExtensionContext; 42 | readonly onChange: vscodeTypes.Event; 43 | private _onChange: vscodeTypes.EventEmitter; 44 | showBrowser: Setting; 45 | showTrace: Setting; 46 | runGlobalSetupOnEachRun: Setting; 47 | updateSnapshots: Setting<'all' | 'changed' | 'missing' | 'none' | 'no-override'>; 48 | updateSourceMethod: Setting<'overwrite' | 'patch' | '3way' | 'no-override'>; 49 | pickLocatorCopyToClipboard: Setting; 50 | 51 | constructor(vscode: vscodeTypes.VSCode, context: vscodeTypes.ExtensionContext) { 52 | super(); 53 | this._vscode = vscode; 54 | this._context = context; 55 | this._onChange = new vscode.EventEmitter(); 56 | this.onChange = this._onChange.event; 57 | 58 | this.showBrowser = this._createSetting('reuseBrowser'); 59 | this.showTrace = this._createSetting('showTrace'); 60 | this.runGlobalSetupOnEachRun = this._createSetting('runGlobalSetupOnEachRun'); 61 | this.updateSnapshots = this._createSetting('updateSnapshots'); 62 | this.updateSourceMethod = this._createSetting('updateSourceMethod'); 63 | this.pickLocatorCopyToClipboard = this._createSetting('pickLocatorCopyToClipboard'); 64 | 65 | this._disposables.push( 66 | this._onChange, 67 | this.showBrowser.onChange(enabled => { 68 | if (enabled && this.showTrace.get()) 69 | void this.showTrace.set(false); 70 | }), 71 | this.showTrace.onChange(enabled => { 72 | if (enabled && this.showBrowser.get()) 73 | void this.showBrowser.set(false); 74 | }), 75 | ); 76 | 77 | this._modernize(); 78 | } 79 | 80 | private _modernize() { 81 | const workspaceSettings = this._vscode.workspace.getConfiguration('playwright').get('workspaceSettings') as any; 82 | if (workspaceSettings?.configs && !this._context.workspaceState.get(workspaceStateKey)) { 83 | void this._context.workspaceState.update(workspaceStateKey, { configs: workspaceSettings.configs }); 84 | void this._vscode.workspace.getConfiguration('playwright').update('workspaceSettings', undefined); 85 | } 86 | } 87 | 88 | setting(settingName: string): Setting | undefined { 89 | return this._settings.get(settingName); 90 | } 91 | 92 | private _createSetting(settingName: string): Setting { 93 | const setting = new PersistentSetting(this._vscode, settingName); 94 | this._disposables.push(setting); 95 | this._disposables.push(setting.onChange(() => this._onChange.fire())); 96 | this._settings.set(settingName, setting); 97 | return setting; 98 | } 99 | 100 | json(): Record { 101 | const result: Record = {}; 102 | for (const [key, setting] of this._settings) 103 | result[key] = setting.get(); 104 | return result; 105 | } 106 | } 107 | 108 | export interface Setting extends vscodeTypes.Disposable { 109 | readonly onChange: vscodeTypes.Event; 110 | get(): T | undefined; 111 | set(value: T): Promise; 112 | } 113 | 114 | class SettingBase extends DisposableBase implements Setting { 115 | readonly settingName: string; 116 | readonly onChange: vscodeTypes.Event; 117 | protected _onChange: vscodeTypes.EventEmitter; 118 | protected _vscode: vscodeTypes.VSCode; 119 | 120 | constructor(vscode: vscodeTypes.VSCode, settingName: string) { 121 | super(); 122 | this._vscode = vscode; 123 | this.settingName = settingName; 124 | this._onChange = new vscode.EventEmitter(); 125 | this.onChange = this._onChange.event; 126 | } 127 | get(): T | undefined { 128 | throw new Error('Method not implemented.'); 129 | } 130 | 131 | set(value: T): Promise { 132 | throw new Error('Method not implemented.'); 133 | } 134 | } 135 | 136 | class PersistentSetting extends SettingBase { 137 | constructor(vscode: vscodeTypes.VSCode, settingName: string) { 138 | super(vscode, settingName); 139 | 140 | const settingFQN = `playwright.${settingName}`; 141 | this._disposables = [ 142 | this._onChange, 143 | vscode.workspace.onDidChangeConfiguration(event => { 144 | if (event.affectsConfiguration(settingFQN)) 145 | this._onChange.fire(this.get()!); 146 | }), 147 | vscode.commands.registerCommand(`pw.extension.toggle.${settingName}`, async () => { 148 | await this.set(!this.get() as T); 149 | }), 150 | ]; 151 | } 152 | 153 | get(): T | undefined { 154 | const configuration = this._vscode.workspace.getConfiguration('playwright'); 155 | return configuration.get(this.settingName) as T | undefined; 156 | } 157 | 158 | async set(value: T) { 159 | const configuration = this._vscode.workspace.getConfiguration('playwright'); 160 | const existsInWorkspace = configuration.inspect(this.settingName)?.workspaceValue !== undefined; 161 | if (existsInWorkspace) 162 | await configuration.update(this.settingName, value, false); 163 | // Intentionally fall through. 164 | await configuration.update(this.settingName, value, true); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/babelHighlightUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from 'path'; 18 | import { t, parse, ParseResult, traverse, SourceLocation, babelPresetTypescript, babelPluginProposalDecorators } from './babelBundle'; 19 | import { asyncMatchers, pageMethods, locatorMethods } from './methodNames'; 20 | 21 | const astCache = new Map(); 22 | 23 | export function pruneAstCaches(fsPathsToRetain: string[]) { 24 | const retain = new Set(fsPathsToRetain); 25 | for (const key of astCache.keys()) { 26 | if (!retain.has(key)) 27 | astCache.delete(key); 28 | } 29 | } 30 | 31 | export type SourcePosition = { 32 | line: number; // 1-based 33 | column: number; // 1-based 34 | }; 35 | 36 | function getAst(text: string, fsPath: string) { 37 | const cached = astCache.get(fsPath); 38 | let ast = cached?.ast; 39 | if (!cached || cached.text !== text) { 40 | try { 41 | ast = parse(text, { 42 | filename: path.basename(fsPath), 43 | plugins: [ 44 | [babelPluginProposalDecorators, { version: '2023-05' }], 45 | ], 46 | presets: [[babelPresetTypescript, { onlyRemoveTypeImports: false }]], 47 | babelrc: false, 48 | configFile: false, 49 | sourceType: 'module', 50 | }); 51 | astCache.set(fsPath, { text, ast }); 52 | } catch (e) { 53 | astCache.set(fsPath, { text, ast: undefined }); 54 | } 55 | } 56 | return ast; 57 | } 58 | 59 | export function locatorForSourcePosition(text: string, vars: { pages: string[], locators: string[] }, fsPath: string, position: SourcePosition): string | undefined { 60 | const ast = getAst(text, fsPath); 61 | if (!ast) 62 | return; 63 | 64 | let rangeMatch: string | undefined; 65 | let lineMatch: string | undefined; 66 | traverse(ast, { 67 | enter(path) { 68 | let expressionNode; 69 | let pageSelectorNode; 70 | let pageSelectorCallee; 71 | 72 | // Hover over page.[click,check,...](selector) will highlight `page.locator(selector)`. 73 | if (t.isCallExpression(path.node) && 74 | t.isMemberExpression(path.node.callee) && 75 | t.isIdentifier(path.node.callee.object) && 76 | t.isIdentifier(path.node.callee.property) && 77 | (vars.pages.includes(path.node.callee.object.name) && pageMethods.includes(path.node.callee.property.name))) { 78 | expressionNode = path.node; 79 | pageSelectorNode = path.node.arguments[0]; 80 | pageSelectorCallee = path.node.callee.object.name; 81 | } 82 | 83 | 84 | // Hover over locator variables will highlight `locator`. 85 | if (t.isIdentifier(path.node) && 86 | vars.locators.includes(path.node.name)) 87 | expressionNode = path.node; 88 | 89 | 90 | // Web assertions: expect(a).to*. 91 | if (t.isMemberExpression(path.node) && 92 | t.isIdentifier(path.node.property) && 93 | asyncMatchers.includes(path.node.property.name) && 94 | t.isCallExpression(path.node.object) && 95 | t.isIdentifier(path.node.object.callee) && 96 | path.node.object.callee.name === 'expect') 97 | expressionNode = path.node.object.arguments[0]; 98 | 99 | 100 | // *.locator(), *.getBy*(), *.click(), *.fill(), *.type() call 101 | if (t.isCallExpression(path.node) && 102 | t.isMemberExpression(path.node.callee) && 103 | t.isIdentifier(path.node.callee.property) && 104 | locatorMethods.includes(path.node.callee.property.name)) 105 | expressionNode = path.node; 106 | 107 | 108 | if (!expressionNode || !expressionNode.loc) 109 | return; 110 | const isRangeMatch = containsPosition(expressionNode.loc, position); 111 | const isLineMatch = expressionNode.loc.start.line === position.line; 112 | if (isRangeMatch || isLineMatch) { 113 | let expression; 114 | if (pageSelectorNode) 115 | expression = `${pageSelectorCallee}.locator(${text.substring(pageSelectorNode.start!, pageSelectorNode.end!)})`; 116 | else 117 | expression = text.substring(expressionNode.start!, expressionNode.end!); 118 | 119 | if (isRangeMatch && (!rangeMatch || expression.length < rangeMatch.length)) { 120 | // Prefer shortest range match to better support chains. 121 | rangeMatch = expression; 122 | } 123 | if (isLineMatch && (!lineMatch || lineMatch.length < expression.length)) { 124 | // Prefer longest line match to better support chains. 125 | lineMatch = expression; 126 | } 127 | } 128 | } 129 | }); 130 | return rangeMatch || lineMatch; 131 | } 132 | 133 | function containsPosition(location: SourceLocation, position: SourcePosition): boolean { 134 | if (position.line < location.start.line || position.line > location.end.line) 135 | return false; 136 | if (position.line === location.start.line && position.column < location.start.column) 137 | return false; 138 | if (position.line === location.end.line && position.column > location.end.column) 139 | return false; 140 | return true; 141 | } 142 | 143 | export function findTestEndPosition(text: string, fsPath: string, startPosition: SourcePosition): SourcePosition | undefined { 144 | const ast = getAst(text, fsPath); 145 | if (!ast) 146 | return; 147 | let result: SourcePosition | undefined; 148 | traverse(ast, { 149 | enter(path) { 150 | if (t.isCallExpression(path.node) && path.node.loc && containsPosition(path.node.loc, startPosition)) { 151 | const callNode = path.node; 152 | const funcNode = callNode.arguments[callNode.arguments.length - 1]; 153 | if (callNode.arguments.length >= 2 && t.isFunction(funcNode) && funcNode.body.loc) 154 | result = funcNode.body.loc.end; 155 | } 156 | } 157 | }); 158 | return result; 159 | } 160 | -------------------------------------------------------------------------------- /tests/trace-viewer.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { enableConfigs, expect, selectConfig, selectTestItem, test, traceViewerInfo } from './utils'; 18 | 19 | test.skip(({ showTrace }) => !showTrace); 20 | 21 | test('@smoke should open trace viewer', async ({ activate }) => { 22 | const { vscode, testController } = await activate({ 23 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 24 | 'tests/test.spec.ts': ` 25 | import { test } from '@playwright/test'; 26 | test('should pass', async () => {}); 27 | `, 28 | }); 29 | 30 | await testController.run(); 31 | await testController.expandTestItems(/test.spec/); 32 | selectTestItem(testController.findTestItems(/pass/)[0]); 33 | 34 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 35 | traceFile: expect.stringContaining('pass'), 36 | }); 37 | }); 38 | 39 | test('should change opened file in trace viewer', async ({ activate }) => { 40 | const { vscode, testController } = await activate({ 41 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 42 | 'tests/test.spec.ts': ` 43 | import { test } from '@playwright/test'; 44 | test('one', async () => {}); 45 | test('two', async () => {}); 46 | `, 47 | }); 48 | 49 | await testController.run(); 50 | await testController.expandTestItems(/test.spec/); 51 | 52 | selectTestItem(testController.findTestItems(/one/)[0]); 53 | 54 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 55 | traceFile: expect.stringContaining('one'), 56 | }); 57 | 58 | selectTestItem(testController.findTestItems(/two/)[0]); 59 | 60 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 61 | traceFile: expect.stringContaining('two'), 62 | }); 63 | }); 64 | 65 | test('should not open trace viewer if test did not run', async ({ activate }) => { 66 | const { vscode, testController } = await activate({ 67 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 68 | 'tests/test.spec.ts': ` 69 | import { test } from '@playwright/test'; 70 | test('should pass', async () => {}); 71 | `, 72 | }); 73 | 74 | await testController.expandTestItems(/test.spec/); 75 | selectTestItem(testController.findTestItems(/pass/)[0]); 76 | 77 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 78 | traceFile: undefined, 79 | }); 80 | }); 81 | 82 | test('should refresh trace viewer while test is running', async ({ activate, createLatch }) => { 83 | const latch = createLatch(); 84 | 85 | const { vscode, testController } = await activate({ 86 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 87 | 'tests/test.spec.ts': ` 88 | import { test } from '@playwright/test'; 89 | test('should pass', async () => ${latch.blockingCode}); 90 | `, 91 | }); 92 | 93 | await testController.expandTestItems(/test.spec/); 94 | selectTestItem(testController.findTestItems(/pass/)[0]); 95 | 96 | const testRunPromise = testController.run(); 97 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 98 | traceFile: expect.stringMatching(/\.json$/), 99 | }); 100 | 101 | latch.open(); 102 | await testRunPromise; 103 | 104 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 105 | traceFile: expect.stringMatching(/\.zip$/), 106 | }); 107 | }); 108 | 109 | test('should close trace viewer if test configs refreshed', async ({ activate }) => { 110 | const { vscode, testController } = await activate({ 111 | 'playwright.config.js': `module.exports = { testDir: 'tests' }`, 112 | 'tests/test.spec.ts': ` 113 | import { test } from '@playwright/test'; 114 | test('should pass', async () => {}); 115 | `, 116 | }); 117 | 118 | await testController.run(); 119 | await testController.expandTestItems(/test.spec/); 120 | selectTestItem(testController.findTestItems(/pass/)[0]); 121 | 122 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 123 | traceFile: expect.stringContaining('pass'), 124 | }); 125 | 126 | await testController.refreshHandler!(null); 127 | 128 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 129 | traceFile: undefined, 130 | visible: false, 131 | }); 132 | }); 133 | 134 | test('should open new trace viewer when another test config is selected', async ({ activate }) => { 135 | const { vscode, testController } = await activate({ 136 | 'playwright1.config.js': `module.exports = { testDir: 'tests1' }`, 137 | 'playwright2.config.js': `module.exports = { testDir: 'tests2' }`, 138 | 'tests1/test.spec.ts': ` 139 | import { test } from '@playwright/test'; 140 | test('one', () => {}); 141 | `, 142 | 'tests2/test.spec.ts': ` 143 | import { test } from '@playwright/test'; 144 | test('one', () => {}); 145 | `, 146 | }); 147 | 148 | await enableConfigs(vscode, ['playwright1.config.js', 'playwright2.config.js']); 149 | await selectConfig(vscode, 'playwright1.config.js'); 150 | 151 | await testController.expandTestItems(/test.spec/); 152 | const testItems = testController.findTestItems(/one/); 153 | await testController.run(testItems); 154 | 155 | selectTestItem(testItems[0]); 156 | 157 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 158 | serverUrlPrefix: expect.stringContaining('http'), 159 | testConfigFile: expect.stringContaining('playwright1.config.js'), 160 | }); 161 | const serverUrlPrefix1 = traceViewerInfo(vscode); 162 | 163 | // closes opened trace viewer 164 | await selectConfig(vscode, 'playwright2.config.js'); 165 | 166 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 167 | traceFile: undefined, 168 | visible: false, 169 | }); 170 | 171 | // opens trace viewer from selected test config 172 | selectTestItem(testItems[0]); 173 | 174 | await expect.poll(() => traceViewerInfo(vscode)).toMatchObject({ 175 | serverUrlPrefix: expect.stringContaining('http'), 176 | testConfigFile: expect.stringContaining('playwright2.config.js'), 177 | }); 178 | const serverUrlPrefix2 = traceViewerInfo(vscode); 179 | 180 | expect(serverUrlPrefix2).not.toBe(serverUrlPrefix1); 181 | }); 182 | -------------------------------------------------------------------------------- /tests/project-tree.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { enableConfigs, enableProjects, expect, test } from './utils'; 18 | import path from 'path'; 19 | 20 | test('should switch between configs', async ({ activate }) => { 21 | const { vscode, testController } = await activate({ 22 | 'tests1/playwright.config.js': `module.exports = { testDir: '.', projects: [{ name: 'projectOne' }, { name: 'projectTwo' }] }`, 23 | 'tests2/playwright.config.js': `module.exports = { testDir: '.', projects: [{ name: 'projectThree' }, { name: 'projectFour' }] }`, 24 | 'tests1/test.spec.ts': ` 25 | import { test } from '@playwright/test'; 26 | test('one', async () => {}); 27 | `, 28 | 'tests2/test.spec.ts': ` 29 | import { test } from '@playwright/test'; 30 | test('one', async () => {}); 31 | `, 32 | }); 33 | await expect(testController).toHaveTestTree(` 34 | - tests1 35 | - test.spec.ts 36 | `); 37 | await expect(vscode).toHaveProjectTree(` 38 | config: tests1/playwright.config.js 39 | [x] projectOne 40 | [x] projectTwo 41 | `); 42 | 43 | await expect(vscode).toHaveConnectionLog([ 44 | { method: 'listFiles', params: {} }, 45 | ]); 46 | 47 | await enableConfigs(vscode, [`tests2${path.sep}playwright.config.js`]); 48 | 49 | await expect(vscode).toHaveProjectTree(` 50 | config: tests2/playwright.config.js 51 | [x] projectThree 52 | [x] projectFour 53 | `); 54 | 55 | await expect(testController).toHaveTestTree(` 56 | - tests2 57 | - test.spec.ts 58 | `); 59 | await expect(vscode).toHaveConnectionLog([ 60 | { method: 'listFiles', params: {} }, 61 | { method: 'listFiles', params: {} }, 62 | ]); 63 | }); 64 | 65 | test('should switch between projects', async ({ activate }) => { 66 | const { vscode, testController } = await activate({ 67 | 'playwright.config.js': `module.exports = { 68 | projects: [ 69 | { name: 'projectOne', testDir: 'tests1', }, 70 | { name: 'projectTwo', testDir: 'tests2', }, 71 | ] 72 | }`, 73 | 'tests1/test.spec.ts': ` 74 | import { test } from '@playwright/test'; 75 | test('one', async () => {}); 76 | `, 77 | 'tests2/test.spec.ts': ` 78 | import { test } from '@playwright/test'; 79 | test('two', async () => {}); 80 | `, 81 | }); 82 | 83 | await enableProjects(vscode, ['projectOne']); 84 | 85 | await expect(testController).toHaveTestTree(` 86 | - tests1 87 | - test.spec.ts 88 | - [playwright.config.js [projectTwo] — disabled] 89 | `); 90 | 91 | await expect(vscode).toHaveProjectTree(` 92 | config: playwright.config.js 93 | [x] projectOne 94 | [ ] projectTwo 95 | `); 96 | 97 | await enableProjects(vscode, ['projectOne', 'projectTwo']); 98 | 99 | await expect(vscode).toHaveProjectTree(` 100 | config: playwright.config.js 101 | [x] projectOne 102 | [x] projectTwo 103 | `); 104 | 105 | await expect(testController).toHaveTestTree(` 106 | - tests1 107 | - test.spec.ts 108 | - tests2 109 | - test.spec.ts 110 | `); 111 | 112 | await enableProjects(vscode, ['projectTwo']); 113 | 114 | await expect(vscode).toHaveProjectTree(` 115 | config: playwright.config.js 116 | [ ] projectOne 117 | [x] projectTwo 118 | `); 119 | }); 120 | 121 | test('should hide unchecked projects', async ({ activate }) => { 122 | const { vscode, testController } = await activate({ 123 | 'playwright.config.js': `module.exports = { 124 | projects: [ 125 | { name: 'projectOne', testDir: 'tests1', }, 126 | { name: 'projectTwo', testDir: 'tests2', }, 127 | ] 128 | }`, 129 | 'tests1/test.spec.ts': ` 130 | import { test } from '@playwright/test'; 131 | test('one', async () => {}); 132 | `, 133 | 'tests2/test.spec.ts': ` 134 | import { test } from '@playwright/test'; 135 | test('two', async () => {}); 136 | `, 137 | }); 138 | 139 | await enableProjects(vscode, ['projectOne']); 140 | 141 | await expect(testController).toHaveTestTree(` 142 | - tests1 143 | - test.spec.ts 144 | - [playwright.config.js [projectTwo] — disabled] 145 | `); 146 | 147 | await expect(vscode).toHaveProjectTree(` 148 | config: playwright.config.js 149 | [x] projectOne 150 | [ ] projectTwo 151 | `); 152 | 153 | await enableProjects(vscode, []); 154 | 155 | await expect(vscode).toHaveProjectTree(` 156 | config: playwright.config.js 157 | [ ] projectOne 158 | [ ] projectTwo 159 | `); 160 | 161 | await expect(testController).toHaveTestTree(` 162 | - [playwright.config.js [projectOne] — disabled] 163 | - [playwright.config.js [projectTwo] — disabled] 164 | `); 165 | }); 166 | 167 | test('should hide project section when there is just one', async ({ activate }) => { 168 | const { vscode } = await activate({ 169 | 'playwright.config.js': `module.exports = { 170 | projects: [ 171 | { name: 'projectOne', testDir: 'tests1', }, 172 | ] 173 | }` 174 | }); 175 | 176 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 177 | await expect(webView.getByRole('heading', { name: 'PROJECTS' })).not.toBeVisible(); 178 | }); 179 | 180 | test('should treat project as enabled when UI for it is hidden', async ({ activate }) => { 181 | const { vscode, workspaceFolder, testController } = await activate({ 182 | 'playwright.config.js': `module.exports = { 183 | projects: [ 184 | { name: 'projectOne', testDir: 'tests1', }, 185 | { name: 'projectTwo', testDir: 'tests2', }, 186 | ] 187 | }`, 188 | 'tests1/test.spec.ts': ` 189 | import { test } from '@playwright/test'; 190 | test('one', async () => {}); 191 | `, 192 | }); 193 | 194 | const webView = vscode.webViews.get('pw.extension.settingsView')!; 195 | 196 | await enableProjects(vscode, ['projectTwo']); 197 | await expect(vscode).toHaveProjectTree(` 198 | config: playwright.config.js 199 | [ ] projectOne 200 | [x] projectTwo 201 | `); 202 | 203 | await workspaceFolder.changeFile('playwright.config.js', `module.exports = { 204 | projects: [ 205 | { name: 'projectOne', testDir: 'tests1', }, 206 | ] 207 | }`); 208 | await expect(webView.getByRole('heading', { name: 'PROJECTS' })).not.toBeVisible(); 209 | await expect(testController).toHaveTestTree(` 210 | - tests1 211 | - test.spec.ts 212 | `); 213 | }); 214 | -------------------------------------------------------------------------------- /src/locatorsView.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DisposableBase } from './disposableBase'; 18 | import { ReusedBrowser } from './reusedBrowser'; 19 | import { pickElementAction } from './settingsView'; 20 | import { getNonce, html } from './utils'; 21 | import type { SettingsModel } from './settingsModel'; 22 | import * as vscodeTypes from './vscodeTypes'; 23 | 24 | export class LocatorsView extends DisposableBase implements vscodeTypes.WebviewViewProvider { 25 | private _vscode: vscodeTypes.VSCode; 26 | private _view: vscodeTypes.WebviewView | undefined; 27 | private _extensionUri: vscodeTypes.Uri; 28 | private _locator: { locator: string, error?: string } = { locator: '' }; 29 | private _ariaSnapshot: { yaml: string, error?: string } = { yaml: '' }; 30 | private _settingsModel: SettingsModel; 31 | private _reusedBrowser: ReusedBrowser; 32 | private _backendVersion = 0; 33 | 34 | constructor(vscode: vscodeTypes.VSCode, settingsModel: SettingsModel, reusedBrowser: ReusedBrowser, extensionUri: vscodeTypes.Uri) { 35 | super(); 36 | this._vscode = vscode; 37 | this._extensionUri = extensionUri; 38 | this._settingsModel = settingsModel; 39 | this._reusedBrowser = reusedBrowser; 40 | this._disposables = [ 41 | vscode.window.registerWebviewViewProvider('pw.extension.locatorsView', this), 42 | this._reusedBrowser.onInspectRequested(async ({ locator, ariaSnapshot, backendVersion }) => { 43 | await vscode.commands.executeCommand('pw.extension.locatorsView.focus'); 44 | this._backendVersion = backendVersion; 45 | this._locator = { locator: locator || '' }; 46 | this._ariaSnapshot = { yaml: ariaSnapshot || '' }; 47 | this._updateValues(); 48 | }), 49 | reusedBrowser.onRunningTestsChanged(() => this._updateActions()), 50 | reusedBrowser.onPageCountChanged(() => this._updateActions()), 51 | settingsModel.onChange(() => this._updateSettings()), 52 | ]; 53 | } 54 | 55 | public resolveWebviewView(webviewView: vscodeTypes.WebviewView, context: vscodeTypes.WebviewViewResolveContext, token: vscodeTypes.CancellationToken) { 56 | this._view = webviewView; 57 | 58 | webviewView.webview.options = { 59 | enableScripts: true, 60 | localResourceRoots: [this._extensionUri] 61 | }; 62 | 63 | webviewView.webview.html = htmlForWebview(this._vscode, this._extensionUri, webviewView.webview); 64 | this._disposables.push(webviewView.webview.onDidReceiveMessage(data => { 65 | if (data.method === 'execute') { 66 | void this._vscode.commands.executeCommand(data.params.command); 67 | } else if (data.method === 'locatorChanged') { 68 | this._locator.locator = data.params.locator; 69 | this._reusedBrowser.highlight(this._locator.locator).then(() => { 70 | this._locator.error = undefined; 71 | this._updateValues(); 72 | }).catch(e => { 73 | this._locator.error = e.message; 74 | this._updateValues(); 75 | }); 76 | } else if (data.method === 'ariaSnapshotChanged') { 77 | this._ariaSnapshot.yaml = data.params.ariaSnapshot; 78 | this._reusedBrowser.highlightAria(this._ariaSnapshot.yaml).then(() => { 79 | this._ariaSnapshot.error = undefined; 80 | this._updateValues(); 81 | }).catch(e => { 82 | this._ariaSnapshot.error = e.message; 83 | this._updateValues(); 84 | }); 85 | } else if (data.method === 'toggle') { 86 | void this._vscode.commands.executeCommand(`pw.extension.toggle.${data.params.setting}`); 87 | } 88 | })); 89 | 90 | this._disposables.push(webviewView.onDidChangeVisibility(() => { 91 | if (!webviewView.visible) 92 | return; 93 | this._updateActions(); 94 | this._updateValues(); 95 | this._updateSettings(); 96 | })); 97 | this._updateActions(); 98 | this._updateValues(); 99 | this._updateSettings(); 100 | } 101 | 102 | private _updateActions() { 103 | const actions = [ 104 | pickElementAction(this._vscode), 105 | { 106 | ...pickElementAction(this._vscode), 107 | location: 'actions-2', 108 | } 109 | ]; 110 | if (this._view) 111 | void this._view.webview.postMessage({ method: 'actions', params: { actions } }); 112 | } 113 | 114 | private _updateValues() { 115 | void this._view?.webview.postMessage({ 116 | method: 'update', 117 | params: { 118 | locator: this._locator, 119 | ariaSnapshot: this._ariaSnapshot, 120 | hideAria: this._backendVersion && this._backendVersion < 1.50 121 | } 122 | }); 123 | } 124 | 125 | private _updateSettings() { 126 | if (this._view) 127 | void this._view.webview.postMessage({ method: 'settings', params: { settings: this._settingsModel.json() } }); 128 | } 129 | } 130 | 131 | function htmlForWebview(vscode: vscodeTypes.VSCode, extensionUri: vscodeTypes.Uri, webview: vscodeTypes.Webview) { 132 | const style = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'media', 'common.css')); 133 | const script = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'out', 'locatorsView.script.js')); 134 | const nonce = getNonce(); 135 | 136 | return html` 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | Playwright 145 | 146 | 147 |
148 |
149 |
150 | 151 |
152 | 156 |
157 | 158 |

159 |
160 |
161 |
162 |
163 | 164 |
165 | 166 |

167 |
168 | 169 | 170 | 171 | `; 172 | } 173 | --------------------------------------------------------------------------------