diff --git a/.claude/skills/roll-playwright.md b/.claude/skills/roll-playwright.md new file mode 100644 index 000000000..97d4c9d93 --- /dev/null +++ b/.claude/skills/roll-playwright.md @@ -0,0 +1,43 @@ +--- +name: roll-playwright +description: Roll @playwright/test to the latest next version, build, test, and push a PR branch +user_invocable: true +--- + +# Roll Playwright Dependency + +Follow these steps in order. Stop and report to the user if any step fails. + +## 1. Get the latest version + +Run `npm info @playwright/test@next version` to get the latest available next version. Save this version string for later. + +## 2. Update package.json + +Update the `@playwright/test` version in `devDependencies` in `package.json` to the version from step 1. + +## 3. Install dependencies + +Run `npm i` to update `package-lock.json`. + +## 4. Copy reused code + +Run `node ./utils/roll-locally`. + +## 5. Build + +Run `npm run build`. +If this fails, attempt best effort at fixing. + +## 6. Test + +Run `npm run test -- --project=default`. +If this fails, attempt best effort at fixing. + +## 7. Create branch, commit, and push + +- Create a new branch named `roll-pwt-` (e.g. `roll-pwt-1.58.2-beta-1770322573000`) +- Stage `package.json` and `package-lock.json` +- Commit with message: `chore: roll playwright to ` +- Do NOT add Co-Authored-By to the commit message +- Push the branch to origin diff --git a/package-lock.json b/package-lock.json index b064d30b5..1482343a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.23.2", - "@playwright/test": "1.58.2-beta-1770322573000", + "@playwright/test": "1.59.0-alpha-1773706743000", "@types/babel__core": "^7.20.3", "@types/babel__helper-plugin-utils": "^7.10.2", "@types/babel__traverse": "^7.20.3", @@ -1470,13 +1470,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.58.2-beta-1770322573000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2-beta-1770322573000.tgz", - "integrity": "sha512-N0lyKVhj8fNXjpE8a3DLyaRSfLgB7tj9h+o+13Xu9FXrpwpSt97GrCRhNIxWv61uQGhwucIoQAhdpiQv1+xRsA==", + "version": "1.59.0-alpha-1773706743000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1773706743000.tgz", + "integrity": "sha512-qJPaJ+W9asFsF8kIXkIoif/JTE6k1Q1zTodmOSanf+beP/VT045mvUR+8gyi8qrMjwrIqSnsiqTToIj8E5dIEA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2-beta-1770322573000" + "playwright": "1.59.0-alpha-1773706743000" }, "bin": { "playwright": "cli.js" @@ -5809,13 +5809,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2-beta-1770322573000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2-beta-1770322573000.tgz", - "integrity": "sha512-i/EqrnGb7m+uvdbvTMjKyfdM5F+82Pc9NRAKWU8v+UFKep0x5OMp/BNMPGQKubNkfD6J1kl407BumDkvlUrg+A==", + "version": "1.59.0-alpha-1773706743000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1773706743000.tgz", + "integrity": "sha512-SmxpdshL6BjxI6qWp0ex4rXTeG31behtE69jgDIvU0LiWL/mWlx03OGvyLnwF2p2AVxajtPZ8x3q67BULIgV6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2-beta-1770322573000" + "playwright-core": "1.59.0-alpha-1773706743000" }, "bin": { "playwright": "cli.js" @@ -5828,9 +5828,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2-beta-1770322573000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2-beta-1770322573000.tgz", - "integrity": "sha512-j6yjesptmGQS4rt4uBjBVZKFL4qk52U68UzeEG3HZ2Iihm53MSl/ci2pkcAcr4+hMeeqbM7xtrhXaGLAzRXNWg==", + "version": "1.59.0-alpha-1773706743000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1773706743000.tgz", + "integrity": "sha512-3u7RJC8r3/vRO+9ebehetjvL2813HYVPdgc+oxT1iONMvvSyWi7yOkc70sbJSFzSJfdSMsJcU7avw+kcIvYrlg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index d6b5a84f1..a276c3bc1 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.23.2", - "@playwright/test": "1.58.2-beta-1770322573000", + "@playwright/test": "1.59.0-alpha-1773706743000", "@types/babel__core": "^7.20.3", "@types/babel__helper-plugin-utils": "^7.10.2", "@types/babel__traverse": "^7.20.3", diff --git a/src/playwrightTestServer.ts b/src/playwrightTestServer.ts index eb47f86e2..a05cfb11b 100644 --- a/src/playwrightTestServer.ts +++ b/src/playwrightTestServer.ts @@ -216,7 +216,7 @@ export class PlaywrightTestServer { return; // Locations are regular expressions. - const locationPatterns = locations ? locations.map(escapeRegex) : undefined; + const locationPatterns = locations ? locations.map(escapeRegex) : []; const options: Parameters['0'] = { projects: this._model.enabledProjectsFilter(), locations: locationPatterns, @@ -344,7 +344,7 @@ export class PlaywrightTestServer { } // Locations are regular expressions. - const locationPatterns = locations ? locations.map(escapeRegex) : undefined; + const locationPatterns = locations ? locations.map(escapeRegex) : []; const options: Parameters['0'] = { projects: this._model.enabledProjectsFilter(), locations: locationPatterns, diff --git a/src/testModel.ts b/src/testModel.ts index 0417f695b..184b2ebb1 100644 --- a/src/testModel.ts +++ b/src/testModel.ts @@ -710,7 +710,7 @@ export class TestModel extends DisposableBase { if (representsPath) hasPathItem = true; else - testIds.push(...collectTestIds(treeItem)); + testIds.push(...collectTestIds(treeItem).testIds); } // known bug: for a combination of location items, and test IDs outside those locations, those test IDs will never be run. diff --git a/src/testTree.ts b/src/testTree.ts index 3f35e8956..ec609e4d9 100644 --- a/src/testTree.ts +++ b/src/testTree.ts @@ -109,7 +109,7 @@ export class TestTree extends DisposableBase { } } - const upstreamTree = new upstream.TestTree(workspaceFSPath, rootSuite, [], undefined, path.sep); + const upstreamTree = new upstream.TestTree(workspaceFSPath, rootSuite, [], undefined, path.sep, false); upstreamTree.sortAndPropagateStatus(); upstreamTree.flattenForSingleProject(); diff --git a/src/upstream/testServerInterface.ts b/src/upstream/testServerInterface.ts index 18ee4856d..de654d89f 100644 --- a/src/upstream/testServerInterface.ts +++ b/src/upstream/testServerInterface.ts @@ -83,13 +83,14 @@ export interface TestServerInterface { locations?: string[]; grep?: string; grepInvert?: string; + onlyChanged?: string; }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] }>; runTests(params: { - locations?: string[]; + locations: string[]; grep?: string; grepInvert?: string; testIds?: string[]; diff --git a/src/upstream/testTree.ts b/src/upstream/testTree.ts index 948c12fd4..5b31165d3 100644 --- a/src/upstream/testTree.ts +++ b/src/upstream/testTree.ts @@ -64,7 +64,7 @@ export class TestTree { private _treeItemByTestId = new Map(); readonly pathSeparator: string; - constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map | undefined, pathSeparator: string) { + constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map | undefined, pathSeparator: string, hideFiles: boolean) { const filterProjects = projectFilters && [...projectFilters.values()].some(Boolean); this.pathSeparator = pathSeparator; this.rootItem = { @@ -81,11 +81,11 @@ export class TestTree { }; this._treeItemById.set(rootFolder, this.rootItem); - const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => { - for (const suite of parentSuite.suites) { + const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem, mode: 'tests' | 'suites' | 'all') => { + for (const suite of mode === 'tests' ? [] : parentSuite.suites) { if (!suite.title) { // Flatten anonymous describes. - visitSuite(project, suite, parentGroup); + visitSuite(project, suite, parentGroup, 'all'); continue; } @@ -105,10 +105,10 @@ export class TestTree { }; this._addChild(parentGroup, group); } - visitSuite(project, suite, group); + visitSuite(project, suite, group, 'all'); } - for (const test of parentSuite.tests) { + for (const test of mode === 'suites' ? [] : parentSuite.tests) { const title = test.title; let testCaseItem = parentGroup.children.find(t => t.kind !== 'group' && t.title === title) as TestCaseItem; if (!testCaseItem) { @@ -167,8 +167,16 @@ export class TestTree { if (filterProjects && !projectFilters.get(projectSuite.title)) continue; for (const fileSuite of projectSuite.suites) { - const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true); - visitSuite(projectSuite.project()!, fileSuite, fileItem); + if (hideFiles) { + visitSuite(projectSuite.project()!, fileSuite, this.rootItem, 'suites'); + if (fileSuite.tests.length) { + const defaultDescribeItem = this._defaultDescribeItem(); + visitSuite(projectSuite.project()!, fileSuite, defaultDescribeItem, 'tests'); + } + } else { + const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true); + visitSuite(projectSuite.project()!, fileSuite, fileItem, 'all'); + } } } @@ -242,6 +250,26 @@ export class TestTree { return fileItem; } + private _defaultDescribeItem(): GroupItem { + let defaultDescribeItem = this._treeItemById.get('') as GroupItem; + if (!defaultDescribeItem) { + defaultDescribeItem = { + kind: 'group', + subKind: 'describe', + id: '', + title: '', + location: { file: '', line: 0, column: 0 }, + duration: 0, + parent: this.rootItem, + children: [], + status: 'none', + hasLoadErrors: false, + }; + this._addChild(this.rootItem, defaultDescribeItem); + } + return defaultDescribeItem; + } + sortAndPropagateStatus() { sortAndPropagateStatus(this.rootItem); } @@ -268,17 +296,6 @@ export class TestTree { this.rootItem = shortRoot; } - testIds(): Set { - const result = new Set(); - const visit = (treeItem: TreeItem) => { - if (treeItem.kind === 'case') - treeItem.tests.forEach(t => result.add(t.id)); - treeItem.children.forEach(visit); - }; - visit(this.rootItem); - return result; - } - fileNames(): string[] { const result = new Set(); const visit = (treeItem: TreeItem) => { @@ -305,8 +322,8 @@ export class TestTree { return this._treeItemById.get(id); } - collectTestIds(treeItem?: TreeItem): Set { - return treeItem ? collectTestIds(treeItem) : new Set(); + collectTestIds(treeItem: TreeItem) { + return collectTestIds(treeItem); } } @@ -347,18 +364,27 @@ export function sortAndPropagateStatus(treeItem: TreeItem) { treeItem.status = 'passed'; } -export function collectTestIds(treeItem: TreeItem): Set { +export function collectTestIds(treeItem: TreeItem): { testIds: Set, locations: Set } { const testIds = new Set(); + const locations = new Set(); const visit = (treeItem: TreeItem) => { + if (treeItem.kind !== 'test' && treeItem.kind !== 'case') { + treeItem.children.forEach(visit); + return; + } + + let fileItem: TreeItem = treeItem; + while (fileItem && fileItem.parent && !(fileItem.kind === 'group' && fileItem.subKind === 'file')) + fileItem = fileItem.parent; + locations.add(fileItem.location.file); + if (treeItem.kind === 'case') - treeItem.tests.map(t => t.id).forEach(id => testIds.add(id)); - else if (treeItem.kind === 'test') - testIds.add(treeItem.id); + treeItem.tests.forEach(test => testIds.add(test.id)); else - treeItem.children?.forEach(visit); + testIds.add(treeItem.id); }; visit(treeItem); - return testIds; + return { testIds, locations }; } export const statusEx = Symbol('statusEx'); diff --git a/tests-integration/tests/baseTest.ts b/tests-integration/tests/baseTest.ts index e3dbc7ff4..88423c5b9 100644 --- a/tests-integration/tests/baseTest.ts +++ b/tests-integration/tests/baseTest.ts @@ -89,7 +89,7 @@ export const test = base.extend({ if (packageManager === 'pnpm-pnp') await fs.promises.writeFile(path.join(projectPath, '.npmrc'), 'node-linker=pnp'); - spawnSync(`${command} --quiet --browser=chromium --gha --install-deps`, { + spawnSync(`${command} --quiet --browser=chromium --gha${process.env.CI ? ' --install-deps' : ''}`, { cwd: projectPath, stdio: 'inherit', shell: true, diff --git a/tests/mock/vscode.ts b/tests/mock/vscode.ts index 498bcd5f6..d3c1bdf5a 100644 --- a/tests/mock/vscode.ts +++ b/tests/mock/vscode.ts @@ -509,7 +509,10 @@ export class TestRun { result.push(' Output:'); result.push(...this._renderOutput()); } - return trimLog(result.join(`\n${indent}`)) + `\n${indent}`; + let log = trimLog(result.join(`\n${indent}`)) + `\n${indent}`; + // Strip Playwright's "Context for AI" details block as it contains absolute paths. + log = log.replace(/\n?\s*

Context for AI<\/summary>[\s\S]*?<\/details>/g, ''); + return log; } renderOutput(): string { diff --git a/tests/run-tests.spec.ts b/tests/run-tests.spec.ts index 0d2080ccf..8fcf84951 100644 --- a/tests/run-tests.spec.ts +++ b/tests/run-tests.spec.ts @@ -1168,7 +1168,8 @@ test('should provide page snapshot to copilot', async ({ activate }) => { }); const testRun = await testController.run(); - const log = testRun.renderLog({ messages: true }); + const allMessages = [...testRun.entries.values()].flat().flatMap(e => e.messages ?? []); + const log = allMessages.map(m => m.message.render()).join('\n'); expect(log).toContain(`
Context for AI`); expect(log).toContain(`# Page snapshot`); expect(log).toContain(`- button "click me"`);