From 262586a46a5429e263c225fd6566e9a10a6ec90c Mon Sep 17 00:00:00 2001 From: ryanrosello-og <50574915+ryanrosello-og@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:09:39 +0800 Subject: [PATCH 001/376] feat(trace-viewer) add copy to clipboard on the Source > Stacktrace tab (#31394) --- .../trace-viewer/src/ui/copyToClipboard.tsx | 7 ++++--- packages/trace-viewer/src/ui/sourceTab.css | 10 ++++++++++ packages/trace-viewer/src/ui/sourceTab.tsx | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/trace-viewer/src/ui/copyToClipboard.tsx b/packages/trace-viewer/src/ui/copyToClipboard.tsx index 96de5f3822..2c5d21aa0d 100644 --- a/packages/trace-viewer/src/ui/copyToClipboard.tsx +++ b/packages/trace-viewer/src/ui/copyToClipboard.tsx @@ -18,7 +18,8 @@ import * as React from 'react'; export const CopyToClipboard: React.FunctionComponent<{ value: string, -}> = ({ value }) => { + description?: string, +}> = ({ value, description }) => { const [iconClassName, setIconClassName] = React.useState('codicon-clippy'); const handleCopy = React.useCallback(() => { @@ -32,5 +33,5 @@ export const CopyToClipboard: React.FunctionComponent<{ }); }, [value]); - return ; -}; + return ; +}; \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/sourceTab.css b/packages/trace-viewer/src/ui/sourceTab.css index 17e66460f4..e4e43d80b6 100644 --- a/packages/trace-viewer/src/ui/sourceTab.css +++ b/packages/trace-viewer/src/ui/sourceTab.css @@ -31,3 +31,13 @@ box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px; z-index: 10; } + +.source-tab-file-name .copy-icon.codicon { + display: block; + cursor: pointer; +} + +.source-copy-to-clipboard { + display: block; + padding-left: 4px; +} \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index a3acad64a2..f8952849cb 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -23,6 +23,7 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import type { SourceHighlight } from '@web/components/codeMirrorWrapper'; import type { SourceLocation, SourceModel } from './modelUtil'; import type { StackFrame } from '@protocol/channels'; +import { CopyToClipboard } from './copyToClipboard'; export const SourceTab: React.FunctionComponent<{ stack: StackFrame[] | undefined, @@ -82,7 +83,14 @@ export const SourceTab: React.FunctionComponent<{ return
- {fileName &&
{fileName}
} + {fileName && ( +
+ {fileName} + + + +
+ )}
@@ -100,3 +108,11 @@ export async function calculateSha1(text: string): Promise { } return hexCodes.join(''); } + +function getFileName(fullPath?: string, lineNum?: number): string { + if (!fullPath) + return ''; + const pathSep = fullPath?.includes('/') ? '/' : '\\'; + const fileName = fullPath?.split(pathSep).pop() ?? ''; + return lineNum ? `${fileName}:${lineNum}` : fileName; +} From 9caf3b5f72f3313fb700d32741e7cfddefc2e8dc Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Tue, 2 Jul 2024 18:10:42 +0200 Subject: [PATCH 002/376] chore: Remove obsolete Chromium enabled features (#31513) --- packages/playwright-core/src/server/chromium/chromiumSwitches.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index 17d894fa03..7247f8c8a5 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -20,7 +20,6 @@ export const chromiumSwitches = [ '--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md '--disable-background-networking', - '--enable-features=NetworkService,NetworkServiceInProcess', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-back-forward-cache', // Avoids surprises like main request not being intercepted during page.goBack(). From a62260a9f2a194f8fdea80266531aa41b2061fa5 Mon Sep 17 00:00:00 2001 From: Joe-Hendley <95080839+Joe-Hendley@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:45:16 +0100 Subject: [PATCH 003/376] feat(html report): linkify test annotations (#31521) --- .../html-reporter/src/testCaseView.spec.tsx | 39 +++++++++++++++++++ packages/html-reporter/src/testCaseView.tsx | 34 +++++++++++++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 28fe8247f5..c306843107 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -74,3 +74,42 @@ test('should render test case', async ({ mount }) => { await expect(component.getByText('test.spec.ts:42')).toBeVisible(); await expect(component.getByText('My test')).toBeVisible(); }); + +const linkRenderingTestCase: TestCase = { + testId: 'testid', + title: 'My test', + path: [], + projectName: 'chromium', + location: { file: 'test.spec.ts', line: 42, column: 0 }, + annotations: [ + { type: 'more info', description: 'read https://playwright.dev/docs/intro and https://playwright.dev/docs/api/class-playwright' }, + { type: 'related issues', description: 'https://github.com/microsoft/playwright/issues/23180, https://github.com/microsoft/playwright/issues/23181' }, + + ], + tags: [], + outcome: 'expected', + duration: 10, + ok: true, + results: [result] +}; + +test('should correctly render links in annotations', async ({ mount }) => { + const component = await mount(); + // const container = await(component.getByText('Annotations')); + + const firstLink = await component.getByText('https://playwright.dev/docs/intro').first(); + await expect(firstLink).toBeVisible(); + await expect(firstLink).toHaveAttribute('href', 'https://playwright.dev/docs/intro'); + + const secondLink = await component.getByText('https://playwright.dev/docs/api/class-playwright').first(); + await expect(secondLink).toBeVisible(); + await expect(secondLink).toHaveAttribute('href', 'https://playwright.dev/docs/api/class-playwright'); + + const thirdLink = await component.getByText('https://github.com/microsoft/playwright/issues/23180').first(); + await expect(thirdLink).toBeVisible(); + await expect(thirdLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23180'); + + const fourthLink = await component.getByText('https://github.com/microsoft/playwright/issues/23181').first(); + await expect(fourthLink).toBeVisible(); + await expect(fourthLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23181'); +}); \ No newline at end of file diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index eecbdac9f5..66555598f2 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -65,11 +65,35 @@ export const TestCaseView: React.FC<{ }; function renderAnnotationDescription(description: string) { - try { - if (['http:', 'https:'].includes(new URL(description).protocol)) - return {description}; - } catch {} - return description; + const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; + const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); + + const result = []; + let currentIndex = 0; + let match; + + while ((match = WEB_LINK_REGEX.exec(description)) !== null) { + const stringBeforeMatch = description.substring(currentIndex, match.index); + if (stringBeforeMatch) + result.push(stringBeforeMatch); + + const value = match[0]; + result.push(renderLink(value)); + currentIndex = match.index + value.length; + } + const stringAfterMatches = description.substring(currentIndex); + if (stringAfterMatches) + result.push(stringAfterMatches); + + return result; +} + +function renderLink(text: string) { + let link = text; + if (link.startsWith('www.')) + link = 'https://' + link; + + return {text}; } function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) { From 1e55a084bc0ce65dbd8026ff3e265fdd9dfc60e1 Mon Sep 17 00:00:00 2001 From: Vitaliy Potapov Date: Wed, 3 Jul 2024 03:46:24 +0400 Subject: [PATCH 004/376] feat(html-reporter): hide annotations started with "_" (#31489) Fixes: https://github.com/microsoft/playwright/issues/30179 --- docs/src/test-annotations-js.md | 2 +- packages/html-reporter/src/testCaseView.spec.tsx | 2 ++ packages/html-reporter/src/testCaseView.tsx | 8 ++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/src/test-annotations-js.md b/docs/src/test-annotations-js.md index 68bdd79848..5fc2f78955 100644 --- a/docs/src/test-annotations-js.md +++ b/docs/src/test-annotations-js.md @@ -159,7 +159,7 @@ You can also filter tests in the configuration file via [`property: TestConfig.g ## Annotate tests -If you would like to annotate your tests with something more substantial than a tag, you can do that when declaring a test. Annotations have a `type` and a `description` for more context, and will be visible in the test report. +If you would like to annotate your tests with something more substantial than a tag, you can do that when declaring a test. Annotations have a `type` and a `description` for more context and available in reporter API. Playwright's built-in HTML reporter shows all annotations, except those where `type` starts with `_` symbol. For example, to annotate a test with an issue url: diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index c306843107..3bde6e8a83 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -54,6 +54,7 @@ const testCase: TestCase = { annotations: [ { type: 'annotation', description: 'Annotation text' }, { type: 'annotation', description: 'Another annotation text' }, + { type: '_annotation', description: 'Hidden annotation' }, ], tags: [], outcome: 'expected', @@ -65,6 +66,7 @@ const testCase: TestCase = { test('should render test case', async ({ mount }) => { const component = await mount(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); + await expect(component.getByText('Hidden annotation')).toBeHidden(); await component.getByText('Annotations').click(); await expect(component.getByText('Annotation text')).not.toBeVisible(); await expect(component.getByText('Outer step')).toBeVisible(); diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 66555598f2..507ddf4ffb 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -40,6 +40,10 @@ export const TestCaseView: React.FC<{ return test.tags; }, [test]); + const visibleAnnotations = React.useMemo(() => { + return test?.annotations?.filter(annotation => !annotation.type.startsWith('_')) || []; + }, [test?.annotations]); + return
{test &&
{test.path.join(' › ')}
} {test &&
{test?.title}
} @@ -52,8 +56,8 @@ export const TestCaseView: React.FC<{ {test && !!test.projectName && } {labels && }
} - {test && !!test.annotations.length && - {test?.annotations.map(annotation => )} + {!!visibleAnnotations.length && + {visibleAnnotations.map(annotation => )} } {test && ({ From 5bdced9c9bb2e7e57b0f1b63254ba52da252a6f1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 3 Jul 2024 09:40:37 +0200 Subject: [PATCH 005/376] chore: bump @types/node and chokidar (#31527) --- package-lock.json | 8 +++---- package.json | 2 +- packages/playwright/ThirdPartyNotices.txt | 6 ++--- .../bundles/utils/package-lock.json | 23 ++++++++----------- .../playwright/bundles/utils/package.json | 2 +- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index b63578e29e..87d30f0d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/babel__core": "^7.20.2", "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", - "@types/node": "^18.15.3", + "@types/node": "^18.19.39", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/resize-observer-browser": "^0.1.7", @@ -1851,9 +1851,9 @@ } }, "node_modules/@types/node": { - "version": "18.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", - "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", "devOptional": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index 6caf9c748d..4b3b9439ca 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@types/babel__core": "^7.20.2", "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", - "@types/node": "^18.15.3", + "@types/node": "^18.19.39", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/resize-observer-browser": "^0.1.7", diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index d77271ad73..21bb223549 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -96,7 +96,7 @@ This project incorporates components from the projects listed below. The origina - caniuse-lite@1.0.30001579 (https://github.com/browserslist/caniuse-lite) - chalk@2.4.2 (https://github.com/chalk/chalk) - chalk@4.1.2 (https://github.com/chalk/chalk) -- chokidar@3.5.3 (https://github.com/paulmillr/chokidar) +- chokidar@3.6.0 (https://github.com/paulmillr/chokidar) - ci-info@3.8.0 (https://github.com/watson/ci-info) - codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror) - color-convert@1.9.3 (https://github.com/Qix-/color-convert) @@ -3076,7 +3076,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF chalk@4.1.2 AND INFORMATION -%% chokidar@3.5.3 NOTICES AND INFORMATION BEGIN HERE +%% chokidar@3.6.0 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -3100,7 +3100,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF chokidar@3.5.3 AND INFORMATION +END OF chokidar@3.6.0 AND INFORMATION %% ci-info@3.8.0 NOTICES AND INFORMATION BEGIN HERE ========================================= diff --git a/packages/playwright/bundles/utils/package-lock.json b/packages/playwright/bundles/utils/package-lock.json index 9cc1d09041..fcf9f972fe 100644 --- a/packages/playwright/bundles/utils/package-lock.json +++ b/packages/playwright/bundles/utils/package-lock.json @@ -8,7 +8,7 @@ "name": "utils-bundle", "version": "0.0.1", "dependencies": { - "chokidar": "3.5.3", + "chokidar": "3.6.0", "enquirer": "2.3.6", "json5": "2.2.3", "pirates": "4.0.4", @@ -89,15 +89,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -110,6 +104,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -343,9 +340,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", diff --git a/packages/playwright/bundles/utils/package.json b/packages/playwright/bundles/utils/package.json index d660138349..69477909c5 100644 --- a/packages/playwright/bundles/utils/package.json +++ b/packages/playwright/bundles/utils/package.json @@ -9,7 +9,7 @@ "generate-license": "node ../../../../utils/generate_third_party_notice.js" }, "dependencies": { - "chokidar": "3.5.3", + "chokidar": "3.6.0", "enquirer": "2.3.6", "json5": "2.2.3", "pirates": "4.0.4", From bfbd5f6f2f147e52017369eeadab1346833549fa Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 3 Jul 2024 09:40:50 +0200 Subject: [PATCH 006/376] test: snapshot with all: unset in StyleSheet (#31514) --- tests/library/snapshotter.spec.ts | 6 ++-- tests/library/trace-viewer.spec.ts | 45 +++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/tests/library/snapshotter.spec.ts b/tests/library/snapshotter.spec.ts index b0866a5d3f..b6c45b9252 100644 --- a/tests/library/snapshotter.spec.ts +++ b/tests/library/snapshotter.spec.ts @@ -249,12 +249,10 @@ it.describe('snapshots', () => { }); it('empty adopted style sheets should not prevent node refs', async ({ page, toImpl, snapshotter, browserName }) => { - it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.'); - await page.setContent(''); await page.evaluate(() => { const sheet = new CSSStyleSheet(); - (document as any).adoptedStyleSheets = [sheet]; + document.adoptedStyleSheets = [sheet]; const sheet2 = new CSSStyleSheet(); for (const element of [document.createElement('div'), document.createElement('span')]) { @@ -262,7 +260,7 @@ it.describe('snapshots', () => { mode: 'open' }); root.append('foo'); - (root as any).adoptedStyleSheets = [sheet2]; + root.adoptedStyleSheets = [sheet2]; document.body.appendChild(element); } }); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 69864380d8..dc9e9facaf 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -347,14 +347,12 @@ test('should capture data-url svg iframe', async ({ page, server, runAndTrace }) }); test('should contain adopted style sheets', async ({ page, runAndTrace, browserName }) => { - test.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.'); - const traceViewer = await runAndTrace(async () => { await page.setContent(''); await page.evaluate(() => { const sheet = new CSSStyleSheet(); sheet.addRule('button', 'color: red'); - (document as any).adoptedStyleSheets = [sheet]; + document.adoptedStyleSheets = [sheet]; const sheet2 = new CSSStyleSheet(); sheet2.addRule(':host', 'color: blue'); @@ -364,7 +362,7 @@ test('should contain adopted style sheets', async ({ page, runAndTrace, browserN mode: 'open' }); root.append('foo'); - (root as any).adoptedStyleSheets = [sheet2]; + root.adoptedStyleSheets = [sheet2]; document.body.appendChild(element); } }); @@ -377,22 +375,20 @@ test('should contain adopted style sheets', async ({ page, runAndTrace, browserN }); test('should work with adopted style sheets and replace/replaceSync', async ({ page, runAndTrace, browserName }) => { - test.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.'); - const traceViewer = await runAndTrace(async () => { await page.setContent(''); await page.evaluate(() => { const sheet = new CSSStyleSheet(); sheet.addRule('button', 'color: red'); - (document as any).adoptedStyleSheets = [sheet]; + document.adoptedStyleSheets = [sheet]; }); await page.evaluate(() => { - const [sheet] = (document as any).adoptedStyleSheets; + const [sheet] = document.adoptedStyleSheets; sheet.replaceSync(`button { color: blue }`); }); - await page.evaluate(() => { - const [sheet] = (document as any).adoptedStyleSheets; - sheet.replace(`button { color: #0F0 }`); + await page.evaluate(async () => { + const [sheet] = document.adoptedStyleSheets; + await sheet.replace(`button { color: #0F0 }`); }); }); @@ -410,7 +406,30 @@ test('should work with adopted style sheets and replace/replaceSync', async ({ p } }); -test('should restore scroll positions', async ({ page, runAndTrace, browserName }) => { +test('should work with adopted style sheets and all: unset', async ({ page, runAndTrace, browserName }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31500' }); + test.fixme(browserName === 'chromium', 'https://issues.chromium.org/u/1/issues/41416124'); + + const traceViewer = await runAndTrace(async () => { + await page.setContent(''); + await page.evaluate(() => { + const stylesheet = new CSSStyleSheet(); + // 'all: unset' is the problem here. + stylesheet.replaceSync('button { all: unset; border-radius: 24px; background-color: deepskyblue; color: black; padding: 5px }'); + document.adoptedStyleSheets = [stylesheet]; + }); + await page.getByRole('button').click(); + }); + { + const frame = await traceViewer.snapshotFrame('page.evaluate', 0); + await expect(frame.locator('button')).toHaveCSS('border-radius', '24px'); + await expect(frame.locator('button')).toHaveCSS('background-color', 'rgb(0, 191, 255)'); + await expect(frame.locator('button')).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(frame.locator('button')).toHaveCSS('padding', '5px'); + } +}); + +test('should restore scroll positions', async ({ page, runAndTrace }) => { const traceViewer = await runAndTrace(async () => { await page.setContent(` + `); + await page.evaluate(() => { }); + }); + { + const frame = await traceViewer.snapshotFrame('page.evaluate', 0); + await expect(frame.getByTestId('green-element')).toHaveCSS('color', /* green */'rgb(0, 128, 0)'); + await expect(frame.getByTestId('red-element')).toHaveCSS('color', /* red */'rgb(255, 0, 0)'); + } +}); + test('should restore scroll positions', async ({ page, runAndTrace }) => { const traceViewer = await runAndTrace(async () => { await page.setContent(` From de39d227f7a3a602c7b94a54f51e77b55557266d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 15 Jul 2024 12:20:22 -0700 Subject: [PATCH 058/376] chore: linkify urls in attachments body (#31673) Reference: https://github.com/microsoft/playwright/issues/31284 --- packages/html-reporter/src/labelUtils.tsx | 23 --------- packages/html-reporter/src/links.tsx | 5 +- packages/html-reporter/src/renderUtils.tsx | 47 ++++++++++++++++++ .../html-reporter/src/testCaseView.spec.tsx | 49 +++++++++++++++++-- packages/html-reporter/src/testCaseView.tsx | 38 ++------------ packages/html-reporter/src/testFileView.tsx | 3 +- packages/html-reporter/src/testFilesView.tsx | 2 +- packages/html-reporter/src/testResultView.tsx | 2 +- .../src/{uiUtils.ts => utils.ts} | 9 ++++ 9 files changed, 111 insertions(+), 67 deletions(-) delete mode 100644 packages/html-reporter/src/labelUtils.tsx create mode 100644 packages/html-reporter/src/renderUtils.tsx rename packages/html-reporter/src/{uiUtils.ts => utils.ts} (79%) diff --git a/packages/html-reporter/src/labelUtils.tsx b/packages/html-reporter/src/labelUtils.tsx deleted file mode 100644 index 014ec77d59..0000000000 --- a/packages/html-reporter/src/labelUtils.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// hash string to integer in range [0, 6] for color index, to get same color for same tag -export function hashStringToInt(str: string) { - let hash = 0; - for (let i = 0; i < str.length; i++) - hash = str.charCodeAt(i) + ((hash << 8) - hash); - return Math.abs(hash % 6); -} diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index cededa0dbc..4ddaf20a91 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -20,6 +20,7 @@ import * as icons from './icons'; import { TreeItem } from './treeItem'; import { CopyToClipboard } from './copyToClipboard'; import './links.css'; +import { linkifyText } from './renderUtils'; export function navigate(href: string) { window.history.pushState({}, '', href); @@ -77,9 +78,9 @@ export const AttachmentLink: React.FunctionComponent<{ return {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.path && {linkName || attachment.name}} - {attachment.body && {attachment.name}} + {attachment.body && {linkifyText(attachment.name)}}
} loadChildren={attachment.body ? () => { - return [
{attachment.body}
]; + return [
{linkifyText(attachment.body!)}
]; } : undefined} depth={0} style={{ lineHeight: '32px' }}>; }; diff --git a/packages/html-reporter/src/renderUtils.tsx b/packages/html-reporter/src/renderUtils.tsx new file mode 100644 index 0000000000..e8d92dc609 --- /dev/null +++ b/packages/html-reporter/src/renderUtils.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function linkifyText(description: string) { + const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; + const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); + + const result = []; + let currentIndex = 0; + let match; + + while ((match = WEB_LINK_REGEX.exec(description)) !== null) { + const stringBeforeMatch = description.substring(currentIndex, match.index); + if (stringBeforeMatch) + result.push(stringBeforeMatch); + + const value = match[0]; + result.push(renderLink(value)); + currentIndex = match.index + value.length; + } + const stringAfterMatches = description.substring(currentIndex); + if (stringAfterMatches) + result.push(stringAfterMatches); + + return result; +} + +function renderLink(text: string) { + let link = text; + if (link.startsWith('www.')) + link = 'https://' + link; + + return {text}; +} diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 3bde6e8a83..d73407582d 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -77,7 +77,7 @@ test('should render test case', async ({ mount }) => { await expect(component.getByText('My test')).toBeVisible(); }); -const linkRenderingTestCase: TestCase = { +const annotationLinkRenderingTestCase: TestCase = { testId: 'testid', title: 'My test', path: [], @@ -96,8 +96,7 @@ const linkRenderingTestCase: TestCase = { }; test('should correctly render links in annotations', async ({ mount }) => { - const component = await mount(); - // const container = await(component.getByText('Annotations')); + const component = await mount(); const firstLink = await component.getByText('https://playwright.dev/docs/intro').first(); await expect(firstLink).toBeVisible(); @@ -114,4 +113,48 @@ test('should correctly render links in annotations', async ({ mount }) => { const fourthLink = await component.getByText('https://github.com/microsoft/playwright/issues/23181').first(); await expect(fourthLink).toBeVisible(); await expect(fourthLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23181'); +}); + +const resultWithAttachment: TestResult = { + retry: 0, + startTime: new Date(0).toUTCString(), + duration: 100, + errors: [], + steps: [{ + title: 'Outer step', + startTime: new Date(100).toUTCString(), + duration: 10, + location: { file: 'test.spec.ts', line: 62, column: 0 }, + count: 1, + steps: [], + }], + attachments: [{ + name: 'first attachment', + body: 'The body with https://playwright.dev/docs/intro link and https://github.com/microsoft/playwright/issues/31284.', + contentType: 'text/plain' + }], + status: 'passed', +}; + +const attachmentLinkRenderingTestCase: TestCase = { + testId: 'testid', + title: 'My test', + path: [], + projectName: 'chromium', + location: { file: 'test.spec.ts', line: 42, column: 0 }, + tags: [], + outcome: 'expected', + duration: 10, + ok: true, + annotations: [], + results: [resultWithAttachment] +}; + +test('should correctly render links in attachments', async ({ mount }) => { + const component = await mount(); + await component.getByText('first attachment').click(); + const body = await component.getByText('The body with https://playwright.dev/docs/intro link'); + await expect(body).toBeVisible(); + await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro'); + await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); }); \ No newline at end of file diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 507ddf4ffb..f4d76653cf 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -23,8 +23,8 @@ import { ProjectLink } from './links'; import { statusIcon } from './statusIcon'; import './testCaseView.css'; import { TestResultView } from './testResultView'; -import { hashStringToInt } from './labelUtils'; -import { msToString } from './uiUtils'; +import { linkifyText } from './renderUtils'; +import { hashStringToInt, msToString } from './utils'; export const TestCaseView: React.FC<{ projectNames: string[], @@ -68,43 +68,11 @@ export const TestCaseView: React.FC<{ ; }; -function renderAnnotationDescription(description: string) { - const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; - const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); - - const result = []; - let currentIndex = 0; - let match; - - while ((match = WEB_LINK_REGEX.exec(description)) !== null) { - const stringBeforeMatch = description.substring(currentIndex, match.index); - if (stringBeforeMatch) - result.push(stringBeforeMatch); - - const value = match[0]; - result.push(renderLink(value)); - currentIndex = match.index + value.length; - } - const stringAfterMatches = description.substring(currentIndex); - if (stringAfterMatches) - result.push(stringAfterMatches); - - return result; -} - -function renderLink(text: string) { - let link = text; - if (link.startsWith('www.')) - link = 'https://' + link; - - return {text}; -} - function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) { return (
{type} - {description && : {renderAnnotationDescription(description)}} + {description && : {linkifyText(description)}}
); } diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index 8e478f95d7..a5f9a7a358 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -16,14 +16,13 @@ import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types'; import * as React from 'react'; -import { msToString } from './uiUtils'; +import { hashStringToInt, msToString } from './utils'; import { Chip } from './chip'; import { filterWithToken, type Filter } from './filter'; import { generateTraceUrl, Link, navigate, ProjectLink } from './links'; import { statusIcon } from './statusIcon'; import './testFileView.css'; import { video, image, trace } from './icons'; -import { hashStringToInt } from './labelUtils'; export const TestFileView: React.FC Date: Mon, 15 Jul 2024 22:34:57 +0100 Subject: [PATCH 059/376] fix: add 'window-management' to chromium browser (#31687) --- docs/src/api/class-browsercontext.md | 1 + packages/playwright-core/src/server/chromium/crBrowser.ts | 1 + packages/playwright-core/types/types.d.ts | 1 + tests/library/permissions.spec.ts | 8 ++++++++ 4 files changed, 11 insertions(+) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 48542a2603..e5ccf7b5ce 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -980,6 +980,7 @@ A permission or an array of permissions to grant. Permissions can be one of the * `'notifications'` * `'payment-handler'` * `'storage-access'` +* `'window-management'` ### option: BrowserContext.grantPermissions.origin * since: v1.8 diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 777ff2eee8..9d1e672132 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -434,6 +434,7 @@ export class CRBrowserContext extends BrowserContext { // chrome-specific permissions we have. ['midi-sysex', 'midiSysex'], ['storage-access', 'storageAccess'], + ['window-management', 'windowManagement'] ]); const filtered = permissions.map(permission => { const protocolPermission = webPermissionToProtocol.get(permission); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f9e25c8c40..fc468defe0 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8472,6 +8472,7 @@ export interface BrowserContext { * - `'notifications'` * - `'payment-handler'` * - `'storage-access'` + * - `'window-management'` * @param options */ grantPermissions(permissions: ReadonlyArray, options?: { diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index ac2ec7a0b5..7e77bba10f 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -48,6 +48,14 @@ it.describe('permissions', () => { expect(await getPermission(page, 'geolocation')).toBe('granted'); }); + it('should grant window-management permission when origin is listed', async ({ page, context, server, browserName }) => { + it.fail(browserName === 'firefox'); + + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['window-management'], { origin: server.EMPTY_PAGE }); + expect(await getPermission(page, 'window-management')).toBe('granted'); + }); + it('should prompt for geolocation permission when origin is not listed', async ({ page, context, server }) => { await page.goto(server.EMPTY_PAGE); await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); From 37ffbd757e732b52c3497ff65009fbe3a5743df5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 15 Jul 2024 14:35:11 -0700 Subject: [PATCH 060/376] chore: remove unused project to id mapping from html builder (#31698) --- packages/playwright/src/reporters/html.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 2a5bbc94d4..7b279a70a1 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -226,8 +226,6 @@ class HtmlBuilder { private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; - private _projectToId: Map = new Map(); - private _lastProjectId = 0; constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) { this._config = config; @@ -406,16 +404,6 @@ class HtmlBuilder { }; } - private _projectId(suite: Suite): number { - const project = projectSuite(suite); - let id = this._projectToId.get(project); - if (!id) { - id = ++this._lastProjectId; - this._projectToId.set(project, id); - } - return id; - } - private _serializeAttachments(attachments: JsonAttachment[]) { let lastAttachment: TestAttachment | undefined; return attachments.map(a => { @@ -653,10 +641,4 @@ function createSnippets(stepsInFile: MultiMap) { } } -function projectSuite(suite: Suite): Suite { - while (suite.parent?.parent) - suite = suite.parent; - return suite; -} - export default HtmlReporter; From a5ca9b7d37a9c09b6da62a5d18d929a9c44e054e Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:28:58 -0700 Subject: [PATCH 061/376] feat(webkit): roll to r2047 (#31701) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 7911ee8f1e..1f00780f4f 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2045", + "revision": "2047", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", From 96e0a96ac1278ac58723c1f6b79d673be3fc2ee2 Mon Sep 17 00:00:00 2001 From: damar Zaky Date: Tue, 16 Jul 2024 20:15:25 +0700 Subject: [PATCH 062/376] docs: fix grammar and typos in various files (#31678) - docs/src/best-practices-js.md - docs/src/codegen.md - docs/src/debug.md - docs/src/events.md - docs/src/library-js.md - docs/src/locators.md - docs/src/other-locators.md - docs/src/test-components-js.md - docs/src/trace-viewer.md --------- Signed-off-by: damar Zaky --- docs/src/best-practices-js.md | 2 +- docs/src/codegen.md | 8 ++++---- docs/src/debug.md | 12 ++++++------ docs/src/events.md | 2 +- docs/src/library-js.md | 2 +- docs/src/locators.md | 6 +++--- docs/src/other-locators.md | 2 +- docs/src/test-components-js.md | 2 +- docs/src/trace-viewer.md | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md index 02e43786e0..3694418533 100644 --- a/docs/src/best-practices-js.md +++ b/docs/src/best-practices-js.md @@ -214,7 +214,7 @@ Playwright comes with a range of tooling to help you write tests. - The [VS Code extension](./getting-started-vscode.md) gives you a great developer experience when writing, running, and debugging tests. - The [test generator](./codegen.md) can generate tests and pick locators for you. - The [trace viewer](./trace-viewer.md) gives you a full trace of your tests as a local PWA that can easily be shared. With the trace viewer you can view the timeline, inspect DOM snapshots for each action, view network requests and more. -- The [UI Mode](./test-ui-mode) let's you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. +- The [UI Mode](./test-ui-mode) lets you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. - [Typescript](./test-typescript) in Playwright works out of the box and gives you better IDE integrations. Your IDE will show you everything you can do and highlight when you do something wrong. No TypeScript experience is needed and it is not necessary for your code to be in TypeScript, all you need to do is create your tests with a `.ts` extension. ### Test across all browsers diff --git a/docs/src/codegen.md b/docs/src/codegen.md index b23c023ac2..4b45f12d01 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -125,14 +125,14 @@ With the test generator you can record: When you have finished interacting with the page, press the **record** button to stop the recording and use the **copy** button to copy the generated code to your editor. -Use the **clear** button to clear the code to start recording again. Once finished close the Playwright inspector window or stop the terminal command. +Use the **clear** button to clear the code to start recording again. Once finished, close the Playwright inspector window or stop the terminal command. ### Generating locators You can generate [locators](/locators.md) with the test generator. * Press the `'Record'` button to stop the recording and the `'Pick Locator'` button will appear. * Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element. -* To choose a locator click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button. +* To choose a locator, click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button. * You can then edit the locator in this field to fine tune it or use the copy button to copy it and paste it into your code. ###### @@ -284,7 +284,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --color-scheme=dark playwright.dev Record scripts and tests while emulating timezone, language & location using the `--timezone`, `--geolocation` and `--lang` options. Once the page opens: 1. Accept the cookies -1. On the top right click on the locate me button to see geolocation in action. +1. On the top right, click on the locate me button to see geolocation in action. ```bash js npx playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" bing.com/maps @@ -375,7 +375,7 @@ Make sure you only use the `auth.json` locally as it contains sensitive informat #### Load authenticated state -Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can can continue generating tests from the logged in state. +Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state. ```bash js npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright diff --git a/docs/src/debug.md b/docs/src/debug.md index bc4b0c1a80..aff5077d3c 100644 --- a/docs/src/debug.md +++ b/docs/src/debug.md @@ -46,7 +46,7 @@ A browser window will open and the test will run and pause at where the breakpoi ### Debug in different Browsers -By default debugging is done using the Chromium profile. You can debug your tests on different browsers by right clicking on the debug icon in the testing sidebar and clicking on the 'Select Default Profile' option from the dropdown. +By default, debugging is done using the Chromium profile. You can debug your tests on different browsers by right clicking on the debug icon in the testing sidebar and clicking on the 'Select Default Profile' option from the dropdown. debugging on specific profile @@ -80,7 +80,7 @@ npx playwright test --debug ``` #### Debug one test on all browsers -To debug one test on a specific line run the test command followed by the name of the test file and the line number of the test you want to debug, followed by the `--debug` flag. This will run a single test in each browser configured in your [`playwright.config`](./test-projects.md#configure-projects-for-multiple-browsers) and open the inspector. +To debug one test on a specific line, run the test command followed by the name of the test file and the line number of the test you want to debug, followed by the `--debug` flag. This will run a single test in each browser configured in your [`playwright.config`](./test-projects.md#configure-projects-for-multiple-browsers) and open the inspector. ```bash npx playwright test example.spec.ts:10 --debug @@ -207,7 +207,7 @@ While running in debug mode you can live edit the locators. Next to the 'Pick Lo ### Picking locators -While debugging you might need to choose a more resilient locator. You can do this by clicking on the **Pick Locator** button and hovering over any element in the browser window. While hovering over an element you will see the code needed to locate this element highlighted below. Clicking an element in the browser will add the locator into the field where you can then either tweak it or copy it into your code. +While debugging, you might need to choose a more resilient locator. You can do this by clicking on the **Pick Locator** button and hovering over any element in the browser window. While hovering over an element you will see the code needed to locate this element highlighted below. Clicking an element in the browser will add the locator into the field where you can then either tweak it or copy it into your code. Picking locators @@ -242,7 +242,7 @@ This will also set the default timeouts of Playwright to 0 (= no timeout). Browser Developer Tools with Playwright object -To debug your tests using the browser developer tools start by setting a breakpoint in your test to pause the execution using the [`method: Page.pause`] method. +To debug your tests using the browser developer tools, start by setting a breakpoint in your test to pause the execution using the [`method: Page.pause`] method. ```js await page.pause(); @@ -264,7 +264,7 @@ page.pause() await page.PauseAsync(); ``` -Once you have set a breakpoint in your test you can then run your test with `PWDEBUG=console`. +Once you have set a breakpoint in your test, you can then run your test with `PWDEBUG=console`. ```bash tab=bash-bash lang=js PWDEBUG=console npx playwright test @@ -327,7 +327,7 @@ $env:PWDEBUG=console dotnet test ``` -Once Playwright launches the browser window you can open the developer tools. +Once Playwright launches the browser window, you can open the developer tools. The `playwright` object will be available in the console panel. #### playwright.$(selector) diff --git a/docs/src/events.md b/docs/src/events.md index 900cde1a95..23d3c90c3e 100644 --- a/docs/src/events.md +++ b/docs/src/events.md @@ -5,7 +5,7 @@ title: "Events" ## Introduction -Playwright allows listening to various types of events happening on the web page, such as network requests, creation of child pages, dedicated workers etc. There are several ways to subscribe to such events such as waiting for events or adding or removing event listeners. +Playwright allows listening to various types of events happening on the web page, such as network requests, creation of child pages, dedicated workers etc. There are several ways to subscribe to such events, such as waiting for events or adding or removing event listeners. ## Waiting for event diff --git a/docs/src/library-js.md b/docs/src/library-js.md index ae3d1916ec..9085290bba 100644 --- a/docs/src/library-js.md +++ b/docs/src/library-js.md @@ -103,7 +103,7 @@ The key differences to note are as follows: | Installation | `npm install playwright` | `npm init playwright@latest` - note `install` vs. `init` | | Install browsers | Install `@playwright/browser-chromium`, `@playwright/browser-firefox` and/or `@playwright/browser-webkit` | `npx playwright install` or `npx playwright install chromium` for a single one | | `import` from | `playwright` | `@playwright/test` | -| Initialization | Explicitly need to:
  1. Pick a browser to use, e.g. `chromium`
  2. Launch browser with [`method: BrowserType.launch`]
  3. Create a context with [`method: Browser.newContext`], and pass any context options explicitly, e.g. `devices['iPhone 11']`
  4. Create a page with [`method: BrowserContext.newPage`]
| An isolated `page` and `context` are provided to each test out-of the box, along with other [built-in fixtures](./test-fixtures.md#built-in-fixtures). No explicit creation. If referenced by the test in it's arguments, the Test Runner will create them for the test. (i.e. lazy-initialization) | +| Initialization | Explicitly need to:
  1. Pick a browser to use, e.g. `chromium`
  2. Launch browser with [`method: BrowserType.launch`]
  3. Create a context with [`method: Browser.newContext`], and pass any context options explicitly, e.g. `devices['iPhone 11']`
  4. Create a page with [`method: BrowserContext.newPage`]
| An isolated `page` and `context` are provided to each test out-of the box, along with other [built-in fixtures](./test-fixtures.md#built-in-fixtures). No explicit creation. If referenced by the test in its arguments, the Test Runner will create them for the test. (i.e. lazy-initialization) | | Assertions | No built-in Web-First Assertions | [Web-First assertions](./test-assertions.md) like:
  • [`method: PageAssertions.toHaveTitle`]
  • [`method: PageAssertions.toHaveScreenshot#1`]
which auto-wait and retry for the condition to be met.| | Cleanup | Explicitly need to:
  1. Close context with [`method: BrowserContext.close`]
  2. Close browser with [`method: Browser.close`]
| No explicit close of [built-in fixtures](./test-fixtures.md#built-in-fixtures); the Test Runner will take care of it. | Running | When using the Library, you run the code as a node script, possibly with some compilation first. | When using the Test Runner, you use the `npx playwright test` command. Along with your [config](./test-configuration.md), the Test Runner handles any compilation and choosing what to run and how to run it. | diff --git a/docs/src/locators.md b/docs/src/locators.md index 052894a172..8ade71efb7 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -10,7 +10,7 @@ a way to find element(s) on the page at any moment. ### Quick Guide -These are the recommended built in locators. +These are the recommended built-in locators. - [`method: Page.getByRole`](#locate-by-role) to locate by explicit and implicit accessibility attributes. - [`method: Page.getByText`](#locate-by-text) to locate by text content. @@ -513,7 +513,7 @@ Use this locator when your element has the `title` attribute. ### Locate by test id -Testing by test ids is the most resilient way of testing as even if your text or role of the attribute changes the test will still pass. QA's and developers should define explicit test ids and query them with [`method: Page.getByTestId`]. However testing by test ids is not user facing. If the role or text value is important to you then consider using user facing locators such as [role](#locate-by-role) and [text locators](#locate-by-text). +Testing by test ids is the most resilient way of testing as even if your text or role of the attribute changes, the test will still pass. QA's and developers should define explicit test ids and query them with [`method: Page.getByTestId`]. However testing by test ids is not user facing. If the role or text value is important to you then consider using user facing locators such as [role](#locate-by-role) and [text locators](#locate-by-text). For example, consider the following DOM structure. @@ -1501,7 +1501,7 @@ For example, consider the following DOM structure: ``` -Locate an item by it's test id of "orange" and then click it. +Locate an item by its test id of "orange" and then click it. ```js await page.getByTestId('orange').click(); diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md index c37b97f6e6..b42407231d 100644 --- a/docs/src/other-locators.md +++ b/docs/src/other-locators.md @@ -668,7 +668,7 @@ We recommend [locating by label text](./locators.md#locate-by-label) instead of Targeted input actions in Playwright automatically distinguish between labels and controls, so you can target the label to perform an action on the associated control. -For example, consider the following DOM structure: ``. You can target the label by it's "Password" text using [`method: Page.getByText`]. However, the following actions will be performed on the input instead of the label: +For example, consider the following DOM structure: ``. You can target the label by its "Password" text using [`method: Page.getByText`]. However, the following actions will be performed on the input instead of the label: - [`method: Locator.click`] will click the label and automatically focus the input field; - [`method: Locator.fill`] will fill the input field; - [`method: Locator.inputValue`] will return the value of the input field; diff --git a/docs/src/test-components-js.md b/docs/src/test-components-js.md index 87cf263711..60d920ee73 100644 --- a/docs/src/test-components-js.md +++ b/docs/src/test-components-js.md @@ -458,7 +458,7 @@ test('slot', async ({ mount }) => { ### hooks -You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you setup things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below: +You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you set up things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below: Date: Tue, 16 Jul 2024 15:55:35 +0200 Subject: [PATCH 063/376] fix(setInputFiles): throw when uploading file in directory upload (#31706) --- CONTRIBUTING.md | 2 +- packages/playwright-core/src/server/dom.ts | 2 ++ tests/page/page-set-input-files.spec.ts | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b71ccde04..30e89e6bee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ All API classes, methods, and events should have a description in [`docs/src`](h To run the documentation linter, use: ```bash -npm run doclint +npm run doc ``` To build the documentation site locally and test how your changes will look in practice: diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 30eb04d3f6..52e648abe0 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -639,6 +639,8 @@ export class ElementHandle extends js.JSHandle { throw injected.createStacklessError('Non-multiple file input can only accept single file'); if (directoryUpload && !inputElement.webkitdirectory) throw injected.createStacklessError('File input does not support directories, pass individual files instead'); + if (!directoryUpload && inputElement.webkitdirectory) + throw injected.createStacklessError('[webkitdirectory] input requires passing a path to a directory'); return inputElement; }, { multiple, directoryUpload: !!localDirectory }); if (result === 'error:notconnected' || !result.asElement()) diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index 76a53fa571..a32db203bd 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -120,6 +120,15 @@ it('should throw when uploading a folder in a normal file upload input', async ( await expect(input.setInputFiles(dir)).rejects.toThrow('File input does not support directories, pass individual files instead'); }); +it('should throw when uploading a file in a directory upload input', async ({ page, server, isAndroid, asset }) => { + it.skip(isAndroid); + it.skip(os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21, 'WebKit on macOS-12 is frozen'); + + await page.goto(server.PREFIX + '/input/folderupload.html'); + const input = await page.$('input'); + await expect(input.setInputFiles(asset('file to upload.txt'))).rejects.toThrow('[webkitdirectory] input requires passing a path to a directory'); +}); + it('should upload a file after popup', async ({ page, server, asset }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29923' }); await page.goto(server.PREFIX + '/input/fileupload.html'); @@ -341,8 +350,7 @@ it('should emit event via prepend', async ({ page, server }) => { expect(chooser).toBeTruthy(); }); -it('should emit event for iframe', async ({ page, server, browserName }) => { - it.skip(browserName === 'firefox'); +it('should emit event for iframe', async ({ page, server }) => { const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE); await frame.setContent(``); const [chooser] = await Promise.all([ From f66f5b800ea925662139e202470b35e4e3d29f3b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 16 Jul 2024 16:11:20 +0200 Subject: [PATCH 064/376] docs: fix broken anchor links (#31707) --- docs/src/other-locators.md | 10 +--------- docs/src/release-notes-java.md | 2 +- docs/src/release-notes-js.md | 2 +- docs/src/release-notes-python.md | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md index b42407231d..c7f6e234d1 100644 --- a/docs/src/other-locators.md +++ b/docs/src/other-locators.md @@ -881,7 +881,7 @@ await page.Locator("data-test-id=submit").ClickAsync(); ``` :::note -Attribute selectors are not CSS selectors, so anything CSS-specific like `:enabled` is not supported. For more features, use a proper [css] selector, e.g. `css=[data-test="login"]:enabled`. +Attribute selectors are not CSS selectors, so anything CSS-specific like `:enabled` is not supported. For more features, use a proper [css](#css-locator) selector, e.g. `css=[data-test="login"]:enabled`. ::: ## Chaining selectors @@ -918,11 +918,3 @@ We recommend [filtering by another locator](./locators.md#filter-by-childdescend By default, chained selectors resolve to an element queried by the last selector. A selector can be prefixed with `*` to capture elements that are queried by an intermediate selector. For example, `css=article >> text=Hello` captures the element with the text `Hello`, and `*css=article >> text=Hello` (note the `*`) captures the `article` element that contains some element with the text `Hello`. - - -[text]: #text-selector -[css]: #css-selector -[xpath]: #xpath-selectors -[react]: #react-selectors -[vue]: #vue-selectors -[id]: #id-data-testid-data-test-id-data-test-selectors diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 6dce675b8f..54c4963216 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -1457,7 +1457,7 @@ This version of Playwright was also tested against the following stable channels #### New APIs -- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers). +- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers). ## Version 1.9 diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 18f07daa14..cd4ec8354e 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -2651,7 +2651,7 @@ This version of Playwright was also tested against the following stable channels #### New APIs -- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers). +- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers). ## Version 1.9 diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index e7a4341a7e..25fbd3ef04 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -1433,7 +1433,7 @@ This version of Playwright was also tested against the following stable channels #### New APIs -- [`browserType.launch()`](./api/class-browsertype#browsertypelaunchoptions) now accepts the new `'channel'` option. Read more in [our documentation](./browsers). +- [`method: BrowserType.launch`] now accepts the new `'channel'` option. Read more in [our documentation](./browsers). ## Version 1.9 From 8021312c99e242138d7eb0aec1036cd320a93996 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 16 Jul 2024 09:44:38 -0700 Subject: [PATCH 065/376] chore: enable notification permission tests in WebKit (#31699) The Notifications API has been supported in WebKit since 2022, enable related permission and tests. --- packages/playwright-core/src/server/webkit/wkPage.ts | 1 + tests/library/permissions.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index adfd06c05e..ae691f0ea3 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -1176,6 +1176,7 @@ export class WKPage implements PageDelegate { async _grantPermissions(origin: string, permissions: string[]) { const webPermissionToProtocol = new Map([ ['geolocation', 'geolocation'], + ['notifications', 'notifications'], ['clipboard-read', 'clipboard-read'], ]); const filtered = permissions.map(permission => { diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index 7e77bba10f..ad56b418d3 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -22,7 +22,7 @@ function getPermission(page, name) { } it.describe('permissions', () => { - it.skip(({ browserName }) => browserName === 'webkit', 'Permissions API is not implemented in WebKit (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)'); + it.fixme(({ browserName, isWindows }) => browserName === 'webkit' && isWindows, 'Permissions API is disabled on Windows WebKit'); it('should be prompt by default', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); @@ -49,7 +49,7 @@ it.describe('permissions', () => { }); it('should grant window-management permission when origin is listed', async ({ page, context, server, browserName }) => { - it.fail(browserName === 'firefox'); + it.skip(browserName !== 'chromium', 'Only Chromium supports window management API.'); await page.goto(server.EMPTY_PAGE); await context.grantPermissions(['window-management'], { origin: server.EMPTY_PAGE }); From 6a9e60d6a17b2a5b88c2bfa6826a9cec5b37fe0a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 16 Jul 2024 19:32:51 +0200 Subject: [PATCH 066/376] fix(ct): import ct* flavour types from ct-core and then from pwt (#31642) --- packages/playwright-ct-core/index.d.ts | 2 +- packages/playwright-ct-react/index.d.ts | 6 ++---- packages/playwright-ct-react17/index.d.ts | 6 ++---- packages/playwright-ct-solid/index.d.ts | 6 ++---- packages/playwright-ct-svelte/index.d.ts | 6 ++---- packages/playwright-ct-vue/index.d.ts | 6 ++---- packages/playwright-ct-vue2/index.d.ts | 6 ++---- 7 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/playwright-ct-core/index.d.ts b/packages/playwright-ct-core/index.d.ts index b397169b74..1006bd59ec 100644 --- a/packages/playwright-ct-core/index.d.ts +++ b/packages/playwright-ct-core/index.d.ts @@ -56,4 +56,4 @@ export function defineConfig(config: PlaywrightTestConfig, ...configs: Playwrigh export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export { expect, devices } from 'playwright/test'; +export { expect, devices, Locator } from 'playwright/test'; diff --git a/packages/playwright-ct-react/index.d.ts b/packages/playwright-ct-react/index.d.ts index c086d4bec5..c5e6d6d2da 100644 --- a/packages/playwright-ct-react/index.d.ts +++ b/packages/playwright-ct-react/index.d.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator } from '@playwright/experimental-ct-core'; export interface MountOptions { hooksConfig?: HooksConfig; @@ -33,5 +32,4 @@ export const test: TestType<{ ): Promise; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-react17/index.d.ts b/packages/playwright-ct-react17/index.d.ts index c086d4bec5..748212c45e 100644 --- a/packages/playwright-ct-react17/index.d.ts +++ b/packages/playwright-ct-react17/index.d.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator} from '@playwright/experimental-ct-core'; export interface MountOptions { hooksConfig?: HooksConfig; @@ -33,5 +32,4 @@ export const test: TestType<{ ): Promise; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-solid/index.d.ts b/packages/playwright-ct-solid/index.d.ts index c086d4bec5..c5e6d6d2da 100644 --- a/packages/playwright-ct-solid/index.d.ts +++ b/packages/playwright-ct-solid/index.d.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator } from '@playwright/experimental-ct-core'; export interface MountOptions { hooksConfig?: HooksConfig; @@ -33,5 +32,4 @@ export const test: TestType<{ ): Promise; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-svelte/index.d.ts b/packages/playwright-ct-svelte/index.d.ts index eb6d464032..761831f331 100644 --- a/packages/playwright-ct-svelte/index.d.ts +++ b/packages/playwright-ct-svelte/index.d.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; import type { SvelteComponent, ComponentProps } from 'svelte/types/runtime'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator } from '@playwright/experimental-ct-core'; type ComponentSlot = string | string[]; type ComponentSlots = Record & { default?: ComponentSlot }; @@ -44,5 +43,4 @@ export const test: TestType<{ ): Promise>; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts index fee68f3cd0..2d182d4588 100644 --- a/packages/playwright-ct-vue/index.d.ts +++ b/packages/playwright-ct-vue/index.d.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator } from '@playwright/experimental-ct-core'; type ComponentSlot = string | string[]; type ComponentSlots = Record & { default?: ComponentSlot }; @@ -64,5 +63,4 @@ export const test: TestType<{ ): Promise>; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-vue2/index.d.ts b/packages/playwright-ct-vue2/index.d.ts index f1a2a0489f..133b4a60f2 100644 --- a/packages/playwright-ct-vue2/index.d.ts +++ b/packages/playwright-ct-vue2/index.d.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Locator } from 'playwright/test'; -import type { TestType } from '@playwright/experimental-ct-core'; +import type { TestType, Locator } from '@playwright/experimental-ct-core'; type Slot = string | string[]; type ComponentSlots = Record & { default?: Slot }; @@ -64,5 +63,4 @@ export const test: TestType<{ ): Promise>; }>; -export { defineConfig, PlaywrightTestConfig } from '@playwright/experimental-ct-core'; -export { expect, devices } from 'playwright/test'; +export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; From bb2e9d1175198290556b387d8987609d2e85565a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 16 Jul 2024 20:55:12 +0200 Subject: [PATCH 067/376] chore: make 'npm run clean' ignore .DS_Store (#31710) --- utils/build/clean.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils/build/clean.js b/utils/build/clean.js index b0ad6f2278..53079baf29 100644 --- a/utils/build/clean.js +++ b/utils/build/clean.js @@ -11,7 +11,9 @@ for (const pkg of workspace.packages()) { rmSync(path.join(pkg.path, 'src', 'generated')); const bundles = path.join(pkg.path, 'bundles'); if (fs.existsSync(bundles) && fs.statSync(bundles).isDirectory()) { - for (const bundle of fs.readdirSync(bundles)) - rmSync(path.join(bundles, bundle, 'node_modules')); + for (const bundle of fs.readdirSync(bundles, { withFileTypes: true })) { + if (bundle.isDirectory()) + rmSync(path.join(bundles, bundle.name, 'node_modules')); + } } } From 3694c1422d9a541776602fb870d0b8e249eff35d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 16 Jul 2024 21:16:55 +0200 Subject: [PATCH 068/376] Revert "test: rebase golden snapshots on Chromium macOS arm64 (#31344)" (#31711) This reverts commit 02416877da13d21b277bc81221602e210171f6c7. Since we landed https://github.com/microsoft/playwright/commit/3127571b24e1a8a93960fa4a24fa011c3df20989 - we should revert this one as well. --- tests/page/page-screenshot.spec.ts | 5 ++--- .../screenshot-canvas-macOS-arm64-chromium.png | Bin 1939 -> 0 bytes 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index dd1be3fbaa..7f374ac59b 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -280,13 +280,12 @@ it.describe('page screenshot', () => { expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png'); }); - it('should work for canvas', async ({ page, server, isElectron, isMac, browserName }) => { + it('should work for canvas', async ({ page, server, isElectron, isMac }) => { it.fixme(isElectron && isMac, 'Fails on the bots'); await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/screenshots/canvas.html'); const screenshot = await page.screenshot(); - const screenshotPrefix = browserName === 'chromium' && isMac && process.arch === 'arm64' ? '-macOS-arm64' : ''; - expect(screenshot).toMatchSnapshot(`screenshot-canvas${screenshotPrefix}.png`); + expect(screenshot).toMatchSnapshot('screenshot-canvas.png'); }); it('should capture canvas changes', async ({ page, isElectron, browserName, isMac, isWebView2 }) => { diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-macOS-arm64-chromium.png deleted file mode 100644 index 830872e8d2e6ac8f68cc70a8d99bb838b2270fbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1939 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y985qFmn|N5ffQqLkh>GZx^prw85r1oJzX3_ zD(1YsYnXS~K!D+(!H2_Nis>UG%Ppb=u~gaDTl0 zclPh@#};Az9~RF|XU;qEj8EW;-|XL)^Q=GReP+0EhjG>sSp|&OxTRU8sQT*?>Wb&V%H@S~l-@SWJ{KkZ{a4%$h-=7-yvyQhQ+KjUB1V+aKLtx}E zGpHklCS4K--BRJGMfA(@qhUQ7)^x03sG8g^h+imH$hkdliaxNGW$<+Mb6Mw<&;$TH CDVNRw From e11c0c0cbb177fbf4fd208f181b99ba5f6109f9e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 17 Jul 2024 09:00:47 +0200 Subject: [PATCH 069/376] fix(connect): annotate internal api calls correctly (#31715) --- .../playwright-core/src/client/browserType.ts | 4 +- .../playwright-core/src/client/connection.ts | 2 + .../playwright.reuse.browser.spec.ts | 51 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 97d82de92d..9df049a641 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -155,7 +155,7 @@ export class BrowserType extends ChannelOwner imple connection.close(reason || closeError); }; pipe.on('closed', params => onPipeClosed(params.reason)); - connection.onmessage = message => pipe.send({ message }).catch(() => onPipeClosed()); + connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true); pipe.on('message', ({ message }) => { try { @@ -181,7 +181,7 @@ export class BrowserType extends ChannelOwner imple this._didLaunchBrowser(browser, {}, logger); browser._shouldCloseConnectionOnClose = true; browser._connectHeaders = connectHeaders; - browser.on(Events.Browser.Disconnected, closePipe); + browser.on(Events.Browser.Disconnected, () => this._wrapApiCall(() => closePipe(), /* isInternal */ true)); return browser; }, deadline); if (!result.timedOut) { diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 294b5d9f6e..953fc279e9 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -194,6 +194,8 @@ export class Connection extends EventEmitter { } close(cause?: string) { + if (this._closedError) + return; this._closedError = new TargetClosedError(cause); for (const callback of this._callbacks.values()) callback.reject(this._closedError); diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts index ecada652a4..bccce95b3e 100644 --- a/tests/playwright-test/playwright.reuse.browser.spec.ts +++ b/tests/playwright-test/playwright.reuse.browser.spec.ts @@ -98,3 +98,54 @@ test('should reuse browser with special characters in the launch options', async expect(guid2).toBe(guid1); expect(workerIndex2).not.toBe(workerIndex1); }); + +test('should produce correct test steps', async ({ runInlineTest, runServer }) => { + const server = await runServer(); + const result = await runInlineTest({ + 'reporter.ts': ` + class Reporter { + onStepBegin(test, result, step) { + console.log('%% onStepBegin ' + step.title); + } + onStepEnd(test, result, step) { + console.log('%% onStepEnd ' + step.title); + } + } + module.exports = Reporter; + `, + 'src/a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('a', async ({ page }) => { + await page.goto('about:blank'); + await page.evaluate(() => console.log('hello')); + }); + `, + }, { reporter: './reporter.ts,list' }, { PW_TEST_REUSE_CONTEXT: '1', PW_TEST_CONNECT_WS_ENDPOINT: server.wsEndpoint() }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.outputLines).toEqual([ + 'onStepBegin Before Hooks', + 'onStepBegin fixture: browser', + 'onStepBegin browserType.connect', + 'onStepEnd browserType.connect', + 'onStepEnd fixture: browser', + 'onStepBegin fixture: context', + 'onStepEnd fixture: context', + 'onStepBegin fixture: page', + 'onStepBegin browserContext.newPage', + 'onStepEnd browserContext.newPage', + 'onStepEnd fixture: page', + 'onStepEnd Before Hooks', + 'onStepBegin page.goto(about:blank)', + 'onStepEnd page.goto(about:blank)', + 'onStepBegin page.evaluate', + 'onStepEnd page.evaluate', + 'onStepBegin After Hooks', + 'onStepBegin fixture: page', + 'onStepEnd fixture: page', + 'onStepBegin fixture: context', + 'onStepEnd fixture: context', + 'onStepEnd After Hooks' + ]); +}); \ No newline at end of file From ed6abf86c743c50baf3b0f033c8c5ef1f707172b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 17 Jul 2024 13:22:00 +0200 Subject: [PATCH 070/376] fix(expect): throw unsupported error when using this.equals() in expect (#31723) --- docs/src/test-assertions-js.md | 2 ++ packages/playwright/src/matchers/expect.ts | 5 +++++ tests/playwright-test/expect.spec.ts | 24 ++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md index 4745659cf9..defbc3a293 100644 --- a/docs/src/test-assertions-js.md +++ b/docs/src/test-assertions-js.md @@ -308,6 +308,8 @@ test('amount', async () => { }); ``` +### Compatibility with expect library + :::note Do not confuse Playwright's `expect` with the [`expect` library](https://jestjs.io/docs/expect). The latter is not fully integrated with Playwright test runner, so make sure to use Playwright's own `expect`. ::: diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 9a010992da..3e49360eb7 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -138,6 +138,7 @@ function createExpect(info: ExpectMetaInfo) { utils, timeout: currentExpectTimeout() }; + (newThis as any).equals = throwUnsupportedExpectMatcherError; return (matcher as any).call(newThis, ...args); }; } @@ -183,6 +184,10 @@ function createExpect(info: ExpectMetaInfo) { return expectInstance; } +function throwUnsupportedExpectMatcherError() { + throw new Error('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility'); +} + expectLibrary.setState({ expand: false }); const customAsyncMatchers = { diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 1393bdd3ff..63928e86fb 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -1039,3 +1039,27 @@ test('should expose timeout to custom matchers', async ({ runInlineTest, runTSC expect(result.failed).toBe(0); expect(result.passed).toBe(2); }); + +test('should throw error when using .equals()', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + import { test as base, expect } from '@playwright/test'; + expect.extend({ + toBeWithinRange(received, floor, ceiling) { + this.equals(1, 2); + }, + }); + export const test = base; + `, + 'expect-test.spec.ts': ` + import { test } from './helper'; + test('numeric ranges', () => { + test.expect(() => { + test.expect(100).toBeWithinRange(90, 110); + }).toThrowError('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility'); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); From 8eab28d858fdf7df9077aff24e4047cd34ae0c39 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 17 Jul 2024 13:36:37 +0200 Subject: [PATCH 071/376] fix(list reporter): print step ends in non-TTY mode (#31703) When used in a terminal, the `list` reporter prints out information about test steps to help debugging. In non-TTY environments like GitHub Actions, currently it doesn't. This PR changes that, so that in non-TTY environments you'll see the "step end" messages appearing, but not the "step begin" messages. This is a good middleground, because it helps the user understand test progress, without being too verbose. Closes https://github.com/microsoft/playwright/issues/31674 --- packages/playwright/src/reporters/list.ts | 46 +++++----- tests/playwright-test/reporter-blob.spec.ts | 50 ++++++----- tests/playwright-test/reporter-list.spec.ts | 95 ++++++++++++++------- 3 files changed, 118 insertions(+), 73 deletions(-) diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index 94e507fd4b..f87a7d4fe8 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -36,7 +36,7 @@ class ListReporter extends BaseReporter { constructor(options: { printSteps?: boolean } = {}) { super(); - this._printSteps = isTTY && getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps); + this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps); } override printsToStdio() { @@ -54,11 +54,13 @@ class ListReporter extends BaseReporter { override onTestBegin(test: TestCase, result: TestResult) { super.onTestBegin(test, result); + + const index = String(this._resultIndex.size + 1); + this._resultIndex.set(result, index); + if (!isTTY) return; this._maybeWriteNewLine(); - const index = String(this._resultIndex.size + 1); - this._resultIndex.set(result, index); this._testRows.set(test, this._lastRow); const prefix = this._testPrefix(index, ''); const line = colors.dim(formatTestTitle(this.config, test)) + this._retrySuffix(result); @@ -75,28 +77,34 @@ class ListReporter extends BaseReporter { this._dumpToStdio(test, chunk, process.stderr); } - override onStepBegin(test: TestCase, result: TestResult, step: TestStep) { - super.onStepBegin(test, result, step); - if (step.category !== 'test.step') - return; - const testIndex = this._resultIndex.get(result) || ''; - if (!this._printSteps) { - if (isTTY) - this._updateLine(this._testRows.get(test)!, colors.dim(formatTestTitle(this.config, test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, '')); - return; - } + private getStepIndex(testIndex: string, result: TestResult, step: TestStep): string { + if (this._stepIndex.has(step)) + return this._stepIndex.get(step)!; const ordinal = ((result as any)[lastStepOrdinalSymbol] || 0) + 1; (result as any)[lastStepOrdinalSymbol] = ordinal; const stepIndex = `${testIndex}.${ordinal}`; this._stepIndex.set(step, stepIndex); + return stepIndex; + } - if (isTTY) { + override onStepBegin(test: TestCase, result: TestResult, step: TestStep) { + super.onStepBegin(test, result, step); + if (step.category !== 'test.step') + return; + const testIndex = this._resultIndex.get(result) || ''; + + if (!isTTY) + return; + + if (this._printSteps) { this._maybeWriteNewLine(); this._stepRows.set(step, this._lastRow); - const prefix = this._testPrefix(stepIndex, ''); + const prefix = this._testPrefix(this.getStepIndex(testIndex, result, step), ''); const line = test.title + colors.dim(stepSuffix(step)); this._appendLine(line, prefix); + } else { + this._updateLine(this._testRows.get(test)!, colors.dim(formatTestTitle(this.config, test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, '')); } } @@ -112,8 +120,8 @@ class ListReporter extends BaseReporter { return; } - const index = this._stepIndex.get(step)!; - const title = test.title + colors.dim(stepSuffix(step)); + const index = this.getStepIndex(testIndex, result, step); + const title = isTTY ? test.title + colors.dim(stepSuffix(step)) : formatTestTitle(this.config, test, step); const prefix = this._testPrefix(index, ''); let text = ''; if (step.error) @@ -204,7 +212,7 @@ class ListReporter extends BaseReporter { private _appendLine(text: string, prefix: string) { const line = prefix + this.fitToScreen(text, prefix); if (process.env.PW_TEST_DEBUG_REPORTERS) { - process.stdout.write(this._lastRow + ' : ' + line + '\n'); + process.stdout.write('#' + this._lastRow + ' : ' + line + '\n'); } else { process.stdout.write(line); process.stdout.write('\n'); @@ -215,7 +223,7 @@ class ListReporter extends BaseReporter { private _updateLine(row: number, text: string, prefix: string) { const line = prefix + this.fitToScreen(text, prefix); if (process.env.PW_TEST_DEBUG_REPORTERS) - process.stdout.write(row + ' : ' + line + '\n'); + process.stdout.write('#' + row + ' : ' + line + '\n'); else this._updateLineForTTY(row, line); } diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 99882777a8..8f3064a073 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -478,28 +478,36 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports } const text = stripAnsi(output); expect(text).toContain('Running 10 tests using 3 workers'); - const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); + const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); expect(lines).toEqual([ - `0 : 1 a.test.js:3:11 › math 1`, - `0 : ${POSITIVE_STATUS_MARK} 1 a.test.js:3:11 › math 1 (Xms)`, - `1 : 2 a.test.js:6:11 › failing 1`, - `1 : ${NEGATIVE_STATUS_MARK} 2 a.test.js:6:11 › failing 1 (Xms)`, - `2 : 3 a.test.js:6:11 › failing 1 (retry #1)`, - `2 : ${NEGATIVE_STATUS_MARK} 3 a.test.js:6:11 › failing 1 (retry #1) (Xms)`, - `3 : 4 a.test.js:9:11 › flaky 1`, - `3 : ${NEGATIVE_STATUS_MARK} 4 a.test.js:9:11 › flaky 1 (Xms)`, - `4 : 5 a.test.js:9:11 › flaky 1 (retry #1)`, - `4 : ${POSITIVE_STATUS_MARK} 5 a.test.js:9:11 › flaky 1 (retry #1) (Xms)`, - `5 : 6 a.test.js:12:12 › skipped 1`, - `5 : - 6 a.test.js:12:12 › skipped 1`, - `6 : 7 b.test.js:3:11 › math 2`, - `6 : ${POSITIVE_STATUS_MARK} 7 b.test.js:3:11 › math 2 (Xms)`, - `7 : 8 b.test.js:6:11 › failing 2`, - `7 : ${NEGATIVE_STATUS_MARK} 8 b.test.js:6:11 › failing 2 (Xms)`, - `8 : 9 b.test.js:6:11 › failing 2 (retry #1)`, - `8 : ${NEGATIVE_STATUS_MARK} 9 b.test.js:6:11 › failing 2 (retry #1) (Xms)`, - `9 : 10 b.test.js:9:12 › skipped 2`, - `9 : - 10 b.test.js:9:12 › skipped 2` + `#0 : 1 a.test.js:3:11 › math 1`, + `#0 : ${POSITIVE_STATUS_MARK} 1 a.test.js:3:11 › math 1 (Xms)`, + `#1 : 2 a.test.js:6:11 › failing 1`, + `#1 : ${NEGATIVE_STATUS_MARK} 2 a.test.js:6:11 › failing 1 (Xms)`, + `#2 : 3 a.test.js:6:11 › failing 1 (retry #1)`, + `#2 : ${NEGATIVE_STATUS_MARK} 3 a.test.js:6:11 › failing 1 (retry #1) (Xms)`, + `#3 : 4 a.test.js:9:11 › flaky 1`, + `#3 : ${NEGATIVE_STATUS_MARK} 4 a.test.js:9:11 › flaky 1 (Xms)`, + `#4 : 5 a.test.js:9:11 › flaky 1 (retry #1)`, + `#4 : ${POSITIVE_STATUS_MARK} 5 a.test.js:9:11 › flaky 1 (retry #1) (Xms)`, + `#5 : 6 a.test.js:12:12 › skipped 1`, + `#5 : - 6 a.test.js:12:12 › skipped 1`, + `#6 : 7 b.test.js:3:11 › math 2`, + `#6 : ${POSITIVE_STATUS_MARK} 7 b.test.js:3:11 › math 2 (Xms)`, + `#7 : 8 b.test.js:6:11 › failing 2`, + `#7 : ${NEGATIVE_STATUS_MARK} 8 b.test.js:6:11 › failing 2 (Xms)`, + `#8 : 9 b.test.js:6:11 › failing 2 (retry #1)`, + `#8 : ${NEGATIVE_STATUS_MARK} 9 b.test.js:6:11 › failing 2 (retry #1) (Xms)`, + `#9 : 10 b.test.js:9:12 › skipped 2`, + `#9 : - 10 b.test.js:9:12 › skipped 2`, + `#10 : 11 c.test.js:3:11 › math 3`, + `#10 : ${POSITIVE_STATUS_MARK} 11 c.test.js:3:11 › math 3 (Xms)`, + `#11 : 12 c.test.js:6:11 › flaky 2`, + `#11 : ${NEGATIVE_STATUS_MARK} 12 c.test.js:6:11 › flaky 2 (Xms)`, + `#12 : 13 c.test.js:6:11 › flaky 2 (retry #1)`, + `#12 : ${POSITIVE_STATUS_MARK} 13 c.test.js:6:11 › flaky 2 (retry #1) (Xms)`, + `#13 : 14 c.test.js:9:12 › skipped 3`, + `#13 : - 14 c.test.js:9:12 › skipped 3`, ]); }); diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 489e97feb7..c57190a5d7 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -72,22 +72,22 @@ for (const useIntermediateMergeReport of [false, true] as const) { `, }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; - const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); + const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); lines.pop(); // Remove last item that contains [v] and time in ms. expect(lines).toEqual([ - '0 : 1 a.test.ts:3:15 › passes', - '1 : 1.1 passes › outer 1.0', - '2 : 1.2 passes › outer 1.0 › inner 1.1', - '2 : 1.2 passes › outer 1.0 › inner 1.1 (Xms)', - '3 : 1.3 passes › outer 1.0 › inner 1.2', - '3 : 1.3 passes › outer 1.0 › inner 1.2 (Xms)', - '1 : 1.1 passes › outer 1.0 (Xms)', - '4 : 1.4 passes › outer 2.0', - '5 : 1.5 passes › outer 2.0 › inner 2.1', - '5 : 1.5 passes › outer 2.0 › inner 2.1 (Xms)', - '6 : 1.6 passes › outer 2.0 › inner 2.2', - '6 : 1.6 passes › outer 2.0 › inner 2.2 (Xms)', - '4 : 1.4 passes › outer 2.0 (Xms)', + '#0 : 1 a.test.ts:3:15 › passes', + '#1 : 1.1 passes › outer 1.0', + '#2 : 1.2 passes › outer 1.0 › inner 1.1', + '#2 : 1.2 passes › outer 1.0 › inner 1.1 (Xms)', + '#3 : 1.3 passes › outer 1.0 › inner 1.2', + '#3 : 1.3 passes › outer 1.0 › inner 1.2 (Xms)', + '#1 : 1.1 passes › outer 1.0 (Xms)', + '#4 : 1.4 passes › outer 2.0', + '#5 : 1.5 passes › outer 2.0 › inner 2.1', + '#5 : 1.5 passes › outer 2.0 › inner 2.1 (Xms)', + '#6 : 1.6 passes › outer 2.0 › inner 2.2', + '#6 : 1.6 passes › outer 2.0 › inner 2.2 (Xms)', + '#4 : 1.4 passes › outer 2.0 (Xms)', ]); }); @@ -107,22 +107,51 @@ for (const useIntermediateMergeReport of [false, true] as const) { });`, }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; - const lines = text.split('\n').filter(l => l.match(/^\d :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); + const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); lines.pop(); // Remove last item that contains [v] and time in ms. expect(lines).toEqual([ - '0 : 1 a.test.ts:3:11 › passes', - '0 : 1 a.test.ts:4:20 › passes › outer 1.0', - '0 : 1 a.test.ts:5:22 › passes › outer 1.0 › inner 1.1', - '0 : 1 a.test.ts:4:20 › passes › outer 1.0', - '0 : 1 a.test.ts:6:22 › passes › outer 1.0 › inner 1.2', - '0 : 1 a.test.ts:4:20 › passes › outer 1.0', - '0 : 1 a.test.ts:3:11 › passes', - '0 : 1 a.test.ts:8:20 › passes › outer 2.0', - '0 : 1 a.test.ts:9:22 › passes › outer 2.0 › inner 2.1', - '0 : 1 a.test.ts:8:20 › passes › outer 2.0', - '0 : 1 a.test.ts:10:22 › passes › outer 2.0 › inner 2.2', - '0 : 1 a.test.ts:8:20 › passes › outer 2.0', - '0 : 1 a.test.ts:3:11 › passes', + '#0 : 1 a.test.ts:3:11 › passes', + '#0 : 1 a.test.ts:4:20 › passes › outer 1.0', + '#0 : 1 a.test.ts:5:22 › passes › outer 1.0 › inner 1.1', + '#0 : 1 a.test.ts:4:20 › passes › outer 1.0', + '#0 : 1 a.test.ts:6:22 › passes › outer 1.0 › inner 1.2', + '#0 : 1 a.test.ts:4:20 › passes › outer 1.0', + '#0 : 1 a.test.ts:3:11 › passes', + '#0 : 1 a.test.ts:8:20 › passes › outer 2.0', + '#0 : 1 a.test.ts:9:22 › passes › outer 2.0 › inner 2.1', + '#0 : 1 a.test.ts:8:20 › passes › outer 2.0', + '#0 : 1 a.test.ts:10:22 › passes › outer 2.0 › inner 2.2', + '#0 : 1 a.test.ts:8:20 › passes › outer 2.0', + '#0 : 1 a.test.ts:3:11 › passes', + ]); + }); + + test('render steps in non-TTY mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', async ({}) => { + await test.step('outer 1.0', async () => { + await test.step('inner 1.1', async () => {}); + await test.step('inner 1.2', async () => {}); + }); + await test.step('outer 2.0', async () => { + await test.step('inner 2.1', async () => {}); + await test.step('inner 2.2', async () => {}); + }); + }); + `, + }, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1' }); + const text = result.output; + const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms')); + expect(lines).toEqual([ + '#0 : 1.1 a.test.ts:5:26 › passes › outer 1.0 › inner 1.1 (Xms)', + '#1 : 1.2 a.test.ts:6:26 › passes › outer 1.0 › inner 1.2 (Xms)', + '#2 : 1.3 a.test.ts:4:24 › passes › outer 1.0 (Xms)', + '#3 : 1.4 a.test.ts:9:26 › passes › outer 2.0 › inner 2.1 (Xms)', + '#4 : 1.5 a.test.ts:10:26 › passes › outer 2.0 › inner 2.2 (Xms)', + '#5 : 1.6 a.test.ts:8:24 › passes › outer 2.0 (Xms)', + `#6 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes (Xms)`, ]); }); @@ -156,13 +185,13 @@ for (const useIntermediateMergeReport of [false, true] as const) { `, }, { reporter: 'list', retries: '1' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' }); const text = result.output; - const lines = text.split('\n').filter(l => l.startsWith('0 :') || l.startsWith('1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms')); + const lines = text.split('\n').filter(l => l.startsWith('#0 :') || l.startsWith('#1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms')); expect(lines).toEqual([ - `0 : 1 a.test.ts:3:15 › flaky`, - `0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:15 › flaky (XXms)`, - `1 : 2 a.test.ts:3:15 › flaky (retry #1)`, - `1 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:3:15 › flaky (retry #1) (XXms)`, + `#0 : 1 a.test.ts:3:15 › flaky`, + `#0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:15 › flaky (XXms)`, + `#1 : 2 a.test.ts:3:15 › flaky (retry #1)`, + `#1 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:3:15 › flaky (retry #1) (XXms)`, ]); }); From f4399f7f062315034a52c2ae395e00979b7cc54d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 17 Jul 2024 07:08:43 -0700 Subject: [PATCH 072/376] fix(toHaveScreenshot): sanitize attachment names and paths (#31712) ... unless an array of file-system-friendly parts is provided. Motivation: attachment name is used as a file system path when downloading attachments, so we keep them fs-friendly. References #30693. --- .../src/matchers/toMatchSnapshot.ts | 108 +++++++++--------- tests/playwright-test/golden.spec.ts | 8 +- .../to-have-screenshot.spec.ts | 46 ++++++++ 3 files changed, 105 insertions(+), 57 deletions(-) diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index 7a3242100e..a533f13b48 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -75,10 +75,10 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [ class SnapshotHelper { readonly testInfo: TestInfoImpl; - readonly outputBaseName: string; + readonly attachmentBaseName: string; readonly legacyExpectedPath: string; readonly previousPath: string; - readonly snapshotPath: string; + readonly expectedPath: string; readonly actualPath: string; readonly diffPath: string; readonly mimeType: string; @@ -117,40 +117,42 @@ class SnapshotHelper { (testInfo as any)[snapshotNamesSymbol] = snapshotNames; } - // Consider the use case below. We should save actual to different paths. - // - // expect.toMatchSnapshot('a.png') - // // noop - // expect.toMatchSnapshot('a.png') - - let inputPathSegments: string[]; + let expectedPathSegments: string[]; + let outputBasePath: string; if (!name) { + // Consider the use case below. We should save actual to different paths. + // Therefore we auto-increment |anonymousSnapshotIndex|. + // + // expect.toMatchSnapshot('a.png') + // // noop + // expect.toMatchSnapshot('a.png') const fullTitleWithoutSpec = [ ...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex, ].join(' '); - inputPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension]; + // Note: expected path must not ever change for backwards compatibility. + expectedPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension]; // Trim the output file paths more aggressively to avoid hitting Windows filesystem limits. - this.outputBaseName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension; + const sanitizedName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension; + outputBasePath = testInfo._getOutputPath(sanitizedName); + this.attachmentBaseName = sanitizedName; } else { - // We intentionally do not sanitize user-provided array of segments, but for backwards - // compatibility we do sanitize the name if it is a single string. - // See https://github.com/microsoft/playwright/pull/9156 - inputPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)]; - const joinedName = Array.isArray(name) ? name.join(path.sep) : name; + // We intentionally do not sanitize user-provided array of segments, assuming + // it is a file system path. See https://github.com/microsoft/playwright/pull/9156. + // Note: expected path must not ever change for backwards compatibility. + expectedPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)]; + const joinedName = Array.isArray(name) ? name.join(path.sep) : sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength)); snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1; const index = snapshotNames.namedSnapshotIndex[joinedName]; - if (index > 1) - this.outputBaseName = addSuffixToFilePath(joinedName, `-${index - 1}`); - else - this.outputBaseName = joinedName; + const sanitizedName = index > 1 ? addSuffixToFilePath(joinedName, `-${index - 1}`) : joinedName; + outputBasePath = testInfo._getOutputPath(sanitizedName); + this.attachmentBaseName = sanitizedName; } - this.snapshotPath = testInfo.snapshotPath(...inputPathSegments); - const outputFile = testInfo._getOutputPath(sanitizeFilePathBeforeExtension(this.outputBaseName)); - this.legacyExpectedPath = addSuffixToFilePath(outputFile, '-expected'); - this.previousPath = addSuffixToFilePath(outputFile, '-previous'); - this.actualPath = addSuffixToFilePath(outputFile, '-actual'); - this.diffPath = addSuffixToFilePath(outputFile, '-diff'); + this.expectedPath = testInfo.snapshotPath(...expectedPathSegments); + this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected'); + this.previousPath = addSuffixToFilePath(outputBasePath, '-previous'); + this.actualPath = addSuffixToFilePath(outputBasePath, '-actual'); + this.diffPath = addSuffixToFilePath(outputBasePath, '-diff'); const filteredConfigOptions = { ...configOptions }; for (const prop of NonConfigProperties) @@ -176,7 +178,7 @@ class SnapshotHelper { this.locator = locator; this.updateSnapshots = testInfo.config.updateSnapshots; - this.mimeType = mime.getType(path.basename(this.snapshotPath)) ?? 'application/octet-string'; + this.mimeType = mime.getType(path.basename(this.expectedPath)) ?? 'application/octet-string'; this.comparator = getComparator(this.mimeType); this.testInfo = testInfo; @@ -186,7 +188,7 @@ class SnapshotHelper { createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult { const unfiltered: ImageMatcherResult = { name: this.matcherName, - expected: this.snapshotPath, + expected: this.expectedPath, actual: this.actualPath, diff: this.diffPath, pass, @@ -198,7 +200,7 @@ class SnapshotHelper { handleMissingNegated(): ImageMatcherResult { const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing'; - const message = `A snapshot doesn't exist at ${this.snapshotPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`; + const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`; // NOTE: 'isNot' matcher implies inversed value. return this.createMatcherResult(message, true); } @@ -221,11 +223,11 @@ class SnapshotHelper { handleMissing(actual: Buffer | string): ImageMatcherResult { const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing'; if (isWriteMissingMode) { - writeFileSync(this.snapshotPath, actual); + writeFileSync(this.expectedPath, actual); writeFileSync(this.actualPath, actual); - this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath }); + this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath }); } - const message = `A snapshot doesn't exist at ${this.snapshotPath}${isWriteMissingMode ? ', writing actual.' : '.'}`; + const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`; if (this.updateSnapshots === 'all') { /* eslint-disable no-console */ console.log(message); @@ -258,22 +260,22 @@ class SnapshotHelper { // Copy the expectation inside the `test-results/` folder for backwards compatibility, // so that one can upload `test-results/` directory and have all the data inside. writeFileSync(this.legacyExpectedPath, expected); - this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-expected'), contentType: this.mimeType, path: this.snapshotPath }); - output.push(`\nExpected: ${colors.yellow(this.snapshotPath)}`); + this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath }); + output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`); } if (previous !== undefined) { writeFileSync(this.previousPath, previous); - this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath }); + this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath }); output.push(`Previous: ${colors.yellow(this.previousPath)}`); } if (actual !== undefined) { writeFileSync(this.actualPath, actual); - this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath }); + this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath }); output.push(`Received: ${colors.yellow(this.actualPath)}`); } if (diff !== undefined) { writeFileSync(this.diffPath, diff); - this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath }); + this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath }); output.push(` Diff: ${colors.yellow(this.diffPath)}`); } @@ -311,25 +313,25 @@ export function toMatchSnapshot( configOptions, nameOrOptions, optOptions); if (this.isNot) { - if (!fs.existsSync(helper.snapshotPath)) + if (!fs.existsSync(helper.expectedPath)) return helper.handleMissingNegated(); - const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.snapshotPath), helper.options); + const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.expectedPath), helper.options); return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); } - if (!fs.existsSync(helper.snapshotPath)) + if (!fs.existsSync(helper.expectedPath)) return helper.handleMissing(received); - const expected = fs.readFileSync(helper.snapshotPath); + const expected = fs.readFileSync(helper.expectedPath); const result = helper.comparator(received, expected, helper.options); if (!result) return helper.handleMatching(); if (helper.updateSnapshots === 'all') { - writeFileSync(helper.snapshotPath, received); + writeFileSync(helper.expectedPath, received); /* eslint-disable no-console */ - console.log(helper.snapshotPath + ' does not match, writing actual.'); - return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true); + console.log(helper.expectedPath + ' does not match, writing actual.'); + return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true); } return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined); @@ -364,8 +366,8 @@ export async function toHaveScreenshot( const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator]; const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {}; const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions); - if (!helper.snapshotPath.toLowerCase().endsWith('.png')) - throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`); + if (!helper.expectedPath.toLowerCase().endsWith('.png')) + throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`); expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot'); const style = await loadScreenshotStyles(helper.options.stylePath); const expectScreenshotOptions: ExpectScreenshotOptions = { @@ -387,7 +389,7 @@ export async function toHaveScreenshot( threshold: helper.options.threshold, }; - const hasSnapshot = fs.existsSync(helper.snapshotPath); + const hasSnapshot = fs.existsSync(helper.expectedPath); if (this.isNot) { if (!hasSnapshot) return helper.handleMissingNegated(); @@ -395,14 +397,14 @@ export async function toHaveScreenshot( // Having `errorMessage` means we timed out while waiting // for screenshots not to match, so screenshots // are actually the same in the end. - expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath); + expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath); const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage; return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); } // Fast path: there's no screenshot and we don't intend to update it. if (helper.updateSnapshots === 'none' && !hasSnapshot) - return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.snapshotPath}.`, false); + return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false); if (!hasSnapshot) { // Regenerate a new screenshot by waiting until two screenshots are the same. @@ -420,18 +422,18 @@ export async function toHaveScreenshot( // - snapshot exists // - regular matcher (i.e. not a `.not`) // - perhaps an 'all' flag to update non-matching screenshots - expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath); + expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath); const { actual, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions); if (!errorMessage) return helper.handleMatching(); if (helper.updateSnapshots === 'all') { - writeFileSync(helper.snapshotPath, actual!); + writeFileSync(helper.expectedPath, actual!); writeFileSync(helper.actualPath, actual!); /* eslint-disable no-console */ - console.log(helper.snapshotPath + ' is re-generated, writing actual.'); - return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true); + console.log(helper.expectedPath + ' is re-generated, writing actual.'); + return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true); } return helper.handleDifferent(actual, expectScreenshotOptions.expected, undefined, diff, errorMessage, log); diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts index 41cdb9da5a..205eeee7f0 100644 --- a/tests/playwright-test/golden.spec.ts +++ b/tests/playwright-test/golden.spec.ts @@ -145,7 +145,7 @@ test('should generate separate actual results for repeating names', async ({ run { 'name': 'bar/baz-actual.txt', 'contentType': 'text/plain', - 'path': 'test-results/a-is-a-test/bar-baz-actual.txt' + 'path': 'test-results/a-is-a-test/bar/baz-actual.txt' }, { 'name': 'bar/baz-1-expected.txt', @@ -155,7 +155,7 @@ test('should generate separate actual results for repeating names', async ({ run { 'name': 'bar/baz-1-actual.txt', 'contentType': 'text/plain', - 'path': 'test-results/a-is-a-test/bar-baz-1-actual.txt' + 'path': 'test-results/a-is-a-test/bar/baz-1-actual.txt' } ]); }); @@ -977,12 +977,12 @@ test('should attach expected/actual/diff with snapshot path', async ({ runInline { name: 'test/path/snapshot-actual.png', contentType: 'image/png', - path: 'a-is-a-test/test-path-snapshot-actual.png' + path: 'a-is-a-test/test/path/snapshot-actual.png' }, { name: 'test/path/snapshot-diff.png', contentType: 'image/png', - path: 'a-is-a-test/test-path-snapshot-diff.png' + path: 'a-is-a-test/test/path/snapshot-diff.png' } ]); }); diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 07fb590375..39044e460a 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -1320,3 +1320,49 @@ function playwrightConfig(obj: any) { `, }; } + +test('should trim+sanitize attachment names and paths', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + ...playwrightConfig({ + snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', + }), + 'a.spec.js': ` + const { test, expect } = require('@playwright/test'); + test.afterEach(async ({}, testInfo) => { + console.log('## ' + JSON.stringify(testInfo.attachments)); + }); + const title = 'long '.repeat(30) + 'title'; + test(title, async ({ page }) => { + await expect.soft(page).toHaveScreenshot(); + const name = 'long '.repeat(30) + 'name.png'; + await expect.soft(page).toHaveScreenshot(name); + await expect.soft(page).toHaveScreenshot(['dir', name]); + }); + ` + }); + + expect(result.exitCode).toBe(1); + const attachments = result.output.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0]; + for (const attachment of attachments) { + attachment.path = attachment.path.replace(testInfo.outputDir, '').substring(1).replace(/\\/g, '/'); + attachment.name = attachment.name.replace(/\\/g, '/'); + } + expect(attachments).toEqual([ + { + name: 'long-long-long-long-long-l-852e1-long-long-long-long-title-1-actual.png', + contentType: 'image/png', + path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/long-long-long-long-long-l-852e1-long-long-long-long-title-1-actual.png', + }, + { + name: 'long-long-long-long-long-l-6bf1e-ong-long-long-long-name-actual.png', + contentType: 'image/png', + path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/long-long-long-long-long-l-6bf1e-ong-long-long-long-name-actual.png', + }, + { + name: 'dir/long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long name-actual.png', + contentType: 'image/png', + path: 'test-results/a-long-long-long-long-long-abd51-g-long-long-long-long-title/dir/long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long name-actual.png', + }, + ]); +}); + From 3f15fe8518b1c0bfded61f902300a267b44a4530 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 17 Jul 2024 09:30:49 -0700 Subject: [PATCH 073/376] feat(reporter): links in attachment names, attachments name only (#31714) * Allow calling `test.info().attach('My text');` without options (no path nor body). * Highlight links in attachment names: image Fixes https://github.com/microsoft/playwright/issues/31284 --- packages/html-reporter/src/links.tsx | 2 +- .../html-reporter/src/testCaseView.spec.tsx | 12 +++++++++++- packages/playwright/src/reporters/base.ts | 2 +- packages/playwright/src/util.ts | 2 ++ .../reporter-attachment.spec.ts | 19 +++++++++++++++++-- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 4ddaf20a91..42b9f9b5b9 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -78,7 +78,7 @@ export const AttachmentLink: React.FunctionComponent<{ return {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.path && {linkName || attachment.name}} - {attachment.body && {linkifyText(attachment.name)}} + {!attachment.path && {linkifyText(attachment.name)}}
} loadChildren={attachment.body ? () => { return [
{linkifyText(attachment.body!)}
]; } : undefined} depth={0} style={{ lineHeight: '32px' }}>; diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index d73407582d..624a93805f 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -132,6 +132,9 @@ const resultWithAttachment: TestResult = { name: 'first attachment', body: 'The body with https://playwright.dev/docs/intro link and https://github.com/microsoft/playwright/issues/31284.', contentType: 'text/plain' + }, { + name: 'attachment with inline link https://github.com/microsoft/playwright/issues/31284', + contentType: 'text/plain' }], status: 'passed', }; @@ -157,4 +160,11 @@ test('should correctly render links in attachments', async ({ mount }) => { await expect(body).toBeVisible(); await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro'); await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); -}); \ No newline at end of file +}); + +test('should correctly render links in attachment name', async ({ mount }) => { + const component = await mount(); + const link = component.getByText('attachment with inline link').locator('a'); + await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); + await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284'); +}); diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index e4575a8627..c9ce2f7bcd 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -319,7 +319,7 @@ export function formatFailure(config: FullConfig, test: TestCase, options: {inde if (includeAttachments) { for (let i = 0; i < result.attachments.length; ++i) { const attachment = result.attachments[i]; - const hasPrintableContent = attachment.contentType.startsWith('text/') && attachment.body; + const hasPrintableContent = attachment.contentType.startsWith('text/'); if (!attachment.path && !hasPrintableContent) continue; resultLines.push(''); diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index d6cabd07c6..a4ddce7a3b 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -256,6 +256,8 @@ export function resolveReporterOutputPath(defaultValue: string, configDir: strin } export async function normalizeAndSaveAttachment(outputPath: string, name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}): Promise<{ name: string; path?: string | undefined; body?: Buffer | undefined; contentType: string; }> { + if (options.path === undefined && options.body === undefined) + return { name, contentType: 'text/plain' }; if ((options.path !== undefined ? 1 : 0) + (options.body !== undefined ? 1 : 0) !== 1) throw new Error(`Exactly one of "path" and "body" must be specified`); if (options.path !== undefined) { diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index fd938be437..50d6d1ee0c 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -99,8 +99,8 @@ test(`testInfo.attach errors`, async ({ runInlineTest }) => { const text = result.output.replace(/\\/g, '/'); expect(text).toMatch(/Error: ENOENT: no such file or directory, copyfile '.*foo.txt.*'/); expect(text).toContain(`Exactly one of "path" and "body" must be specified`); - expect(result.passed).toBe(0); - expect(result.failed).toBe(3); + expect(result.passed).toBe(1); + expect(result.failed).toBe(2); expect(result.exitCode).toBe(1); }); @@ -175,6 +175,21 @@ test(`testInfo.attach allow empty string body`, async ({ runInlineTest }) => { expect(result.output).toMatch(/^.*attachment #1: name \(text\/plain\).*\n.*\n.*──────/gm); }); +test(`testInfo.attach allow without options`, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('success', async ({}, testInfo) => { + await testInfo.attach('Full name'); + expect(0).toBe(1); + }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toMatch(/^.*attachment #1: Full name \(text\/plain\).*\n.*──────/gm); +}); + test(`testInfo.attach allow empty buffer body`, async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` From 3cb41739a03f2e213b10d9ed32f17d6a4aab9f11 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:33:47 -0700 Subject: [PATCH 074/376] feat(firefox-beta): roll to r1457 (#31733) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 1f00780f4f..d7abe551c1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -21,9 +21,9 @@ }, { "name": "firefox-beta", - "revision": "1456", + "revision": "1457", "installByDefault": false, - "browserVersion": "128.0b3" + "browserVersion": "129.0b2" }, { "name": "webkit", From e06481a33254fc7e8d94cf891dcdd0b5c7eb5acc Mon Sep 17 00:00:00 2001 From: Matt Kleinsmith <8968171+MattKleinsmith@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:45:48 -0700 Subject: [PATCH 075/376] fix(recorder): address custom context menus (#31634) --- .../src/server/injected/recorder/recorder.ts | 22 ++++++++ tests/library/inspector/cli-codegen-3.spec.ts | 54 +++++++++++++++++++ tests/library/inspector/inspectorTest.ts | 6 +-- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 4cfa512a4f..116fca63bc 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -259,6 +259,28 @@ class RecordActionTool implements RecorderTool { }); } + onContextMenu(event: MouseEvent) { + // the 'contextmenu' event is triggered by a right-click or equivalent action, + // and it prevents the click event from firing for that action, so we always + // convert 'contextmenu' into a right-click. + if (this._shouldIgnoreMouseEvent(event)) + return; + if (this._actionInProgress(event)) + return; + if (this._consumedDueToNoModel(event, this._hoveredModel)) + return; + + this._performAction({ + name: 'click', + selector: this._hoveredModel!.selector, + position: positionForEvent(event), + signals: [], + button: 'right', + modifiers: 0, + clickCount: 0 + }); + } + onPointerDown(event: PointerEvent) { if (this._shouldIgnoreMouseEvent(event)) return; diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 2c0fff974a..8ad14c87b9 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -559,6 +559,60 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`); ]); }); + test('should consume contextmenu events, despite a custom context menu', async ({ page, openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(` + + + + `); + + await recorder.hoverOverElement('button'); + expect(await page.evaluate('log')).toEqual(['button: pointermove', 'button: mousemove']); + + const [message] = await Promise.all([ + page.waitForEvent('console', msg => msg.type() !== 'error'), + recorder.waitForOutput('JavaScript', `button: 'right'`), + recorder.trustedClick({ button: 'right' }), + ]); + expect(message.text()).toBe('right-clicked'); + expect(await page.evaluate('log')).toEqual([ + // hover + 'button: pointermove', + 'button: mousemove', + // trusted right click + 'button: pointerup', + 'button: pointermove', + 'button: mousemove', + 'button: pointerdown', + 'button: mousedown', + 'button: contextmenu', + 'menu: pointerup', + 'menu: mouseup' + ]); + }); + test('should assert value', async ({ openRecorder }) => { const recorder = await openRecorder(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 1d5e30825a..6ebbc1fdd1 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -186,9 +186,9 @@ class Recorder { await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); } - async trustedClick() { - await this.page.mouse.down(); - await this.page.mouse.up(); + async trustedClick(options?: { button?: 'left' | 'right' | 'middle' }) { + await this.page.mouse.down(options); + await this.page.mouse.up(options); } async focusElement(selector: string): Promise { From 6491e5b415a7abc72706450f463286277db7fd1f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 18 Jul 2024 00:19:08 -0700 Subject: [PATCH 076/376] chore: deprecate/remove noWaitAfter from some actions (#31739) The following actions keep `noWaitAfter` option: `click`, `selectOption` and `press`. All other actions that used to have `noWaitAfter` now behave like it was set to true, not waiting for follow-up navigations. In the docs, this option is marked as completely ignored. A small logic change was made to compensate for this behavior: when waiting for the `hitTargetInterceptor`, we now race it against navigations to avoid stalling when navigation stalls. Previously, waiting for the interceptor was disabled when `noWaitAfter` was passed, and since it's impossible to pass this option now, we mitigate by never stalling instead. Fixes #31469. --- docs/src/api/class-elementhandle.md | 25 +- docs/src/api/class-filechooser.md | 2 +- docs/src/api/class-frame.md | 26 +- docs/src/api/class-locator.md | 31 +-- docs/src/api/class-page.md | 27 +- docs/src/api/params.md | 7 + .../playwright-core/src/protocol/validator.ts | 17 -- packages/playwright-core/src/server/dom.ts | 163 +++++------ packages/playwright-core/src/server/frames.ts | 35 +-- packages/playwright-core/src/server/types.ts | 8 +- packages/playwright-core/types/types.d.ts | 253 +++++++----------- packages/protocol/src/channels.ts | 34 --- packages/protocol/src/protocol.yml | 17 -- tests/library/tap.spec.ts | 27 +- tests/page/locator-misc-1.spec.ts | 10 - tests/page/page-autowaiting-basic.spec.ts | 4 +- tests/page/page-mouse.spec.ts | 10 - 17 files changed, 258 insertions(+), 438 deletions(-) diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index 24b8bb2b6f..1793798c8c 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -164,7 +164,6 @@ This method checks the element by performing the following steps: 1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set. 1. Scroll the element into view if needed. 1. Use [`property: Page.mouse`] to click in the center of the element. -1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. 1. Ensure that the element is now checked. If not, this method throws. If the element is detached from the DOM at any moment during the action, this method throws. @@ -178,7 +177,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.check.force = %%-input-force-%% * since: v1.8 -### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: ElementHandle.check.timeout = %%-input-timeout-%% @@ -251,8 +250,6 @@ This method double clicks the element by performing the following steps: 1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set. 1. Scroll the element into view if needed. 1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`]. -1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that - if the first click of the `dblclick()` triggers a navigation event, this method will throw. If the element is detached from the DOM at any moment during the action, this method throws. @@ -278,7 +275,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.dblclick.force = %%-input-force-%% * since: v1.8 -### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: ElementHandle.dblclick.timeout = %%-input-timeout-%% @@ -537,7 +534,7 @@ Value to set for the ``, `'); + await expect(page.locator('input')).toBeChecked(); + await expect(page.locator('input')).toBeDisabled(); + await expect(page.locator('textarea')).not.toBeEditable(); + await expect(page.locator('span')).toBeEmpty(); + await expect(page.locator('button')).not.toBeEnabled(); + await expect(page.locator('button')).toBeFocused(); + await expect(page.locator('span')).toBeHidden(); + await expect(page.locator('div')).not.toBeInViewport(); + await expect(page.locator('div')).not.toBeVisible(); + await expect(page.locator('span')).toContainText('World'); + await expect(page.locator('span')).toHaveAccessibleDescription('World'); + await expect(page.locator('span')).toHaveAccessibleName('World'); + await expect(page.locator('span')).toHaveAttribute('name', 'value'); + await expect(page.locator('span')).toHaveAttribute('name'); + await expect(page.locator('span')).toHaveClass('name'); + await expect(page.locator('span')).toHaveCount(2); + await expect(page.locator('span')).toHaveCSS('width', '10'); + await expect(page.locator('span')).toHaveId('id'); + await expect(page.locator('span')).toHaveJSProperty('name', 'value'); + await expect(page.locator('span')).toHaveRole('role'); + await expect(page.locator('span')).toHaveText('World'); + await expect(page.locator('textarea')).toHaveValue('value'); + await expect(page.locator('select')).toHaveValues(['value']); + }); + ` + }, { }); + expect(result.exitCode).toBe(1); + + const { errors } = result.report.suites[0].specs[0].tests[0].results[0]; + const matcherResults = errors.map(e => e.matcherResult); + expect(matcherResults).toEqual([ + { name: 'toBeChecked', pass: false, expected: 'checked', actual: 'unchecked', timeout: 1 }, + { name: 'toBeDisabled', pass: false, expected: 'disabled', actual: 'enabled', timeout: 1 }, + { name: 'toBeEditable', pass: true, expected: 'editable', actual: 'editable', timeout: 1 }, + { name: 'toBeEmpty', pass: false, expected: 'empty', actual: 'notEmpty', timeout: 1 }, + { name: 'toBeEnabled', pass: true, expected: 'enabled', actual: 'enabled', timeout: 1 }, + { name: 'toBeFocused', pass: false, expected: 'focused', actual: 'inactive', timeout: 1 }, + { name: 'toBeHidden', pass: false, expected: 'hidden', actual: 'visible', timeout: 1 }, + { name: 'toBeInViewport', pass: true, expected: 'in viewport', actual: 'in viewport', timeout: 1 }, + { name: 'toBeVisible', pass: true, expected: 'visible', actual: 'visible', timeout: 1 }, + { name: 'toContainText', pass: false, expected: 'World', actual: 'Hello', timeout: 1 }, + { name: 'toHaveAccessibleDescription', pass: false, expected: 'World', actual: '', timeout: 1 }, + { name: 'toHaveAccessibleName', pass: false, expected: 'World', actual: '', timeout: 1 }, + { name: 'toHaveAttribute', pass: false, expected: 'value', actual: null, timeout: 1 }, + { name: 'toHaveAttribute', pass: false, expected: 'have attribute', actual: 'not have attribute', timeout: 1 }, + { name: 'toHaveClass', pass: false, expected: 'name', actual: '', timeout: 1 }, + { name: 'toHaveCount', pass: false, expected: 2, actual: 1, timeout: 1 }, + { name: 'toHaveCSS', pass: false, expected: '10', actual: 'auto', timeout: 1 }, + { name: 'toHaveId', pass: false, expected: 'id', actual: '', timeout: 1 }, + { name: 'toHaveJSProperty', pass: false, expected: 'value', timeout: 1 }, + { name: 'toHaveRole', pass: false, expected: 'role', actual: '', timeout: 1 }, + { name: 'toHaveText', pass: false, expected: 'World', actual: 'Hello', timeout: 1 }, + { name: 'toHaveValue', pass: false, expected: 'value', actual: '', timeout: 1 }, + { name: 'toHaveValues', pass: false, expected: ['value'], actual: [], timeout: 1 }, + ]); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 8494671c76..90ef7fa75a 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -384,6 +384,7 @@ export type MatcherReturnType = { expected?: unknown; actual?: any; log?: string[]; + timeout?: number; }; type MakeMatchers = { diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index 51eab7e370..0b81724373 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -105,6 +105,7 @@ export interface JSONReportTest { export interface JSONReportError { message: string; location?: Location; + matcherResult?: TestErrorMatcherResult; } export interface JSONReportTestResult { From 5d4a65b31835fcec13fccb84f703f92606a20b0d Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 6 Sep 2024 10:28:17 +0200 Subject: [PATCH 364/376] docs: update release notes for 1.47 to our changes from yesterday (#32482) I'll also cherry-pick this into the release branch and update playwright.dev. --- docs/src/release-notes-csharp.md | 10 +++++----- docs/src/release-notes-java.md | 10 +++++----- docs/src/release-notes-js.md | 10 +++++----- docs/src/release-notes-python.md | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index f4532376de..6c357ba3b5 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -8,14 +8,14 @@ toc_max_heading_level: 2 ### Network Tab improvements -The Network tab in the trace viewer now allows searching and filtering by asset type: +The Network tab in the trace viewer has several nice improvements: + +- filtering by asset type and URL +- better display of query string parameters +- preview of font assets ![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2) -And for fonts, it now shows a nice preview: - -![Font requests have a preview now](https://github.com/user-attachments/assets/769d64cc-cdcb-421d-9849-227d2f874d1f) - ### Miscellaneous - The `mcr.microsoft.com/playwright-dotnet:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble. diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 4eab504435..2b9789e8bc 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -8,14 +8,14 @@ toc_max_heading_level: 2 ### Network Tab improvements -The Network tab in the trace viewer now allows searching and filtering by asset type: +The Network tab in the trace viewer has several nice improvements: + +- filtering by asset type and URL +- better display of query string parameters +- preview of font assets ![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2) -And for fonts, it now shows a nice preview: - -![Font requests have a preview now](https://github.com/user-attachments/assets/769d64cc-cdcb-421d-9849-227d2f874d1f) - ### Miscellaneous - The `mcr.microsoft.com/playwright-java:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble. diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 386fa4abf8..a89b79c5ce 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -10,14 +10,14 @@ import LiteYouTube from '@site/src/components/LiteYouTube'; ### Network Tab improvements -The Network tab in the UI mode and trace viewer now allows searching and filtering by asset type: +The Network tab in the UI mode and trace viewer has several nice improvements: + +- filtering by asset type and URL +- better display of query string parameters +- preview of font assets ![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2) -And for fonts, it now shows a nice preview: - -![Font requests have a preview now](https://github.com/user-attachments/assets/769d64cc-cdcb-421d-9849-227d2f874d1f) - ### `--tsconfig` CLI option diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 5fbf11873b..985c19d388 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -8,14 +8,14 @@ toc_max_heading_level: 2 ### Network Tab improvements -The Network tab in the trace viewer now allows searching and filtering by asset type: +The Network tab in the trace viewer has several nice improvements: + +- filtering by asset type and URL +- better display of query string parameters +- preview of font assets ![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2) -And for fonts, it now shows a nice preview: - -![Font requests have a preview now](https://github.com/user-attachments/assets/769d64cc-cdcb-421d-9849-227d2f874d1f) - ### Miscellaneous - The `mcr.microsoft.com/playwright-python:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble. From a8f67a42b8995d0e3f8dbf82ca755e93de7ee0b0 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 6 Sep 2024 11:27:35 +0200 Subject: [PATCH 365/376] docs(dotnet): fix wrong snippets (#32484) Fixes https://github.com/microsoft/playwright-dotnet/issues/2994 --- docs/src/api/class-pageassertions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index 2dcd17b3ac..5e56907656 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -83,7 +83,7 @@ assertThat(page).not().hasURL("error"); ``` ```csharp -await Expect(Page).Not.ToHaveURL("error"); +await Expect(Page).Not.ToHaveURLAsync("error"); ``` ## async method: PageAssertions.NotToHaveTitle @@ -271,7 +271,7 @@ expect(page).to_have_title(re.compile(r".*checkout")) ``` ```csharp -await Expect(Page).ToHaveTitle("Playwright"); +await Expect(Page).ToHaveTitleAsync("Playwright"); ``` ### param: PageAssertions.toHaveTitle.titleOrRegExp @@ -320,7 +320,7 @@ expect(page).to_have_url(re.compile(".*checkout")) ``` ```csharp -await Expect(Page).ToHaveURL(new Regex(".*checkout")); +await Expect(Page).ToHaveURLAsync(new Regex(".*checkout")); ``` ### param: PageAssertions.toHaveURL.urlOrRegExp From 0e3f4736cc5f31823174e4b57c9c30ac2320d168 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 6 Sep 2024 11:35:20 +0200 Subject: [PATCH 366/376] fix(test runner): always show all projects in selection (#32450) Follow-up to https://github.com/microsoft/playwright/pull/32156#discussion_r1741628770, alternative solution to https://github.com/microsoft/playwright/pull/32425. Ensures we always show all projects in the watch mode project selector by performing the initial `listTests` without any filters, and using its result for the project selector. --- packages/playwright/src/runner/watchMode.ts | 6 ++++-- tests/playwright-test/watch.spec.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 924449adce..ba2c5a34e7 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -125,9 +125,11 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true }); await testServerConnection.runGlobalSetup({}); - const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep }); + const { report } = await testServerConnection.listTests({}); teleSuiteUpdater.processListReport(report); + const projectNames = teleSuiteUpdater.rootSuite!.suites.map(s => s.title); + let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestIds?: string[] } = { type: 'regular' }; let result: FullResult['status'] = 'passed'; @@ -167,7 +169,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp type: 'multiselect', name: 'selectedProjects', message: 'Select projects', - choices: teleSuiteUpdater.rootSuite!.suites.map(s => s.title), + choices: projectNames, }).catch(() => ({ selectedProjects: null })); if (!selectedProjects) continue; diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index 0dba9dd861..f4d1fd72ce 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -282,8 +282,8 @@ test('should respect file filter P', async ({ runWatchTest }) => { await testProcess.waitForOutput('Waiting for file changes.'); }); -test('should respect project filter C', async ({ runWatchTest }) => { - const testProcess = await runWatchTest({ +test('should respect project filter C', async ({ runWatchTest, writeFiles }) => { + const files = { 'playwright.config.ts': ` import { defineConfig } from '@playwright/test'; export default defineConfig({ projects: [{name: 'foo'}, {name: 'bar'}] }); @@ -292,9 +292,9 @@ test('should respect project filter C', async ({ runWatchTest }) => { import { test, expect } from '@playwright/test'; test('passes', () => {}); `, - }); + }; + const testProcess = await runWatchTest(files, { project: 'foo' }); await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); - await testProcess.waitForOutput('[bar] › a.test.ts:3:11 › passes'); await testProcess.waitForOutput('Waiting for file changes.'); testProcess.clearOutput(); testProcess.write('c'); @@ -306,6 +306,16 @@ test('should respect project filter C', async ({ runWatchTest }) => { await testProcess.waitForOutput('npx playwright test --project foo #1'); await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); expect(testProcess.output).not.toContain('[bar] › a.test.ts:3:11 › passes'); + + testProcess.clearOutput(); + + await writeFiles(files); // file change triggers listTests with project filter + await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes'); + + testProcess.write('c'); + await testProcess.waitForOutput('Select projects'); + await testProcess.waitForOutput('foo'); + await testProcess.waitForOutput('bar'); // second selection should still show all }); test('should respect file filter P and split files', async ({ runWatchTest }) => { From ed303208b3750ac9662894fbeb5d655ae21b3f9c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 6 Sep 2024 14:27:56 +0200 Subject: [PATCH 367/376] test: update to android-35 SDK (Android 15) (#32430) --- tests/android/webview.spec.ts | 4 ++++ tests/page/page-request-intercept.spec.ts | 3 ++- utils/avd_install.sh | 2 +- utils/avd_recreate.sh | 6 +++--- utils/avd_start.sh | 6 +++--- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/android/webview.spec.ts b/tests/android/webview.spec.ts index 46e0e385a0..ca7a114bb1 100644 --- a/tests/android/webview.spec.ts +++ b/tests/android/webview.spec.ts @@ -16,6 +16,10 @@ import { androidTest as test, expect } from './androidTest'; +test.beforeEach(async ({ androidDevice }) => { + await androidDevice.shell('am force-stop com.google.android.googlequicksearchbox'); +}); + test.afterEach(async ({ androidDevice }) => { await androidDevice.shell('am force-stop org.chromium.webview_shell'); await androidDevice.shell('am force-stop com.android.chrome'); diff --git a/tests/page/page-request-intercept.spec.ts b/tests/page/page-request-intercept.spec.ts index 33516606e7..eb9baae47e 100644 --- a/tests/page/page-request-intercept.spec.ts +++ b/tests/page/page-request-intercept.spec.ts @@ -284,8 +284,9 @@ it('should fulfill popup main request using alias', async ({ page, server, isEle it('request.postData is not null when fetching FormData with a Blob', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/24077' } -}, async ({ server, page, browserName, isElectron, electronMajorVersion }) => { +}, async ({ server, page, browserName, isElectron, electronMajorVersion, isAndroid }) => { it.skip(isElectron && electronMajorVersion < 31); + it.fixme(isAndroid, 'postData is null for some reason'); it.fixme(browserName === 'webkit', 'The body is empty in WebKit when intercepting'); await page.goto(server.EMPTY_PAGE); await page.setContent(` diff --git a/utils/avd_install.sh b/utils/avd_install.sh index 6beb8bf43f..a0586d6936 100755 --- a/utils/avd_install.sh +++ b/utils/avd_install.sh @@ -24,7 +24,7 @@ echo Installing emulator... yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install platform-tools emulator echo Installing platform SDK... -yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-33" +yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-35" echo Starting ADB... ${ANDROID_HOME}/platform-tools/adb devices diff --git a/utils/avd_recreate.sh b/utils/avd_recreate.sh index 0877e76ed1..81c3484907 100755 --- a/utils/avd_recreate.sh +++ b/utils/avd_recreate.sh @@ -13,7 +13,7 @@ if [[ "$(uname -m)" == "arm64" ]]; then ANDROID_ARCH="arm64-v8a" fi -${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager delete avd --name android33 || true -yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "system-images;android-33;google_apis;$ANDROID_ARCH" platform-tools emulator -echo "no" | ${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager create avd --force --name android33 --device "Nexus 5X" --package "system-images;android-33;google_apis;$ANDROID_ARCH" +${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager delete avd --name android35 || true +yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "system-images;android-35;google_apis;$ANDROID_ARCH" platform-tools emulator +echo "no" | ${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager create avd --force --name android35 --device "Nexus 5X" --package "system-images;android-35;google_apis;$ANDROID_ARCH" ${ANDROID_HOME}/emulator/emulator -list-avds diff --git a/utils/avd_start.sh b/utils/avd_start.sh index 67acb1c63e..1c3bc53418 100755 --- a/utils/avd_start.sh +++ b/utils/avd_start.sh @@ -9,15 +9,15 @@ fi bash $PWD/utils/avd_stop.sh echo "Starting emulator" -# nohup ${ANDROID_HOME}/emulator/emulator -avd android33 -gpu swiftshader & -nohup ${ANDROID_HOME}/emulator/emulator -avd android33 -no-audio -no-window -no-boot-anim -no-snapshot & +# nohup ${ANDROID_HOME}/emulator/emulator -avd android35 -gpu swiftshader & +nohup ${ANDROID_HOME}/emulator/emulator -avd android35 -no-audio -no-window -no-boot-anim -no-snapshot & ${ANDROID_HOME}/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' ${ANDROID_HOME}/platform-tools/adb devices echo "Emulator started" echo "Installing Chromium WebView" # See here for the latest revision: https://storage.googleapis.com/chromium-browser-snapshots/Android/LAST_CHANGE -CHROMIUM_ANDROID_REVISION="1190572" +CHROMIUM_ANDROID_REVISION="1340145" WEBVIEW_TMP_DIR="$(mktemp -d)" WEBVIEW_TMP_FILE="$WEBVIEW_TMP_DIR/chrome-android-zip" curl -s -o "${WEBVIEW_TMP_FILE}" "https://storage.googleapis.com/chromium-browser-snapshots/Android/${CHROMIUM_ANDROID_REVISION}/chrome-android.zip" From 3fe1263643b3b8590c63b0f4df677d174a626bc8 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 6 Sep 2024 16:24:33 +0200 Subject: [PATCH 368/376] feat(trace viewer): show Screenshot instead of Snapshot (#32248) Closes https://github.com/microsoft/playwright/issues/23964. Trace snapshots are a best-effort snapshots of the browser DOM, but we can't guarantee them to be exactly what the browser showed. One example of this is `canvas` elements, where you just can't see their contents. That makes snapshots useful, but not perfect. For those cases where the snapshot doesn't show everything, this PR introduces a new setting to show a screenshot instead. You won't be able to scroll or inspect the DOM or select a locator anymore. But if the snapshot was missing something, or displaying something wrong, you can now check the screenshot instead. --- packages/trace-viewer/src/entries.ts | 1 + packages/trace-viewer/src/snapshotServer.ts | 3 +- packages/trace-viewer/src/traceModernizer.ts | 1 + packages/trace-viewer/src/ui/inspectorTab.tsx | 6 ++- packages/trace-viewer/src/ui/modelUtil.ts | 6 +++ packages/trace-viewer/src/ui/snapshotTab.tsx | 38 +++++++++++++++---- packages/trace-viewer/src/ui/uiModeView.tsx | 3 ++ packages/trace-viewer/src/ui/workbench.tsx | 8 +++- packages/web/src/uiUtils.ts | 2 +- tests/config/traceViewerFixtures.ts | 6 +-- tests/library/trace-viewer.spec.ts | 30 +++++++++++++++ 11 files changed, 88 insertions(+), 16 deletions(-) diff --git a/packages/trace-viewer/src/entries.ts b/packages/trace-viewer/src/entries.ts index bca5751ab2..98d39927f7 100644 --- a/packages/trace-viewer/src/entries.ts +++ b/packages/trace-viewer/src/entries.ts @@ -41,6 +41,7 @@ export type ContextEntry = { }; export type PageEntry = { + pageId: string, screencastFrames: { sha1: string, timestamp: number, diff --git a/packages/trace-viewer/src/snapshotServer.ts b/packages/trace-viewer/src/snapshotServer.ts index b1dd371cb3..d41dcbdbbd 100644 --- a/packages/trace-viewer/src/snapshotServer.ts +++ b/packages/trace-viewer/src/snapshotServer.ts @@ -44,7 +44,8 @@ export class SnapshotServer { const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams); return this._respondWithJson(snapshot ? { viewport: snapshot.viewport(), - url: snapshot.snapshot().frameUrl + url: snapshot.snapshot().frameUrl, + timestamp: snapshot.snapshot().timestamp, } : { error: 'No snapshot found' }); diff --git a/packages/trace-viewer/src/traceModernizer.ts b/packages/trace-viewer/src/traceModernizer.ts index b15ad527b0..7c6ff19ce6 100644 --- a/packages/trace-viewer/src/traceModernizer.ts +++ b/packages/trace-viewer/src/traceModernizer.ts @@ -58,6 +58,7 @@ export class TraceModernizer { let pageEntry = this._pageEntries.get(pageId); if (!pageEntry) { pageEntry = { + pageId, screencastFrames: [], }; this._pageEntries.set(pageId, pageEntry); diff --git a/packages/trace-viewer/src/ui/inspectorTab.tsx b/packages/trace-viewer/src/ui/inspectorTab.tsx index 7b3a90b709..ba37cf1bd1 100644 --- a/packages/trace-viewer/src/ui/inspectorTab.tsx +++ b/packages/trace-viewer/src/ui/inspectorTab.tsx @@ -17,7 +17,7 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import type { Language } from '@web/components/codeMirrorWrapper'; import { ToolbarButton } from '@web/components/toolbarButton'; -import { copy } from '@web/uiUtils'; +import { copy, useSetting } from '@web/uiUtils'; import * as React from 'react'; import './sourceTab.css'; @@ -27,10 +27,12 @@ export const InspectorTab: React.FunctionComponent<{ highlightedLocator: string, setHighlightedLocator: (locator: string) => void, }> = ({ sdkLanguage, setIsInspecting, highlightedLocator, setHighlightedLocator }) => { + const [showScreenshot] = useSetting('screenshot-instead-of-snapshot', false); + return
Locator
- { + { // Updating text needs to go first - react can squeeze a render between the state updates. setHighlightedLocator(text); setIsInspecting(false); diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index c82cbc99a6..0c22d954f4 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -22,6 +22,7 @@ import type { ContextEntry, PageEntry } from '../entries'; import type { StackFrame } from '@protocol/channels'; const contextSymbol = Symbol('context'); +const pageSymbol = Symbol('page'); const nextInContextSymbol = Symbol('next'); const prevInListSymbol = Symbol('prev'); const eventsSymbol = Symbol('events'); @@ -148,6 +149,7 @@ function indexModel(context: ContextEntry) { for (let i = 0; i < context.actions.length; ++i) { const action = context.actions[i] as any; action[contextSymbol] = context; + action[pageSymbol] = context.pages.find(page => page.pageId === action.pageId); } let lastNonRouteAction = undefined; for (let i = context.actions.length - 1; i >= 0; i--) { @@ -356,6 +358,10 @@ export function prevInList(action: ActionTraceEvent): ActionTraceEvent { return (action as any)[prevInListSymbol]; } +export function pageForAction(action: ActionTraceEvent): PageEntry { + return (action as any)[pageSymbol]; +} + export function stats(action: ActionTraceEvent): { errors: number, warnings: number } { let errors = 0; let warnings = 0; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 4faa668677..229d9f8cef 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -17,10 +17,10 @@ import './snapshotTab.css'; import * as React from 'react'; import type { ActionTraceEvent } from '@trace/trace'; -import { context, prevInList } from './modelUtil'; +import { context, type MultiTraceModel, pageForAction, prevInList } from './modelUtil'; import { Toolbar } from '@web/components/toolbar'; import { ToolbarButton } from '@web/components/toolbarButton'; -import { clsx, useMeasure } from '@web/uiUtils'; +import { clsx, useMeasure, useSetting } from '@web/uiUtils'; import { InjectedScript } from '@injected/injectedScript'; import { Recorder } from '@injected/recorder/recorder'; import ConsoleAPI from '@injected/consoleApi'; @@ -30,8 +30,18 @@ import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser'; import { TabbedPaneTab } from '@web/components/tabbedPane'; import { BrowserFrame } from './browserFrame'; +function findClosest(items: T[], target: number) { + return items.find((item, index) => { + if (index === items.length - 1) + return true; + const next = items[index + 1]; + return Math.abs(item.timestamp - target) < Math.abs(next.timestamp - target); + }); +} + export const SnapshotTab: React.FunctionComponent<{ action: ActionTraceEvent | undefined, + model?: MultiTraceModel, sdkLanguage: Language, testIdAttributeName: string, isInspecting: boolean, @@ -39,9 +49,10 @@ export const SnapshotTab: React.FunctionComponent<{ highlightedLocator: string, setHighlightedLocator: (locator: string) => void, openPage?: (url: string, target?: string) => Window | any, -}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { +}> = ({ action, model, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { const [measure, ref] = useMeasure(); const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); + const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false); type Snapshot = { action: ActionTraceEvent, snapshotName: string, point?: { x: number, y: number } }; const { snapshots } = React.useMemo(() => { @@ -90,7 +101,7 @@ export const SnapshotTab: React.FunctionComponent<{ const iframeRef0 = React.useRef(null); const iframeRef1 = React.useRef(null); - const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' }); + const [snapshotInfo, setSnapshotInfo] = React.useState<{ viewport: typeof kDefaultViewport, url: string, timestamp?: number }>({ viewport: kDefaultViewport, url: '', timestamp: undefined }); const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 }); React.useEffect(() => { @@ -99,13 +110,14 @@ export const SnapshotTab: React.FunctionComponent<{ const newVisibleIframe = 1 - loadingRef.current.visibleIframe; loadingRef.current.iteration = thisIteration; - const newSnapshotInfo = { url: '', viewport: kDefaultViewport }; + const newSnapshotInfo = { url: '', viewport: kDefaultViewport, timestamp: undefined }; if (snapshotInfoUrl) { const response = await fetch(snapshotInfoUrl); const info = await response.json(); if (!info.error) { newSnapshotInfo.url = info.url; newSnapshotInfo.viewport = info.viewport; + newSnapshotInfo.timestamp = info.timestamp; } } @@ -154,6 +166,15 @@ export const SnapshotTab: React.FunctionComponent<{ y: (measure.height - snapshotContainerSize.height) / 2, }; + const page = action ? pageForAction(action) : undefined; + const screencastFrame = React.useMemo( + () => { + if (snapshotInfo.timestamp && page?.screencastFrames) + return findClosest(page.screencastFrames, snapshotInfo.timestamp); + }, + [page?.screencastFrames, snapshotInfo.timestamp] + ); + return
- setIsInspecting(!isInspecting)} /> + setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} /> {['action', 'before', 'after'].map(tab => { return ; })}
- { + { if (!openPage) openPage = window.open; const win = openPage(popoutUrl || '', '_blank'); @@ -209,7 +230,8 @@ export const SnapshotTab: React.FunctionComponent<{ transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`, }}> -
+ {(showScreenshotInsteadOfSnapshot && screencastFrame) && {`Screenshot ${renderTitle(snapshotTab)}`} src={`sha1/${screencastFrame.sha1}`} width={screencastFrame.width} height={screencastFrame.height} />} +
diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 86e6bff80e..cad1c2eb58 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -107,6 +107,8 @@ export const UIModeView: React.FC<{}> = ({ const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all'); const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); const [darkMode, setDarkMode] = useDarkModeSetting(); + const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); + const inputRef = React.useRef(null); @@ -517,6 +519,7 @@ export const UIModeView: React.FC<{}> = ({ {settingsVisible && }
} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 491a8eac16..3565eb3850 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -73,6 +73,8 @@ export const Workbench: React.FunctionComponent<{ const [selectedTime, setSelectedTime] = React.useState(); const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); + const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); + const filteredActions = React.useMemo(() => { return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action)); @@ -299,7 +301,10 @@ export const Workbench: React.FunctionComponent<{ const settingsTab: TabbedPaneTabModel = { id: 'settings', title: 'Settings', - component: , + component: , }; return
@@ -325,6 +330,7 @@ export const Workbench: React.FunctionComponent<{ settingName='actionListSidebar' main={(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch>] { +export function useSetting(name: string | undefined, defaultValue: S): [S, React.Dispatch>] { if (name) defaultValue = settings.getObject(name, defaultValue); const [value, setValue] = React.useState(defaultValue); diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 7044e30a29..3b79a55e98 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -31,7 +31,7 @@ type BaseWorkerFixtures = { export type TraceViewerFixtures = { showTraceViewer: (trace: string[], options?: {host?: string, port?: number}) => Promise; - runAndTrace: (body: () => Promise) => Promise; + runAndTrace: (body: () => Promise, optsOverrides?: Parameters[0]) => Promise; }; class TraceViewerPage { @@ -127,9 +127,9 @@ export const traceViewerFixtures: Fixtures { - await use(async (body: () => Promise) => { + await use(async (body: () => Promise, optsOverrides = {}) => { const traceFile = testInfo.outputPath('trace.zip'); - await context.tracing.start({ snapshots: true, screenshots: true, sources: true }); + await context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides }); await body(); await context.tracing.stop({ path: traceFile }); return showTraceViewer([traceFile]); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 4ebffc1f42..f8aaf9d0df 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1465,3 +1465,33 @@ test('should serve css without content-type', async ({ page, runAndTrace, server const snapshotFrame = await traceViewer.snapshotFrame('page.goto'); await expect(snapshotFrame.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)', { timeout: 0 }); }); + +test('should allow showing screenshots instead of snapshots', async ({ runAndTrace, page, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.PREFIX + '/one-style.html'); + }); + + const screenshot = traceViewer.page.getByAltText(`Screenshot of page.goto > Action`); + const snapshot = (await traceViewer.snapshotFrame('page.goto')).owner(); + await expect(snapshot).toBeVisible(); + await expect(screenshot).not.toBeVisible(); + + await traceViewer.page.getByTitle('Settings').click(); + await traceViewer.page.getByText('Show screenshot instead of snapshot').setChecked(true); + + await expect(snapshot).not.toBeVisible(); + await expect(screenshot).toBeVisible(); +}); + +test('should handle case where neither snapshots nor screenshots exist', async ({ runAndTrace, page, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.PREFIX + '/one-style.html'); + }, { snapshots: false, screenshots: false }); + + await traceViewer.page.getByTitle('Settings').click(); + await traceViewer.page.getByText('Show screenshot instead of snapshot').setChecked(true); + + const screenshot = traceViewer.page.getByAltText(`Screenshot of page.goto > Action`); + await expect(screenshot).not.toBeVisible(); +}); + From 1402dee9e6589d0d01130809e9114048d23d2fc9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 6 Sep 2024 12:08:10 -0700 Subject: [PATCH 369/376] Revert "fix(test runner): align with typescript behaviour for resolving `index.js` and `package.json` through path mapping (#32078)" (#32492) This reverts commit effb1ae2349aeef1c0bfb0b6cd886b9b4b32c8e5. This broke path mapping into directories in ESM mode. References #32480. --- packages/playwright-ct-core/src/vitePlugin.ts | 2 +- packages/playwright-ct-core/src/viteUtils.ts | 6 +- .../playwright/src/transform/esmLoader.ts | 2 +- .../playwright/src/transform/transform.ts | 16 +-- packages/playwright/src/util.ts | 19 +-- tests/playwright-test/resolver.spec.ts | 135 ------------------ 6 files changed, 14 insertions(+), 166 deletions(-) diff --git a/packages/playwright-ct-core/src/vitePlugin.ts b/packages/playwright-ct-core/src/vitePlugin.ts index 462730c61c..ec738c8700 100644 --- a/packages/playwright-ct-core/src/vitePlugin.ts +++ b/packages/playwright-ct-core/src/vitePlugin.ts @@ -262,7 +262,7 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil async writeBundle(this: PluginContext) { for (const importInfo of importInfos.values()) { - const importPath = resolveHook(importInfo.filename, importInfo.importSource, true); + const importPath = resolveHook(importInfo.filename, importInfo.importSource); if (!importPath) continue; const deps = new Set(); diff --git a/packages/playwright-ct-core/src/viteUtils.ts b/packages/playwright-ct-core/src/viteUtils.ts index d1160c16f0..a9f7020999 100644 --- a/packages/playwright-ct-core/src/viteUtils.ts +++ b/packages/playwright-ct-core/src/viteUtils.ts @@ -147,13 +147,13 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe for (const importInfo of importList) componentRegistry.set(importInfo.id, importInfo); if (componentsByImportingFile) - componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource, true)).filter(Boolean) as string[]); + componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource)).filter(Boolean) as string[]); } } export function hasJSComponents(components: ImportInfo[]): boolean { for (const component of components) { - const importPath = resolveHook(component.filename, component.importSource, true); + const importPath = resolveHook(component.filename, component.importSource); const extname = importPath ? path.extname(importPath) : ''; if (extname === '.js' || (importPath && !extname && fs.existsSync(importPath + '.js'))) return true; @@ -183,7 +183,7 @@ export function transformIndexFile(id: string, content: string, templateDir: str lines.push(registerSource); for (const value of importInfos.values()) { - const importPath = resolveHook(value.filename, value.importSource, true) || value.importSource; + const importPath = resolveHook(value.filename, value.importSource) || value.importSource; lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`); } diff --git a/packages/playwright/src/transform/esmLoader.ts b/packages/playwright/src/transform/esmLoader.ts index 0ef0f29eaf..c84d15146b 100644 --- a/packages/playwright/src/transform/esmLoader.ts +++ b/packages/playwright/src/transform/esmLoader.ts @@ -26,7 +26,7 @@ import { fileIsModule } from '../util'; async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) { if (context.parentURL && context.parentURL.startsWith('file://')) { const filename = url.fileURLToPath(context.parentURL); - const resolved = resolveHook(filename, specifier, true); + const resolved = resolveHook(filename, specifier); if (resolved !== undefined) specifier = url.pathToFileURL(resolved).toString(); } diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts index 52d05dde90..9ca80399fb 100644 --- a/packages/playwright/src/transform/transform.ts +++ b/packages/playwright/src/transform/transform.ts @@ -129,21 +129,15 @@ function loadAndValidateTsconfigsForFolder(folder: string): ParsedTsConfigData[] const pathSeparator = process.platform === 'win32' ? ';' : ':'; const builtins = new Set(Module.builtinModules); -export function resolveHook(filename: string, specifier: string, isESM: boolean): string | undefined { +export function resolveHook(filename: string, specifier: string): string | undefined { if (specifier.startsWith('node:') || builtins.has(specifier)) return; if (!shouldTransform(filename)) return; if (isRelativeSpecifier(specifier)) - return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier), false, isESM); + return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier)); - /** - * TypeScript discourages path-mapping into node_modules - * (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). - * It seems like TypeScript tries path-mapping first, but does not look at the `package.json` or `index.js` files in ESM. - * If path-mapping doesn't yield a result, TypeScript falls back to the default resolution (typically node_modules). - */ const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); const tsconfigs = loadAndValidateTsconfigsForFile(filename); for (const tsconfig of tsconfigs) { @@ -185,7 +179,7 @@ export function resolveHook(filename: string, specifier: string, isESM: boolean) if (value.includes('*')) candidate = candidate.replace('*', matchedPartOfSpecifier); candidate = path.resolve(tsconfig.pathsBase!, candidate); - const existing = resolveImportSpecifierExtension(candidate, true, isESM); + const existing = resolveImportSpecifierExtension(candidate); if (existing) { longestPrefixLength = keyPrefix.length; pathMatchedByLongestPrefix = existing; @@ -199,7 +193,7 @@ export function resolveHook(filename: string, specifier: string, isESM: boolean) if (path.isAbsolute(specifier)) { // Handle absolute file paths like `import '/path/to/file'` // Do not handle module imports like `import 'fs'` - return resolveImportSpecifierExtension(specifier, false, isESM); + return resolveImportSpecifierExtension(specifier); } } @@ -281,7 +275,7 @@ function installTransformIfNeeded() { const originalResolveFilename = (Module as any)._resolveFilename; function resolveFilename(this: any, specifier: string, parent: Module, ...rest: any[]) { if (parent) { - const resolved = resolveHook(parent.filename, specifier, false); + const resolved = resolveHook(parent.filename, specifier); if (resolved !== undefined) specifier = resolved; } diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 4c75d9d841..0531f167a0 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -316,7 +316,7 @@ const kExtLookups = new Map([ ['.mjs', ['.mts']], ['', ['.js', '.ts', '.jsx', '.tsx', '.cjs', '.mjs', '.cts', '.mts']], ]); -export function resolveImportSpecifierExtension(resolved: string, isPathMapping: boolean, isESM: boolean): string | undefined { +export function resolveImportSpecifierExtension(resolved: string): string | undefined { if (fileExists(resolved)) return resolved; @@ -331,25 +331,14 @@ export function resolveImportSpecifierExtension(resolved: string, isPathMapping: break; // Do not try '' when a more specific extension like '.jsx' matched. } - // After TypeScript path mapping, here's how directories with a `package.json` are resolved: - // - `package.json#exports` is not respected - // - `package.json#main` is respected only in CJS mode - // - `index.js` default is respected only in CJS mode - // - // More info: - // - https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages - // - https://www.typescriptlang.org/docs/handbook/modules/reference.html#directory-modules-index-file-resolution - // - https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#folders-as-modules - - const shouldNotResolveDirectory = isPathMapping && isESM; - - if (!shouldNotResolveDirectory && dirExists(resolved)) { + if (dirExists(resolved)) { // If we import a package, let Node.js figure out the correct import based on package.json. if (fileExists(path.join(resolved, 'package.json'))) return resolved; + // Otherwise, try to find a corresponding index file. const dirImport = path.join(resolved, 'index'); - return resolveImportSpecifierExtension(dirImport, isPathMapping, isESM); + return resolveImportSpecifierExtension(dirImport); } } diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index b87e22babb..5a0e91b099 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -606,79 +606,6 @@ test('should import packages with non-index main script through path resolver', expect(result.output).toContain(`foo=42`); }); -test('should not honor `package.json#main` field in ESM mode', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'app/pkg/main.ts': ` - export const foo = 42; - `, - 'app/pkg/package.json': ` - { "main": "main.ts" } - `, - 'package.json': ` - { "name": "example-project", "type": "module" } - `, - 'playwright.config.ts': ` - export default {}; - `, - 'tsconfig.json': `{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "app/*": ["app/*"], - }, - }, - }`, - 'example.spec.ts': ` - import { foo } from 'app/pkg'; - import { test, expect } from '@playwright/test'; - test('test', ({}) => { - console.log('foo=' + foo); - }); - `, - }); - - expect(result.exitCode).toBe(1); - expect(result.output).toContain(`Cannot find package 'app'`); -}); - - -test('does not honor `exports` field after type mapping', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'app/pkg/main.ts': ` - export const filename = 'main.ts'; - `, - 'app/pkg/index.js': ` - export const filename = 'index.js'; - `, - 'app/pkg/package.json': JSON.stringify({ - exports: { '.': { require: './main.ts' } } - }), - 'package.json': JSON.stringify({ - name: 'example-project' - }), - 'playwright.config.ts': ` - export default {}; - `, - 'tsconfig.json': JSON.stringify({ - compilerOptions: { - baseUrl: '.', - paths: { - 'app/*': ['app/*'], - }, - } - }), - 'example.spec.ts': ` - import { filename } from 'app/pkg'; - import { test, expect } from '@playwright/test'; - test('test', ({}) => { - console.log('filename=' + filename); - }); - `, - }); - - expect(result.output).toContain('filename=index.js'); -}); - test('should respect tsconfig project references', async ({ runInlineTest }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29256' }); @@ -766,65 +693,3 @@ test('should respect --tsconfig option', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.output).not.toContain(`Could not`); }); - - -test('should resolve index.js in CJS after path mapping', async ({ runInlineTest }) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31811' }); - - const result = await runInlineTest({ - '@acme/lib/index.js': ` - exports.greet = () => console.log('hello playwright'); - `, - '@acme/lib/index.d.ts': ` - export const greet: () => void; - `, - 'tests/hello.test.ts': ` - import { greet } from '@acme/lib'; - import { test } from '@playwright/test'; - test('hello', async ({}) => { - greet(); - }); - `, - 'tests/tsconfig.json': JSON.stringify({ - compilerOptions: { - 'paths': { - '@acme/*': ['../@acme/*'], - } - } - }) - }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should not resolve index.js in ESM after path mapping', async ({ runInlineTest }) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31811' }); - - const result = await runInlineTest({ - '@acme/lib/index.js': ` - export const greet = () => console.log('hello playwright'); - `, - '@acme/lib/index.d.ts': ` - export const greet: () => void; - `, - 'tests/hello.test.ts': ` - import { greet } from '@acme/lib'; - import { test } from '@playwright/test'; - test('hello', async ({}) => { - greet(); - }); - `, - 'tests/tsconfig.json': JSON.stringify({ - compilerOptions: { - 'paths': { - '@acme/*': ['../@acme/*'], - } - } - }), - 'package.json': JSON.stringify({ type: 'module' }), - }); - - expect(result.exitCode).toBe(1); - expect(result.output).toContain(`Cannot find package '@acme/lib'`); -}); From d85527e9f63b0bc067d0209ed29ec4a5cb4a774c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 13:13:44 -0700 Subject: [PATCH 370/376] test: some tests for expected API behavior (#32495) Adding some tests discussed in https://github.com/microsoft/playwright/pull/32434 --- tests/library/chromium/oopif.spec.ts | 10 ++++++++++ tests/page/frame-frame-element.spec.ts | 16 +++++++++++++++ tests/page/interception.spec.ts | 19 +++++++++++++++++- tests/page/page-mouse.spec.ts | 27 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/library/chromium/oopif.spec.ts b/tests/library/chromium/oopif.spec.ts index cc1279b8af..0143120629 100644 --- a/tests/library/chromium/oopif.spec.ts +++ b/tests/library/chromium/oopif.spec.ts @@ -297,6 +297,16 @@ it('should click', async function({ page, browser, server }) { expect(await handle1.evaluate(() => (window as any)['_clicked'])).toBe(true); }); +it('contentFrame should work', async ({ page, browser, server }) => { + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(page.frames().length).toBe(2); + expect(await countOOPIFs(browser)).toBe(1); + expect(await page.locator('iframe').contentFrame().locator('div').count()).toBe(200); + const oopif = await page.$('iframe'); + const content = await oopif.contentFrame(); + expect(await content.locator('div').count()).toBe(200); +}); + it('should allow cdp sessions on oopifs', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); diff --git a/tests/page/frame-frame-element.spec.ts b/tests/page/frame-frame-element.spec.ts index e3b25a8fe1..e44e7deb3e 100644 --- a/tests/page/frame-frame-element.spec.ts +++ b/tests/page/frame-frame-element.spec.ts @@ -75,3 +75,19 @@ it('should work inside closed shadow root', async ({ page, server, browserName } const element = await frame.frameElement(); expect(await element.getAttribute('name')).toBe('myframe'); }); + +it('should work inside declarative shadow root', async ({ page, server, browserName }) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` +
+ + footer +
+ `); + const frame = page.frame({ name: 'myframe' }); + const element = await frame.frameElement(); + expect(await element.getAttribute('name')).toBe('myframe'); +}); diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index fd798cb715..7f3ef98000 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -164,7 +164,7 @@ it('should work with regular expression passed from a different context', async expect(intercepted).toBe(true); }); -it('should not break remote worker importScripts', async ({ page, server, browserName, browserMajorVersion }) => { +it('should not break remote worker importScripts', async ({ page, server }) => { await page.route('**', async route => { await route.continue(); }); @@ -189,3 +189,20 @@ it('should disable memory cache when intercepting', async ({ page, server }) => await expect(page).toHaveURL(server.PREFIX + '/page.html'); expect(interceted).toBe(2); }); + +it('should intercept blob url requests', async function({ page, server, browserName }) { + it.fixme(browserName !== 'webkit'); + await page.goto(server.EMPTY_PAGE); + await page.route('**/*', route => { + route.fulfill({ + status: 200, + body: 'intercepted', + }).catch(e => null); + }); + page.on('console', msg => console.log(msg.text())); + const response = await page.evaluate(async () => { + const blobUrl = URL.createObjectURL(new Blob(['failed to intercept'], { type: 'text/plain' })); + return await fetch(blobUrl).then(response => response.text()); + }); + expect(response).toBe('intercepted'); +}); diff --git a/tests/page/page-mouse.spec.ts b/tests/page/page-mouse.spec.ts index 3e7223319f..5d29be2bb7 100644 --- a/tests/page/page-mouse.spec.ts +++ b/tests/page/page-mouse.spec.ts @@ -78,6 +78,33 @@ it('should dblclick the div', async ({ page, server }) => { expect(event.button).toBe(0); }); +it('down and up should generate click', async ({ page, server }) => { + await page.evaluate(() => { + window['clickPromise'] = new Promise(resolve => { + document.addEventListener('click', event => { + resolve({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button + }); + }); + }); + }); + await page.mouse.move(50, 60); + await page.mouse.down(); + await page.mouse.up(); + const event = await page.evaluate(() => window['clickPromise']); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); +}); + it('should pointerdown the div with a custom button', async ({ page, server, browserName }) => { await page.setContent(`
Click me
`); await page.evaluate(() => { From 11441c0fe1407b2448ad1a8b806b06a7e0b7a532 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 13:17:32 -0700 Subject: [PATCH 371/376] fix: add missing await in adoptIfNeeded (#32497) Otherwise it throws in Bidi. --- packages/playwright-core/src/server/frameSelectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 66f2e2e514..4be2a9c285 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -160,7 +160,7 @@ export class FrameSelectors { async function adoptIfNeeded(handle: ElementHandle, context: FrameExecutionContext): Promise> { if (handle._context === context) return handle; - const adopted = handle._page._delegate.adoptElementHandle(handle, context); + const adopted = await handle._page._delegate.adoptElementHandle(handle, context); handle.dispose(); return adopted; } From df2bc2d0dc59017493e9f2733aab3e6c9c309f01 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 13:17:49 -0700 Subject: [PATCH 372/376] test: worker interception for existing workers (#32494) Failing test for https://github.com/microsoft/playwright/issues/32355 --- tests/page/interception.spec.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 7f3ef98000..338ac97a05 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -100,8 +100,7 @@ it('should work with glob', async () => { expect(globToRegex('$^+.\\*()|\\?\\{\\}\\[\\]')).toEqual(/^\$\^\+\.\*\(\)\|\?\{\}\[\]$/); }); -it('should intercept network activity from worker', async function({ page, server, isAndroid, browserName, browserMajorVersion }) { - it.skip(browserName === 'firefox' && browserMajorVersion < 114, 'https://github.com/microsoft/playwright/issues/21760'); +it('should intercept network activity from worker', async function({ page, server, isAndroid }) { it.skip(isAndroid); await page.goto(server.EMPTY_PAGE); @@ -122,6 +121,35 @@ it('should intercept network activity from worker', async function({ page, serve expect(msg.text()).toBe('intercepted'); }); +it('should intercept worker requests when enabled after worker creation', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32355' } +}, async function({ page, server, isAndroid, browserName }) { + it.skip(isAndroid); + it.fixme(browserName === 'chromium'); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept')); + const url = server.PREFIX + '/data_for_worker'; + await page.evaluate(url => { + (window as any).w = new Worker(URL.createObjectURL(new Blob([` + onmessage = function(e) { + fetch("${url}").then(response => response.text()).then(console.log); + }; + `], { type: 'application/javascript' }))); + }, url); + await page.route(url, route => { + route.fulfill({ + status: 200, + body: 'intercepted', + }).catch(e => null); + }); + const [msg] = await Promise.all([ + page.waitForEvent('console'), + page.evaluate(() => (window as any).w.postMessage('')) + ]); + expect(msg.text()).toBe('intercepted'); +}); + it('should intercept network activity from worker 2', async function({ page, server, isAndroid }) { it.skip(isAndroid); From a113553f1468b5c8d14e85cd87dd018fda5cb49f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 13:49:37 -0700 Subject: [PATCH 373/376] test: allow running oopif test without newBrowserCDPSession (#32496) --- tests/bidi/playwright.config.ts | 1 - tests/library/chromium/oopif.spec.ts | 58 +++++++++++++++------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index 7aa17f35ba..7adaa56638 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -81,7 +81,6 @@ for (const [key, channels] of Object.entries(browserToChannels)) { channel, video: 'off', launchOptions: { - channel: 'bidi-chrome-canary', executablePath, }, trace: trace ? 'on' : undefined, diff --git a/tests/library/chromium/oopif.spec.ts b/tests/library/chromium/oopif.spec.ts index 0143120629..bea16bdb75 100644 --- a/tests/library/chromium/oopif.spec.ts +++ b/tests/library/chromium/oopif.spec.ts @@ -25,14 +25,14 @@ it.use({ it('should report oopif frames', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); }); it('should handle oopif detach', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); const frame = page.frames()[1]; expect(await frame.evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); @@ -46,20 +46,20 @@ it('should handle oopif detach', async function({ page, browser, server }) { it('should handle remote -> local -> remote transitions', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); await Promise.all([ page.frames()[1].waitForNavigation(), page.evaluate('goLocal()'), ]); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.PREFIX + '/grid.html'); - expect(await countOOPIFs(browser)).toBe(0); + await assertOOPIFCount(browser, 0); await Promise.all([ page.frames()[1].waitForNavigation(), page.evaluate('goRemote()'), ]); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); }); it('should get the proper viewport', async ({ page, browser, server }) => { @@ -68,7 +68,7 @@ it('should get the proper viewport', async ({ page, browser, server }) => { expect(page.viewportSize()).toEqual({ width: 1280, height: 720 }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const oopif = page.frames()[1]; expect(await oopif.evaluate(() => screen.width)).toBe(1280); expect(await oopif.evaluate(() => screen.height)).toBe(720); @@ -86,7 +86,7 @@ it('should get the proper viewport', async ({ page, browser, server }) => { it('should expose function', async ({ page, browser, server }) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const oopif = page.frames()[1]; await page.exposeFunction('mul', (a: number, b: number) => a * b); const result = await oopif.evaluate(async function() { @@ -98,7 +98,7 @@ it('should expose function', async ({ page, browser, server }) => { it('should emulate media', async ({ page, browser, server }) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const oopif = page.frames()[1]; expect(await oopif.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false); await page.emulateMedia({ colorScheme: 'dark' }); @@ -108,7 +108,7 @@ it('should emulate media', async ({ page, browser, server }) => { it('should emulate offline', async ({ page, browser, server }) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const oopif = page.frames()[1]; expect(await oopif.evaluate(() => navigator.onLine)).toBe(true); await page.context().setOffline(true); @@ -125,7 +125,7 @@ it('should support context options', async ({ browser, server, playwright }) => page.goto(server.PREFIX + '/dynamic-oopif.html'), ]); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const oopif = page.frames()[1]; expect(await oopif.evaluate(() => 'ontouchstart' in window)).toBe(true); @@ -145,7 +145,7 @@ it('should respect route', async ({ page, browser, server }) => { }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(intercepted).toBe(true); }); @@ -153,14 +153,14 @@ it('should take screenshot', async ({ page, browser, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(await page.screenshot()).toMatchSnapshot('screenshot-oopif.png'); }); it('should load oopif iframes with subresources and route', async function({ page, browser, server }) { await page.route('**/*', route => route.continue()); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); }); it('should report main requests', async function({ page, browser, server }) { @@ -192,7 +192,7 @@ it('should report main requests', async function({ page, browser, server }) { const grandChild = child.childFrames()[0]; await grandChild.waitForLoadState('domcontentloaded'); - expect(await countOOPIFs(browser)).toBe(2); + await assertOOPIFCount(browser, 2); expect(requestFrames[0]).toBe(main); expect(finishedFrames[0]).toBe(main); expect(requestFrames[1]).toBe(child); @@ -205,7 +205,7 @@ it('should support exposeFunction', async function({ page, browser, server }) { await page.context().exposeFunction('dec', (a: number) => a - 1); await page.exposeFunction('inc', (a: number) => a + 1); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); expect(await page.frames()[0].evaluate(() => (window as any)['inc'](3))).toBe(4); expect(await page.frames()[1].evaluate(() => (window as any)['inc'](4))).toBe(5); @@ -217,7 +217,7 @@ it('should support addInitScript', async function({ page, browser, server }) { await page.context().addInitScript(() => (window as any)['bar'] = 17); await page.addInitScript(() => (window as any)['foo'] = 42); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); expect(await page.frames()[0].evaluate(() => (window as any)['foo'])).toBe(42); expect(await page.frames()[1].evaluate(() => (window as any)['foo'])).toBe(42); @@ -227,7 +227,7 @@ it('should support addInitScript', async function({ page, browser, server }) { // @see https://github.com/microsoft/playwright/issues/1240 it('should click a button when it overlays oopif', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/button-overlay-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); await page.click('button'); expect(await page.evaluate(() => (window as any)['BUTTON_CLICKED'])).toBe(true); }); @@ -248,7 +248,7 @@ it('should report google.com frame with headed', async ({ browserType, server }) return new Promise(x => frame.onload = x); }); await page.waitForSelector('iframe[src="https://google.com/"]'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const urls = page.frames().map(frame => frame.url()); expect(urls).toEqual([ server.EMPTY_PAGE, @@ -267,7 +267,7 @@ it('ElementHandle.boundingBox() should work', async function({ page, browser, se }); await page.frames()[1].goto(page.frames()[1].url()); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const handle1 = await page.frames()[1].$('.box:nth-of-type(13)'); expect(await handle1!.boundingBox()).toEqual({ x: 100 + 42, y: 50 + 17, width: 50, height: 50 }); @@ -275,7 +275,7 @@ it('ElementHandle.boundingBox() should work', async function({ page, browser, se page.frames()[1].waitForNavigation(), page.evaluate('goLocal()'), ]); - expect(await countOOPIFs(browser)).toBe(0); + await assertOOPIFCount(browser, 0); const handle2 = await page.frames()[1].$('.box:nth-of-type(13)'); expect(await handle2!.boundingBox()).toEqual({ x: 100 + 42, y: 50 + 17, width: 50, height: 50 }); }); @@ -290,7 +290,7 @@ it('should click', async function({ page, browser, server }) { }); await page.frames()[1].goto(page.frames()[1].url()); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); const handle1 = (await page.frames()[1].$('.box:nth-of-type(13)'))!; await handle1.evaluate(div => div.addEventListener('click', () => (window as any)['_clicked'] = true, false)); await handle1.click(); @@ -300,7 +300,7 @@ it('should click', async function({ page, browser, server }) { it('contentFrame should work', async ({ page, browser, server }) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(await page.locator('iframe').contentFrame().locator('div').count()).toBe(200); const oopif = await page.$('iframe'); const content = await oopif.contentFrame(); @@ -309,7 +309,7 @@ it('contentFrame should work', async ({ page, browser, server }) => { it('should allow cdp sessions on oopifs', async function({ page, browser, server }) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); @@ -326,7 +326,7 @@ it('should emit filechooser event for iframe', async ({ page, server, browser }) // Add listener before OOPIF is created. const chooserPromise = page.waitForEvent('filechooser'); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); const frame = page.frames()[1]; await frame.setContent(``); @@ -340,7 +340,7 @@ it('should emit filechooser event for iframe', async ({ page, server, browser }) it('should be able to click in iframe', async ({ page, server, browser }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28023' }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); expect(page.frames().length).toBe(2); const frame = page.frames()[1]; await frame.setContent(``); @@ -353,7 +353,7 @@ it('should be able to click in iframe', async ({ page, server, browser }) => { it('should not throw on exposeFunction when oopif detaches', async ({ page, browser, server }) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); - expect(await countOOPIFs(browser)).toBe(1); + await assertOOPIFCount(browser, 1); await Promise.all([ page.exposeFunction('myFunc', () => 2022), page.evaluate(() => document.querySelector('iframe')!.remove()), @@ -370,6 +370,12 @@ it('should intercept response body from oopif', async function({ page, browser, expect(await response.text()).toBeTruthy(); }); +async function assertOOPIFCount(browser: Browser, count: number) { + if (browser.browserType().name() !== 'chromium') + return; + expect(await countOOPIFs(browser)).toBe(count); +} + async function countOOPIFs(browser: Browser) { const browserSession = await browser.newBrowserCDPSession(); const oopifs = []; From 37bc4858273d3cc63ca4fed1eaeadf0433e7a448 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 16:40:24 -0700 Subject: [PATCH 374/376] chore: remove browser-specific bidi hacks (#32498) Those were just workarounds for browser-specific bugs, they should be fixed upstream. * individual mouse down/up/down/up events don't trigger dblclick event in Firefox * setContent throws when document.open/write is called in the utility context in Firefox --- .../src/server/bidi/bidiInput.ts | 18 ------------------ .../src/server/bidi/bidiPage.ts | 4 ---- packages/playwright-core/src/server/frames.ts | 2 +- packages/playwright-core/src/server/input.ts | 3 --- packages/playwright-core/src/server/page.ts | 2 -- 5 files changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiInput.ts b/packages/playwright-core/src/server/bidi/bidiInput.ts index 29d1dd48db..e14e90529c 100644 --- a/packages/playwright-core/src/server/bidi/bidiInput.ts +++ b/packages/playwright-core/src/server/bidi/bidiInput.ts @@ -90,24 +90,6 @@ export class RawMouseImpl implements input.RawMouse { await this._performActions([{ type: 'pointerUp', button: toBidiButton(button) }]); } - async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}) { - x = Math.round(x); - y = Math.round(y); - const button = toBidiButton(options.button || 'left'); - const { delay = null, clickCount = 1 } = options; - const actions: bidi.Input.PointerSourceAction[] = []; - actions.push({ type: 'pointerMove', x, y }); - for (let cc = 1; cc <= clickCount; ++cc) { - actions.push({ type: 'pointerDown', button }); - if (delay) - actions.push({ type: 'pause', duration: delay }); - actions.push({ type: 'pointerUp', button }); - if (delay && cc < clickCount) - actions.push({ type: 'pause', duration: delay }); - } - await this._performActions(actions); - } - async wheel(x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise { } diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 2802aa1452..9ecb5c789b 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -505,10 +505,6 @@ export class BidiPage implements PageDelegate { shouldToggleStyleSheetToSyncAnimations(): boolean { return true; } - - useMainWorldForSetContent(): boolean { - return true; - } } function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 32699a199f..3b952ea02a 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -900,7 +900,7 @@ export class Frame extends SdkObject { const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; progress.log(`setting frame content, waiting until "${waitUntil}"`); const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`; - const context = this._page._delegate.useMainWorldForSetContent?.() ? await this._mainContext() : await this._utilityContext(); + const context = await this._utilityContext(); const lifecyclePromise = new Promise((resolve, reject) => { this._page._frameManager._consoleMessageTags.set(tag, () => { // Clear lifecycle right after document.open() - see 'tag' below. diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index a4407d36d7..4e4c95a8f3 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -162,7 +162,6 @@ export interface RawMouse { move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise; down(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise; up(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise; - click?(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number }): Promise; wheel(x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise; } @@ -217,8 +216,6 @@ export class Mouse { async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}, metadata?: CallMetadata) { if (metadata) metadata.point = { x, y }; - if (this._raw.click) - return await this._raw.click(x, y, options); const { delay = null, clickCount = 1 } = options; if (delay) { this.move(x, y, { forClick: true }); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 144b34c28e..25394b1b5c 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -98,8 +98,6 @@ export interface PageDelegate { resetForReuse(): Promise; // WebKit hack. shouldToggleStyleSheetToSyncAnimations(): boolean; - // Bidi throws on attempt to document.open() in utility context. - useMainWorldForSetContent?(): boolean; } type EmulatedSize = { screen: types.Size, viewport: types.Size }; From f3ada9c6540ad6e2b0fe93f6204b63d27d9c42b4 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Sep 2024 17:10:14 -0700 Subject: [PATCH 375/376] chore: wheel input in bidi (#32499) --- .../playwright-core/src/server/bidi/bidiInput.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/bidi/bidiInput.ts b/packages/playwright-core/src/server/bidi/bidiInput.ts index e14e90529c..3550051a6a 100644 --- a/packages/playwright-core/src/server/bidi/bidiInput.ts +++ b/packages/playwright-core/src/server/bidi/bidiInput.ts @@ -76,7 +76,7 @@ export class RawMouseImpl implements input.RawMouse { } async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, forClick: boolean): Promise { - // TODO: bidi throws when x/y are not integers. + // Bidi throws when x/y are not integers. x = Math.round(x); y = Math.round(y); await this._performActions([{ type: 'pointerMove', x, y }]); @@ -91,6 +91,19 @@ export class RawMouseImpl implements input.RawMouse { } async wheel(x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise { + // Bidi throws when x/y are not integers. + x = Math.round(x); + y = Math.round(y); + await this._session.send('input.performActions', { + context: this._session.sessionId, + actions: [ + { + type: 'wheel', + id: 'pw_mouse_wheel', + actions: [{ type: 'scroll', x, y, deltaX, deltaY }], + } + ] + }); } private async _performActions(actions: bidi.Input.PointerSourceAction[]) { From 718bd9b35fd206245401a9ecb320289f427592d9 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sat, 7 Sep 2024 09:16:42 +0200 Subject: [PATCH 376/376] devops: run BiDi tests (#32493) --- .github/workflows/tests_bidi.yml | 42 +++++++++++++++++++ .../src/server/registry/index.ts | 7 +++- tests/bidi/playwright.config.ts | 8 ++-- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/tests_bidi.yml diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml new file mode 100644 index 0000000000..433294dbea --- /dev/null +++ b/.github/workflows/tests_bidi.yml @@ -0,0 +1,42 @@ +name: tests BiDi + +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - .github/workflows/tests_bidi.yml + schedule: + # Run every day at midnight + - cron: '0 0 * * *' + +env: + FORCE_COLOR: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + +jobs: + test_bidi: + name: BiDi + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + runs-on: ubuntu-24.04 + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + strategy: + fail-fast: false + matrix: + # TODO: add Firefox + channel: [bidi-chrome-stable] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + - run: npm run build + - run: npx playwright install --with-deps chromium + - name: Run tests + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}* diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index cafef726fb..058e0bcbc3 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -354,7 +354,7 @@ function readDescriptors(browsersJSON: BrowsersJSON) { export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi'; type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android'; -type BidiChannel = 'bidi-firefox-stable' | 'bidi-chrome-canary'; +type BidiChannel = 'bidi-firefox-stable' | 'bidi-chrome-canary' | 'bidi-chrome-stable'; type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary'; const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree']; @@ -530,6 +530,11 @@ export class Registry { 'darwin': '/Applications/Firefox.app/Contents/MacOS/firefox', 'win32': '\\Mozilla Firefox\\firefox.exe', })); + this._executables.push(this._createBidiChannel('bidi-chrome-stable', { + 'linux': '/opt/google/chrome/chrome', + 'darwin': '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'win32': `\\Google\\Chrome\\Application\\chrome.exe`, + })); this._executables.push(this._createBidiChannel('bidi-chrome-canary', { 'linux': '', 'darwin': '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index 7adaa56638..bc7b29e56b 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -47,9 +47,9 @@ const config: Config