From 615f1dbd635f9d5b8217dad9eac16c9651c2fe4c Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:51:57 -0700 Subject: [PATCH 01/35] feat(chromium-tip-of-tree): roll to r1269 (#33117) 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 53fedf1923..a579acc365 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1268", + "revision": "1269", "installByDefault": false, - "browserVersion": "131.0.6768.0" + "browserVersion": "131.0.6778.0" }, { "name": "firefox", From d40425ea589c7644ffdefa85427b3c726151ef9a Mon Sep 17 00:00:00 2001 From: Anand M Cherian <63868951+Anand-M-Cherian@users.noreply.github.com> Date: Wed, 16 Oct 2024 01:15:03 +0530 Subject: [PATCH 02/35] docs: update to "Matching one of the two alternative locators" section (#33079) Signed-off-by: Anand M Cherian <63868951+Anand-M-Cherian@users.noreply.github.com> Co-authored-by: Dmitry Gozman --- docs/src/locators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/locators.md b/docs/src/locators.md index 0aa918e53c..648a654177 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -1218,7 +1218,7 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe")); ### Matching one of the two alternative locators -If you'd like to target one of the two or more elements, and you don't know which one it will be, use [`method: Locator.or`] to create a locator that matches all of the alternatives. +If you'd like to target one of the two or more elements, and you don't know which one it will be, use [`method: Locator.or`] to create a locator that matches any one or both of the alternatives. For example, consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. From 23b1012c704ad7d4c479be1bc5144d1ce6986fad Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Oct 2024 13:34:08 -0700 Subject: [PATCH 03/35] chore: fix ff test for codegen (#33122) --- tests/library/inspector/pause.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index 405fffbe5b..12b7ed940f 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -483,6 +483,7 @@ it.describe('pause', () => { }); it('should record from debugger', async ({ page, recorderPageGetter }) => { + await page.setContent(''); const scriptPromise = (async () => { await page.pause(); })(); From 4b1fbde2adbfc3d9c9160d2b46671eb0c47c506e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Oct 2024 13:38:55 -0700 Subject: [PATCH 04/35] chore: generate match snapshot (#33105) --- .../src/server/codegen/csharp.ts | 2 + .../src/server/codegen/java.ts | 2 + .../src/server/codegen/javascript.ts | 19 +++++- .../src/server/codegen/python.ts | 2 + .../src/server/injected/ariaSnapshot.ts | 25 ++++---- .../src/server/injected/highlight.css | 5 ++ .../src/server/injected/injectedScript.ts | 6 +- .../src/server/injected/recorder/clipPaths.ts | 2 +- .../server/injected/recorder/icons/gist.svg | 1 + .../src/server/injected/recorder/recorder.ts | 59 ++++++++++++++++--- .../playwright-core/src/server/recorder.ts | 2 +- .../src/utils/isomorphic/recorderUtils.ts | 9 +++ packages/recorder/src/actions.ts | 12 +++- packages/recorder/src/recorder.tsx | 1 + packages/recorder/src/recorderTypes.ts | 3 +- tests/page/to-match-aria-snapshot.spec.ts | 15 +++-- utils/generate_clip_paths.js | 1 + 17 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 packages/playwright-core/src/server/injected/recorder/icons/gist.svg diff --git a/packages/playwright-core/src/server/codegen/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts index 8e6561f04e..2e4526d0a2 100644 --- a/packages/playwright-core/src/server/codegen/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -146,6 +146,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`; return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } + case 'assertSnapshot': + return `await Expect(${subject}.${this._asLocator(action.selector)}).ToMatchAriaSnapshotAsync(${quote(action.snapshot)});`; } } diff --git a/packages/playwright-core/src/server/codegen/java.ts b/packages/playwright-core/src/server/codegen/java.ts index 5b417c6c3a..c6d41e607b 100644 --- a/packages/playwright-core/src/server/codegen/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -133,6 +133,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`; return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`; } + case 'assertSnapshot': + return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).matchesAriaSnapshot(${quote(action.snapshot)});`; } } diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index b68a8104a8..c0e62d9df3 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -117,6 +117,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`; return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } + case 'assertSnapshot': + return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot)});`; } } @@ -228,11 +230,13 @@ export class JavaScriptFormatter { } prepend(text: string) { - this._lines = text.trim().split('\n').map(line => line.trim()).concat(this._lines); + const trim = isMultilineString(text) ? (line: string) => line : (line: string) => line.trim(); + this._lines = text.trim().split('\n').map(trim).concat(this._lines); } add(text: string) { - this._lines.push(...text.trim().split('\n').map(line => line.trim())); + const trim = isMultilineString(text) ? (line: string) => line : (line: string) => line.trim(); + this._lines.push(...text.trim().split('\n').map(trim)); } newLine() { @@ -269,3 +273,14 @@ function wrapWithStep(description: string | undefined, body: string) { ${body} });` : body; } + +export function quoteMultiline(text: string, indent = ' ') { + const lines = text.split('\n'); + if (lines.length === 1) + return '`' + text.replace(/`/g, '\\`').replace(/\${/g, '\\${') + '`'; + return '`\n' + lines.map(line => indent + line.replace(/`/g, '\\`').replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``; +} + +function isMultilineString(text: string) { + return text.match(/`[\S\s]*`/)?.[0].includes('\n'); +} diff --git a/packages/playwright-core/src/server/codegen/python.ts b/packages/playwright-core/src/server/codegen/python.ts index 38894695bc..50afe1b1a5 100644 --- a/packages/playwright-core/src/server/codegen/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -126,6 +126,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`; return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } + case 'assertSnapshot': + return `expect(${subject}.${this._asLocator(action.selector)}).to_match_aria_snapshot(${quote(action.snapshot)})`; } } diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 8e08ae7016..22ec9b5c42 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -90,8 +90,7 @@ export function generateAriaTree(rootElement: Element): AriaNode { } beginAriaCaches(); - const result = toAriaNode(rootElement); - const ariaRoot = result?.ariaNode || { role: '' }; + const ariaRoot: AriaNode = { role: '' }; try { visit(ariaRoot, rootElement); } finally { @@ -218,7 +217,11 @@ function nodeMatches(root: AriaNode, template: AriaTemplateNode): boolean { export function renderAriaTree(ariaNode: AriaNode): string { const lines: string[] = []; - const visit = (ariaNode: AriaNode, indent: string) => { + const visit = (ariaNode: AriaNode | string, indent: string) => { + if (typeof ariaNode === 'string') { + lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); + return; + } let line = `${indent}- ${ariaNode.role}`; if (ariaNode.name) line += ` ${escapeWithQuotes(ariaNode.name, '"')}`; @@ -231,14 +234,16 @@ export function renderAriaTree(ariaNode: AriaNode): string { return; } lines.push(line + (ariaNode.children ? ':' : '')); - for (const child of ariaNode.children || []) { - if (typeof child === 'string') - lines.push(indent + ' - text: ' + escapeYamlString(child)); - else - visit(child, indent + ' '); - } + for (const child of ariaNode.children || []) + visit(child, indent + ' '); }; - visit(ariaNode, ''); + if (ariaNode.role === '') { + // Render fragment. + for (const child of ariaNode.children || []) + visit(child, ''); + } else { + visit(ariaNode, ''); + } return lines.join('\n'); } diff --git a/packages/playwright-core/src/server/injected/highlight.css b/packages/playwright-core/src/server/injected/highlight.css index 83123011bc..096f931161 100644 --- a/packages/playwright-core/src/server/injected/highlight.css +++ b/packages/playwright-core/src/server/injected/highlight.css @@ -220,6 +220,11 @@ x-pw-tool-item.value > x-div { clip-path: url(#icon-symbol-constant); } +x-pw-tool-item.snapshot > x-div { + /* codicon: eye */ + clip-path: url(#icon-gist); +} + x-pw-tool-item.accept > x-div { clip-path: url(#icon-check); } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 0f3308fe7c..66a18848db 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -34,7 +34,7 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; -import { matchesAriaTree } from './ariaSnapshot'; +import { matchesAriaTree, renderedAriaTree } from './ariaSnapshot'; export type FrameExpectParams = Omit & { expectedValue?: any }; @@ -206,6 +206,10 @@ export class InjectedScript { return new Set(result.map(r => r.element)); } + renderedAriaTree(target: Element): string { + return renderedAriaTree(target); + } + querySelectorAll(selector: ParsedSelector, root: Node): Element[] { if (selector.capture !== undefined) { if (selector.parts.some(part => part.name === 'nth')) diff --git a/packages/playwright-core/src/server/injected/recorder/clipPaths.ts b/packages/playwright-core/src/server/injected/recorder/clipPaths.ts index faa77a63d6..a1e016542a 100644 --- a/packages/playwright-core/src/server/injected/recorder/clipPaths.ts +++ b/packages/playwright-core/src/server/injected/recorder/clipPaths.ts @@ -27,5 +27,5 @@ import type { SvgJson } from './recorder'; // eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes -const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]}]}]}; +const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]}; export default svgJson; \ No newline at end of file diff --git a/packages/playwright-core/src/server/injected/recorder/icons/gist.svg b/packages/playwright-core/src/server/injected/recorder/icons/gist.svg new file mode 100644 index 0000000000..f6d50e43d4 --- /dev/null +++ b/packages/playwright-core/src/server/injected/recorder/icons/gist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index cdc29a1050..d620ca273c 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -608,9 +608,9 @@ class TextAssertionTool implements RecorderTool { private _action: actions.AssertAction | null = null; private _dialog: Dialog; private _textCache = new Map(); - private _kind: 'text' | 'value'; + private _kind: 'text' | 'value' | 'snapshot'; - constructor(recorder: Recorder, kind: 'text' | 'value') { + constructor(recorder: Recorder, kind: 'text' | 'value' | 'snapshot') { this._recorder = recorder; this._kind = kind; this._dialog = new Dialog(recorder); @@ -656,7 +656,7 @@ class TextAssertionTool implements RecorderTool { const target = this._recorder.deepEventTarget(event); if (this._hoverHighlight?.elements[0] === target) return; - if (this._kind === 'text') + if (this._kind === 'text' || this._kind === 'snapshot') this._hoverHighlight = this._recorder.injectedScript.utils.elementText(this._textCache, target).full ? { elements: [target], selector: '' } : null; else this._hoverHighlight = this._elementHasValue(target) ? this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null; @@ -704,6 +704,18 @@ class TextAssertionTool implements RecorderTool { value: (target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).value, }; } + } if (this._kind === 'snapshot') { + this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }); + this._hoverHighlight.color = '#8acae480'; + // forTextExpect can update the target, re-highlight it. + this._recorder.updateHighlight(this._hoverHighlight, true); + + return { + name: 'assertSnapshot', + selector: this._hoverHighlight.selector, + signals: [], + snapshot: this._recorder.injectedScript.renderedAriaTree(target), + }; } else { this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }); this._hoverHighlight.color = '#8acae480'; @@ -727,6 +739,8 @@ class TextAssertionTool implements RecorderTool { return String(action.checked); if (action?.name === 'assertValue') return action.value; + if (action?.name === 'assertSnapshot') + return action.snapshot; return ''; } @@ -742,13 +756,19 @@ class TextAssertionTool implements RecorderTool { if (!this._hoverHighlight?.elements[0]) return; this._action = this._generateAction(); - if (!this._action || this._action.name !== 'assertText') - return; + if (this._action?.name === 'assertText') { + this._showTextDialog(this._action); + } else if (this._action?.name === 'assertSnapshot') { + this._recorder.recordAction(this._action); + this._recorder.setMode('recording'); + this._recorder.overlay?.flashToolSucceeded('assertingSnapshot'); + } + } - const action = this._action; + private _showTextDialog(action: actions.AssertTextAction) { const textElement = this._recorder.document.createElement('textarea'); textElement.setAttribute('spellcheck', 'false'); - textElement.value = this._renderValue(this._action); + textElement.value = this._renderValue(action); textElement.classList.add('text-editor'); const updateAndValidate = () => { @@ -796,6 +816,7 @@ class Overlay { private _assertVisibilityToggle: HTMLElement; private _assertTextToggle: HTMLElement; private _assertValuesToggle: HTMLElement; + private _assertSnapshotToggle: HTMLElement; private _offsetX = 0; private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined; private _measure: { width: number, height: number } = { width: 0, height: 0 }; @@ -842,6 +863,12 @@ class Overlay { this._assertValuesToggle.appendChild(this._recorder.document.createElement('x-div')); toolsListElement.appendChild(this._assertValuesToggle); + this._assertSnapshotToggle = this._recorder.document.createElement('x-pw-tool-item'); + this._assertSnapshotToggle.title = 'Assert snapshot'; + this._assertSnapshotToggle.classList.add('snapshot'); + this._assertSnapshotToggle.appendChild(this._recorder.document.createElement('x-div')); + toolsListElement.appendChild(this._assertSnapshotToggle); + this._updateVisualPosition(); this._refreshListeners(); } @@ -865,6 +892,7 @@ class Overlay { 'assertingText': 'recording-inspecting', 'assertingVisibility': 'recording-inspecting', 'assertingValue': 'recording-inspecting', + 'assertingSnapshot': 'recording-inspecting', }; this._recorder.setMode(newMode[this._recorder.state.mode]); }), @@ -880,6 +908,10 @@ class Overlay { if (!this._assertValuesToggle.classList.contains('disabled')) this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); }), + addEventListener(this._assertSnapshotToggle, 'click', () => { + if (!this._assertSnapshotToggle.classList.contains('disabled')) + this._recorder.setMode(this._recorder.state.mode === 'assertingSnapshot' ? 'recording' : 'assertingSnapshot'); + }), ]; } @@ -902,6 +934,8 @@ class Overlay { this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue'); this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); + this._assertSnapshotToggle.classList.toggle('active', state.mode === 'assertingSnapshot'); + this._assertSnapshotToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); if (this._offsetX !== state.overlay.offsetX) { this._offsetX = state.overlay.offsetX; this._updateVisualPosition(); @@ -912,8 +946,14 @@ class Overlay { this._showOverlay(); } - flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') { - const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle; + flashToolSucceeded(tool: 'assertingVisibility' | 'assertingSnapshot' | 'assertingValue') { + let element: Element; + if (tool === 'assertingVisibility') + element = this._assertVisibilityToggle; + else if (tool === 'assertingSnapshot') + element = this._assertSnapshotToggle; + else + element = this._assertValuesToggle; element.classList.add('succeeded'); this._recorder.injectedScript.builtinSetTimeout(() => element.classList.remove('succeeded'), 2000); } @@ -1004,6 +1044,7 @@ export class Recorder { 'assertingText': new TextAssertionTool(this, 'text'), 'assertingVisibility': new InspectTool(this, true), 'assertingValue': new TextAssertionTool(this, 'value'), + 'assertingSnapshot': new TextAssertionTool(this, 'snapshot'), }; this._currentTool = this._tools.none; if (injectedScript.window.top === injectedScript.window) { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 386e4dece6..c5e133c069 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -216,7 +216,7 @@ export class Recorder implements InstrumentationListener, IRecorder { this._highlightedSelector = ''; this._mode = mode; this._recorderApp?.setMode(this._mode); - this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue'); + this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue' || this._mode === 'assertingSnapshot'); this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue'); if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1) this._context.pages()[0].bringToFront().catch(() => {}); diff --git a/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts b/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts index 40ce3acd44..349afe95a5 100644 --- a/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts @@ -130,6 +130,15 @@ export function traceParamsForAction(actionInContext: recorderActions.ActionInCo }; return { method: 'expect', params }; } + case 'assertSnapshot': { + const params: channels.FrameExpectParams = { + selector, + expression: 'to.match.snapshot', + expectedText: [], + isNot: false, + }; + return { method: 'expect', params }; + } } } diff --git a/packages/recorder/src/actions.ts b/packages/recorder/src/actions.ts index a17e0c172b..d4c74b2656 100644 --- a/packages/recorder/src/actions.ts +++ b/packages/recorder/src/actions.ts @@ -30,7 +30,8 @@ export type ActionName = 'assertText' | 'assertValue' | 'assertChecked' | - 'assertVisible'; + 'assertVisible' | + 'assertSnapshot'; export type ActionBase = { name: ActionName, @@ -113,8 +114,13 @@ export type AssertVisibleAction = ActionWithSelector & { name: 'assertVisible', }; -export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; -export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction; +export type AssertSnapshotAction = ActionWithSelector & { + name: 'assertSnapshot', + snapshot: string, +}; + +export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction; +export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction; export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; // Signals. diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 19b7bc12a7..a8cd2b2719 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -116,6 +116,7 @@ export const Recorder: React.FC = ({ 'assertingText': 'recording-inspecting', 'assertingVisibility': 'recording-inspecting', 'assertingValue': 'recording-inspecting', + 'assertingSnapshot': 'recording-inspecting', }[mode]; window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { }); }}> diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index dd379f7ccd..a0e04a7283 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -26,7 +26,8 @@ export type Mode = | 'recording-inspecting' | 'standby' | 'assertingVisibility' - | 'assertingValue'; + | 'assertingValue' + | 'assertingSnapshot'; export type EventData = { event: diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index 826f8cc90e..5e58ba94e0 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -78,7 +78,7 @@ test('should match complex', async ({ page }) => { test('should match regex', async ({ page }) => { await page.setContent(`

Issues 12

`); await expect(page.locator('body')).toMatchAriaSnapshot(` - - heading /Issues \\d+/ + - heading ${/Issues \d+/} `); }); @@ -178,14 +178,17 @@ test('expected formatter', async ({ page }) => { - heading "todos" - textbox "Wrong text" `, { timeout: 1 }).catch(e => e); - expect(stripAnsi(error.message)).toContain(`- Expected - 3 + + expect(stripAnsi(error.message)).toContain(` +Locator: locator('body') +- Expected - 4 + Received string + 3 - -+ - : -+ - banner: - - heading "todos" ++ - banner: +- - heading "todos" ++ - heading "todos" - - textbox "Wrong text" - -+ - textbox "What needs to be done?"`); ++ - textbox "What needs to be done?"`); }); diff --git a/utils/generate_clip_paths.js b/utils/generate_clip_paths.js index cbef6ece9d..83d26a905c 100644 --- a/utils/generate_clip_paths.js +++ b/utils/generate_clip_paths.js @@ -64,6 +64,7 @@ const iconNames = [ 'check', 'close', 'pass', + 'gist', ]; (async () => { From b421bd8b0da0477c13684597e18c811dcfc71519 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Oct 2024 15:21:45 -0700 Subject: [PATCH 05/35] chore: add a basic snapshot generator test (#33123) --- docs/src/api/class-locatorassertions.md | 39 ++++++++-- .../playwright-core/ThirdPartyNotices.txt | 21 +++++- .../bundles/utils/package-lock.json | 19 ++++- .../bundles/utils/package.json | 1 + .../bundles/utils/src/utilsBundleImpl.ts | 3 + .../src/server/ariaSnapshot.ts | 74 +++++++++++++++++++ .../src/server/dispatchers/frameDispatcher.ts | 5 +- .../src/server/injected/recorder/recorder.ts | 2 +- packages/playwright-core/src/utilsBundle.ts | 1 + packages/playwright/ThirdPartyNotices.txt | 21 +----- .../bundles/utils/package-lock.json | 19 +---- .../playwright/bundles/utils/package.json | 3 +- .../bundles/utils/src/utilsBundleImpl.ts | 3 - .../src/matchers/toMatchAriaSnapshot.ts | 61 +-------------- packages/playwright/src/utilsBundle.ts | 1 - packages/playwright/types/test.d.ts | 2 - .../inspector/cli-codegen-aria.spec.ts | 42 +++++++++++ tests/library/inspector/inspectorTest.ts | 9 +++ 18 files changed, 211 insertions(+), 115 deletions(-) create mode 100644 packages/playwright-core/src/server/ariaSnapshot.ts create mode 100644 tests/library/inspector/cli-codegen-aria.spec.ts diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 46f8f610e0..b48b3fce7e 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -2106,15 +2106,14 @@ Expected options currently selected. ## async method: LocatorAssertions.toMatchAriaSnapshot * since: v1.49 -* langs: js +* langs: + - alias-java: matchesAriaSnapshot Asserts that the target element matches the given accessibility snapshot. **Usage** ```js -import { role as x } from '@playwright/test'; -// ... await page.goto('https://demo.playwright.dev/todomvc/'); await expect(page.locator('body')).toMatchAriaSnapshot(` - heading "todos" @@ -2122,11 +2121,41 @@ await expect(page.locator('body')).toMatchAriaSnapshot(` `); ``` +```python async +await page.goto('https://demo.playwright.dev/todomvc/') +await expect(page.locator('body')).to_match_aria_snapshot(''' + - heading "todos" + - textbox "What needs to be done?" +''') +``` + +```python sync +page.goto('https://demo.playwright.dev/todomvc/') +expect(page.locator('body')).to_match_aria_snapshot(''' + - heading "todos" + - textbox "What needs to be done?" +''') +``` + +```csharp +await page.GotoAsync("https://demo.playwright.dev/todomvc/"); +await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@" + - heading ""todos"" + - textbox ""What needs to be done?"" +"); +``` + +```java +page.navigate("https://demo.playwright.dev/todomvc/"); +assertThat(page.locator("body")).matchesAriaSnapshot(""" + - heading "todos" + - textbox "What needs to be done?" +"""); +``` + ### param: LocatorAssertions.toMatchAriaSnapshot.expected * since: v1.49 -* langs: js - `expected` ### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%% * since: v1.49 -* langs: js diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index a5d4ca7d0b..23e3cff257 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -48,6 +48,7 @@ This project incorporates components from the projects listed below. The origina - stack-utils@2.0.5 (https://github.com/tapjs/stack-utils) - wrappy@1.0.2 (https://github.com/npm/wrappy) - ws@8.17.1 (https://github.com/websockets/ws) +- yaml@2.6.0 (https://github.com/eemeli/yaml) - yauzl@2.10.0 (https://github.com/thejoshwolfe/yauzl) - yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) @@ -1121,6 +1122,24 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF ws@8.17.1 AND INFORMATION +%% yaml@2.6.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright Eemeli Aro + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +========================================= +END OF yaml@2.6.0 AND INFORMATION + %% yauzl@2.10.0 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1175,6 +1194,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 46 +Total Packages: 47 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 0e5e761433..f786cb4db7 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -25,7 +25,8 @@ "signal-exit": "3.0.7", "socks-proxy-agent": "8.0.4", "stack-utils": "2.0.5", - "ws": "8.17.1" + "ws": "8.17.1", + "yaml": "^2.5.1" }, "devDependencies": { "@types/debug": "^4.1.7", @@ -432,6 +433,17 @@ "optional": true } } + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } }, "dependencies": { @@ -726,6 +738,11 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} + }, + "yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==" } } } diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 06637adabe..005e32fbe6 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -26,6 +26,7 @@ "signal-exit": "3.0.7", "socks-proxy-agent": "8.0.4", "stack-utils": "2.0.5", + "yaml": "^2.5.1", "ws": "8.17.1" }, "devDependencies": { diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index dcb3790629..975e291bb5 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -54,6 +54,9 @@ export { SocksProxyAgent } from 'socks-proxy-agent'; import StackUtilsLibrary from 'stack-utils'; export const StackUtils = StackUtilsLibrary; +import yamlLibrary from 'yaml'; +export const yaml = yamlLibrary; + // @ts-ignore import wsLibrary, { WebSocketServer, Receiver, Sender } from 'ws'; export const ws = wsLibrary; diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts new file mode 100644 index 0000000000..6f89dd21cf --- /dev/null +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -0,0 +1,74 @@ +/** + * 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. + */ + +import type { AriaTemplateNode } from './injected/ariaSnapshot'; +import { yaml } from '../utilsBundle'; + +export function parseAriaSnapshot(text: string): AriaTemplateNode { + type YamlNode = Record | string>; + + const parseKey = (key: string): AriaTemplateNode => { + if (!key) + return { role: '' }; + + const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/); + + if (!match) + throw new Error(`Invalid key ${key}`); + + const role = match[1]; + if (role && role !== 'text' && !allRoles.includes(role)) + throw new Error(`Invalid role ${role}`); + + if (match[2]) + return { role, name: match[2] }; + if (match[3]) + return { role, name: new RegExp(match[3]) }; + return { role }; + }; + + const valueOrRegex = (value: string): string | RegExp => { + return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : value; + }; + + const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => { + const key = typeof object === 'string' ? object : Object.keys(object)[0]; + const value = typeof object === 'string' ? undefined : object[key]; + const parsed = parseKey(key); + if (parsed.role === 'text') { + if (typeof value !== 'string') + throw new Error(`Generic role must have a text value`); + return valueOrRegex(value as string); + } + if (Array.isArray(value)) + parsed.children = value.map(convert); + else if (value) + parsed.children = [valueOrRegex(value)]; + return parsed; + }; + const fragment = yaml.parse(text) as YamlNode[]; + return convert({ '': fragment }) as AriaTemplateNode; +} + +const allRoles = [ + 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command', + 'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', + 'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu', + 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', + 'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider', + 'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', + 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window' +]; diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 6dcb9a7220..d058085cb7 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -26,6 +26,7 @@ import type { CallMetadata } from '../instrumentation'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { PageDispatcher } from './pageDispatcher'; import { debugAssert } from '../../utils'; +import { parseAriaSnapshot } from '../ariaSnapshot'; export class FrameDispatcher extends Dispatcher implements channels.FrameChannel { _type_Frame = true; @@ -258,7 +259,9 @@ export class FrameDispatcher extends Dispatcher { metadata.potentiallyClosesScope = true; - const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined; + let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined; + if (params.expression === 'to.match.aria' && expectedValue) + expectedValue = parseAriaSnapshot(expectedValue); const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue }); if (result.received !== undefined) result.received = serializeResult(result.received); diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index d620ca273c..fd2cdcdb7c 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -704,7 +704,7 @@ class TextAssertionTool implements RecorderTool { value: (target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).value, }; } - } if (this._kind === 'snapshot') { + } else if (this._kind === 'snapshot') { this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }); this._hoverHighlight.color = '#8acae480'; // forTextExpect can update the target, re-highlight it. diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index a2a62be867..b330491e0c 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -31,6 +31,7 @@ export const PNG: typeof import('../bundles/utils/node_modules/@types/pngjs').PN export const program: typeof import('../bundles/utils/node_modules/commander').program = require('./utilsBundleImpl').program; export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress; export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent; +export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml; export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws; export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer; export const wsReceiver = require('./utilsBundleImpl').wsReceiver; diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index 46c2f60cd2..f2bb64d661 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -155,7 +155,6 @@ This project incorporates components from the projects listed below. The origina - undici-types@6.19.8 (https://github.com/nodejs/undici) - update-browserslist-db@1.0.13 (https://github.com/browserslist/update-db) - yallist@3.1.1 (https://github.com/isaacs/yallist) -- yaml@2.5.1 (https://github.com/eemeli/yaml) %% @ampproject/remapping@2.2.1 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -4398,26 +4397,8 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF yallist@3.1.1 AND INFORMATION -%% yaml@2.5.1 NOTICES AND INFORMATION BEGIN HERE -========================================= -Copyright Eemeli Aro - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. -========================================= -END OF yaml@2.5.1 AND INFORMATION - SUMMARY BEGIN HERE ========================================= -Total Packages: 152 +Total Packages: 151 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright/bundles/utils/package-lock.json b/packages/playwright/bundles/utils/package-lock.json index 90df9a258b..fcf9f972fe 100644 --- a/packages/playwright/bundles/utils/package-lock.json +++ b/packages/playwright/bundles/utils/package-lock.json @@ -13,8 +13,7 @@ "json5": "2.2.3", "pirates": "4.0.4", "source-map-support": "0.5.21", - "stoppable": "1.1.0", - "yaml": "^2.5.1" + "stoppable": "1.1.0" }, "devDependencies": { "@types/source-map-support": "^0.5.4", @@ -281,17 +280,6 @@ "engines": { "node": ">=8.0" } - }, - "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } } }, "dependencies": { @@ -476,11 +464,6 @@ "requires": { "is-number": "^7.0.0" } - }, - "yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==" } } } diff --git a/packages/playwright/bundles/utils/package.json b/packages/playwright/bundles/utils/package.json index dc807c0d95..69477909c5 100644 --- a/packages/playwright/bundles/utils/package.json +++ b/packages/playwright/bundles/utils/package.json @@ -14,8 +14,7 @@ "json5": "2.2.3", "pirates": "4.0.4", "source-map-support": "0.5.21", - "stoppable": "1.1.0", - "yaml": "^2.5.1" + "stoppable": "1.1.0" }, "devDependencies": { "@types/source-map-support": "^0.5.4", diff --git a/packages/playwright/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright/bundles/utils/src/utilsBundleImpl.ts index 76cf961ab7..7c29c301a8 100644 --- a/packages/playwright/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright/bundles/utils/src/utilsBundleImpl.ts @@ -31,6 +31,3 @@ export const enquirer = enquirerLibrary; import chokidarLibrary from 'chokidar'; export const chokidar = chokidarLibrary; - -import yamlLibrary from 'yaml'; -export const yaml = yamlLibrary; diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 949e44af6f..cd79ccab61 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -18,8 +18,6 @@ import type { LocatorEx } from './matchers'; import type { ExpectMatcherState } from '../../types/test'; import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint'; -import type { AriaTemplateNode } from 'playwright-core/lib/server/injected/ariaSnapshot'; -import { yaml } from '../utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle'; import { EXPECTED_COLOR } from '../common/expectBundle'; import { callLogText } from '../util'; @@ -46,9 +44,8 @@ export async function toMatchAriaSnapshot( ].join('\n\n')); } - const ariaTree = toAriaTree(expected) as AriaTemplateNode; const timeout = options.timeout ?? this.timeout; - const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: ariaTree, isNot: this.isNot, timeout }); + const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout }); const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; @@ -76,59 +73,3 @@ export async function toMatchAriaSnapshot( timeout: timedOut ? timeout : undefined, }; } - -function parseKey(key: string): AriaTemplateNode { - if (!key) - return { role: '' }; - - const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/); - - if (!match) - throw new Error(`Invalid key ${key}`); - - const role = match[1]; - if (role && role !== 'text' && !allRoles.includes(role)) - throw new Error(`Invalid role ${role}`); - - if (match[2]) - return { role, name: match[2] }; - if (match[3]) - return { role, name: new RegExp(match[3]) }; - return { role }; -} - -function valueOrRegex(value: string): string | RegExp { - return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : value; -} - -type YamlNode = Record | string>; - -function toAriaTree(text: string): AriaTemplateNode { - const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => { - const key = typeof object === 'string' ? object : Object.keys(object)[0]; - const value = typeof object === 'string' ? undefined : object[key]; - const parsed = parseKey(key); - if (parsed.role === 'text') { - if (typeof value !== 'string') - throw new Error(`Generic role must have a text value`); - return valueOrRegex(value as string); - } - if (Array.isArray(value)) - parsed.children = value.map(convert); - else if (value) - parsed.children = [valueOrRegex(value)]; - return parsed; - }; - const fragment = yaml.parse(text) as YamlNode[]; - return convert({ '': fragment }) as AriaTemplateNode; -} - -const allRoles = [ - 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command', - 'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', - 'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu', - 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', - 'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider', - 'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', - 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window' -]; diff --git a/packages/playwright/src/utilsBundle.ts b/packages/playwright/src/utilsBundle.ts index 5ded7993b7..072e16bb03 100644 --- a/packages/playwright/src/utilsBundle.ts +++ b/packages/playwright/src/utilsBundle.ts @@ -20,4 +20,3 @@ export const sourceMapSupport: typeof import('../bundles/utils/node_modules/@typ export const stoppable: typeof import('../bundles/utils/node_modules/@types/stoppable') = require('./utilsBundleImpl').stoppable; export const enquirer: typeof import('../bundles/utils/node_modules/enquirer') = require('./utilsBundleImpl').enquirer; export const chokidar: typeof import('../bundles/utils/node_modules/chokidar') = require('./utilsBundleImpl').chokidar; -export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 3f1179e34f..c96de2091a 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -7644,8 +7644,6 @@ interface LocatorAssertions { * **Usage** * * ```js - * import { role as x } from '@playwright/test'; - * // ... * await page.goto('https://demo.playwright.dev/todomvc/'); * await expect(page.locator('body')).toMatchAriaSnapshot(` * - heading "todos" diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts new file mode 100644 index 0000000000..f99f65fd6f --- /dev/null +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -0,0 +1,42 @@ +/** + * 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. + */ + +import { test, expect } from './inspectorTest'; + +test.describe(() => { + test.skip(({ mode }) => mode !== 'default'); + test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); + + test('should generate aria snapshot', async ({ openRecorder }) => { + const { recorder } = await openRecorder(); + await recorder.setContentAndWait(`
`); + + await recorder.page.click('x-pw-tool-item.snapshot'); + await recorder.page.hover('button'); + await recorder.trustedClick(); + + await expect.poll(() => + recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit"\`);`); + await expect.poll(() => + recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\"")`); + await expect.poll(() => + recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\"")`); + await expect.poll(() => + recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\"");`); + await expect.poll(() => + recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`); + }); +}); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 524fab1e57..3ed700a208 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -160,6 +160,15 @@ export class Recorder { return this._sources; } + async text(file: string): Promise { + const sources: Source[] = await this.recorderPage.evaluate(() => (window as any).playwrightSourcesEchoForTest || []); + for (const source of sources) { + if (codegenLangId2lang.get(source.id) === file) + return source.text; + } + return ''; + } + async waitForHighlight(action: () => Promise): Promise { await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove())); await this.page.$$eval('x-pw-tooltip', els => els.forEach(e => e.remove())); From b92b855638d1dd409883a49ef1094d52435306bd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Oct 2024 16:21:55 -0700 Subject: [PATCH 06/35] test: unflake ff debugger test (#33124) --- tests/library/inspector/inspectorTest.ts | 11 +++++++++-- tests/library/inspector/pause.spec.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 3ed700a208..d77ad86697 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -180,6 +180,13 @@ export class Recorder { return this.page.locator('x-pw-tooltip').textContent(); } + async waitForHighlightNoTooltip(action: () => Promise): Promise { + await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove())); + await action(); + await this.page.locator('x-pw-highlight').waitFor(); + return ''; + } + async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> { let callback; const listener = async msg => { @@ -194,8 +201,8 @@ export class Recorder { return new Promise(f => callback = f); } - async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise { - return this.waitForHighlight(async () => { + async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }, omitTooltip?: boolean }): Promise { + return (options?.omitTooltip ? this.waitForHighlightNoTooltip : this.waitForHighlight).call(this, async () => { const box = await this.page.locator(selector).first().boundingBox(); const offset = options?.position || { x: box.width / 2, y: box.height / 2 }; await this.page.mouse.move(box.x + offset.x, box.y + offset.y); diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index 12b7ed940f..647706956f 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -15,7 +15,7 @@ */ import type { Page } from 'playwright-core'; -import { test as it, expect } from './inspectorTest'; +import { test as it, expect, Recorder } from './inspectorTest'; import { waitForTestLog } from '../../config/utils'; @@ -491,7 +491,11 @@ it.describe('pause', () => { await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/); await expect(recorderPage.locator('.source-line-paused')).toHaveText(/await page\.pause\(\)/); await recorderPage.getByRole('button', { name: 'Record' }).click(); - await page.locator('body').click(); + + const recorder = new Recorder(page, recorderPage); + await recorder.hoverOverElement('body', { omitTooltip: true }); + await recorder.trustedClick(); + await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('javascript'); await expect(recorderPage.locator('.cm-wrapper')).toContainText(`await page.locator('body').click();`); await recorderPage.getByRole('button', { name: 'Resume' }).click(); From 94321fef1c94f9851b6fcc4304d3844760e986cb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Oct 2024 18:47:26 -0700 Subject: [PATCH 07/35] chore: implement locator.ariaSnapshot (#33125) --- docs/src/api/class-locator.md | 57 +++++++++++++++++++ .../playwright-core/src/client/locator.ts | 5 ++ .../playwright-core/src/protocol/validator.ts | 7 +++ .../src/server/dispatchers/frameDispatcher.ts | 4 ++ packages/playwright-core/src/server/dom.ts | 4 ++ packages/playwright-core/src/server/frames.ts | 7 +++ .../src/server/injected/consoleApi.ts | 3 +- .../src/server/injected/injectedScript.ts | 6 +- .../src/server/injected/recorder/recorder.ts | 2 +- packages/playwright-core/types/types.d.ts | 51 +++++++++++++++++ packages/protocol/src/channels.ts | 11 ++++ packages/protocol/src/protocol.yml | 7 +++ tests/page/page-aria-snapshot.spec.ts | 40 +++++++++++++ 13 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 tests/page/page-aria-snapshot.spec.ts diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 8f0b53e16d..ba6dc91353 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -150,6 +150,63 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe")); Additional locator to match. +## async method: Locator.ariaSnapshot +* since: v1.49 +- returns: <[string]> + +Captures the aria snapshot of the given element. See [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion. + +**Usage** + +```js +await page.getByRole('link').ariaSnapshot(); +``` + +```java +page.getByRole(AriaRole.LINK).ariaSnapshot(); +``` + +```python async +await page.get_by_role("link").aria_snapshot() +``` + +```python sync +page.get_by_role("link").aria_snapshot() +``` + +```csharp +await page.GetByRole(AriaRole.Link).AriaSnapshotAsync(); +``` + +**Details** + +This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the element and its children. +The snapshot can be used to assert the state of the element in the test, or to compare it to state in the future. + +The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language: +* The keys of the objects are the roles and optional accessible names of the elements. +* The values are either text content or an array of child elements. +* Generic static text can be represented with the `text` key. + +Below is the HTML markup and the respective ARIA snapshot: + +```html +
    +
  • Home
  • +
  • About
  • +
      +``` + +```yml +- list "Links": + - listitem: + - link "Home" + - listitem: + - link "About" +``` + +### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%% +* since: v1.49 ## async method: Locator.blur * since: v1.28 diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index b6058e0abb..1b43db8de7 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -288,6 +288,11 @@ export class Locator implements api.Locator { return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout); } + async ariaSnapshot(options?: TimeoutOptions): Promise { + const result = await this._frame._channel.ariaSnapshot({ ...options, selector: this._selector }); + return result.snapshot; + } + async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) { return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), options.timeout); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index d5d91b165d..24ddf0014c 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1424,6 +1424,13 @@ scheme.FrameAddStyleTagParams = tObject({ scheme.FrameAddStyleTagResult = tObject({ element: tChannel(['ElementHandle']), }); +scheme.FrameAriaSnapshotParams = tObject({ + selector: tString, + timeout: tOptional(tNumber), +}); +scheme.FrameAriaSnapshotResult = tObject({ + snapshot: tString, +}); scheme.FrameBlurParams = tObject({ selector: tString, strict: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index d058085cb7..2f172df694 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -267,4 +267,8 @@ export class FrameDispatcher extends Dispatcher { + return { snapshot: await this._frame.ariaSnapshot(metadata, params.selector, params) }; + } } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 76708245d4..4bb301bffd 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -789,6 +789,10 @@ export class ElementHandle extends js.JSHandle { return this._page._delegate.getBoundingBox(this); } + async ariaSnapshot(): Promise { + return await this.evaluateInUtility(([injected, element]) => injected.ariaSnapshot(element), {}); + } + async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions = {}): Promise { const controller = new ProgressController(metadata, this); return controller.run( diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 6dc412c235..597ff35951 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1405,6 +1405,13 @@ export class Frame extends SdkObject { }); } + async ariaSnapshot(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { + return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot()); + }, this._page._timeoutSettings.timeout(options)); + } + async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { const result = await this._expectImpl(metadata, selector, options); // Library mode special case for the expect errors which are return values, not exceptions. diff --git a/packages/playwright-core/src/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts index 526694745b..0287240493 100644 --- a/packages/playwright-core/src/server/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/injected/consoleApi.ts @@ -20,7 +20,6 @@ import { escapeForTextSelector } from '../../utils/isomorphic/stringUtils'; import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { InjectedScript } from './injectedScript'; -import { renderedAriaTree } from './ariaSnapshot'; const selectorSymbol = Symbol('selector'); @@ -86,7 +85,7 @@ class ConsoleAPI { inspect: (selector: string) => this._inspect(selector), selector: (element: Element) => this._selector(element), generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language), - ariaSnapshot: (element?: Element) => renderedAriaTree(element || this._injectedScript.document.body), + ariaSnapshot: (element?: Element) => this._injectedScript.ariaSnapshot(element || this._injectedScript.document.body), resume: () => this._resume(), ...new Locator(injectedScript, ''), }; diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 66a18848db..7abc5850bb 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -206,8 +206,10 @@ export class InjectedScript { return new Set(result.map(r => r.element)); } - renderedAriaTree(target: Element): string { - return renderedAriaTree(target); + ariaSnapshot(node: Node): string { + if (node.nodeType !== Node.ELEMENT_NODE) + throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); + return renderedAriaTree(node as Element); } querySelectorAll(selector: ParsedSelector, root: Node): Element[] { diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index fd2cdcdb7c..c82f55c984 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -714,7 +714,7 @@ class TextAssertionTool implements RecorderTool { name: 'assertSnapshot', selector: this._hoverHighlight.selector, signals: [], - snapshot: this._recorder.injectedScript.renderedAriaTree(target), + snapshot: this._recorder.injectedScript.ariaSnapshot(target), }; } else { this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7d1d736a13..092f75b263 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -12424,6 +12424,57 @@ export interface Locator { */ and(locator: Locator): Locator; + /** + * Captures the aria snapshot of the given element. See + * [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot) + * for the corresponding assertion. + * + * **Usage** + * + * ```js + * await page.getByRole('link').ariaSnapshot(); + * ``` + * + * **Details** + * + * This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of + * the element and its children. The snapshot can be used to assert the state of the element in the test, or to + * compare it to state in the future. + * + * The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language: + * - The keys of the objects are the roles and optional accessible names of the elements. + * - The values are either text content or an array of child elements. + * - Generic static text can be represented with the `text` key. + * + * Below is the HTML markup and the respective ARIA snapshot: + * + * ```html + *
        + *
      • Home
      • + *
      • About
      • + *
          + * ``` + * + * ```yml + * - list "Links": + * - listitem: + * - link "Home" + * - listitem: + * - link "About" + * ``` + * + * @param options + */ + ariaSnapshot(options?: { + /** + * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` + * option in the config, or by using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + }): Promise; + /** * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element. * @param options diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index a0d6a30b9f..5ecc2f4077 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2509,6 +2509,7 @@ export interface FrameChannel extends FrameEventTarget, Channel { evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, metadata?: CallMetadata): Promise; addScriptTag(params: FrameAddScriptTagParams, metadata?: CallMetadata): Promise; addStyleTag(params: FrameAddStyleTagParams, metadata?: CallMetadata): Promise; + ariaSnapshot(params: FrameAriaSnapshotParams, metadata?: CallMetadata): Promise; blur(params: FrameBlurParams, metadata?: CallMetadata): Promise; check(params: FrameCheckParams, metadata?: CallMetadata): Promise; click(params: FrameClickParams, metadata?: CallMetadata): Promise; @@ -2613,6 +2614,16 @@ export type FrameAddStyleTagOptions = { export type FrameAddStyleTagResult = { element: ElementHandleChannel, }; +export type FrameAriaSnapshotParams = { + selector: string, + timeout?: number, +}; +export type FrameAriaSnapshotOptions = { + timeout?: number, +}; +export type FrameAriaSnapshotResult = { + snapshot: string, +}; export type FrameBlurParams = { selector: string, strict?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index a32ebde3d4..c91cecbe6c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1875,6 +1875,13 @@ Frame: flags: snapshot: true + ariaSnapshot: + parameters: + selector: string + timeout: number? + returns: + snapshot: string + blur: parameters: selector: string diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts new file mode 100644 index 0000000000..766df6d485 --- /dev/null +++ b/tests/page/page-aria-snapshot.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications 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. + */ + +import { test as it, expect } from './pageTest'; + +it('should snapshot the check box @smoke', async ({ page }) => { + await page.setContent(``); + expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox'); +}); + +it('should snapshot nested element', async ({ page }) => { + await page.setContent(` +
          + +
          `); + expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox'); +}); + +it('should snapshot fragment', async ({ page }) => { + await page.setContent(` +
          + Link + Link +
          `); + expect(await page.locator('body').ariaSnapshot()).toBe(`- link "Link"\n- link "Link"`); +}); From 183720b56a3e8a502e40bc9ec7d5cf9b4dc36b6e Mon Sep 17 00:00:00 2001 From: Lars Hanisch Date: Wed, 16 Oct 2024 10:15:40 +0200 Subject: [PATCH 08/35] fix(docker): correct Ubuntu Noble name in name template (#33133) --- utils/docker/Dockerfile.noble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 7236acbbfc..29ca98c4e4 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -2,7 +2,7 @@ FROM ubuntu:noble ARG DEBIAN_FRONTEND=noninteractive ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy" +ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-noble" ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 From d10a5e56938f8183cc22651eeab4ec7f2610b0ce Mon Sep 17 00:00:00 2001 From: Pengoose Date: Wed, 16 Oct 2024 22:47:23 +0900 Subject: [PATCH 09/35] feat(testType): add support for test.fail.only method (#33001) --- docs/src/test-api/class-test.md | 51 ++++++++ packages/playwright/src/common/testType.ts | 7 +- packages/playwright/types/test.d.ts | 126 +++++++++++++++++-- tests/playwright-test/basic.spec.ts | 69 +++++++++- tests/playwright-test/test-modifiers.spec.ts | 27 ++++ tests/playwright-test/types-2.spec.ts | 2 + utils/generate_types/overrides-test.d.ts | 13 +- 7 files changed, 278 insertions(+), 17 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 31f60b7e9c..7ea05d4c56 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1138,6 +1138,57 @@ Optional description that will be reflected in a test report. +## method: Test.fail.only +* since: v1.49 + +You can use `test.fail.only` to focus on a specific test that is expected to fail. This is particularly useful when debugging a failing test or working on a specific issue. + +To declare a focused "failing" test: +* `test.fail.only(title, body)` +* `test.fail.only(title, details, body)` + +**Usage** + +You can declare a focused failing test, so that Playwright runs only this test and ensures it actually fails. + +```js +import { test, expect } from '@playwright/test'; + +test.fail.only('focused failing test', async ({ page }) => { + // This test is expected to fail +}); +test('not in the focused group', async ({ page }) => { + // This test will not run +}); +``` + +### param: Test.fail.only.title +* since: v1.49 + +- `title` ?<[string]> + +Test title. + +### param: Test.fail.only.details +* since: v1.49 + +- `details` ?<[Object]> + - `tag` ?<[string]|[Array]<[string]>> + - `annotation` ?<[Object]|[Array]<[Object]>> + - `type` <[string]> + - `description` ?<[string]> + +See [`method: Test.describe`] for test details description. + +### param: Test.fail.only.body +* since: v1.49 + +- `body` ?<[function]\([Fixtures], [TestInfo]\)> + +Test body that takes one or two arguments: an object with fixtures and optional [TestInfo]. + + + ## method: Test.fixme * since: v1.10 diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index adf6bc3734..f22fd159d8 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -52,6 +52,7 @@ export class TestTypeImpl { test.skip = wrapFunctionWithLocation(this._modifier.bind(this, 'skip')); test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme')); test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail')); + test.fail.only = wrapFunctionWithLocation(this._createTest.bind(this, 'fail.only')); test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow')); test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this)); test.step = this._step.bind(this); @@ -81,7 +82,7 @@ export class TestTypeImpl { return suite; } - private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) { + private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail' | 'fail.only', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) { throwIfRunningInsideJest(); const suite = this._currentSuite(location, 'test()'); if (!suite) @@ -104,10 +105,12 @@ export class TestTypeImpl { test._tags.push(...validatedDetails.tags); suite._addTest(test); - if (type === 'only') + if (type === 'only' || type === 'fail.only') test._only = true; if (type === 'skip' || type === 'fixme' || type === 'fail') test._staticAnnotations.push({ type }); + else if (type === 'fail.only') + test._staticAnnotations.push({ type: 'fail' }); } private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) { diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index c96de2091a..78f8eb7f4f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -3625,8 +3625,8 @@ export interface TestType Promise | void): void; - /** + fail: { + /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. * @@ -3702,8 +3702,8 @@ export interface TestType Promise | void): void; - /** + (title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. * @@ -3779,8 +3779,8 @@ export interface TestType Promise | void): void; + /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. * @@ -3856,8 +3856,8 @@ export interface TestType boolean, description?: string): void; - /** + (condition: boolean, description?: string): void; + /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. * @@ -3933,7 +3933,115 @@ export interface TestType boolean, description?: string): void; + /** + * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful + * for documentation purposes to acknowledge that some functionality is broken until it is fixed. + * + * To declare a "failing" test: + * - `test.fail(title, body)` + * - `test.fail(title, details, body)` + * + * To annotate test as "failing" at runtime: + * - `test.fail(condition, description)` + * - `test.fail(callback, description)` + * - `test.fail()` + * + * **Usage** + * + * You can declare a test as failing, so that Playwright ensures it actually fails. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.fail('not yet ready', async ({ page }) => { + * // ... + * }); + * ``` + * + * If your test fails in some configurations, but not all, you can mark the test as failing inside the test body based + * on some condition. We recommend passing a `description` argument in this case. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('fail in WebKit', async ({ page, browserName }) => { + * test.fail(browserName === 'webkit', 'This feature is not implemented for Mac yet'); + * // ... + * }); + * ``` + * + * You can mark all tests in a file or + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) group as + * "should fail" based on some condition with a single `test.fail(callback, description)` call. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.fail(({ browserName }) => browserName === 'webkit', 'not implemented yet'); + * + * test('fail in WebKit 1', async ({ page }) => { + * // ... + * }); + * test('fail in WebKit 2', async ({ page }) => { + * // ... + * }); + * ``` + * + * You can also call `test.fail()` without arguments inside the test body to always mark the test as failed. We + * recommend declaring a failing test with `test.fail(title, body)` instead. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('less readable', async ({ page }) => { + * test.fail(); + * // ... + * }); + * ``` + * + * @param title Test title. + * @param details See [test.(call)(title[, details, body])](https://playwright.dev/docs/api/class-test#test-call) for test details + * description. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + * @param condition Test is marked as "should fail" when the condition is `true`. + * @param callback A function that returns whether to mark as "should fail", based on test fixtures. Test or tests are marked as + * "should fail" when the return value is `true`. + * @param description Optional description that will be reflected in a test report. + */ + (): void; + /** + * You can use `test.fail.only` to focus on a specific test that is expected to fail. This is particularly useful when + * debugging a failing test or working on a specific issue. + * + * To declare a focused "failing" test: + * - `test.fail.only(title, body)` + * - `test.fail.only(title, details, body)` + * + * **Usage** + * + * You can declare a focused failing test, so that Playwright runs only this test and ensures it actually fails. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.fail.only('focused failing test', async ({ page }) => { + * // This test is expected to fail + * }); + * test('not in the focused group', async ({ page }) => { + * // This test will not run + * }); + * ``` + * + * @param title Test title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for test + * details description. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + only: TestFunction; + } /** * Marks a test as "slow". Slow test will be given triple the default timeout. * diff --git a/tests/playwright-test/basic.spec.ts b/tests/playwright-test/basic.spec.ts index 3b47603c25..ce6825c559 100644 --- a/tests/playwright-test/basic.spec.ts +++ b/tests/playwright-test/basic.spec.ts @@ -153,6 +153,10 @@ test('should respect focused tests', async ({ runInlineTest }) => { }); }); + test.fail.only('focused fail.only test', () => { + expect(1 + 1).toBe(3); + }); + test.describe('non-focused describe', () => { test('describe test', () => { expect(1 + 1).toBe(3); @@ -172,13 +176,46 @@ test('should respect focused tests', async ({ runInlineTest }) => { test.only('test4', () => { expect(1 + 1).toBe(2); }); + test.fail.only('test5', () => { + expect(1 + 1).toBe(3); + }); }); ` }); - expect(passed).toBe(5); + expect(passed).toBe(7); expect(exitCode).toBe(0); }); +test('should respect focused tests with test.fail', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'fail-only.spec.ts': ` + import { test, expect } from '@playwright/test'; + + test('test1', () => { + console.log('test1 should not run'); + expect(1 + 1).toBe(2); + }); + + test.fail.only('test2', () => { + console.log('test2 should run and fail'); + expect(1 + 1).toBe(3); + }); + + test('test3', () => { + console.log('test3 should not run'); + expect(1 + 1).toBe(2); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.skipped).toBe(0); + expect(result.output).toContain('test2 should run and fail'); + expect(result.output).not.toContain('test1 should not run'); + expect(result.output).not.toContain('test3 should not run'); +}); + test('skip should take priority over fail', async ({ runInlineTest }) => { const { passed, skipped, failed } = await runInlineTest({ 'test.spec.ts': ` @@ -550,3 +587,33 @@ test('should support describe.fixme', async ({ runInlineTest }) => { expect(result.skipped).toBe(3); expect(result.output).toContain('heytest4'); }); + +test('should fail when test.fail.only passes unexpectedly', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'fail-only-pass.spec.ts': ` + import { test, expect } from '@playwright/test'; + + test('test1', () => { + console.log('test1 should not run'); + expect(1 + 1).toBe(2); + }); + + test.fail.only('test2', () => { + console.log('test2 should run and pass unexpectedly'); + expect(1 + 1).toBe(2); + }); + + test('test3', () => { + console.log('test3 should not run'); + expect(1 + 1).toBe(2); + }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.skipped).toBe(0); + expect(result.output).toContain('should run and pass unexpectedly'); + expect(result.output).not.toContain('test1 should not run'); + expect(result.output).not.toContain('test3 should not run'); +}); diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts index 0dd41bd0ab..f7fb9a6ae0 100644 --- a/tests/playwright-test/test-modifiers.spec.ts +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -279,6 +279,33 @@ test.describe('test modifier annotations', () => { expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']); }); + test('should work with fail.only inside describe.only', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + + test.describe.only("suite", () => { + test.skip('focused skip by suite', () => {}); + test.fixme('focused fixme by suite', () => {}); + test.fail.only('focused fail by suite', () => { expect(1).toBe(2); }); + }); + + test.describe.skip('not focused', () => { + test('no marker', () => {}); + }); + `, + }); + const expectTest = expectTestHelper(result); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.skipped).toBe(0); + expectTest('focused skip by suite', 'skipped', 'skipped', ['skip']); + expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']); + expectTest('focused fail by suite', 'failed', 'expected', ['fail']); + }); + test('should not multiple on retry', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` diff --git a/tests/playwright-test/types-2.spec.ts b/tests/playwright-test/types-2.spec.ts index e61d4870ed..f794e06798 100644 --- a/tests/playwright-test/types-2.spec.ts +++ b/tests/playwright-test/types-2.spec.ts @@ -33,6 +33,7 @@ test('basics should work', async ({ runTSC }) => { test.skip('my test', async () => {}); test.fixme('my test', async () => {}); test.fail('my test', async () => {}); + test.fail.only('my test', async () => {}); }); test.describe(() => { test('my test', () => {}); @@ -59,6 +60,7 @@ test('basics should work', async ({ runTSC }) => { test.fixme('title', { tag: '@foo' }, () => {}); test.only('title', { tag: '@foo' }, () => {}); test.fail('title', { tag: '@foo' }, () => {}); + test.fail.only('title', { tag: '@foo' }, () => {}); test.describe('title', { tag: '@foo' }, () => {}); test.describe('title', { annotation: { type: 'issue' } }, () => {}); // @ts-expect-error diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index be1fa7ee37..54fffb5345 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -110,11 +110,14 @@ export interface TestType boolean, description?: string): void; - fail(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; - fail(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; - fail(condition: boolean, description?: string): void; - fail(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; - fail(): void; + fail: { + (title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + (title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + (condition: boolean, description?: string): void; + (callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; + (): void; + only: TestFunction; + } slow(): void; slow(condition: boolean, description?: string): void; slow(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; From 9848ebec5a760839e226e1ef5ccf7dfb9f45f2d2 Mon Sep 17 00:00:00 2001 From: Debbie O'Brien Date: Wed, 16 Oct 2024 18:36:46 +0200 Subject: [PATCH 10/35] docs: add video to release notes (#33147) --- docs/src/release-notes-js.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index b366c43dbd..f6bd430163 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube'; ## Version 1.48 + + ### WebSocket routing New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWebSocket`] allow to intercept, modify and mock WebSocket connections initiated in the page. Below is a simple example that mocks WebSocket communication by responding to a `"request"` with a `"response"`. From 7af9e93304797481e8f8725581b0ead7530f2a56 Mon Sep 17 00:00:00 2001 From: Amaechi Hope <51549388+Honyii@users.noreply.github.com> Date: Thu, 17 Oct 2024 08:11:53 +0100 Subject: [PATCH 11/35] docs(api): fix code snippets for locator ele (#33153) --- docs/src/locators.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/src/locators.md b/docs/src/locators.md index 648a654177..c3a2817670 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -62,11 +62,11 @@ expect(page.get_by_text("Welcome, John!")).to_be_visible() ``` ```csharp -await page.GetByLabel("User Name").FillAsync("John"); +await Page.GetByLabel("User Name").FillAsync("John"); -await page.GetByLabel("Password").FillAsync("secret-password"); +await Page.GetByLabel("Password").FillAsync("secret-password"); -await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); +await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync(); ``` @@ -101,7 +101,7 @@ page.get_by_role("button", name="Sign in").click() ``` ```csharp -await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); +await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); ``` :::note @@ -143,7 +143,7 @@ locator.click() ``` ```csharp -var locator = page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }); +var locator = Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }); await locator.HoverAsync(); await locator.ClickAsync(); @@ -180,7 +180,7 @@ locator.click() ``` ```csharp -var locator = page +var locator = Page .FrameLocator("#my-frame") .GetByRole(AriaRole.Button, new() { Name = "Sign in" }); @@ -249,11 +249,11 @@ await Expect(Page .GetByRole(AriaRole.Heading, new() { Name = "Sign up" })) .ToBeVisibleAsync(); -await page +await Page .GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" }) .CheckAsync(); -await page +await Page .GetByRole(AriaRole.Button, new() { NameRegex = new Regex("submit", RegexOptions.IgnoreCase) }) @@ -298,7 +298,7 @@ page.get_by_label("Password").fill("secret") ``` ```csharp -await page.GetByLabel("Password").FillAsync("secret"); +await Page.GetByLabel("Password").FillAsync("secret"); ``` :::note[When to use label locators] @@ -335,7 +335,7 @@ page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com") ``` ```csharp -await page +await Page .GetByPlaceholder("name@example.com") .FillAsync("playwright@microsoft.com"); ``` @@ -468,7 +468,7 @@ page.get_by_alt_text("playwright logo").click() ``` ```csharp -await page.GetByAltText("playwright logo").ClickAsync(); +await Page.GetByAltText("playwright logo").ClickAsync(); ``` :::note[When to use alt locators] @@ -540,7 +540,7 @@ page.get_by_test_id("directions").click() ``` ```csharp -await page.GetByTestId("directions").ClickAsync(); +await Page.GetByTestId("directions").ClickAsync(); ``` :::note[When to use testid locators] @@ -604,7 +604,7 @@ page.get_by_test_id("directions").click() ``` ```csharp -await page.GetByTestId("directions").ClickAsync(); +await Page.GetByTestId("directions").ClickAsync(); ``` ### Locate by CSS or XPath @@ -644,11 +644,11 @@ page.locator("//button").click() ``` ```csharp -await page.Locator("css=button").ClickAsync(); -await page.Locator("xpath=//button").ClickAsync(); +await Page.Locator("css=button").ClickAsync(); +await Page.Locator("xpath=//button").ClickAsync(); -await page.Locator("button").ClickAsync(); -await page.Locator("//button").ClickAsync(); +await Page.Locator("button").ClickAsync(); +await Page.Locator("//button").ClickAsync(); ``` XPath and CSS selectors can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. Long CSS or XPath chains below are an example of a **bad practice** that leads to unstable tests: @@ -688,9 +688,9 @@ page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click() ``` ```csharp -await page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync(); +await Page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync(); -await page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync(); +await Page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync(); ``` :::note[When to use this] From 2d150eec25958f28e0b0ba7ae214572f1df7546b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 17 Oct 2024 03:34:05 -0700 Subject: [PATCH 12/35] fix: correct types for things like `test.describe.only` (#33142) --- packages/playwright/types/test.d.ts | 1282 ++++++++++++++++------ utils/generate_types/index.js | 23 +- utils/generate_types/overrides-test.d.ts | 91 +- utils/generate_types/parseOverrides.js | 6 +- 4 files changed, 1039 insertions(+), 363 deletions(-) diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 78f8eb7f4f..a989fe69b2 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1857,8 +1857,316 @@ export type TestDetails = { annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } -interface SuiteFunction { +type TestBody = (args: TestArgs, testInfo: TestInfo) => Promise | void; +type ConditionBody = (args: TestArgs) => boolean; + +/** + * Playwright Test provides a `test` function to declare tests and `expect` function to write assertions. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * const name = await page.innerText('.navbar__title'); + * expect(name).toBe('Playwright'); + * }); + * ``` + * + */ +export interface TestType { /** + * Declares a test. + * - `test(title, body)` + * - `test(title, details, body)` + * + * **Usage** + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * **Tags** + * + * You can tag tests by providing additional test details. Alternatively, you can include tags in the test title. Note + * that each tag must start with `@` symbol. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', { + * tag: '@smoke', + * }, async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * + * test('another test @smoke', async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * Test tags are displayed in the test report, and are available to a custom reporter via `TestCase.tags` property. + * + * You can also filter tests by their tags during test execution: + * - in the [command line](https://playwright.dev/docs/test-cli#reference); + * - in the config with [testConfig.grep](https://playwright.dev/docs/api/class-testconfig#test-config-grep) and + * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep); + * + * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). + * + * **Annotations** + * + * You can annotate tests by providing additional test details. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', { + * annotation: { + * type: 'issue', + * description: 'https://github.com/microsoft/playwright/issues/23180', + * }, + * }, async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * Test annotations are displayed in the test report, and are available to a custom reporter via + * `TestCase.annotations` property. + * + * You can also add annotations during runtime by manipulating + * [testInfo.annotations](https://playwright.dev/docs/api/class-testinfo#test-info-annotations). + * + * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). + * @param title Test title. + * @param details Additional test details. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + (title: string, body: TestBody): void; + /** + * Declares a test. + * - `test(title, body)` + * - `test(title, details, body)` + * + * **Usage** + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * **Tags** + * + * You can tag tests by providing additional test details. Alternatively, you can include tags in the test title. Note + * that each tag must start with `@` symbol. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', { + * tag: '@smoke', + * }, async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * + * test('another test @smoke', async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * Test tags are displayed in the test report, and are available to a custom reporter via `TestCase.tags` property. + * + * You can also filter tests by their tags during test execution: + * - in the [command line](https://playwright.dev/docs/test-cli#reference); + * - in the config with [testConfig.grep](https://playwright.dev/docs/api/class-testconfig#test-config-grep) and + * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep); + * + * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). + * + * **Annotations** + * + * You can annotate tests by providing additional test details. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test('basic test', { + * annotation: { + * type: 'issue', + * description: 'https://github.com/microsoft/playwright/issues/23180', + * }, + * }, async ({ page }) => { + * await page.goto('https://playwright.dev/'); + * // ... + * }); + * ``` + * + * Test annotations are displayed in the test report, and are available to a custom reporter via + * `TestCase.annotations` property. + * + * You can also add annotations during runtime by manipulating + * [testInfo.annotations](https://playwright.dev/docs/api/class-testinfo#test-info-annotations). + * + * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). + * @param title Test title. + * @param details Additional test details. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + (title: string, details: TestDetails, body: TestBody): void; + + /** + * Declares a focused test. If there are some focused tests or suites, all of them will be run but nothing else. + * - `test.only(title, body)` + * - `test.only(title, details, body)` + * + * **Usage** + * + * ```js + * test.only('focus this test', async ({ page }) => { + * // Run only focused tests in the entire project. + * }); + * ``` + * + * @param title Test title. + * @param details See [test.(call)(title[, details, body])](https://playwright.dev/docs/api/class-test#test-call) for test details + * description. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + only(title: string, body: TestBody): void; + /** + * Declares a focused test. If there are some focused tests or suites, all of them will be run but nothing else. + * - `test.only(title, body)` + * - `test.only(title, details, body)` + * + * **Usage** + * + * ```js + * test.only('focus this test', async ({ page }) => { + * // Run only focused tests in the entire project. + * }); + * ``` + * + * @param title Test title. + * @param details See [test.(call)(title[, details, body])](https://playwright.dev/docs/api/class-test#test-call) for test details + * description. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + only(title: string, details: TestDetails, body: TestBody): void; + + /** + * Declares a group of tests. + * - `test.describe(title, callback)` + * - `test.describe(callback)` + * - `test.describe(title, details, callback)` + * + * **Usage** + * + * You can declare a group of tests with a title. The title will be visible in the test report as a part of each + * test's title. + * + * ```js + * test.describe('two tests', () => { + * test('one', async ({ page }) => { + * // ... + * }); + * + * test('two', async ({ page }) => { + * // ... + * }); + * }); + * ``` + * + * **Anonymous group** + * + * You can also declare a test group without a title. This is convenient to give a group of tests a common option with + * [test.use(options)](https://playwright.dev/docs/api/class-test#test-use). + * + * ```js + * test.describe(() => { + * test.use({ colorScheme: 'dark' }); + * + * test('one', async ({ page }) => { + * // ... + * }); + * + * test('two', async ({ page }) => { + * // ... + * }); + * }); + * ``` + * + * **Tags** + * + * You can tag all tests in a group by providing additional details. Note that each tag must start with `@` symbol. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.describe('two tagged tests', { + * tag: '@smoke', + * }, () => { + * test('one', async ({ page }) => { + * // ... + * }); + * + * test('two', async ({ page }) => { + * // ... + * }); + * }); + * ``` + * + * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). + * + * **Annotations** + * + * You can annotate all tests in a group by providing additional details. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.describe('two annotated tests', { + * annotation: { + * type: 'issue', + * description: 'https://github.com/microsoft/playwright/issues/23180', + * }, + * }, () => { + * test('one', async ({ page }) => { + * // ... + * }); + * + * test('two', async ({ page }) => { + * // ... + * }); + * }); + * ``` + * + * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). + * @param title Group title. + * @param details Additional details for all tests in the group. + * @param callback A callback that is run immediately when calling + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Any tests + * declared in this callback will belong to the group. + */ + describe: { + /** * Declares a group of tests. * - `test.describe(title, callback)` * - `test.describe(callback)` @@ -1953,7 +2261,7 @@ interface SuiteFunction { * declared in this callback will belong to the group. */ (title: string, callback: () => void): void; - /** + /** * Declares a group of tests. * - `test.describe(title, callback)` * - `test.describe(callback)` @@ -2048,7 +2356,7 @@ interface SuiteFunction { * declared in this callback will belong to the group. */ (callback: () => void): void; - /** + /** * Declares a group of tests. * - `test.describe(title, callback)` * - `test.describe(callback)` @@ -2143,295 +2451,7 @@ interface SuiteFunction { * declared in this callback will belong to the group. */ (title: string, details: TestDetails, callback: () => void): void; -} -interface TestFunction { - /** - * Declares a test. - * - `test(title, body)` - * - `test(title, details, body)` - * - * **Usage** - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * **Tags** - * - * You can tag tests by providing additional test details. Alternatively, you can include tags in the test title. Note - * that each tag must start with `@` symbol. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', { - * tag: '@smoke', - * }, async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * - * test('another test @smoke', async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * Test tags are displayed in the test report, and are available to a custom reporter via `TestCase.tags` property. - * - * You can also filter tests by their tags during test execution: - * - in the [command line](https://playwright.dev/docs/test-cli#reference); - * - in the config with [testConfig.grep](https://playwright.dev/docs/api/class-testconfig#test-config-grep) and - * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep); - * - * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). - * - * **Annotations** - * - * You can annotate tests by providing additional test details. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', { - * annotation: { - * type: 'issue', - * description: 'https://github.com/microsoft/playwright/issues/23180', - * }, - * }, async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * Test annotations are displayed in the test report, and are available to a custom reporter via - * `TestCase.annotations` property. - * - * You can also add annotations during runtime by manipulating - * [testInfo.annotations](https://playwright.dev/docs/api/class-testinfo#test-info-annotations). - * - * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). - * @param title Test title. - * @param details Additional test details. - * @param body Test body that takes one or two arguments: an object with fixtures and optional - * [TestInfo](https://playwright.dev/docs/api/class-testinfo). - */ - (title: string, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; - /** - * Declares a test. - * - `test(title, body)` - * - `test(title, details, body)` - * - * **Usage** - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * **Tags** - * - * You can tag tests by providing additional test details. Alternatively, you can include tags in the test title. Note - * that each tag must start with `@` symbol. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', { - * tag: '@smoke', - * }, async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * - * test('another test @smoke', async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * Test tags are displayed in the test report, and are available to a custom reporter via `TestCase.tags` property. - * - * You can also filter tests by their tags during test execution: - * - in the [command line](https://playwright.dev/docs/test-cli#reference); - * - in the config with [testConfig.grep](https://playwright.dev/docs/api/class-testconfig#test-config-grep) and - * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep); - * - * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). - * - * **Annotations** - * - * You can annotate tests by providing additional test details. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', { - * annotation: { - * type: 'issue', - * description: 'https://github.com/microsoft/playwright/issues/23180', - * }, - * }, async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * // ... - * }); - * ``` - * - * Test annotations are displayed in the test report, and are available to a custom reporter via - * `TestCase.annotations` property. - * - * You can also add annotations during runtime by manipulating - * [testInfo.annotations](https://playwright.dev/docs/api/class-testinfo#test-info-annotations). - * - * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). - * @param title Test title. - * @param details Additional test details. - * @param body Test body that takes one or two arguments: an object with fixtures and optional - * [TestInfo](https://playwright.dev/docs/api/class-testinfo). - */ - (title: string, details: TestDetails, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; -} - -/** - * Playwright Test provides a `test` function to declare tests and `expect` function to write assertions. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test('basic test', async ({ page }) => { - * await page.goto('https://playwright.dev/'); - * const name = await page.innerText('.navbar__title'); - * expect(name).toBe('Playwright'); - * }); - * ``` - * - */ -export interface TestType extends TestFunction { - /** - * Declares a focused test. If there are some focused tests or suites, all of them will be run but nothing else. - * - `test.only(title, body)` - * - `test.only(title, details, body)` - * - * **Usage** - * - * ```js - * test.only('focus this test', async ({ page }) => { - * // Run only focused tests in the entire project. - * }); - * ``` - * - * @param title Test title. - * @param details See [test.(call)(title[, details, body])](https://playwright.dev/docs/api/class-test#test-call) for test details - * description. - * @param body Test body that takes one or two arguments: an object with fixtures and optional - * [TestInfo](https://playwright.dev/docs/api/class-testinfo). - */ - only: TestFunction; - /** - * Declares a group of tests. - * - `test.describe(title, callback)` - * - `test.describe(callback)` - * - `test.describe(title, details, callback)` - * - * **Usage** - * - * You can declare a group of tests with a title. The title will be visible in the test report as a part of each - * test's title. - * - * ```js - * test.describe('two tests', () => { - * test('one', async ({ page }) => { - * // ... - * }); - * - * test('two', async ({ page }) => { - * // ... - * }); - * }); - * ``` - * - * **Anonymous group** - * - * You can also declare a test group without a title. This is convenient to give a group of tests a common option with - * [test.use(options)](https://playwright.dev/docs/api/class-test#test-use). - * - * ```js - * test.describe(() => { - * test.use({ colorScheme: 'dark' }); - * - * test('one', async ({ page }) => { - * // ... - * }); - * - * test('two', async ({ page }) => { - * // ... - * }); - * }); - * ``` - * - * **Tags** - * - * You can tag all tests in a group by providing additional details. Note that each tag must start with `@` symbol. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test.describe('two tagged tests', { - * tag: '@smoke', - * }, () => { - * test('one', async ({ page }) => { - * // ... - * }); - * - * test('two', async ({ page }) => { - * // ... - * }); - * }); - * ``` - * - * Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests). - * - * **Annotations** - * - * You can annotate all tests in a group by providing additional details. - * - * ```js - * import { test, expect } from '@playwright/test'; - * - * test.describe('two annotated tests', { - * annotation: { - * type: 'issue', - * description: 'https://github.com/microsoft/playwright/issues/23180', - * }, - * }, () => { - * test('one', async ({ page }) => { - * // ... - * }); - * - * test('two', async ({ page }) => { - * // ... - * }); - * }); - * ``` - * - * Learn more about [test annotations](https://playwright.dev/docs/test-annotations). - * @param title Group title. - * @param details Additional details for all tests in the group. - * @param callback A callback that is run immediately when calling - * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Any tests - * declared in this callback will belong to the group. - */ - describe: SuiteFunction & { /** * Declares a focused group of tests. If there are some focused tests or suites, all of them will be run but nothing * else. @@ -2467,7 +2487,80 @@ export interface TestType void): void; + /** + * Declares a focused group of tests. If there are some focused tests or suites, all of them will be run but nothing + * else. + * - `test.describe.only(title, callback)` + * - `test.describe.only(callback)` + * - `test.describe.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.only('focused group', () => { + * test('in the focused group', async ({ page }) => { + * // This test will run + * }); + * }); + * test('not in the focused group', async ({ page }) => { + * // This test will not run + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.only([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-only). + * Any tests added in this callback will belong to the group. + */ + only(callback: () => void): void; + /** + * Declares a focused group of tests. If there are some focused tests or suites, all of them will be run but nothing + * else. + * - `test.describe.only(title, callback)` + * - `test.describe.only(callback)` + * - `test.describe.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.only('focused group', () => { + * test('in the focused group', async ({ page }) => { + * // This test will run + * }); + * }); + * test('not in the focused group', async ({ page }) => { + * // This test will not run + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.only([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-only). + * Any tests added in this callback will belong to the group. + */ + only(title: string, details: TestDetails, callback: () => void): void; + /** * Declares a skipped test group, similarly to * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in the @@ -2501,7 +2594,76 @@ export interface TestType void): void; + /** + * Declares a skipped test group, similarly to + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in the + * skipped group are never run. + * - `test.describe.skip(title, callback)` + * - `test.describe.skip(title)` + * - `test.describe.skip(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.skip('skipped group', () => { + * test('example', async ({ page }) => { + * // This test will not run + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.skip(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.skip(title[, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-skip). + * Any tests added in this callback will belong to the group, and will not be run. + */ + skip(callback: () => void): void; + /** + * Declares a skipped test group, similarly to + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in the + * skipped group are never run. + * - `test.describe.skip(title, callback)` + * - `test.describe.skip(title)` + * - `test.describe.skip(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.skip('skipped group', () => { + * test('example', async ({ page }) => { + * // This test will not run + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.skip(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.skip(title[, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-skip). + * Any tests added in this callback will belong to the group, and will not be run. + */ + skip(title: string, details: TestDetails, callback: () => void): void; + /** * Declares a test group similarly to * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in @@ -2535,7 +2697,76 @@ export interface TestType void): void; + /** + * Declares a test group similarly to + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in + * this group are marked as "fixme" and will not be executed. + * - `test.describe.fixme(title, callback)` + * - `test.describe.fixme(callback)` + * - `test.describe.fixme(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.fixme('broken tests that should be fixed', () => { + * test('example', async ({ page }) => { + * // This test will not run + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.fixme(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.fixme([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-fixme). + * Any tests added in this callback will belong to the group, and will not be run. + */ + fixme(callback: () => void): void; + /** + * Declares a test group similarly to + * [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe). Tests in + * this group are marked as "fixme" and will not be executed. + * - `test.describe.fixme(title, callback)` + * - `test.describe.fixme(callback)` + * - `test.describe.fixme(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.fixme('broken tests that should be fixed', () => { + * test('example', async ({ page }) => { + * // This test will not run + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.fixme(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.fixme([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-fixme). + * Any tests added in this callback will belong to the group, and will not be run. + */ + fixme(title: string, details: TestDetails, callback: () => void): void; + /** * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for * the preferred way of configuring the execution mode. @@ -2574,7 +2805,125 @@ export interface TestType { + * test('runs first', async ({ page }) => {}); + * test('runs second', async ({ page }) => {}); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.serial(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.serial([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-serial). + * Any tests added in this callback will belong to the group. + */ + (title: string, callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a group of tests that should always be run serially. If one of the tests fails, all subsequent tests are + * skipped. All tests in a group are retried together. + * + * **NOTE** Using serial is not recommended. It is usually better to make your tests isolated, so they can be run + * independently. + * + * - `test.describe.serial(title, callback)` + * - `test.describe.serial(title)` + * - `test.describe.serial(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.serial('group', () => { + * test('runs first', async ({ page }) => {}); + * test('runs second', async ({ page }) => {}); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.serial(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.serial([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-serial). + * Any tests added in this callback will belong to the group. + */ + (callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a group of tests that should always be run serially. If one of the tests fails, all subsequent tests are + * skipped. All tests in a group are retried together. + * + * **NOTE** Using serial is not recommended. It is usually better to make your tests isolated, so they can be run + * independently. + * + * - `test.describe.serial(title, callback)` + * - `test.describe.serial(title)` + * - `test.describe.serial(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.serial('group', () => { + * test('runs first', async ({ page }) => {}); + * test('runs second', async ({ page }) => {}); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.serial(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.serial([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-serial). + * Any tests added in this callback will belong to the group. + */ + (title: string, details: TestDetails, callback: () => void): void; + /** * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for * the preferred way of configuring the execution mode. @@ -2616,8 +2965,93 @@ export interface TestType void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a focused group of tests that should always be run serially. If one of the tests fails, all subsequent + * tests are skipped. All tests in a group are retried together. If there are some focused tests or suites, all of + * them will be run but nothing else. + * + * **NOTE** Using serial is not recommended. It is usually better to make your tests isolated, so they can be run + * independently. + * + * - `test.describe.serial.only(title, callback)` + * - `test.describe.serial.only(title)` + * - `test.describe.serial.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.serial.only('group', () => { + * test('runs first', async ({ page }) => { + * }); + * test('runs second', async ({ page }) => { + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.serial.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.serial.only(title[, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-serial-only). + * Any tests added in this callback will belong to the group. + */ + only(callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a focused group of tests that should always be run serially. If one of the tests fails, all subsequent + * tests are skipped. All tests in a group are retried together. If there are some focused tests or suites, all of + * them will be run but nothing else. + * + * **NOTE** Using serial is not recommended. It is usually better to make your tests isolated, so they can be run + * independently. + * + * - `test.describe.serial.only(title, callback)` + * - `test.describe.serial.only(title)` + * - `test.describe.serial.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.serial.only('group', () => { + * test('runs first', async ({ page }) => { + * }); + * test('runs second', async ({ page }) => { + * }); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.serial.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.serial.only(title[, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-serial-only). + * Any tests added in this callback will belong to the group. + */ + only(title: string, details: TestDetails, callback: () => void): void; }; + /** * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for * the preferred way of configuring the execution mode. @@ -2657,7 +3091,128 @@ export interface TestType { + * test('runs in parallel 1', async ({ page }) => {}); + * test('runs in parallel 2', async ({ page }) => {}); + * }); + * ``` + * + * Note that parallel tests are executed in separate processes and cannot share any state or global variables. Each of + * the parallel tests executes all relevant hooks. + * + * You can also omit the title. + * + * ```js + * test.describe.parallel(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel). + * Any tests added in this callback will belong to the group. + */ + (title: string, callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a group of tests that could be run in parallel. By default, tests in a single test file run one after + * another, but using + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel) + * allows them to run in parallel. + * - `test.describe.parallel(title, callback)` + * - `test.describe.parallel(callback)` + * - `test.describe.parallel(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.parallel('group', () => { + * test('runs in parallel 1', async ({ page }) => {}); + * test('runs in parallel 2', async ({ page }) => {}); + * }); + * ``` + * + * Note that parallel tests are executed in separate processes and cannot share any state or global variables. Each of + * the parallel tests executes all relevant hooks. + * + * You can also omit the title. + * + * ```js + * test.describe.parallel(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel). + * Any tests added in this callback will belong to the group. + */ + (callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a group of tests that could be run in parallel. By default, tests in a single test file run one after + * another, but using + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel) + * allows them to run in parallel. + * - `test.describe.parallel(title, callback)` + * - `test.describe.parallel(callback)` + * - `test.describe.parallel(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.parallel('group', () => { + * test('runs in parallel 1', async ({ page }) => {}); + * test('runs in parallel 2', async ({ page }) => {}); + * }); + * ``` + * + * Note that parallel tests are executed in separate processes and cannot share any state or global variables. Each of + * the parallel tests executes all relevant hooks. + * + * You can also omit the title. + * + * ```js + * test.describe.parallel(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel). + * Any tests added in this callback will belong to the group. + */ + (title: string, details: TestDetails, callback: () => void): void; + /** * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for * the preferred way of configuring the execution mode. @@ -2693,8 +3248,81 @@ export interface TestType void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a focused group of tests that could be run in parallel. This is similar to + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel), + * but focuses the group. If there are some focused tests or suites, all of them will be run but nothing else. + * - `test.describe.parallel.only(title, callback)` + * - `test.describe.parallel.only(callback)` + * - `test.describe.parallel.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.parallel.only('group', () => { + * test('runs in parallel 1', async ({ page }) => {}); + * test('runs in parallel 2', async ({ page }) => {}); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.parallel.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.parallel.only([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel-only). + * Any tests added in this callback will belong to the group. + */ + only(callback: () => void): void; + /** + * **NOTE** See [test.describe.configure([options])](https://playwright.dev/docs/api/class-test#test-describe-configure) for + * the preferred way of configuring the execution mode. + * + * Declares a focused group of tests that could be run in parallel. This is similar to + * [test.describe.parallel([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel), + * but focuses the group. If there are some focused tests or suites, all of them will be run but nothing else. + * - `test.describe.parallel.only(title, callback)` + * - `test.describe.parallel.only(callback)` + * - `test.describe.parallel.only(title, details, callback)` + * + * **Usage** + * + * ```js + * test.describe.parallel.only('group', () => { + * test('runs in parallel 1', async ({ page }) => {}); + * test('runs in parallel 2', async ({ page }) => {}); + * }); + * ``` + * + * You can also omit the title. + * + * ```js + * test.describe.parallel.only(() => { + * // ... + * }); + * ``` + * + * @param title Group title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for + * details description. + * @param callback A callback that is run immediately when calling + * [test.describe.parallel.only([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe-parallel-only). + * Any tests added in this callback will belong to the group. + */ + only(title: string, details: TestDetails, callback: () => void): void; }; + /** * Configures the enclosing scope. Can be executed either on the top level or inside a describe. Configuration applies * to the entire scope, regardless of whether it run before or after the test declaration. @@ -2754,6 +3382,7 @@ export interface TestType void; }; + /** * Skip a test. Playwright will not run the test past the `test.skip()` call. * @@ -2834,7 +3463,7 @@ export interface TestType Promise | void): void; + skip(title: string, body: TestBody): void; /** * Skip a test. Playwright will not run the test past the `test.skip()` call. * @@ -2915,7 +3544,7 @@ export interface TestType Promise | void): void; + skip(title: string, details: TestDetails, body: TestBody): void; /** * Skip a test. Playwright will not run the test past the `test.skip()` call. * @@ -3158,7 +3787,8 @@ export interface TestType boolean, description?: string): void; + skip(callback: ConditionBody, description?: string): void; + /** * Mark a test as "fixme", with the intention to fix it. Playwright will not run the test past the `test.fixme()` * call. @@ -3236,7 +3866,7 @@ export interface TestType Promise | void): void; + fixme(title: string, body: TestBody): void; /** * Mark a test as "fixme", with the intention to fix it. Playwright will not run the test past the `test.fixme()` * call. @@ -3314,7 +3944,7 @@ export interface TestType Promise | void): void; + fixme(title: string, details: TestDetails, body: TestBody): void; /** * Mark a test as "fixme", with the intention to fix it. Playwright will not run the test past the `test.fixme()` * call. @@ -3548,7 +4178,8 @@ export interface TestType boolean, description?: string): void; + fixme(callback: ConditionBody, description?: string): void; + /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. @@ -3702,7 +4333,7 @@ export interface TestType Promise | void): void; + (title: string, body: TestBody): void; /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. @@ -3779,7 +4410,7 @@ export interface TestType Promise | void): void; + (title: string, details: TestDetails, body: TestBody): void; /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. @@ -3933,7 +4564,7 @@ export interface TestType boolean, description?: string): void; + (callback: ConditionBody, description?: string): void; /** * Marks a test as "should fail". Playwright runs this test and ensures that it is actually failing. This is useful * for documentation purposes to acknowledge that some functionality is broken until it is fixed. @@ -4011,6 +4642,7 @@ export interface TestType; + only(title: string, body: TestBody): void; + /** + * You can use `test.fail.only` to focus on a specific test that is expected to fail. This is particularly useful when + * debugging a failing test or working on a specific issue. + * + * To declare a focused "failing" test: + * - `test.fail.only(title, body)` + * - `test.fail.only(title, details, body)` + * + * **Usage** + * + * You can declare a focused failing test, so that Playwright runs only this test and ensures it actually fails. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.fail.only('focused failing test', async ({ page }) => { + * // This test is expected to fail + * }); + * test('not in the focused group', async ({ page }) => { + * // This test will not run + * }); + * ``` + * + * @param title Test title. + * @param details See [test.describe([title, details, callback])](https://playwright.dev/docs/api/class-test#test-describe) for test + * details description. + * @param body Test body that takes one or two arguments: an object with fixtures and optional + * [TestInfo](https://playwright.dev/docs/api/class-testinfo). + */ + only(title: string, details: TestDetails, body: TestBody): void; } + /** * Marks a test as "slow". Slow test will be given triple the default timeout. * @@ -4215,7 +4878,8 @@ export interface TestType boolean, description?: string): void; + slow(callback: ConditionBody, description?: string): void; + /** * Changes the timeout for the test. Zero means no timeout. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts). * diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index ae988ac32c..b3ad1c4f23 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -93,25 +93,8 @@ class TypesGenerator { handledClasses.add(className); return this.writeComment(docClass.comment, '') + '\n'; }, (className, methodName, overloadIndex) => { - if (className === 'SuiteFunction' && methodName === '__call') { - const cls = this.documentation.classes.get('Test'); - if (!cls) - throw new Error(`Unknown class "Test"`); - const method = cls.membersArray.find(m => m.alias === 'describe'); - if (!method) - throw new Error(`Unknown method "Test.describe"`); - return this.memberJSDOC(method, ' ').trimLeft(); - } - if (className === 'TestFunction' && methodName === '__call') { - const cls = this.documentation.classes.get('Test'); - if (!cls) - throw new Error(`Unknown class "Test"`); - const method = cls.membersArray.find(m => m.alias === '(call)'); - if (!method) - throw new Error(`Unknown method "Test.(call)"`); - return this.memberJSDOC(method, ' ').trimLeft(); - } - + if (methodName === '__call') + methodName = '(call)'; const docClass = this.docClassForName(className); let method; if (docClass) { @@ -591,8 +574,6 @@ class TypesGenerator { 'PlaywrightWorkerArgs.playwright', 'PlaywrightWorkerOptions.defaultBrowserType', 'Project', - 'SuiteFunction', - 'TestFunction', ]), doNotExportClassNames: assertionClasses, }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 54fffb5345..ff46ba0e5c 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -75,52 +75,83 @@ export type TestDetails = { annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } -interface SuiteFunction { - (title: string, callback: () => void): void; - (callback: () => void): void; - (title: string, details: TestDetails, callback: () => void): void; -} +type TestBody = (args: TestArgs, testInfo: TestInfo) => Promise | void; +type ConditionBody = (args: TestArgs) => boolean; -interface TestFunction { - (title: string, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; - (title: string, details: TestDetails, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; -} +export interface TestType { + (title: string, body: TestBody): void; + (title: string, details: TestDetails, body: TestBody): void; -export interface TestType extends TestFunction { - only: TestFunction; - describe: SuiteFunction & { - only: SuiteFunction; - skip: SuiteFunction; - fixme: SuiteFunction; - serial: SuiteFunction & { - only: SuiteFunction; + only(title: string, body: TestBody): void; + only(title: string, details: TestDetails, body: TestBody): void; + + describe: { + (title: string, callback: () => void): void; + (callback: () => void): void; + (title: string, details: TestDetails, callback: () => void): void; + + only(title: string, callback: () => void): void; + only(callback: () => void): void; + only(title: string, details: TestDetails, callback: () => void): void; + + skip(title: string, callback: () => void): void; + skip(callback: () => void): void; + skip(title: string, details: TestDetails, callback: () => void): void; + + fixme(title: string, callback: () => void): void; + fixme(callback: () => void): void; + fixme(title: string, details: TestDetails, callback: () => void): void; + + serial: { + (title: string, callback: () => void): void; + (callback: () => void): void; + (title: string, details: TestDetails, callback: () => void): void; + + only(title: string, callback: () => void): void; + only(callback: () => void): void; + only(title: string, details: TestDetails, callback: () => void): void; }; - parallel: SuiteFunction & { - only: SuiteFunction; + + parallel: { + (title: string, callback: () => void): void; + (callback: () => void): void; + (title: string, details: TestDetails, callback: () => void): void; + + only(title: string, callback: () => void): void; + only(callback: () => void): void; + only(title: string, details: TestDetails, callback: () => void): void; }; + configure: (options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) => void; }; - skip(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; - skip(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + + skip(title: string, body: TestBody): void; + skip(title: string, details: TestDetails, body: TestBody): void; skip(): void; skip(condition: boolean, description?: string): void; - skip(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; - fixme(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; - fixme(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + skip(callback: ConditionBody, description?: string): void; + + fixme(title: string, body: TestBody): void; + fixme(title: string, details: TestDetails, body: TestBody): void; fixme(): void; fixme(condition: boolean, description?: string): void; - fixme(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; + fixme(callback: ConditionBody, description?: string): void; + fail: { - (title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; - (title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void; + (title: string, body: TestBody): void; + (title: string, details: TestDetails, body: TestBody): void; (condition: boolean, description?: string): void; - (callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; + (callback: ConditionBody, description?: string): void; (): void; - only: TestFunction; + + only(title: string, body: TestBody): void; + only(title: string, details: TestDetails, body: TestBody): void; } + slow(): void; slow(condition: boolean, description?: string): void; - slow(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void; + slow(callback: ConditionBody, description?: string): void; + setTimeout(timeout: number): void; beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; beforeEach(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; diff --git a/utils/generate_types/parseOverrides.js b/utils/generate_types/parseOverrides.js index ad80ea388f..bb96013842 100644 --- a/utils/generate_types/parseOverrides.js +++ b/utils/generate_types/parseOverrides.js @@ -101,9 +101,9 @@ async function parseOverrides(filePath, commentForClass, commentForMethod, extra * @param {ts.Node} node */ function visitProperties(className, prefix, node) { - // This function supports structs like "a: { b: string; c: number, (): void }" - // and inserts comments for "a.b", "a.c", a. - if (ts.isPropertySignature(node)) { + // This function supports structs like "a: { b: string; c: number, (): void, d(): void }" + // and inserts comments for "a.b", "a.c", "a", "a.d". + if (ts.isPropertySignature(node) || ts.isMethodSignature(node)) { const name = checker.getSymbolAtLocation(node.name).getName(); const pos = node.getStart(file, false); replacers.push({ From 65983b4bf8a260401a1de4ee670bf016adb7afb3 Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:08:14 +0100 Subject: [PATCH 13/35] chore(docs): remove dead link to install config (#33160) Signed-off-by: LeoTM <1881059+leotm@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e865883de9..1d420a8768 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ npx playwright install You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers). * [Getting started](https://playwright.dev/docs/intro) -* [Installation configuration](https://playwright.dev/docs/installation) * [API reference](https://playwright.dev/docs/api/class-playwright) ## Capabilities From edb041f9e388e4201964beb49893e812da32a32c Mon Sep 17 00:00:00 2001 From: LeoTM <1881059+leotm@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:12:19 +0100 Subject: [PATCH 14/35] chore(docs): fix documentation url (#33161) Signed-off-by: LeoTM <1881059+leotm@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d420a8768..860e11db65 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ test('Intercept network requests', async ({ page }) => { ## Resources -* [Documentation](https://playwright.dev/docs/intro) +* [Documentation](https://playwright.dev) * [API reference](https://playwright.dev/docs/api/class-playwright/) * [Contribution guide](CONTRIBUTING.md) * [Changelog](https://github.com/microsoft/playwright/releases) From a2a5b102ab8c421ad3da27a4911b900d3eff648c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 17 Oct 2024 06:13:17 -0700 Subject: [PATCH 15/35] chore: update CONTRIBUTING.md (#33138) --- CONTRIBUTING.md | 269 ++++++++++++++++-------------------------------- 1 file changed, 90 insertions(+), 179 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b25a131d44..cb06a17e77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,92 +1,77 @@ # Contributing -- [How to Contribute](#how-to-contribute) - * [Getting Code](#getting-code) - * [Code reviews](#code-reviews) - * [Code Style](#code-style) - * [API guidelines](#api-guidelines) - * [Commit Messages](#commit-messages) - * [Writing Documentation](#writing-documentation) - * [Adding New Dependencies](#adding-new-dependencies) - * [Running & Writing Tests](#running--writing-tests) - * [Public API Coverage](#public-api-coverage) -- [Contributor License Agreement](#contributor-license-agreement) - * [Code of Conduct](#code-of-conduct) +## Choose an issue -## How to Contribute +Playwright **requires an issue** for every contribution, except for minor documentation updates. We strongly recommend to pick an issue labeled `open-to-a-pull-request` for your first contribution to the project. -We strongly recommend that you open an issue before beginning any code modifications. This is particularly important if the changes involve complex logic or if the existing code isn't immediately clear. By doing so, we can discuss and agree upon the best approach to address a bug or implement a feature, ensuring that our efforts are aligned. +If you are passioned about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will facilitate the discussion and you might get some early feedback from project maintainers before spending your time on creating a pull request. -### Getting Code - -Make sure you're running Node.js 20 to verify and upgrade NPM do: +## Make a change +Make sure you're running Node.js 20 or later. ```bash node --version -npm --version -npm i -g npm@latest ``` -1. Clone this repository - - ```bash - git clone https://github.com/microsoft/playwright - cd playwright - ``` - -2. Install dependencies - - ```bash - npm ci - ``` - -3. Build Playwright - - ```bash - npm run build - ``` - -4. Run tests - - This will run a test on line `23` in `page-fill.spec.ts`: - - ```bash - npm run ctest -- page-fill:23 - ``` - - See [here](#running--writing-tests) for more information about running and writing tests. - -### Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. - -### Code Style - -- Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js) -- Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory. - -To run code linter, use: - +Clone the repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first. ```bash -npm run eslint +git clone https://github.com/microsoft/playwright +cd playwright ``` -### API guidelines +Install dependencies and run the build in watch mode. +```bash +npm ci +npm run watch +npx playwright install +``` -When authoring new API methods, consider the following: +Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright). -- Expose as little information as needed. When in doubt, don’t expose new information. -- Methods are used in favor of getters/setters. - - The only exception is namespaces, e.g. `page.keyboard` and `page.coverage` -- All string literals must be lowercase. This includes event names and option values. -- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** common. +Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). -### Commit Messages +Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js). Before creating a pull request, or at any moment during development, run linter to check all kinds of things: + ```bash + npm run lint + ``` -Commit messages should follow the Semantic Commit Messages format: +Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory. + +### Write documentation + +Every part of the public API should be documented in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src), in the same change that adds/changes the API. We use markdown files with custom structure to specify the API. Take a look around for an example. + +Various other files are generated from the API specification. If you are running `npm run watch`, these will be re-generated automatically. + +Larger changes will require updates to the documentation guides as well. This will be made clear during the code review. + +## Add a test + +Playwright requires a test for almost any new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that. + +There are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. The two most important that you need to run locally are: + +- Library tests cover APIs not related to the test runner. + ```bash + # fast path runs all tests in Chromium + npm run ctest + + # slow path runs all tests in three browsers + npm run test + ``` + +- Test runner tests. + ```bash + npm run ttest + ``` + +Since Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests). + +Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows. + +## Write a commit message + +Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format: ``` label(namespace): title @@ -97,131 +82,57 @@ footer ``` 1. *label* is one of the following: - - `fix` - playwright bug fixes. - - `feat` - playwright features. - - `docs` - changes to docs, e.g. `docs(api): ..` to change documentation. - - `test` - changes to playwright tests infrastructure. - - `devops` - build-related work, e.g. CI related patches and general changes to the browser build infrastructure + - `fix` - bug fixes + - `feat` - new features + - `docs` - documentation-only changes + - `test` - test-only changes + - `devops` - changes to the CI or build - `chore` - everything that doesn't fall under previous categories -2. *namespace* is put in parenthesis after label and is optional. Must be lowercase. -3. *title* is a brief summary of changes. -4. *description* is **optional**, new-line separated from title and is in present tense. -5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues. +1. *namespace* is put in parenthesis after label and is optional. Must be lowercase. +1. *title* is a brief summary of changes. +1. *description* is **optional**, new-line separated from title and is in present tense. +1. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues. Example: ``` -fix(firefox): make sure session cookies work +feat(trace viewer): network panel filtering -This patch fixes session cookies in the firefox browser. +This patch adds a filtering toolbar to the network panel. + -Fixes #123, fixes #234 +Fixes #123, references #234. ``` -### Writing Documentation +## Send a pull request -All API classes, methods, and events should have a description in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). There's a [documentation linter](https://github.com/microsoft/playwright/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase. +All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. -To run the documentation linter, use: +After a successful code review, one of the maintainers will merge your pull request. Congratulations! +## More details + +**No new dependencies** + +There is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies. + +**Custom browser build** + +To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable: ```bash -npm run doc +CRPATH= npm run ctest ``` -To build the documentation site locally and test how your changes will look in practice: +You will also find `DEBUG=pw:browser` useful for debugging custom builds. -1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo -1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress -1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes +**Building documentation site** -### Adding New Dependencies +The [playwright.dev](https://playwright.dev/) documentation site lives in a separate repository, and documentation from [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src) is frequently rolled there. -For all dependencies (both installation and development): -- **Do not add** a dependency if the desired functionality is easily implementable. -- If adding a dependency, it should be well-maintained and trustworthy. - -A barrier for introducing new installation dependencies is especially high: -- **Do not add** installation dependency unless it's critical to project success. - -### Running & Writing Tests - -- Every feature should be accompanied by a test. -- Every public api event/method should be accompanied by a test. -- Tests should be *hermetic*. Tests should not depend on external services. -- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests. - -Playwright tests are located in [`tests`](https://github.com/microsoft/playwright/blob/main/tests) and use `@playwright/test` test runner. -These are integration tests, making sure public API methods and events work as expected. - -- To run all tests: - - ```bash - npx playwright install - npm run test - ``` - - Be sure to run `npm run build` or let `npm run watch` run before you re-run the - tests after making your changes to check them. - -- To run tests in Chromium - - ```bash - npm run ctest # also `ftest` for firefox and `wtest` for WebKit - npm run ctest -- page-fill:23 # runs line 23 of page-fill.spec.ts - ``` - -- To run tests in WebKit / Firefox, use `wtest` or `ftest`. - -- To run the Playwright test runner tests - - ```bash - npm run ttest - npm run ttest -- --grep "specific test" - ``` - -- To run a specific test, substitute `it` with `it.only`, or use the `--grep 'My test'` CLI parameter: - - ```js - ... - // Using "it.only" to run a specific test - it.only('should work', async ({server, page}) => { - const response = await page.goto(server.EMPTY_PAGE); - expect(response.ok).toBe(true); - }); - // or - playwright test --config=xxx --grep 'should work' - ``` - -- To disable a specific test, substitute `it` with `it.skip`: - - ```js - ... - // Using "it.skip" to skip a specific test - it.skip('should work', async ({server, page}) => { - const response = await page.goto(server.EMPTY_PAGE); - expect(response.ok).toBe(true); - }); - ``` - -- To run tests in non-headless (headed) mode: - - ```bash - npm run ctest -- --headed - ``` - -- To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable: - - ```bash - CRPATH= npm run ctest - ``` - -- When should a test be marked with `skip` or `fixme`? - - - **`skip(condition)`**: This test *should ***never*** work* for `condition` - where `condition` is usually something like: `test.skip(browserName === 'chromium', 'This does not work because of ...')`. - - - **`fixme(condition)`**: This test *should ***eventually*** work* for `condition` - where `condition` is usually something like: `test.fixme(browserName === 'chromium', 'We are waiting for version x')`. +Most of the time this should not concern you. However, if you are doing something unusual in the docs, you can build locally and test how your changes will look in practice: +1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo. +1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress. +1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes. ## Contributor License Agreement From aa952c1b03d5676c29760793cdefa0bdc78222f9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 17 Oct 2024 08:33:15 -0700 Subject: [PATCH 16/35] fix(html report): generate test snippets when test dir is non-root (#33162) --- packages/html-reporter/src/testErrorView.tsx | 5 +-- packages/html-reporter/src/testResultView.tsx | 2 +- packages/playwright/src/reporters/html.ts | 4 +-- tests/playwright-test/reporter-html.spec.ts | 32 +++++++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx index 520da1fc19..8d2bb13bd3 100644 --- a/packages/html-reporter/src/testErrorView.tsx +++ b/packages/html-reporter/src/testErrorView.tsx @@ -22,9 +22,10 @@ import { ImageDiffView } from '@web/shared/imageDiffView'; export const TestErrorView: React.FC<{ error: string; -}> = ({ error }) => { + testId?: string; +}> = ({ error, testId }) => { const html = React.useMemo(() => ansiErrorToHtml(error), [error]); - return
          ; + return
          ; }; export const TestScreenshotErrorView: React.FC<{ diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 48a24a2391..bb18422dd0 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -186,7 +186,7 @@ const StepTreeItem: React.FC<{ } loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { const children = step.steps.map((s, i) => ); if (step.snippet) - children.unshift(); + children.unshift(); return children; } : undefined} depth={depth}>; }; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 5aada7e495..584c11bae8 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -505,8 +505,8 @@ class HtmlBuilder { error: step.error?.message, count }; - if (result.location) - this._stepsInFile.set(result.location.file, result); + if (step.location) + this._stepsInFile.set(step.location.file, result); return result; } diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 6a75602bf1..d9e604f994 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -43,7 +43,7 @@ const expect = baseExpect.configure({ timeout: process.env.CI ? 75000 : 25000 }) test.describe.configure({ mode: 'parallel' }); -for (const useIntermediateMergeReport of [false] as const) { +for (const useIntermediateMergeReport of [true, false] as const) { test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => { test.use({ useIntermediateMergeReport }); @@ -612,7 +612,7 @@ for (const useIntermediateMergeReport of [false] as const) { ]); }); `, - }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + }, { reporter: 'html,dot' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); @@ -727,6 +727,34 @@ for (const useIntermediateMergeReport of [false] as const) { ]); }); + test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + export default { testDir: './tests' }; + `, + 'tests/a.test.ts': ` + import { test, expect } from '@playwright/test'; + + test('example', async ({}) => { + await test.step('step title', async () => { + expect(1).toBe(1); + }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.click('text=example'); + await page.click('text=step title'); + await page.click('text=expect.toBe'); + await expect(page.getByTestId('test-snippet')).toContainText([ + `await test.step('step title', async () => {`, + 'expect(1).toBe(1);', + ]); + }); + test('should render annotations', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` From 623a8916f9acf76f3a49287f3d63af9f9dac591a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 17 Oct 2024 16:57:45 -0700 Subject: [PATCH 17/35] chore: implement tree w/o list (#33167) --- .../src/ui/uiModeTestListView.css | 12 +- .../src/ui/uiModeTestListView.tsx | 6 +- packages/web/src/components/gridView.tsx | 3 - packages/web/src/components/listView.tsx | 31 +-- packages/web/src/components/treeView.css | 91 +++++++ packages/web/src/components/treeView.tsx | 242 ++++++++++++++---- packages/web/src/uiUtils.ts | 9 + tests/config/traceViewerFixtures.ts | 4 +- tests/playwright-test/ui-mode-fixtures.ts | 16 +- .../ui-mode-test-annotations.spec.ts | 2 +- .../ui-mode-test-filters.spec.ts | 2 +- .../ui-mode-test-progress.spec.ts | 22 +- .../playwright-test/ui-mode-test-run.spec.ts | 4 +- .../ui-mode-test-setup.spec.ts | 2 +- .../ui-mode-test-update.spec.ts | 4 +- .../ui-mode-test-watch.spec.ts | 16 +- tests/playwright-test/ui-mode-trace.spec.ts | 14 +- 17 files changed, 340 insertions(+), 140 deletions(-) create mode 100644 packages/web/src/components/treeView.css diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.css b/packages/trace-viewer/src/ui/uiModeTestListView.css index ae6fd624ee..335daecfb1 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.css +++ b/packages/trace-viewer/src/ui/uiModeTestListView.css @@ -14,28 +14,28 @@ limitations under the License. */ -.ui-mode-list-item { +.ui-mode-tree-item { flex: auto; } -.ui-mode-list-item-title { +.ui-mode-tree-item-title { flex: auto; text-overflow: ellipsis; overflow: hidden; } -.ui-mode-list-item-time { +.ui-mode-tree-item-time { flex: none; color: var(--vscode-editorCodeLens-foreground); margin: 0 4px; user-select: none; } -.tests-list-view .list-view-entry.selected .ui-mode-list-item-time, -.tests-list-view .list-view-entry.highlighted .ui-mode-list-item-time { +.tests-tree-view .tree-view-entry.selected .ui-mode-tree-item-time, +.tests-tree-view .tree-view-entry.highlighted .ui-mode-tree-item-time { display: none; } -.tests-list-view .list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { +.tests-tree-view .tree-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { display: none; } diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index ce1c0fef37..96fbaadbf7 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -159,12 +159,12 @@ export const TestListView: React.FC<{ rootItem={testTree.rootItem} dataTestId='test-tree' render={treeItem => { - return
          -
          + return
          +
          {treeItem.title} {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
          - {!!treeItem.duration && treeItem.status !== 'skipped' &&
          {msToString(treeItem.duration)}
          } + {!!treeItem.duration && treeItem.status !== 'skipped' &&
          {msToString(treeItem.duration)}
          } runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}> diff --git a/packages/web/src/components/gridView.tsx b/packages/web/src/components/gridView.tsx index 5d9b0a4c6c..10fc48c247 100644 --- a/packages/web/src/components/gridView.tsx +++ b/packages/web/src/components/gridView.tsx @@ -110,15 +110,12 @@ export function GridView(model: GridViewProps) { ; }} icon={model.icon} - indent={model.indent} isError={model.isError} isWarning={model.isWarning} isInfo={model.isInfo} selectedItem={model.selectedItem} onAccepted={model.onAccepted} onSelected={model.onSelected} - onLeftArrow={model.onLeftArrow} - onRightArrow={model.onRightArrow} onHighlighted={model.onHighlighted} onIconClicked={model.onIconClicked} noItemsMessage={model.noItemsMessage} diff --git a/packages/web/src/components/listView.tsx b/packages/web/src/components/listView.tsx index 4f2de5ae54..73f9b65b8f 100644 --- a/packages/web/src/components/listView.tsx +++ b/packages/web/src/components/listView.tsx @@ -16,7 +16,7 @@ import * as React from 'react'; import './listView.css'; -import { clsx } from '@web/uiUtils'; +import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils'; export type ListViewProps = { name: string, @@ -24,15 +24,12 @@ export type ListViewProps = { id?: (item: T, index: number) => string, render: (item: T, index: number) => React.ReactNode, icon?: (item: T, index: number) => string | undefined, - indent?: (item: T, index: number) => number | undefined, isError?: (item: T, index: number) => boolean, isWarning?: (item: T, index: number) => boolean, isInfo?: (item: T, index: number) => boolean, selectedItem?: T, onAccepted?: (item: T, index: number) => void, onSelected?: (item: T, index: number) => void, - onLeftArrow?: (item: T, index: number) => void, - onRightArrow?: (item: T, index: number) => void, onHighlighted?: (item: T | undefined) => void, onIconClicked?: (item: T, index: number) => void, noItemsMessage?: string, @@ -51,12 +48,9 @@ export function ListView({ isError, isWarning, isInfo, - indent, selectedItem, onAccepted, onSelected, - onLeftArrow, - onRightArrow, onHighlighted, onIconClicked, noItemsMessage, @@ -95,21 +89,12 @@ export function ListView({ onAccepted?.(selectedItem, items.indexOf(selectedItem)); return; } - if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return; event.stopPropagation(); event.preventDefault(); - if (selectedItem && event.key === 'ArrowLeft') { - onLeftArrow?.(selectedItem, items.indexOf(selectedItem)); - return; - } - if (selectedItem && event.key === 'ArrowRight') { - onRightArrow?.(selectedItem, items.indexOf(selectedItem)); - return; - } - const index = selectedItem ? items.indexOf(selectedItem) : -1; let newIndex = index; if (event.key === 'ArrowDown') { @@ -135,7 +120,6 @@ export function ListView({ > {noItemsMessage && items.length === 0 &&
          {noItemsMessage}
          } {items.map((item, index) => { - const indentation = indent?.(item, index) || 0; const rendered = render(item, index); return
          ({ onMouseEnter={() => setHighlightedItem(item)} onMouseLeave={() => setHighlightedItem(undefined)} > - {/* eslint-disable-next-line react/jsx-key */} - {indentation ? new Array(indentation).fill(0).map(() =>
          ) : undefined} {icon &&
          ({
          ; } - -function scrollIntoViewIfNeeded(element: Element | undefined) { - if (!element) - return; - if ((element as any)?.scrollIntoViewIfNeeded) - (element as any).scrollIntoViewIfNeeded(false); - else - element?.scrollIntoView(); -} diff --git a/packages/web/src/components/treeView.css b/packages/web/src/components/treeView.css new file mode 100644 index 0000000000..860d560fc9 --- /dev/null +++ b/packages/web/src/components/treeView.css @@ -0,0 +1,91 @@ +/* + 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. +*/ + +.tree-view-content { + display: flex; + flex-direction: column; + flex: auto; + position: relative; + user-select: none; + overflow: hidden auto; + outline: 1px solid transparent; +} + +.tree-view-entry { + display: flex; + flex: none; + cursor: pointer; + align-items: center; + white-space: nowrap; + line-height: 28px; + padding-left: 5px; +} + +.tree-view-content.not-selectable > .tree-view-entry { + cursor: inherit; +} + +.tree-view-entry.highlighted:not(.selected) { + background-color: var(--vscode-list-inactiveSelectionBackground) !important; +} + +.tree-view-entry.selected { + z-index: 10; +} + +.tree-view-indent { + min-width: 16px; +} + +.tree-view-content:focus .tree-view-entry.selected { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-focusBorder); +} + +.tree-view-content .tree-view-entry.selected { + background-color: var(--vscode-list-inactiveSelectionBackground); +} + +.tree-view-content:focus .tree-view-entry.selected * { + color: var(--vscode-list-activeSelectionForeground) !important; + background-color: transparent !important; +} + +.tree-view-content:focus .tree-view-entry.selected .codicon { + color: var(--vscode-list-activeSelectionForeground) !important; +} + +.tree-view-empty { + flex: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.tree-view-entry.error { + color: var(--vscode-list-errorForeground); + background-color: var(--vscode-inputValidation-errorBackground); +} + +.tree-view-entry.warning { + color: var(--vscode-list-warningForeground); + background-color: var(--vscode-inputValidation-warningBackground); +} + +.tree-view-entry.info { + background-color: var(--vscode-inputValidation-infoBackground); +} diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 8341056779..6ad7221455 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -15,7 +15,8 @@ */ import * as React from 'react'; -import { ListView } from './listView'; +import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils'; +import './treeView.css'; export type TreeItem = { id: string, @@ -45,7 +46,7 @@ export type TreeViewProps = { autoExpandDepth?: number, }; -const TreeListView = ListView; +const scrollPositions = new Map(); export function TreeView({ name, @@ -97,61 +98,185 @@ export function TreeView({ return result; }, [treeItems, isVisible]); - return item.id} - dataTestId={dataTestId || (name + '-tree')} - render={item => { - const rendered = render(item as T); - return <> - {icon &&
          } - {typeof rendered === 'string' ?
          {rendered}
          : rendered} - ; - }} - icon={item => { - const expanded = treeItems.get(item as T)!.expanded; - if (typeof expanded === 'boolean') - return expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'; - }} - isError={item => isError?.(item as T) || false} - indent={item => treeItems.get(item as T)!.depth} - selectedItem={selectedItem} - onAccepted={item => onAccepted?.(item as T)} - onSelected={item => onSelected?.(item as T)} - onHighlighted={item => onHighlighted?.(item as T)} - onLeftArrow={item => { - const { expanded, parent } = treeItems.get(item as T)!; - if (expanded) { - treeState.expandedItems.set(item.id, false); - setTreeState({ ...treeState }); - } else if (parent) { - onSelected?.(parent as T); - } - }} - onRightArrow={item => { - if (item.children.length) { - treeState.expandedItems.set(item.id, true); - setTreeState({ ...treeState }); - } - }} - onIconClicked={item => { - const { expanded } = treeItems.get(item as T)!; - if (expanded) { - // Move nested selection up. - for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) { - if (i === item) { - onSelected?.(item as T); - break; - } + const itemListRef = React.useRef(null); + const [highlightedItem, setHighlightedItem] = React.useState(); + + React.useEffect(() => { + onHighlighted?.(highlightedItem); + }, [onHighlighted, highlightedItem]); + + React.useEffect(() => { + const treeElem = itemListRef.current; + if (!treeElem) + return; + const saveScrollPosition = () => { + scrollPositions.set(name, treeElem.scrollTop); + }; + treeElem.addEventListener('scroll', saveScrollPosition, { passive: true }); + return () => treeElem.removeEventListener('scroll', saveScrollPosition); + }, [name]); + + React.useEffect(() => { + if (itemListRef.current) + itemListRef.current.scrollTop = scrollPositions.get(name) || 0; + }, [name]); + + const toggleExpanded = React.useCallback((item: T) => { + const { expanded } = treeItems.get(item)!; + if (expanded) { + // Move nested selection up. + for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) { + if (i === item) { + onSelected?.(item as T); + break; } - treeState.expandedItems.set(item.id, false); - } else { - treeState.expandedItems.set(item.id, true); } - setTreeState({ ...treeState }); - }} - noItemsMessage={noItemsMessage} />; + treeState.expandedItems.set(item.id, false); + } else { + treeState.expandedItems.set(item.id, true); + } + setTreeState({ ...treeState }); + }, [treeItems, selectedItem, onSelected, treeState, setTreeState]); + + return
          +
          { + if (selectedItem && event.key === 'Enter') { + onAccepted?.(selectedItem); + return; + } + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') + return; + + event.stopPropagation(); + event.preventDefault(); + + if (selectedItem && event.key === 'ArrowLeft') { + const { expanded, parent } = treeItems.get(selectedItem)!; + if (expanded) { + treeState.expandedItems.set(selectedItem.id, false); + setTreeState({ ...treeState }); + } else if (parent) { + onSelected?.(parent as T); + } + return; + } + if (selectedItem && event.key === 'ArrowRight') { + if (selectedItem.children.length) { + treeState.expandedItems.set(selectedItem.id, true); + setTreeState({ ...treeState }); + } + return; + } + + const index = selectedItem ? visibleItems.indexOf(selectedItem) : -1; + let newIndex = index; + if (event.key === 'ArrowDown') { + if (index === -1) + newIndex = 0; + else + newIndex = Math.min(index + 1, visibleItems.length - 1); + } + if (event.key === 'ArrowUp') { + if (index === -1) + newIndex = visibleItems.length - 1; + else + newIndex = Math.max(index - 1, 0); + } + + const element = itemListRef.current?.children.item(newIndex); + scrollIntoViewIfNeeded(element || undefined); + onHighlighted?.(undefined); + onSelected?.(visibleItems[newIndex]); + setHighlightedItem(undefined); + }} + ref={itemListRef} + > + {noItemsMessage && visibleItems.length === 0 &&
          {noItemsMessage}
          } + {visibleItems.map(item => { + return
          + +
          ; + })} +
          +
          ; +} + +type TreeItemHeaderProps = { + item: T, + itemData: TreeItemData, + selectedItem: T | undefined, + onSelected?: (item: T) => void, + toggleExpanded: (item: T) => void, + highlightedItem: T | undefined, + isError?: (item: T) => boolean, + onAccepted?: (item: T) => void, + setHighlightedItem: (item: T | undefined) => void, + render: (item: T) => React.ReactNode, + icon?: (item: T) => string | undefined, +}; + +export function TreeItemHeader({ + item, + itemData, + selectedItem, + onSelected, + highlightedItem, + setHighlightedItem, + isError, + onAccepted, + toggleExpanded, + render, + icon }: TreeItemHeaderProps) { + + const indentation = itemData.depth; + const expanded = itemData.expanded; + let expandIcon = 'codicon-blank'; + if (typeof expanded === 'boolean') + expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'; + const rendered = render(item); + + return
          onAccepted?.(item)} + className={clsx( + 'tree-view-entry', + selectedItem === item && 'selected', + highlightedItem === item && 'highlighted', + isError?.(item) && 'error')} + onClick={() => onSelected?.(item)} + onMouseEnter={() => setHighlightedItem(item)} + onMouseLeave={() => setHighlightedItem(undefined)} + > + {indentation ? new Array(indentation).fill(0).map((_, i) =>
          ) : undefined} +
          { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + toggleExpanded(item); + }} + /> + {icon &&
          } + {typeof rendered === 'string' ?
          {rendered}
          : rendered} +
          ; } type TreeItemData = { @@ -160,7 +285,12 @@ type TreeItemData = { parent: TreeItem | null, }; -function flattenTree(rootItem: T, selectedItem: T | undefined, expandedItems: Map, autoExpandDepth: number): Map { +function flattenTree( + rootItem: T, + selectedItem: T | undefined, + expandedItems: Map, + autoExpandDepth: number): Map { + const result = new Map(); const temporaryExpanded = new Set(); for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index 2697177c6f..ea71486014 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -208,5 +208,14 @@ export async function sha1(str: string): Promise { return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer))).map(b => b.toString(16).padStart(2, '0')).join(''); } +export function scrollIntoViewIfNeeded(element: Element | undefined) { + if (!element) + return; + if ((element as any)?.scrollIntoViewIfNeeded) + (element as any).scrollIntoViewIfNeeded(false); + else + element?.scrollIntoView(); +} + const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f'; export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug'); diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 0fe4a9a5c9..3eb3b11a15 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -62,13 +62,13 @@ class TraceViewerPage { } async actionIconsText(action: string) { - const entry = await this.page.waitForSelector(`.list-view-entry:has-text("${action}")`); + const entry = await this.page.waitForSelector(`.tree-view-entry:has-text("${action}")`); await entry.waitForSelector('.action-icon-value:visible'); return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent)); } async actionIcons(action: string) { - return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`); + return await this.page.waitForSelector(`.tree-view-entry:has-text("${action}") .action-icons`); } @step diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index 2952761d60..1e3b11a03a 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -66,16 +66,16 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () = } const result: string[] = []; - const listItems = treeElement.querySelectorAll('[role=listitem]'); - for (const listItem of listItems) { - const iconElements = listItem.querySelectorAll('.codicon'); + const treeItems = treeElement.querySelectorAll('[role=treeitem]'); + for (const treeItem of treeItems) { + const iconElements = treeItem.querySelectorAll('.codicon'); const treeIcon = iconName(iconElements[0]); const statusIcon = iconName(iconElements[1]); - const indent = listItem.querySelectorAll('.list-view-indent').length; - const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; - const selected = listItem.classList.contains('selected') ? ' <=' : ''; - const title = listItem.querySelector('.ui-mode-list-item-title').childNodes[0].textContent; - const timeElement = options.time ? listItem.querySelector('.ui-mode-list-item-time') : undefined; + const indent = treeItem.querySelectorAll('.tree-view-indent').length; + const watch = treeItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; + const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : ''; + const title = treeItem.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent; + const timeElement = options.time ? treeItem.querySelector('.ui-mode-tree-item-time') : undefined; const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : ''; result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected); } diff --git a/tests/playwright-test/ui-mode-test-annotations.spec.ts b/tests/playwright-test/ui-mode-test-annotations.spec.ts index 7a0dea8af1..f32d43aecf 100644 --- a/tests/playwright-test/ui-mode-test-annotations.spec.ts +++ b/tests/playwright-test/ui-mode-test-annotations.spec.ts @@ -33,7 +33,7 @@ test('should display annotations', async ({ runUITest }) => { }); await page.getByTitle('Run all').click(); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); - await page.getByRole('listitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click(); + await page.getByRole('treeitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click(); await page.getByText('annotation test').click(); await page.getByText('Annotations', { exact: true }).click(); diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index 5d70048473..dd59c334b2 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -64,7 +64,7 @@ test('should display native tags and filter by them on click', async ({ runUITes test('pwt', { tag: '@smoke' }, () => {}); `, }); - await page.locator('.ui-mode-list-item-title').getByText('smoke').click(); + await page.locator('.ui-mode-tree-item-title').getByText('smoke').click(); await expect(page.getByPlaceholder('Filter')).toHaveValue('@smoke'); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts index f87eaa8fbc..f2f01a79ce 100644 --- a/tests/playwright-test/ui-mode-test-progress.spec.ts +++ b/tests/playwright-test/ui-mode-test-progress.spec.ts @@ -47,7 +47,7 @@ test('should update trace live', async ({ runUITest, server }) => { await page.getByText('live test').dblclick(); // It should halt on loading one.html. - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -57,11 +57,11 @@ test('should update trace live', async ({ runUITest, server }) => { ]); await expect( - listItem.locator(':scope.selected'), + listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' ).toHaveText(/page.goto/); await expect( - listItem.locator(':scope.selected .codicon.codicon-loading'), + listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' ).toBeVisible(); @@ -83,11 +83,11 @@ test('should update trace live', async ({ runUITest, server }) => { /page.gotohttp:\/\/localhost:\d+\/two.html/ ]); await expect( - listItem.locator(':scope.selected'), + listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' ).toHaveText(/page.goto/); await expect( - listItem.locator(':scope.selected .codicon.codicon-loading'), + listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' ).toBeVisible(); @@ -132,7 +132,7 @@ test('should preserve action list selection upon live trace update', async ({ ru await page.getByText('live test').dblclick(); // It should wait on the latch. - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -157,7 +157,7 @@ test('should preserve action list selection upon live trace update', async ({ ru /page.setContent[\d.]+m?s/, ]); await expect( - listItem.locator(':scope.selected'), + listItem.locator(':scope[aria-selected="true"]'), 'selected action stays the same' ).toHaveText(/page.goto/); }); @@ -193,7 +193,7 @@ test('should update tracing network live', async ({ runUITest, server }) => { await page.getByText('live test').dblclick(); // It should wait on the latch. - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -233,7 +233,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat await page.getByText('live test').dblclick(); // It should wait on the latch. - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -278,7 +278,7 @@ test('should show live trace for serial', async ({ runUITest, server, createLatc await page.getByText('two', { exact: true }).click(); await page.getByTitle('Run all').click(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -318,7 +318,7 @@ test('should show live trace from hooks', async ({ runUITest, createLatch }) => `); await page.getByText('test one').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 5ead1889f0..24731bcbb2 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -93,7 +93,7 @@ test('should run on hover', async ({ runUITest }) => { }); await page.getByText('passes').hover(); - await page.getByRole('listitem').filter({ hasText: 'passes' }).getByTitle('Run').click(); + await page.getByRole('treeitem').filter({ hasText: 'passes' }).getByTitle('Run').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts @@ -275,7 +275,7 @@ test('should run folder', async ({ runUITest }) => { }); await page.getByText('folder-b').hover(); - await page.getByRole('listitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click(); + await page.getByRole('treeitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click(); await expect.poll(dumpTestTree(page)).toContain(` ▼ ✅ folder-b <= diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index cd5503427d..f8de9e262a 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -211,7 +211,7 @@ test('should run part of the setup only', async ({ runUITest }) => { await page.getByLabel('test').setChecked(true); await page.getByText('setup.ts').hover(); - await page.getByRole('listitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click(); + await page.getByRole('treeitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ✅ setup.ts <= diff --git a/tests/playwright-test/ui-mode-test-update.spec.ts b/tests/playwright-test/ui-mode-test-update.spec.ts index 61e2c89dc7..ae5752c3f0 100644 --- a/tests/playwright-test/ui-mode-test-update.spec.ts +++ b/tests/playwright-test/ui-mode-test-update.spec.ts @@ -149,7 +149,7 @@ test('should not loose run information after execution if test wrote into testDi await page.getByTitle('Run all').click(); await page.waitForTimeout(5_000); await expect(page.getByText('Did not run')).toBeHidden(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -215,7 +215,7 @@ test('should update test locations', async ({ runUITest, writeFiles }) => { const messages: any[] = []; await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg)); - const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' }); + const passesItemLocator = page.getByRole('treeitem').filter({ hasText: 'passes' }); await passesItemLocator.hover(); await passesItemLocator.getByTitle('Show source').click(); await page.getByTitle('Open in VS Code').click(); diff --git a/tests/playwright-test/ui-mode-test-watch.spec.ts b/tests/playwright-test/ui-mode-test-watch.spec.ts index bd04750a1f..893a0ef7ac 100644 --- a/tests/playwright-test/ui-mode-test-watch.spec.ts +++ b/tests/playwright-test/ui-mode-test-watch.spec.ts @@ -28,14 +28,14 @@ test('should watch files', async ({ runUITest, writeFiles }) => { }); await page.getByText('fails').click(); - await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Watch').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails 👁 <= `); - await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').click(); + await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Run').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ❌ a.test.ts @@ -75,7 +75,7 @@ test('should watch e2e deps', async ({ runUITest, writeFiles }) => { }); await page.getByText('answer').click(); - await page.getByRole('listitem').filter({ hasText: 'answer' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'answer' }).getByTitle('Watch').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts ◯ answer 👁 <= @@ -102,13 +102,13 @@ test('should batch watch updates', async ({ runUITest, writeFiles }) => { }); await page.getByText('a.test.ts').click(); - await page.getByRole('listitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); await page.getByText('b.test.ts').click(); - await page.getByRole('listitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click(); await page.getByText('c.test.ts').click(); - await page.getByRole('listitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click(); await page.getByText('d.test.ts').click(); - await page.getByRole('listitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts 👁 @@ -229,7 +229,7 @@ test('should run added test in watched file', async ({ runUITest, writeFiles }) }); await page.getByText('a.test.ts').click(); - await page.getByRole('listitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts 👁 <= diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 9f0749893e..def44e9aeb 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -34,7 +34,7 @@ test('should merge trace events', async ({ runUITest }) => { await page.getByText('trace test').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -61,7 +61,7 @@ test('should merge web assertion events', async ({ runUITest }, testInfo) => { await page.getByText('trace test').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -86,7 +86,7 @@ test('should merge screenshot assertions', async ({ runUITest }, testInfo) => { await page.getByText('trace test').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -134,7 +134,7 @@ test('should show snapshots for sync assertions', async ({ runUITest }) => { await page.getByText('trace test').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, 'action list' @@ -214,7 +214,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => { }); await page.getByText('pass').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, @@ -241,7 +241,7 @@ test('should not show caught errors in the errors tab', async ({ runUITest }, te }); await page.getByText('pass').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, @@ -272,7 +272,7 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => { }); await page.getByText('pass').dblclick(); - const listItem = page.getByTestId('actions-tree').getByRole('listitem'); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); await expect( listItem, From 29c84a33c386e10dd294a5bcf9f24b9988d1777c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 17 Oct 2024 17:06:18 -0700 Subject: [PATCH 18/35] chore: compute aria text consistently with the role accumulated text (#33157) --- .../src/server/ariaSnapshot.ts | 6 +- .../src/server/injected/ariaSnapshot.ts | 62 +- .../src/server/injected/roleUtils.ts | 107 ++-- .../src/matchers/toMatchAriaSnapshot.ts | 16 +- tests/assets/codicon.css | 596 ++++++++++++++++++ tests/assets/codicon.ttf | Bin 0 -> 80340 bytes tests/page/page-aria-snapshot.spec.ts | 375 ++++++++++- tests/page/to-match-aria-snapshot.spec.ts | 4 +- .../stable-test-runner/package-lock.json | 50 +- .../stable-test-runner/package.json | 2 +- 10 files changed, 1094 insertions(+), 124 deletions(-) create mode 100644 tests/assets/codicon.css create mode 100644 tests/assets/codicon.ttf diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts index 6f89dd21cf..e450e5b15d 100644 --- a/packages/playwright-core/src/server/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -40,8 +40,12 @@ export function parseAriaSnapshot(text: string): AriaTemplateNode { return { role }; }; + const normalizeWhitespace = (text: string) => { + return text.replace(/[\r\n\s\t]+/g, ' ').trim(); + }; + const valueOrRegex = (value: string): string | RegExp => { - return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : value; + return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value); }; const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => { diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 22ec9b5c42..907006ce0a 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -15,13 +15,13 @@ */ import { escapeWithQuotes } from '@isomorphic/stringUtils'; -import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, isElementIgnoredForAria } from './roleUtils'; -import { isElementVisible, isElementStyleVisibilityVisible } from './domUtils'; +import { accumulatedElementText, beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, getPseudoContent, isElementIgnoredForAria } from './roleUtils'; +import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils'; type AriaNode = { role: string; name?: string; - children?: (AriaNode | string)[]; + children: (AriaNode | string)[]; }; export type AriaTemplateNode = { @@ -38,16 +38,20 @@ export function generateAriaTree(rootElement: Element): AriaNode { const name = role ? getElementAccessibleName(element, false) || undefined : undefined; const isLeaf = leafRoles.has(role); - const result: AriaNode = { role, name }; - if (isLeaf && !name && element.textContent) - result.children = [element.textContent]; + const result: AriaNode = { role, name, children: [] }; + if (isLeaf && !name) { + const text = accumulatedElementText(element); + if (text) + result.children = [text]; + } return { isLeaf, ariaNode: result }; }; const visit = (ariaNode: AriaNode, node: Node) => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { - ariaNode.children = ariaNode.children || []; - ariaNode.children.push(node.nodeValue); + const text = node.nodeValue; + if (text) + ariaNode.children.push(node.nodeValue || ''); return; } @@ -67,10 +71,8 @@ export function generateAriaTree(rootElement: Element): AriaNode { if (visible) { const childAriaNode = toAriaNode(element); const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role); - if (childAriaNode && !isHiddenContainer) { - ariaNode.children = ariaNode.children || []; + if (childAriaNode && !isHiddenContainer) ariaNode.children.push(childAriaNode.ariaNode); - } if (isHiddenContainer || !childAriaNode?.isLeaf) processChildNodes(childAriaNode?.ariaNode || ariaNode, element); } else { @@ -79,18 +81,36 @@ export function generateAriaTree(rootElement: Element): AriaNode { }; function processChildNodes(ariaNode: AriaNode, element: Element) { - // Process light DOM children - for (let child = element.firstChild; child; child = child.nextSibling) - visit(ariaNode, child); - // Process shadow DOM children, if any - if (element.shadowRoot) { - for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) + // Surround every element with spaces for the sake of concatenated text nodes. + const display = getElementComputedStyle(element)?.display || 'inline'; + const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : ''; + if (treatAsBlock) + ariaNode.children.push(treatAsBlock); + + ariaNode.children.push(getPseudoContent(element, '::before')); + const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; + if (assignedNodes.length) { + for (const child of assignedNodes) visit(ariaNode, child); + } else { + for (let child = element.firstChild; child; child = child.nextSibling) { + if (!(child as Element | Text).assignedSlot) + visit(ariaNode, child); + } + if (element.shadowRoot) { + for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) + visit(ariaNode, child); + } } + + ariaNode.children.push(getPseudoContent(element, '::after')); + + if (treatAsBlock) + ariaNode.children.push(treatAsBlock); } beginAriaCaches(); - const ariaRoot: AriaNode = { role: '' }; + const ariaRoot: AriaNode = { role: '', children: [] }; try { visit(ariaRoot, rootElement); } finally { @@ -128,7 +148,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) { } } flushChildren(buffer, normalizedChildren); - ariaNode.children = normalizedChildren.length ? normalizedChildren : undefined; + ariaNode.children = normalizedChildren.length ? normalizedChildren : []; }; visit(rootA11yNode); } @@ -144,7 +164,7 @@ const leafRoles = new Set([ 'textbox', 'time', 'tooltip' ]); -const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\n]+/g, ' '); +const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\t\r\n]+/g, ' '); function matchesText(text: string | undefined, template: RegExp | string | undefined) { if (!template) @@ -233,7 +253,7 @@ export function renderAriaTree(ariaNode: AriaNode): string { lines.push(line); return; } - lines.push(line + (ariaNode.children ? ':' : '')); + lines.push(line + (ariaNode.children.length ? ':' : '')); for (const child of ariaNode.children || []) visit(child, indent + ' '); }; diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 6e05c39901..d085a8e36d 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -363,7 +363,7 @@ function queryInAriaOwned(element: Element, selector: string): Element[] { return result; } -function getPseudoContent(element: Element, pseudo: '::before' | '::after') { +export function getPseudoContent(element: Element, pseudo: '::before' | '::after') { const cache = pseudo === '::before' ? cachePseudoContentBefore : cachePseudoContentAfter; if (cache?.has(element)) return cache?.get(element) || ''; @@ -430,10 +430,6 @@ export function getElementAccessibleName(element: Element, includeHidden: boolea accessibleName = asFlatString(getTextAlternativeInternal(element, { includeHidden, visitedElements: new Set(), - embeddedInDescribedBy: undefined, - embeddedInLabelledBy: undefined, - embeddedInLabel: undefined, - embeddedInNativeTextAlternative: undefined, embeddedInTargetElement: 'self', })); } @@ -458,10 +454,6 @@ export function getElementAccessibleDescription(element: Element, includeHidden: accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, { includeHidden, visitedElements: new Set(), - embeddedInLabelledBy: undefined, - embeddedInLabel: undefined, - embeddedInNativeTextAlternative: undefined, - embeddedInTargetElement: 'none', embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) }, })).join(' ')); } else if (element.hasAttribute('aria-description')) { @@ -480,13 +472,13 @@ export function getElementAccessibleDescription(element: Element, includeHidden: } type AccessibleNameOptions = { - includeHidden: boolean, visitedElements: Set, - embeddedInDescribedBy: { element: Element, hidden: boolean } | undefined, - embeddedInLabelledBy: { element: Element, hidden: boolean } | undefined, - embeddedInLabel: { element: Element, hidden: boolean } | undefined, - embeddedInNativeTextAlternative: { element: Element, hidden: boolean } | undefined, - embeddedInTargetElement: 'none' | 'self' | 'descendant', + includeHidden?: boolean, + embeddedInDescribedBy?: { element: Element, hidden: boolean }, + embeddedInLabelledBy?: { element: Element, hidden: boolean }, + embeddedInLabel?: { element: Element, hidden: boolean }, + embeddedInNativeTextAlternative?: { element: Element, hidden: boolean }, + embeddedInTargetElement?: 'self' | 'descendant', }; function getTextAlternativeInternal(element: Element, options: AccessibleNameOptions): string { @@ -525,7 +517,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt ...options, embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInDescribedBy: undefined, - embeddedInTargetElement: 'none', + embeddedInTargetElement: undefined, embeddedInLabel: undefined, embeddedInNativeTextAlternative: undefined, })).join(' '); @@ -778,42 +770,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy || !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) { options.visitedElements.add(element); - const tokens: string[] = []; - const visit = (node: Node, skipSlotted: boolean) => { - if (skipSlotted && (node as Element | Text).assignedSlot) - return; - if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { - const display = getElementComputedStyle(node as Element)?.display || 'inline'; - let token = getTextAlternativeInternal(node as Element, childOptions); - // SPEC DIFFERENCE. - // Spec says "append the result to the accumulated text", assuming "with space". - // However, multiple tests insist that inline elements do not add a space. - // Additionally,
          insists on a space anyway, see "name_file-label-inline-block-elements-manual.html" - if (display !== 'inline' || node.nodeName === 'BR') - token = ' ' + token + ' '; - tokens.push(token); - } else if (node.nodeType === 3 /* Node.TEXT_NODE */) { - // step 2g. - tokens.push(node.textContent || ''); - } - }; - tokens.push(getPseudoContent(element, '::before')); - const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; - if (assignedNodes.length) { - for (const child of assignedNodes) - visit(child, false); - } else { - for (let child = element.firstChild; child; child = child.nextSibling) - visit(child, true); - if (element.shadowRoot) { - for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) - visit(child, true); - } - for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) - visit(owned, true); - } - tokens.push(getPseudoContent(element, '::after')); - const accessibleName = tokens.join(''); + const accessibleName = innerAccumulatedElementText(element, childOptions); // Spec says "Return the accumulated text if it is not the empty string". However, that is not really // compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title. // So we follow the spec everywhere except for the target element itself. This can probably be improved. @@ -834,6 +791,50 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt return ''; } +function innerAccumulatedElementText(element: Element, options: AccessibleNameOptions): string { + const tokens: string[] = []; + const visit = (node: Node, skipSlotted: boolean) => { + if (skipSlotted && (node as Element | Text).assignedSlot) + return; + if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { + const display = getElementComputedStyle(node as Element)?.display || 'inline'; + let token = getTextAlternativeInternal(node as Element, options); + // SPEC DIFFERENCE. + // Spec says "append the result to the accumulated text", assuming "with space". + // However, multiple tests insist that inline elements do not add a space. + // Additionally,
          insists on a space anyway, see "name_file-label-inline-block-elements-manual.html" + if (display !== 'inline' || node.nodeName === 'BR') + token = ' ' + token + ' '; + tokens.push(token); + } else if (node.nodeType === 3 /* Node.TEXT_NODE */) { + // step 2g. + tokens.push(node.textContent || ''); + } + }; + tokens.push(getPseudoContent(element, '::before')); + const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; + if (assignedNodes.length) { + for (const child of assignedNodes) + visit(child, false); + } else { + for (let child = element.firstChild; child; child = child.nextSibling) + visit(child, true); + if (element.shadowRoot) { + for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) + visit(child, true); + } + for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) + visit(owned, true); + } + tokens.push(getPseudoContent(element, '::after')); + return tokens.join(''); +} + +export function accumulatedElementText(element: Element): string { + const visitedElements = new Set(); + return asFlatString(innerAccumulatedElementText(element, { visitedElements })).trim(); +} + export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem']; export function getAriaSelected(element: Element): boolean { // https://www.w3.org/TR/wai-aria-1.2/#aria-selected @@ -958,7 +959,7 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable !!accessibleName).join(' '); } diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index cd79ccab61..cf043c2ca8 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -49,17 +49,19 @@ export async function toMatchAriaSnapshot( const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; + const escapedExpected = escapePrivateUsePoints(expected); + const escapedReceived = escapePrivateUsePoints(received); const message = () => { if (pass) { if (notFound) - return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedSubstring(received, received.indexOf(expected), expected.length); - return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); + return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log); + const printedReceived = printReceivedStringContainExpectedSubstring(escapedReceived, escapedReceived.indexOf(escapedExpected), escapedExpected.length); + return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived string: ${printedReceived}` + callLogText(log); } else { const labelExpected = `Expected`; if (notFound) - return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - return messagePrefix + this.utils.printDiffOrStringify(expected, received, labelExpected, 'Received string', false) + callLogText(log); + return messagePrefix + `${labelExpected}: ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log); + return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received string', false) + callLogText(log); } }; @@ -73,3 +75,7 @@ export async function toMatchAriaSnapshot( timeout: timedOut ? timeout : undefined, }; } + +function escapePrivateUsePoints(str: string) { + return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`); +} diff --git a/tests/assets/codicon.css b/tests/assets/codicon.css new file mode 100644 index 0000000000..41360ce21d --- /dev/null +++ b/tests/assets/codicon.css @@ -0,0 +1,596 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@font-face { + font-family: "codicon"; + src: url("codicon.ttf") format("truetype"); +} + +.codicon { + font: normal normal normal 16px/1 codicon; + flex: none; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.codicon-add:before { content: '\ea60'; } +.codicon-plus:before { content: '\ea60'; } +.codicon-gist-new:before { content: '\ea60'; } +.codicon-repo-create:before { content: '\ea60'; } +.codicon-lightbulb:before { content: '\ea61'; } +.codicon-light-bulb:before { content: '\ea61'; } +.codicon-repo:before { content: '\ea62'; } +.codicon-repo-delete:before { content: '\ea62'; } +.codicon-gist-fork:before { content: '\ea63'; } +.codicon-repo-forked:before { content: '\ea63'; } +.codicon-git-pull-request:before { content: '\ea64'; } +.codicon-git-pull-request-abandoned:before { content: '\ea64'; } +.codicon-record-keys:before { content: '\ea65'; } +.codicon-keyboard:before { content: '\ea65'; } +.codicon-tag:before { content: '\ea66'; } +.codicon-git-pull-request-label:before { content: '\ea66'; } +.codicon-tag-add:before { content: '\ea66'; } +.codicon-tag-remove:before { content: '\ea66'; } +.codicon-person:before { content: '\ea67'; } +.codicon-person-follow:before { content: '\ea67'; } +.codicon-person-outline:before { content: '\ea67'; } +.codicon-person-filled:before { content: '\ea67'; } +.codicon-git-branch:before { content: '\ea68'; } +.codicon-git-branch-create:before { content: '\ea68'; } +.codicon-git-branch-delete:before { content: '\ea68'; } +.codicon-source-control:before { content: '\ea68'; } +.codicon-mirror:before { content: '\ea69'; } +.codicon-mirror-public:before { content: '\ea69'; } +.codicon-star:before { content: '\ea6a'; } +.codicon-star-add:before { content: '\ea6a'; } +.codicon-star-delete:before { content: '\ea6a'; } +.codicon-star-empty:before { content: '\ea6a'; } +.codicon-comment:before { content: '\ea6b'; } +.codicon-comment-add:before { content: '\ea6b'; } +.codicon-alert:before { content: '\ea6c'; } +.codicon-warning:before { content: '\ea6c'; } +.codicon-search:before { content: '\ea6d'; } +.codicon-search-save:before { content: '\ea6d'; } +.codicon-log-out:before { content: '\ea6e'; } +.codicon-sign-out:before { content: '\ea6e'; } +.codicon-log-in:before { content: '\ea6f'; } +.codicon-sign-in:before { content: '\ea6f'; } +.codicon-eye:before { content: '\ea70'; } +.codicon-eye-unwatch:before { content: '\ea70'; } +.codicon-eye-watch:before { content: '\ea70'; } +.codicon-circle-filled:before { content: '\ea71'; } +.codicon-primitive-dot:before { content: '\ea71'; } +.codicon-close-dirty:before { content: '\ea71'; } +.codicon-debug-breakpoint:before { content: '\ea71'; } +.codicon-debug-breakpoint-disabled:before { content: '\ea71'; } +.codicon-debug-hint:before { content: '\ea71'; } +.codicon-terminal-decoration-success:before { content: '\ea71'; } +.codicon-primitive-square:before { content: '\ea72'; } +.codicon-edit:before { content: '\ea73'; } +.codicon-pencil:before { content: '\ea73'; } +.codicon-info:before { content: '\ea74'; } +.codicon-issue-opened:before { content: '\ea74'; } +.codicon-gist-private:before { content: '\ea75'; } +.codicon-git-fork-private:before { content: '\ea75'; } +.codicon-lock:before { content: '\ea75'; } +.codicon-mirror-private:before { content: '\ea75'; } +.codicon-close:before { content: '\ea76'; } +.codicon-remove-close:before { content: '\ea76'; } +.codicon-x:before { content: '\ea76'; } +.codicon-repo-sync:before { content: '\ea77'; } +.codicon-sync:before { content: '\ea77'; } +.codicon-clone:before { content: '\ea78'; } +.codicon-desktop-download:before { content: '\ea78'; } +.codicon-beaker:before { content: '\ea79'; } +.codicon-microscope:before { content: '\ea79'; } +.codicon-vm:before { content: '\ea7a'; } +.codicon-device-desktop:before { content: '\ea7a'; } +.codicon-file:before { content: '\ea7b'; } +.codicon-file-text:before { content: '\ea7b'; } +.codicon-more:before { content: '\ea7c'; } +.codicon-ellipsis:before { content: '\ea7c'; } +.codicon-kebab-horizontal:before { content: '\ea7c'; } +.codicon-mail-reply:before { content: '\ea7d'; } +.codicon-reply:before { content: '\ea7d'; } +.codicon-organization:before { content: '\ea7e'; } +.codicon-organization-filled:before { content: '\ea7e'; } +.codicon-organization-outline:before { content: '\ea7e'; } +.codicon-new-file:before { content: '\ea7f'; } +.codicon-file-add:before { content: '\ea7f'; } +.codicon-new-folder:before { content: '\ea80'; } +.codicon-file-directory-create:before { content: '\ea80'; } +.codicon-trash:before { content: '\ea81'; } +.codicon-trashcan:before { content: '\ea81'; } +.codicon-history:before { content: '\ea82'; } +.codicon-clock:before { content: '\ea82'; } +.codicon-folder:before { content: '\ea83'; } +.codicon-file-directory:before { content: '\ea83'; } +.codicon-symbol-folder:before { content: '\ea83'; } +.codicon-logo-github:before { content: '\ea84'; } +.codicon-mark-github:before { content: '\ea84'; } +.codicon-github:before { content: '\ea84'; } +.codicon-terminal:before { content: '\ea85'; } +.codicon-console:before { content: '\ea85'; } +.codicon-repl:before { content: '\ea85'; } +.codicon-zap:before { content: '\ea86'; } +.codicon-symbol-event:before { content: '\ea86'; } +.codicon-error:before { content: '\ea87'; } +.codicon-stop:before { content: '\ea87'; } +.codicon-variable:before { content: '\ea88'; } +.codicon-symbol-variable:before { content: '\ea88'; } +.codicon-array:before { content: '\ea8a'; } +.codicon-symbol-array:before { content: '\ea8a'; } +.codicon-symbol-module:before { content: '\ea8b'; } +.codicon-symbol-package:before { content: '\ea8b'; } +.codicon-symbol-namespace:before { content: '\ea8b'; } +.codicon-symbol-object:before { content: '\ea8b'; } +.codicon-symbol-method:before { content: '\ea8c'; } +.codicon-symbol-function:before { content: '\ea8c'; } +.codicon-symbol-constructor:before { content: '\ea8c'; } +.codicon-symbol-boolean:before { content: '\ea8f'; } +.codicon-symbol-null:before { content: '\ea8f'; } +.codicon-symbol-numeric:before { content: '\ea90'; } +.codicon-symbol-number:before { content: '\ea90'; } +.codicon-symbol-structure:before { content: '\ea91'; } +.codicon-symbol-struct:before { content: '\ea91'; } +.codicon-symbol-parameter:before { content: '\ea92'; } +.codicon-symbol-type-parameter:before { content: '\ea92'; } +.codicon-symbol-key:before { content: '\ea93'; } +.codicon-symbol-text:before { content: '\ea93'; } +.codicon-symbol-reference:before { content: '\ea94'; } +.codicon-go-to-file:before { content: '\ea94'; } +.codicon-symbol-enum:before { content: '\ea95'; } +.codicon-symbol-value:before { content: '\ea95'; } +.codicon-symbol-ruler:before { content: '\ea96'; } +.codicon-symbol-unit:before { content: '\ea96'; } +.codicon-activate-breakpoints:before { content: '\ea97'; } +.codicon-archive:before { content: '\ea98'; } +.codicon-arrow-both:before { content: '\ea99'; } +.codicon-arrow-down:before { content: '\ea9a'; } +.codicon-arrow-left:before { content: '\ea9b'; } +.codicon-arrow-right:before { content: '\ea9c'; } +.codicon-arrow-small-down:before { content: '\ea9d'; } +.codicon-arrow-small-left:before { content: '\ea9e'; } +.codicon-arrow-small-right:before { content: '\ea9f'; } +.codicon-arrow-small-up:before { content: '\eaa0'; } +.codicon-arrow-up:before { content: '\eaa1'; } +.codicon-bell:before { content: '\eaa2'; } +.codicon-bold:before { content: '\eaa3'; } +.codicon-book:before { content: '\eaa4'; } +.codicon-bookmark:before { content: '\eaa5'; } +.codicon-debug-breakpoint-conditional-unverified:before { content: '\eaa6'; } +.codicon-debug-breakpoint-conditional:before { content: '\eaa7'; } +.codicon-debug-breakpoint-conditional-disabled:before { content: '\eaa7'; } +.codicon-debug-breakpoint-data-unverified:before { content: '\eaa8'; } +.codicon-debug-breakpoint-data:before { content: '\eaa9'; } +.codicon-debug-breakpoint-data-disabled:before { content: '\eaa9'; } +.codicon-debug-breakpoint-log-unverified:before { content: '\eaaa'; } +.codicon-debug-breakpoint-log:before { content: '\eaab'; } +.codicon-debug-breakpoint-log-disabled:before { content: '\eaab'; } +.codicon-briefcase:before { content: '\eaac'; } +.codicon-broadcast:before { content: '\eaad'; } +.codicon-browser:before { content: '\eaae'; } +.codicon-bug:before { content: '\eaaf'; } +.codicon-calendar:before { content: '\eab0'; } +.codicon-case-sensitive:before { content: '\eab1'; } +.codicon-check:before { content: '\eab2'; } +.codicon-checklist:before { content: '\eab3'; } +.codicon-chevron-down:before { content: '\eab4'; } +.codicon-chevron-left:before { content: '\eab5'; } +.codicon-chevron-right:before { content: '\eab6'; } +.codicon-chevron-up:before { content: '\eab7'; } +.codicon-chrome-close:before { content: '\eab8'; } +.codicon-chrome-maximize:before { content: '\eab9'; } +.codicon-chrome-minimize:before { content: '\eaba'; } +.codicon-chrome-restore:before { content: '\eabb'; } +.codicon-circle-outline:before { content: '\eabc'; } +.codicon-circle:before { content: '\eabc'; } +.codicon-debug-breakpoint-unverified:before { content: '\eabc'; } +.codicon-terminal-decoration-incomplete:before { content: '\eabc'; } +.codicon-circle-slash:before { content: '\eabd'; } +.codicon-circuit-board:before { content: '\eabe'; } +.codicon-clear-all:before { content: '\eabf'; } +.codicon-clippy:before { content: '\eac0'; } +.codicon-close-all:before { content: '\eac1'; } +.codicon-cloud-download:before { content: '\eac2'; } +.codicon-cloud-upload:before { content: '\eac3'; } +.codicon-code:before { content: '\eac4'; } +.codicon-collapse-all:before { content: '\eac5'; } +.codicon-color-mode:before { content: '\eac6'; } +.codicon-comment-discussion:before { content: '\eac7'; } +.codicon-credit-card:before { content: '\eac9'; } +.codicon-dash:before { content: '\eacc'; } +.codicon-dashboard:before { content: '\eacd'; } +.codicon-database:before { content: '\eace'; } +.codicon-debug-continue:before { content: '\eacf'; } +.codicon-debug-disconnect:before { content: '\ead0'; } +.codicon-debug-pause:before { content: '\ead1'; } +.codicon-debug-restart:before { content: '\ead2'; } +.codicon-debug-start:before { content: '\ead3'; } +.codicon-debug-step-into:before { content: '\ead4'; } +.codicon-debug-step-out:before { content: '\ead5'; } +.codicon-debug-step-over:before { content: '\ead6'; } +.codicon-debug-stop:before { content: '\ead7'; } +.codicon-debug:before { content: '\ead8'; } +.codicon-device-camera-video:before { content: '\ead9'; } +.codicon-device-camera:before { content: '\eada'; } +.codicon-device-mobile:before { content: '\eadb'; } +.codicon-diff-added:before { content: '\eadc'; } +.codicon-diff-ignored:before { content: '\eadd'; } +.codicon-diff-modified:before { content: '\eade'; } +.codicon-diff-removed:before { content: '\eadf'; } +.codicon-diff-renamed:before { content: '\eae0'; } +.codicon-diff:before { content: '\eae1'; } +.codicon-diff-sidebyside:before { content: '\eae1'; } +.codicon-discard:before { content: '\eae2'; } +.codicon-editor-layout:before { content: '\eae3'; } +.codicon-empty-window:before { content: '\eae4'; } +.codicon-exclude:before { content: '\eae5'; } +.codicon-extensions:before { content: '\eae6'; } +.codicon-eye-closed:before { content: '\eae7'; } +.codicon-file-binary:before { content: '\eae8'; } +.codicon-file-code:before { content: '\eae9'; } +.codicon-file-media:before { content: '\eaea'; } +.codicon-file-pdf:before { content: '\eaeb'; } +.codicon-file-submodule:before { content: '\eaec'; } +.codicon-file-symlink-directory:before { content: '\eaed'; } +.codicon-file-symlink-file:before { content: '\eaee'; } +.codicon-file-zip:before { content: '\eaef'; } +.codicon-files:before { content: '\eaf0'; } +.codicon-filter:before { content: '\eaf1'; } +.codicon-flame:before { content: '\eaf2'; } +.codicon-fold-down:before { content: '\eaf3'; } +.codicon-fold-up:before { content: '\eaf4'; } +.codicon-fold:before { content: '\eaf5'; } +.codicon-folder-active:before { content: '\eaf6'; } +.codicon-folder-opened:before { content: '\eaf7'; } +.codicon-gear:before { content: '\eaf8'; } +.codicon-gift:before { content: '\eaf9'; } +.codicon-gist-secret:before { content: '\eafa'; } +.codicon-gist:before { content: '\eafb'; } +.codicon-git-commit:before { content: '\eafc'; } +.codicon-git-compare:before { content: '\eafd'; } +.codicon-compare-changes:before { content: '\eafd'; } +.codicon-git-merge:before { content: '\eafe'; } +.codicon-github-action:before { content: '\eaff'; } +.codicon-github-alt:before { content: '\eb00'; } +.codicon-globe:before { content: '\eb01'; } +.codicon-grabber:before { content: '\eb02'; } +.codicon-graph:before { content: '\eb03'; } +.codicon-gripper:before { content: '\eb04'; } +.codicon-heart:before { content: '\eb05'; } +.codicon-home:before { content: '\eb06'; } +.codicon-horizontal-rule:before { content: '\eb07'; } +.codicon-hubot:before { content: '\eb08'; } +.codicon-inbox:before { content: '\eb09'; } +.codicon-issue-reopened:before { content: '\eb0b'; } +.codicon-issues:before { content: '\eb0c'; } +.codicon-italic:before { content: '\eb0d'; } +.codicon-jersey:before { content: '\eb0e'; } +.codicon-json:before { content: '\eb0f'; } +.codicon-kebab-vertical:before { content: '\eb10'; } +.codicon-key:before { content: '\eb11'; } +.codicon-law:before { content: '\eb12'; } +.codicon-lightbulb-autofix:before { content: '\eb13'; } +.codicon-link-external:before { content: '\eb14'; } +.codicon-link:before { content: '\eb15'; } +.codicon-list-ordered:before { content: '\eb16'; } +.codicon-list-unordered:before { content: '\eb17'; } +.codicon-live-share:before { content: '\eb18'; } +.codicon-loading:before { content: '\eb19'; } +.codicon-location:before { content: '\eb1a'; } +.codicon-mail-read:before { content: '\eb1b'; } +.codicon-mail:before { content: '\eb1c'; } +.codicon-markdown:before { content: '\eb1d'; } +.codicon-megaphone:before { content: '\eb1e'; } +.codicon-mention:before { content: '\eb1f'; } +.codicon-milestone:before { content: '\eb20'; } +.codicon-git-pull-request-milestone:before { content: '\eb20'; } +.codicon-mortar-board:before { content: '\eb21'; } +.codicon-move:before { content: '\eb22'; } +.codicon-multiple-windows:before { content: '\eb23'; } +.codicon-mute:before { content: '\eb24'; } +.codicon-no-newline:before { content: '\eb25'; } +.codicon-note:before { content: '\eb26'; } +.codicon-octoface:before { content: '\eb27'; } +.codicon-open-preview:before { content: '\eb28'; } +.codicon-package:before { content: '\eb29'; } +.codicon-paintcan:before { content: '\eb2a'; } +.codicon-pin:before { content: '\eb2b'; } +.codicon-play:before { content: '\eb2c'; } +.codicon-run:before { content: '\eb2c'; } +.codicon-plug:before { content: '\eb2d'; } +.codicon-preserve-case:before { content: '\eb2e'; } +.codicon-preview:before { content: '\eb2f'; } +.codicon-project:before { content: '\eb30'; } +.codicon-pulse:before { content: '\eb31'; } +.codicon-question:before { content: '\eb32'; } +.codicon-quote:before { content: '\eb33'; } +.codicon-radio-tower:before { content: '\eb34'; } +.codicon-reactions:before { content: '\eb35'; } +.codicon-references:before { content: '\eb36'; } +.codicon-refresh:before { content: '\eb37'; } +.codicon-regex:before { content: '\eb38'; } +.codicon-remote-explorer:before { content: '\eb39'; } +.codicon-remote:before { content: '\eb3a'; } +.codicon-remove:before { content: '\eb3b'; } +.codicon-replace-all:before { content: '\eb3c'; } +.codicon-replace:before { content: '\eb3d'; } +.codicon-repo-clone:before { content: '\eb3e'; } +.codicon-repo-force-push:before { content: '\eb3f'; } +.codicon-repo-pull:before { content: '\eb40'; } +.codicon-repo-push:before { content: '\eb41'; } +.codicon-report:before { content: '\eb42'; } +.codicon-request-changes:before { content: '\eb43'; } +.codicon-rocket:before { content: '\eb44'; } +.codicon-root-folder-opened:before { content: '\eb45'; } +.codicon-root-folder:before { content: '\eb46'; } +.codicon-rss:before { content: '\eb47'; } +.codicon-ruby:before { content: '\eb48'; } +.codicon-save-all:before { content: '\eb49'; } +.codicon-save-as:before { content: '\eb4a'; } +.codicon-save:before { content: '\eb4b'; } +.codicon-screen-full:before { content: '\eb4c'; } +.codicon-screen-normal:before { content: '\eb4d'; } +.codicon-search-stop:before { content: '\eb4e'; } +.codicon-server:before { content: '\eb50'; } +.codicon-settings-gear:before { content: '\eb51'; } +.codicon-settings:before { content: '\eb52'; } +.codicon-shield:before { content: '\eb53'; } +.codicon-smiley:before { content: '\eb54'; } +.codicon-sort-precedence:before { content: '\eb55'; } +.codicon-split-horizontal:before { content: '\eb56'; } +.codicon-split-vertical:before { content: '\eb57'; } +.codicon-squirrel:before { content: '\eb58'; } +.codicon-star-full:before { content: '\eb59'; } +.codicon-star-half:before { content: '\eb5a'; } +.codicon-symbol-class:before { content: '\eb5b'; } +.codicon-symbol-color:before { content: '\eb5c'; } +.codicon-symbol-constant:before { content: '\eb5d'; } +.codicon-symbol-enum-member:before { content: '\eb5e'; } +.codicon-symbol-field:before { content: '\eb5f'; } +.codicon-symbol-file:before { content: '\eb60'; } +.codicon-symbol-interface:before { content: '\eb61'; } +.codicon-symbol-keyword:before { content: '\eb62'; } +.codicon-symbol-misc:before { content: '\eb63'; } +.codicon-symbol-operator:before { content: '\eb64'; } +.codicon-symbol-property:before { content: '\eb65'; } +.codicon-wrench:before { content: '\eb65'; } +.codicon-wrench-subaction:before { content: '\eb65'; } +.codicon-symbol-snippet:before { content: '\eb66'; } +.codicon-tasklist:before { content: '\eb67'; } +.codicon-telescope:before { content: '\eb68'; } +.codicon-text-size:before { content: '\eb69'; } +.codicon-three-bars:before { content: '\eb6a'; } +.codicon-thumbsdown:before { content: '\eb6b'; } +.codicon-thumbsup:before { content: '\eb6c'; } +.codicon-tools:before { content: '\eb6d'; } +.codicon-triangle-down:before { content: '\eb6e'; } +.codicon-triangle-left:before { content: '\eb6f'; } +.codicon-triangle-right:before { content: '\eb70'; } +.codicon-triangle-up:before { content: '\eb71'; } +.codicon-twitter:before { content: '\eb72'; } +.codicon-unfold:before { content: '\eb73'; } +.codicon-unlock:before { content: '\eb74'; } +.codicon-unmute:before { content: '\eb75'; } +.codicon-unverified:before { content: '\eb76'; } +.codicon-verified:before { content: '\eb77'; } +.codicon-versions:before { content: '\eb78'; } +.codicon-vm-active:before { content: '\eb79'; } +.codicon-vm-outline:before { content: '\eb7a'; } +.codicon-vm-running:before { content: '\eb7b'; } +.codicon-watch:before { content: '\eb7c'; } +.codicon-whitespace:before { content: '\eb7d'; } +.codicon-whole-word:before { content: '\eb7e'; } +.codicon-window:before { content: '\eb7f'; } +.codicon-word-wrap:before { content: '\eb80'; } +.codicon-zoom-in:before { content: '\eb81'; } +.codicon-zoom-out:before { content: '\eb82'; } +.codicon-list-filter:before { content: '\eb83'; } +.codicon-list-flat:before { content: '\eb84'; } +.codicon-list-selection:before { content: '\eb85'; } +.codicon-selection:before { content: '\eb85'; } +.codicon-list-tree:before { content: '\eb86'; } +.codicon-debug-breakpoint-function-unverified:before { content: '\eb87'; } +.codicon-debug-breakpoint-function:before { content: '\eb88'; } +.codicon-debug-breakpoint-function-disabled:before { content: '\eb88'; } +.codicon-debug-stackframe-active:before { content: '\eb89'; } +.codicon-circle-small-filled:before { content: '\eb8a'; } +.codicon-debug-stackframe-dot:before { content: '\eb8a'; } +.codicon-terminal-decoration-mark:before { content: '\eb8a'; } +.codicon-debug-stackframe:before { content: '\eb8b'; } +.codicon-debug-stackframe-focused:before { content: '\eb8b'; } +.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; } +.codicon-symbol-string:before { content: '\eb8d'; } +.codicon-debug-reverse-continue:before { content: '\eb8e'; } +.codicon-debug-step-back:before { content: '\eb8f'; } +.codicon-debug-restart-frame:before { content: '\eb90'; } +.codicon-debug-alt:before { content: '\eb91'; } +.codicon-call-incoming:before { content: '\eb92'; } +.codicon-call-outgoing:before { content: '\eb93'; } +.codicon-menu:before { content: '\eb94'; } +.codicon-expand-all:before { content: '\eb95'; } +.codicon-feedback:before { content: '\eb96'; } +.codicon-git-pull-request-reviewer:before { content: '\eb96'; } +.codicon-group-by-ref-type:before { content: '\eb97'; } +.codicon-ungroup-by-ref-type:before { content: '\eb98'; } +.codicon-account:before { content: '\eb99'; } +.codicon-git-pull-request-assignee:before { content: '\eb99'; } +.codicon-bell-dot:before { content: '\eb9a'; } +.codicon-debug-console:before { content: '\eb9b'; } +.codicon-library:before { content: '\eb9c'; } +.codicon-output:before { content: '\eb9d'; } +.codicon-run-all:before { content: '\eb9e'; } +.codicon-sync-ignored:before { content: '\eb9f'; } +.codicon-pinned:before { content: '\eba0'; } +.codicon-github-inverted:before { content: '\eba1'; } +.codicon-server-process:before { content: '\eba2'; } +.codicon-server-environment:before { content: '\eba3'; } +.codicon-pass:before { content: '\eba4'; } +.codicon-issue-closed:before { content: '\eba4'; } +.codicon-stop-circle:before { content: '\eba5'; } +.codicon-play-circle:before { content: '\eba6'; } +.codicon-record:before { content: '\eba7'; } +.codicon-debug-alt-small:before { content: '\eba8'; } +.codicon-vm-connect:before { content: '\eba9'; } +.codicon-cloud:before { content: '\ebaa'; } +.codicon-merge:before { content: '\ebab'; } +.codicon-export:before { content: '\ebac'; } +.codicon-graph-left:before { content: '\ebad'; } +.codicon-magnet:before { content: '\ebae'; } +.codicon-notebook:before { content: '\ebaf'; } +.codicon-redo:before { content: '\ebb0'; } +.codicon-check-all:before { content: '\ebb1'; } +.codicon-pinned-dirty:before { content: '\ebb2'; } +.codicon-pass-filled:before { content: '\ebb3'; } +.codicon-circle-large-filled:before { content: '\ebb4'; } +.codicon-circle-large:before { content: '\ebb5'; } +.codicon-circle-large-outline:before { content: '\ebb5'; } +.codicon-combine:before { content: '\ebb6'; } +.codicon-gather:before { content: '\ebb6'; } +.codicon-table:before { content: '\ebb7'; } +.codicon-variable-group:before { content: '\ebb8'; } +.codicon-type-hierarchy:before { content: '\ebb9'; } +.codicon-type-hierarchy-sub:before { content: '\ebba'; } +.codicon-type-hierarchy-super:before { content: '\ebbb'; } +.codicon-git-pull-request-create:before { content: '\ebbc'; } +.codicon-run-above:before { content: '\ebbd'; } +.codicon-run-below:before { content: '\ebbe'; } +.codicon-notebook-template:before { content: '\ebbf'; } +.codicon-debug-rerun:before { content: '\ebc0'; } +.codicon-workspace-trusted:before { content: '\ebc1'; } +.codicon-workspace-untrusted:before { content: '\ebc2'; } +.codicon-workspace-unknown:before { content: '\ebc3'; } +.codicon-terminal-cmd:before { content: '\ebc4'; } +.codicon-terminal-debian:before { content: '\ebc5'; } +.codicon-terminal-linux:before { content: '\ebc6'; } +.codicon-terminal-powershell:before { content: '\ebc7'; } +.codicon-terminal-tmux:before { content: '\ebc8'; } +.codicon-terminal-ubuntu:before { content: '\ebc9'; } +.codicon-terminal-bash:before { content: '\ebca'; } +.codicon-arrow-swap:before { content: '\ebcb'; } +.codicon-copy:before { content: '\ebcc'; } +.codicon-person-add:before { content: '\ebcd'; } +.codicon-filter-filled:before { content: '\ebce'; } +.codicon-wand:before { content: '\ebcf'; } +.codicon-debug-line-by-line:before { content: '\ebd0'; } +.codicon-inspect:before { content: '\ebd1'; } +.codicon-layers:before { content: '\ebd2'; } +.codicon-layers-dot:before { content: '\ebd3'; } +.codicon-layers-active:before { content: '\ebd4'; } +.codicon-compass:before { content: '\ebd5'; } +.codicon-compass-dot:before { content: '\ebd6'; } +.codicon-compass-active:before { content: '\ebd7'; } +.codicon-azure:before { content: '\ebd8'; } +.codicon-issue-draft:before { content: '\ebd9'; } +.codicon-git-pull-request-closed:before { content: '\ebda'; } +.codicon-git-pull-request-draft:before { content: '\ebdb'; } +.codicon-debug-all:before { content: '\ebdc'; } +.codicon-debug-coverage:before { content: '\ebdd'; } +.codicon-run-errors:before { content: '\ebde'; } +.codicon-folder-library:before { content: '\ebdf'; } +.codicon-debug-continue-small:before { content: '\ebe0'; } +.codicon-beaker-stop:before { content: '\ebe1'; } +.codicon-graph-line:before { content: '\ebe2'; } +.codicon-graph-scatter:before { content: '\ebe3'; } +.codicon-pie-chart:before { content: '\ebe4'; } +.codicon-bracket:before { content: '\eb0f'; } +.codicon-bracket-dot:before { content: '\ebe5'; } +.codicon-bracket-error:before { content: '\ebe6'; } +.codicon-lock-small:before { content: '\ebe7'; } +.codicon-azure-devops:before { content: '\ebe8'; } +.codicon-verified-filled:before { content: '\ebe9'; } +.codicon-newline:before { content: '\ebea'; } +.codicon-layout:before { content: '\ebeb'; } +.codicon-layout-activitybar-left:before { content: '\ebec'; } +.codicon-layout-activitybar-right:before { content: '\ebed'; } +.codicon-layout-panel-left:before { content: '\ebee'; } +.codicon-layout-panel-center:before { content: '\ebef'; } +.codicon-layout-panel-justify:before { content: '\ebf0'; } +.codicon-layout-panel-right:before { content: '\ebf1'; } +.codicon-layout-panel:before { content: '\ebf2'; } +.codicon-layout-sidebar-left:before { content: '\ebf3'; } +.codicon-layout-sidebar-right:before { content: '\ebf4'; } +.codicon-layout-statusbar:before { content: '\ebf5'; } +.codicon-layout-menubar:before { content: '\ebf6'; } +.codicon-layout-centered:before { content: '\ebf7'; } +.codicon-target:before { content: '\ebf8'; } +.codicon-indent:before { content: '\ebf9'; } +.codicon-record-small:before { content: '\ebfa'; } +.codicon-error-small:before { content: '\ebfb'; } +.codicon-terminal-decoration-error:before { content: '\ebfb'; } +.codicon-arrow-circle-down:before { content: '\ebfc'; } +.codicon-arrow-circle-left:before { content: '\ebfd'; } +.codicon-arrow-circle-right:before { content: '\ebfe'; } +.codicon-arrow-circle-up:before { content: '\ebff'; } +.codicon-layout-sidebar-right-off:before { content: '\ec00'; } +.codicon-layout-panel-off:before { content: '\ec01'; } +.codicon-layout-sidebar-left-off:before { content: '\ec02'; } +.codicon-blank:before { content: '\ec03'; } +.codicon-heart-filled:before { content: '\ec04'; } +.codicon-map:before { content: '\ec05'; } +.codicon-map-horizontal:before { content: '\ec05'; } +.codicon-fold-horizontal:before { content: '\ec05'; } +.codicon-map-filled:before { content: '\ec06'; } +.codicon-map-horizontal-filled:before { content: '\ec06'; } +.codicon-fold-horizontal-filled:before { content: '\ec06'; } +.codicon-circle-small:before { content: '\ec07'; } +.codicon-bell-slash:before { content: '\ec08'; } +.codicon-bell-slash-dot:before { content: '\ec09'; } +.codicon-comment-unresolved:before { content: '\ec0a'; } +.codicon-git-pull-request-go-to-changes:before { content: '\ec0b'; } +.codicon-git-pull-request-new-changes:before { content: '\ec0c'; } +.codicon-search-fuzzy:before { content: '\ec0d'; } +.codicon-comment-draft:before { content: '\ec0e'; } +.codicon-send:before { content: '\ec0f'; } +.codicon-sparkle:before { content: '\ec10'; } +.codicon-insert:before { content: '\ec11'; } +.codicon-mic:before { content: '\ec12'; } +.codicon-thumbsdown-filled:before { content: '\ec13'; } +.codicon-thumbsup-filled:before { content: '\ec14'; } +.codicon-coffee:before { content: '\ec15'; } +.codicon-snake:before { content: '\ec16'; } +.codicon-game:before { content: '\ec17'; } +.codicon-vr:before { content: '\ec18'; } +.codicon-chip:before { content: '\ec19'; } +.codicon-piano:before { content: '\ec1a'; } +.codicon-music:before { content: '\ec1b'; } +.codicon-mic-filled:before { content: '\ec1c'; } +.codicon-repo-fetch:before { content: '\ec1d'; } +.codicon-copilot:before { content: '\ec1e'; } +.codicon-lightbulb-sparkle:before { content: '\ec1f'; } +.codicon-robot:before { content: '\ec20'; } +.codicon-sparkle-filled:before { content: '\ec21'; } +.codicon-diff-single:before { content: '\ec22'; } +.codicon-diff-multiple:before { content: '\ec23'; } +.codicon-surround-with:before { content: '\ec24'; } +.codicon-share:before { content: '\ec25'; } +.codicon-git-stash:before { content: '\ec26'; } +.codicon-git-stash-apply:before { content: '\ec27'; } +.codicon-git-stash-pop:before { content: '\ec28'; } +.codicon-vscode:before { content: '\ec29'; } +.codicon-vscode-insiders:before { content: '\ec2a'; } +.codicon-code-oss:before { content: '\ec2b'; } +.codicon-run-coverage:before { content: '\ec2c'; } +.codicon-run-all-coverage:before { content: '\ec2d'; } +.codicon-coverage:before { content: '\ec2e'; } +.codicon-github-project:before { content: '\ec2f'; } +.codicon-map-vertical:before { content: '\ec30'; } +.codicon-fold-vertical:before { content: '\ec30'; } +.codicon-map-vertical-filled:before { content: '\ec31'; } +.codicon-fold-vertical-filled:before { content: '\ec31'; } +.codicon-go-to-search:before { content: '\ec32'; } +.codicon-percentage:before { content: '\ec33'; } +.codicon-sort-percentage:before { content: '\ec33'; } +.codicon-attach:before { content: '\ec34'; } +.codicon-git-fetch:before { content: '\f101'; } diff --git a/tests/assets/codicon.ttf b/tests/assets/codicon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..27ee4c68caef1cd22342f481420d6dbda1648012 GIT binary patch literal 80340 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%0DOF`LEiy*`&i@0 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 { - await page.setContent(``); - expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox'); +function unshift(snapshot: string): string { + const lines = snapshot.split('\n'); + let whitespacePrefixLength = 100; + for (const line of lines) { + if (!line.trim()) + continue; + const match = line.match(/^(\s*)/); + if (match && match[1].length < whitespacePrefixLength) + whitespacePrefixLength = match[1].length; + break; + } + return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n'); +} + +async function checkAndMatchSnapshot(locator: Locator, snapshot: string) { + expect.soft(await locator.ariaSnapshot()).toBe(unshift(snapshot)); + await expect.soft(locator).toMatchAriaSnapshot(snapshot); +} + +it('should snapshot', async ({ page }) => { + await page.setContent(`

          title

          `); + await checkAndMatchSnapshot(page.locator('body'), ` + - heading "title" + `); }); -it('should snapshot nested element', async ({ page }) => { +it('should snapshot list', async ({ page }) => { await page.setContent(` -
          - -
          `); - expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox'); +

          title

          +

          title 2

          + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - heading "title" + - heading "title 2" + `); }); -it('should snapshot fragment', async ({ page }) => { +it('should snapshot list with accessible name', async ({ page }) => { await page.setContent(` -
          - Link - Link -
          `); - expect(await page.locator('body').ariaSnapshot()).toBe(`- link "Link"\n- link "Link"`); +
            +
          • one
          • +
          • two
          • +
          + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - list "my list": + - listitem: one + - listitem: two + `); +}); + +it('should snapshot complex', async ({ page }) => { + await page.setContent(` + + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - list: + - listitem: + - link "link" + `); +}); + +it('should allow text nodes', async ({ page }) => { + await page.setContent(` +

          Microsoft

          +
          Open source projects and samples from Microsoft
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - heading "Microsoft" + - text: Open source projects and samples from Microsoft + `); +}); + +it('should snapshot details visibility', async ({ page }) => { + await page.setContent(` +
          + Summary +
          Details
          +
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - group: Summary + `); +}); + +it('should snapshot integration', async ({ page }) => { + await page.setContent(` +

          Microsoft

          +
          Open source projects and samples from Microsoft
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - heading "Microsoft" + - text: Open source projects and samples from Microsoft + - list: + - listitem: + - group: Verified + - listitem: + - link "Sponsor" + `); +}); + +it('should support multiline text', async ({ page }) => { + await page.setContent(` +

          + Line 1 + Line 2 + Line 3 +

          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - paragraph: Line 1 Line 2 Line 3 + `); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - paragraph: | + Line 1 + Line 2 + Line 3 + `); +}); + +it('should concatenate span text', async ({ page }) => { + await page.setContent(` + One Two Three + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - text: One Two Three + `); +}); + +it('should concatenate span text 2', async ({ page }) => { + await page.setContent(` + One Two Three + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - text: One Two Three + `); +}); + +it('should concatenate div text with spaces', async ({ page }) => { + await page.setContent(` +
          One
          Two
          Three
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - text: One Two Three + `); +}); + +it('should include pseudo in text', async ({ page }) => { + await page.setContent(` + + + hello +
          hello
          +
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - link "worldhello hellobye" + `); +}); + +it('should not include hidden pseudo in text', async ({ page }) => { + await page.setContent(` + + + hello +
          hello
          +
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - link "hello hello" + `); +}); + +it('should include new line for block pseudo', async ({ page }) => { + await page.setContent(` + + + hello +
          hello
          +
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - link "world hello hello bye" + `); +}); + +it('should work with slots', async ({ page }) => { + // Text "foo" is assigned to the slot, should not be used twice. + await page.setContent(` + + + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - button "foo" + `); + + // Text "foo" is assigned to the slot, should be used instead of slot content. + await page.setContent(` +
          foo
          + + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - button "foo" + `); + + // Nothing is assigned to the slot, should use slot content. + await page.setContent(` +
          + + `); + await checkAndMatchSnapshot(page.locator('body'), ` + - button "pre" + `); +}); + +it('should snapshot inner text', async ({ page }) => { + await page.setContent(` +
          +
          +
          + a.test.ts +
          +
          + + + +
          +
          +
          +
          +
          +
          + snapshot +
          +
          30ms
          +
          + + + +
          +
          +
          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - listitem: + - text: a.test.ts + - button "Run" + - button "Show source" + - button "Watch" + - listitem: + - text: snapshot 30ms + - button "Run" + - button "Show source" + - button "Watch" + `); +}); + +it('should include pseudo codepoints', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + +

          hello

          + `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - paragraph: \ueab2hello + `); }); diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index 5e58ba94e0..fa573f2704 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -94,7 +94,7 @@ test('should allow text nodes', async ({ page }) => { `); }); -test('details visibility', async ({ page, browserName }) => { +test('details visibility', async ({ page }) => { await page.setContent(`
          Summary @@ -107,7 +107,7 @@ test('details visibility', async ({ page, browserName }) => { `); }); -test('integration test', async ({ page, browserName }) => { +test('integration test', async ({ page }) => { await page.setContent(`

          Microsoft

          Open source projects and samples from Microsoft
          diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 180f4d9b33..df6792d59d 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,15 @@ "packages": { "": { "dependencies": { - "@playwright/test": "1.48.0-beta-1728384960000" + "@playwright/test": "1.49.0-alpha-2024-10-17" } }, "node_modules/@playwright/test": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==", - "license": "Apache-2.0", + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", "dependencies": { - "playwright": "1.48.0-beta-1728384960000" + "playwright": "1.49.0-alpha-2024-10-17" }, "bin": { "playwright": "cli.js" @@ -28,7 +27,6 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -38,12 +36,11 @@ } }, "node_modules/playwright": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==", - "license": "Apache-2.0", + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", "dependencies": { - "playwright-core": "1.48.0-beta-1728384960000" + "playwright-core": "1.49.0-alpha-2024-10-17" }, "bin": { "playwright": "cli.js" @@ -56,10 +53,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg==", - "license": "Apache-2.0", + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==", "bin": { "playwright-core": "cli.js" }, @@ -70,11 +66,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==", + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", "requires": { - "playwright": "1.48.0-beta-1728384960000" + "playwright": "1.49.0-alpha-2024-10-17" } }, "fsevents": { @@ -84,18 +80,18 @@ "optional": true }, "playwright": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==", + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.48.0-beta-1728384960000" + "playwright-core": "1.49.0-alpha-2024-10-17" } }, "playwright-core": { - "version": "1.48.0-beta-1728384960000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz", - "integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg==" + "version": "1.49.0-alpha-2024-10-17", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", + "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 3e32d0bbb7..14625ebe6d 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "1.48.0-beta-1728384960000" + "@playwright/test": "1.49.0-alpha-2024-10-17" } } From 0d63df4875b19930acef78ca25917f84b0629d1c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 18 Oct 2024 11:03:00 +0200 Subject: [PATCH 19/35] feat(test runner): allow multiple global setups (#32955) Signed-off-by: Simon Knott Co-authored-by: Dmitry Gozman --- docs/src/test-api/class-testconfig.md | 8 ++-- packages/playwright/src/common/config.ts | 10 ++++- .../playwright/src/common/configLoader.ts | 16 +++++++- packages/playwright/src/runner/tasks.ts | 20 +++++++--- packages/playwright/types/test.d.ts | 10 +++-- tests/playwright-test/global-setup.spec.ts | 40 +++++++++++++++++++ 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 0d1f4c1538..cd70b21b70 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -110,9 +110,9 @@ export default defineConfig({ ## property: TestConfig.globalSetup * since: v1.10 -- type: ?<[string]> +- type: ?<[string]|[Array]<[string]>> -Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. +Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. Pass an array of paths to specify multiple global setup files. Learn more about [global setup and teardown](../test-global-setup-teardown.md). @@ -128,9 +128,9 @@ export default defineConfig({ ## property: TestConfig.globalTeardown * since: v1.10 -- type: ?<[string]> +- type: ?<[string]|[Array]<[string]>> -Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. +Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. Pass an array of paths to specify multiple global teardown files. Learn more about [global setup and teardown](../test-global-setup-teardown.md). diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index a694839f81..fd78f0c8d9 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -58,6 +58,9 @@ export class FullConfigInternal { testIdMatcher?: Matcher; defineConfigWasUsed = false; + globalSetups: string[] = []; + globalTeardowns: string[] = []; + constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) { if (configCLIOverrides.projects && userConfig.projects) throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); @@ -72,13 +75,16 @@ export class FullConfigInternal { this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig); + this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); + this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); + this.config = { configFile: resolvedConfigFile, rootDir: pathResolve(configDir, userConfig.testDir) || configDir, forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false), - globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null), - globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null), + globalSetup: this.globalSetups[0] ?? null, + globalTeardown: this.globalTeardowns[0] ?? null, globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0), grep: takeFirst(userConfig.grep, defaultGrep), grepInvert: takeFirst(userConfig.grepInvert, null), diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index eef56c4458..37a886d3e8 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -139,13 +139,25 @@ function validateConfig(file: string, config: Config) { } if ('globalSetup' in config && config.globalSetup !== undefined) { - if (typeof config.globalSetup !== 'string') + if (Array.isArray(config.globalSetup)) { + config.globalSetup.forEach((item, index) => { + if (typeof item !== 'string') + throw errorWithFile(file, `config.globalSetup[${index}] must be a string`); + }); + } else if (typeof config.globalSetup !== 'string') { throw errorWithFile(file, `config.globalSetup must be a string`); + } } if ('globalTeardown' in config && config.globalTeardown !== undefined) { - if (typeof config.globalTeardown !== 'string') + if (Array.isArray(config.globalTeardown)) { + config.globalTeardown.forEach((item, index) => { + if (typeof item !== 'string') + throw errorWithFile(file, `config.globalTeardown[${index}] must be a string`); + }); + } else if (typeof config.globalTeardown !== 'string') { throw errorWithFile(file, `config.globalTeardown must be a string`); + } } if ('globalTimeout' in config && config.globalTimeout !== undefined) { diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 77d84419f4..528cac47cd 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -98,8 +98,11 @@ export function createGlobalSetupTasks(config: FullConfigInternal) { if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) tasks.push(createRemoveOutputDirsTask()); tasks.push(...createPluginSetupTasks(config)); - if (config.config.globalSetup || config.config.globalTeardown) - tasks.push(createGlobalSetupTask()); + if (config.globalSetups.length || config.globalTeardowns.length) { + const length = Math.max(config.globalSetups.length, config.globalTeardowns.length); + for (let i = 0; i < length; i++) + tasks.push(createGlobalSetupTask(i, length)); + } return tasks; } @@ -161,15 +164,20 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task { +function createGlobalSetupTask(index: number, length: number): Task { let globalSetupResult: any; let globalSetupFinished = false; let teardownHook: any; + + let title = 'global setup'; + if (length > 1) + title += ` #${index}`; + return { - title: 'global setup', + title, setup: async ({ config }) => { - const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; - teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; + const setupHook = config.globalSetups[index] ? await loadGlobalHook(config, config.globalSetups[index]) : undefined; + teardownHook = config.globalTeardowns[index] ? await loadGlobalHook(config, config.globalTeardowns[index]) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupFinished = true; }, diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index a989fe69b2..ae02d1506e 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1077,7 +1077,8 @@ interface TestConfig { /** * Path to the global setup file. This file will be required and run before all the tests. It must export a single - * function that takes a [FullConfig](https://playwright.dev/docs/api/class-fullconfig) argument. + * function that takes a [FullConfig](https://playwright.dev/docs/api/class-fullconfig) argument. Pass an array of + * paths to specify multiple global setup files. * * Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown). * @@ -1093,12 +1094,13 @@ interface TestConfig { * ``` * */ - globalSetup?: string; + globalSetup?: string|Array; /** * Path to the global teardown file. This file will be required and run after all the tests. It must export a single * function. See also - * [testConfig.globalSetup](https://playwright.dev/docs/api/class-testconfig#test-config-global-setup). + * [testConfig.globalSetup](https://playwright.dev/docs/api/class-testconfig#test-config-global-setup). Pass an array + * of paths to specify multiple global teardown files. * * Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown). * @@ -1114,7 +1116,7 @@ interface TestConfig { * ``` * */ - globalTeardown?: string; + globalTeardown?: string|Array; /** * Maximum time in milliseconds the whole test suite can run. Zero timeout (default) disables this behavior. Useful on diff --git a/tests/playwright-test/global-setup.spec.ts b/tests/playwright-test/global-setup.spec.ts index 3d28be82cd..f1bd7b7458 100644 --- a/tests/playwright-test/global-setup.spec.ts +++ b/tests/playwright-test/global-setup.spec.ts @@ -386,3 +386,43 @@ test('teardown after error', async ({ runInlineTest }) => { 'teardown 1', ]); }); + +test('globalSetup should support multiple', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + globalSetup: ['./globalSetup1.ts','./globalSetup2.ts','./globalSetup3.ts','./globalSetup4.ts'], + globalTeardown: ['./globalTeardown1.ts', './globalTeardown2.ts'], + }; + `, + 'globalSetup1.ts': `module.exports = () => { console.log('%%globalSetup1'); return () => { console.log('%%globalSetup1Function'); throw new Error('kaboom'); } };`, + 'globalSetup2.ts': `module.exports = () => console.log('%%globalSetup2');`, + 'globalSetup3.ts': `module.exports = () => { console.log('%%globalSetup3'); return () => console.log('%%globalSetup3Function'); }`, + 'globalSetup4.ts': `module.exports = () => console.log('%%globalSetup4');`, + 'globalTeardown1.ts': `module.exports = () => console.log('%%globalTeardown1')`, + 'globalTeardown2.ts': `module.exports = () => { console.log('%%globalTeardown2'); throw new Error('kaboom'); }`, + + 'a.test.js': ` + import { test } from '@playwright/test'; + test('a', () => console.log('%%test a')); + test('b', () => console.log('%%test b')); + `, + }, { reporter: 'line' }); + expect(result.passed).toBe(2); + + // behaviour: setups in order, teardowns in reverse order. + // setup-returned functions inherit their position, and take precedence over `globalTeardown` scripts. + expect(result.outputLines).toEqual([ + 'globalSetup1', + 'globalSetup2', + 'globalSetup3', + 'globalSetup4', + 'test a', + 'test b', + 'globalSetup3Function', + 'globalTeardown2', + 'globalSetup1Function', + // 'globalTeardown1' is missing, because globalSetup1Function errored out. + ]); + expect(result.output).toContain('Error: kaboom'); +}); From 58ef9e2e5fc82189bfd3d81d2271b29c468480dc Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:34:28 -0700 Subject: [PATCH 20/35] feat(firefox-beta): roll to r1465 (#33170) 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 a579acc365..846ea1e3d3 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -21,9 +21,9 @@ }, { "name": "firefox-beta", - "revision": "1464", + "revision": "1465", "installByDefault": false, - "browserVersion": "131.0b2" + "browserVersion": "132.0b8" }, { "name": "webkit", From 02f8acce028080e3b8dfb79ebe2f51876079a294 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:34:39 -0700 Subject: [PATCH 21/35] feat(chromium): roll to r1143 (#33163) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/chromium/protocol.d.ts | 68 +++++++++++-- .../src/server/deviceDescriptorsSource.json | 96 +++++++++---------- packages/playwright-core/types/protocol.d.ts | 68 +++++++++++-- 5 files changed, 168 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 860e11db65..e4f15a6d97 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-130.0.6723.44-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-131.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-131.0.6778.3-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-131.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 130.0.6723.44 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 131.0.6778.3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 131.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 846ea1e3d3..11a2b1fad1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1142", + "revision": "1143", "installByDefault": true, - "browserVersion": "130.0.6723.44" + "browserVersion": "131.0.6778.3" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 14416bde1e..35aa6f2eb9 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -695,7 +695,7 @@ percentage [0 - 100] for scroll driven animations frameId: Page.FrameId; } export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; - export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"; + export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; /** * This information is currently necessary, as the front-end has a difficult @@ -934,7 +934,7 @@ Should be updated alongside RequestIdTokenStatus in third_party/blink/public/mojom/devtools/inspector_issue.mojom to include all cases except for success. */ - export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByButtonMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; + export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; export interface FederatedAuthUserInfoRequestIssueDetails { federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason; } @@ -5989,7 +5989,7 @@ Missing optional values will be filled in by the target with what it would norma * Used to specify sensor types to emulate. See https://w3c.github.io/sensors/#automation for more information. */ - export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"proximity"|"relative-orientation"; + export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"relative-orientation"; export interface SensorMetadata { available?: boolean; minimumFrequency?: number; @@ -11397,7 +11397,7 @@ Backend then generates 'inspectNodeRequested' event upon element selection. export type setShowHitTestBordersReturnValue = { } /** - * Request that backend shows an overlay with web vital metrics. + * Deprecated, no longer has any effect. */ export type setShowWebVitalsParameters = { show: boolean; @@ -11498,7 +11498,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -12086,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl /** * List of not restored reasons for back-forward cache. */ - export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; + export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient"; /** * Types of not restored reasons for back-forward cache. */ @@ -16634,6 +16634,17 @@ flag set to this value. Defaults to the authenticator's defaultBackupState value. */ backupState?: boolean; + /** + * The credential's user.name property. Equivalent to empty if not set. +https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name + */ + userName?: string; + /** + * The credential's user.displayName property. Equivalent to empty if +not set. +https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname + */ + userDisplayName?: string; } /** @@ -16643,6 +16654,22 @@ defaultBackupState value. authenticatorId: AuthenticatorId; credential: Credential; } + /** + * Triggered when a credential is deleted, e.g. through +PublicKeyCredential.signalUnknownCredential(). + */ + export type credentialDeletedPayload = { + authenticatorId: AuthenticatorId; + credentialId: binary; + } + /** + * Triggered when a credential is updated, e.g. through +PublicKeyCredential.signalCurrentUserDetails(). + */ + export type credentialUpdatedPayload = { + authenticatorId: AuthenticatorId; + credential: Credential; + } /** * Triggered when a credential is used in a webauthn assertion. */ @@ -17076,7 +17103,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"|"V8OptimizerDisabled"|"PrerenderFailedDuringPrefetch"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17751,7 +17778,7 @@ variables as its properties. /** * Type of the debug symbols. */ - type: "None"|"SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; + type: "SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; /** * URL of the external symbol source. */ @@ -17955,9 +17982,9 @@ scripts upon enabling debugger. */ scriptLanguage?: Debugger.ScriptLanguage; /** - * If the scriptLanguage is WebASsembly, the source of debug symbols for the module. + * If the scriptLanguage is WebAssembly, the source of debug symbols for the module. */ - debugSymbols?: Debugger.DebugSymbols; + debugSymbols?: Debugger.DebugSymbols[]; /** * The name the embedder supplied for this script. */ @@ -18280,6 +18307,19 @@ call stacks (default). } export type setAsyncCallStackDepthReturnValue = { } + /** + * Replace previous blackbox execution contexts with passed ones. Forces backend to skip +stepping/pausing in scripts in these execution contexts. VM will try to leave blackboxed script by +performing 'step in' several times, finally resorting to 'step out' if unsuccessful. + */ + export type setBlackboxExecutionContextsParameters = { + /** + * Array of execution context unique ids for the debugger to ignore. + */ + uniqueIds: string[]; + } + export type setBlackboxExecutionContextsReturnValue = { + } /** * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by @@ -18290,6 +18330,10 @@ performing 'step in' several times, finally resorting to 'step out' if unsuccess * Array of regexps that will be used to check script url for blackbox state. */ patterns: string[]; + /** + * If true, also ignore scripts with no source url. + */ + skipAnonymous?: boolean; } export type setBlackboxPatternsReturnValue = { } @@ -20310,6 +20354,8 @@ Error was thrown. "WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload; "WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload; "WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload; + "WebAuthn.credentialDeleted": WebAuthn.credentialDeletedPayload; + "WebAuthn.credentialUpdated": WebAuthn.credentialUpdatedPayload; "WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload; "Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload; "Media.playerEventsAdded": Media.playerEventsAddedPayload; @@ -20897,6 +20943,7 @@ Error was thrown. "Debugger.resume": Debugger.resumeParameters; "Debugger.searchInContent": Debugger.searchInContentParameters; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters; + "Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsParameters; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters; "Debugger.setBreakpoint": Debugger.setBreakpointParameters; @@ -21507,6 +21554,7 @@ Error was thrown. "Debugger.resume": Debugger.resumeReturnValue; "Debugger.searchInContent": Debugger.searchInContentReturnValue; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue; + "Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsReturnValue; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue; "Debugger.setBreakpoint": Debugger.setBreakpointReturnValue; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 6609089642..aef1b4c62b 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36 Edg/130.0.6723.44", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36 Edg/131.0.6778.3", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.44 Safari/537.36 Edg/130.0.6723.44", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36 Edg/131.0.6778.3", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 14416bde1e..35aa6f2eb9 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -695,7 +695,7 @@ percentage [0 - 100] for scroll driven animations frameId: Page.FrameId; } export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; - export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"; + export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; /** * This information is currently necessary, as the front-end has a difficult @@ -934,7 +934,7 @@ Should be updated alongside RequestIdTokenStatus in third_party/blink/public/mojom/devtools/inspector_issue.mojom to include all cases except for success. */ - export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByButtonMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; + export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; export interface FederatedAuthUserInfoRequestIssueDetails { federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason; } @@ -5989,7 +5989,7 @@ Missing optional values will be filled in by the target with what it would norma * Used to specify sensor types to emulate. See https://w3c.github.io/sensors/#automation for more information. */ - export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"proximity"|"relative-orientation"; + export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"relative-orientation"; export interface SensorMetadata { available?: boolean; minimumFrequency?: number; @@ -11397,7 +11397,7 @@ Backend then generates 'inspectNodeRequested' event upon element selection. export type setShowHitTestBordersReturnValue = { } /** - * Request that backend shows an overlay with web vital metrics. + * Deprecated, no longer has any effect. */ export type setShowWebVitalsParameters = { show: boolean; @@ -11498,7 +11498,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -12086,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl /** * List of not restored reasons for back-forward cache. */ - export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; + export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient"; /** * Types of not restored reasons for back-forward cache. */ @@ -16634,6 +16634,17 @@ flag set to this value. Defaults to the authenticator's defaultBackupState value. */ backupState?: boolean; + /** + * The credential's user.name property. Equivalent to empty if not set. +https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name + */ + userName?: string; + /** + * The credential's user.displayName property. Equivalent to empty if +not set. +https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname + */ + userDisplayName?: string; } /** @@ -16643,6 +16654,22 @@ defaultBackupState value. authenticatorId: AuthenticatorId; credential: Credential; } + /** + * Triggered when a credential is deleted, e.g. through +PublicKeyCredential.signalUnknownCredential(). + */ + export type credentialDeletedPayload = { + authenticatorId: AuthenticatorId; + credentialId: binary; + } + /** + * Triggered when a credential is updated, e.g. through +PublicKeyCredential.signalCurrentUserDetails(). + */ + export type credentialUpdatedPayload = { + authenticatorId: AuthenticatorId; + credential: Credential; + } /** * Triggered when a credential is used in a webauthn assertion. */ @@ -17076,7 +17103,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"|"V8OptimizerDisabled"|"PrerenderFailedDuringPrefetch"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17751,7 +17778,7 @@ variables as its properties. /** * Type of the debug symbols. */ - type: "None"|"SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; + type: "SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; /** * URL of the external symbol source. */ @@ -17955,9 +17982,9 @@ scripts upon enabling debugger. */ scriptLanguage?: Debugger.ScriptLanguage; /** - * If the scriptLanguage is WebASsembly, the source of debug symbols for the module. + * If the scriptLanguage is WebAssembly, the source of debug symbols for the module. */ - debugSymbols?: Debugger.DebugSymbols; + debugSymbols?: Debugger.DebugSymbols[]; /** * The name the embedder supplied for this script. */ @@ -18280,6 +18307,19 @@ call stacks (default). } export type setAsyncCallStackDepthReturnValue = { } + /** + * Replace previous blackbox execution contexts with passed ones. Forces backend to skip +stepping/pausing in scripts in these execution contexts. VM will try to leave blackboxed script by +performing 'step in' several times, finally resorting to 'step out' if unsuccessful. + */ + export type setBlackboxExecutionContextsParameters = { + /** + * Array of execution context unique ids for the debugger to ignore. + */ + uniqueIds: string[]; + } + export type setBlackboxExecutionContextsReturnValue = { + } /** * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by @@ -18290,6 +18330,10 @@ performing 'step in' several times, finally resorting to 'step out' if unsuccess * Array of regexps that will be used to check script url for blackbox state. */ patterns: string[]; + /** + * If true, also ignore scripts with no source url. + */ + skipAnonymous?: boolean; } export type setBlackboxPatternsReturnValue = { } @@ -20310,6 +20354,8 @@ Error was thrown. "WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload; "WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload; "WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload; + "WebAuthn.credentialDeleted": WebAuthn.credentialDeletedPayload; + "WebAuthn.credentialUpdated": WebAuthn.credentialUpdatedPayload; "WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload; "Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload; "Media.playerEventsAdded": Media.playerEventsAddedPayload; @@ -20897,6 +20943,7 @@ Error was thrown. "Debugger.resume": Debugger.resumeParameters; "Debugger.searchInContent": Debugger.searchInContentParameters; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters; + "Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsParameters; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters; "Debugger.setBreakpoint": Debugger.setBreakpointParameters; @@ -21507,6 +21554,7 @@ Error was thrown. "Debugger.resume": Debugger.resumeReturnValue; "Debugger.searchInContent": Debugger.searchInContentReturnValue; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue; + "Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsReturnValue; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue; "Debugger.setBreakpoint": Debugger.setBreakpointReturnValue; From 6ea17a5d82e8a9f783a86f76f7a0c1219d4a495a Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 18 Oct 2024 16:38:55 +0200 Subject: [PATCH 22/35] chore: move non-test utility workflows to ubuntu-24.04 (#33176) --- .github/workflows/infra.yml | 2 +- .github/workflows/pr_check_client_side_changes.yml | 2 +- .github/workflows/publish_canary.yml | 2 +- .github/workflows/publish_release_npm.yml | 2 +- .github/workflows/publish_release_traceviewer.yml | 2 +- .github/workflows/roll_browser_into_playwright.yml | 2 +- .github/workflows/trigger_tests.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index 905597c8bd..1d52ddb96d 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -16,7 +16,7 @@ env: jobs: doc-and-lint: name: "docs & lint" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/.github/workflows/pr_check_client_side_changes.yml b/.github/workflows/pr_check_client_side_changes.yml index 7748b5d514..17a8da9ca3 100644 --- a/.github/workflows/pr_check_client_side_changes.yml +++ b/.github/workflows/pr_check_client_side_changes.yml @@ -12,7 +12,7 @@ on: jobs: check: name: Check - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish_canary.yml b/.github/workflows/publish_canary.yml index 64d25dbd6d..78fb0ba5a9 100644 --- a/.github/workflows/publish_canary.yml +++ b/.github/workflows/publish_canary.yml @@ -65,7 +65,7 @@ jobs: publish-trace-viewer: name: "publish Trace Viewer to trace.playwright.dev" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish_release_npm.yml b/.github/workflows/publish_release_npm.yml index 46b5816834..bbef0c5c62 100644 --- a/.github/workflows/publish_release_npm.yml +++ b/.github/workflows/publish_release_npm.yml @@ -10,7 +10,7 @@ env: jobs: publish-npm-release: name: "publish to NPM" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' permissions: contents: read diff --git a/.github/workflows/publish_release_traceviewer.yml b/.github/workflows/publish_release_traceviewer.yml index 60af5442e9..e61ac76ccd 100644 --- a/.github/workflows/publish_release_traceviewer.yml +++ b/.github/workflows/publish_release_traceviewer.yml @@ -7,7 +7,7 @@ on: jobs: publish-trace-viewer: name: "publish Trace Viewer to trace.playwright.dev" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/roll_browser_into_playwright.yml b/.github/workflows/roll_browser_into_playwright.yml index da90513160..e24d015cf8 100644 --- a/.github/workflows/roll_browser_into_playwright.yml +++ b/.github/workflows/roll_browser_into_playwright.yml @@ -12,7 +12,7 @@ permissions: jobs: roll: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/.github/workflows/trigger_tests.yml b/.github/workflows/trigger_tests.yml index dcd68dca37..1ea2ec424d 100644 --- a/.github/workflows/trigger_tests.yml +++ b/.github/workflows/trigger_tests.yml @@ -9,7 +9,7 @@ on: jobs: trigger: name: "trigger" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - run: | curl -X POST \ From 2e8e7a66cd9d6dae8f4a9d2a60331bcc4c7d24f0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 18 Oct 2024 13:50:43 -0700 Subject: [PATCH 23/35] chore: implement tree w/o list (#33169) --- .../src/matchers/toMatchAriaSnapshot.ts | 18 +- packages/trace-viewer/src/ui/actionList.tsx | 36 ++- .../src/ui/uiModeTestListView.tsx | 3 +- packages/web/src/components/toolbarButton.tsx | 2 +- packages/web/src/components/treeView.tsx | 231 +++++++++++------- tests/page/to-match-aria-snapshot.spec.ts | 8 +- tests/playwright-test/ui-mode-fixtures.ts | 11 +- .../ui-mode-test-annotations.spec.ts | 2 +- .../playwright-test/ui-mode-test-run.spec.ts | 8 +- .../ui-mode-test-setup.spec.ts | 26 +- .../ui-mode-test-update.spec.ts | 2 +- .../ui-mode-test-watch.spec.ts | 16 +- 12 files changed, 221 insertions(+), 142 deletions(-) diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index cf043c2ca8..5b2c204410 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -49,8 +49,8 @@ export async function toMatchAriaSnapshot( const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; - const escapedExpected = escapePrivateUsePoints(expected); - const escapedReceived = escapePrivateUsePoints(received); + const escapedExpected = unshift(escapePrivateUsePoints(expected)); + const escapedReceived = unshift(escapePrivateUsePoints(received)); const message = () => { if (pass) { if (notFound) @@ -79,3 +79,17 @@ export async function toMatchAriaSnapshot( function escapePrivateUsePoints(str: string) { return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`); } + +function unshift(snapshot: string): string { + const lines = snapshot.split('\n'); + let whitespacePrefixLength = 100; + for (const line of lines) { + if (!line.trim()) + continue; + const match = line.match(/^(\s*)/); + if (match && match[1].length < whitespacePrefixLength) + whitespacePrefixLength = match[1].length; + break; + } + return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n'); +} diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index f375ab0baa..d369aeede3 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -59,6 +59,30 @@ export const ActionList: React.FC = ({ return { selectedItem }; }, [itemMap, selectedAction]); + const isError = React.useCallback((item: ActionTreeItem) => { + return !!item.action?.error?.message; + }, []); + + const onAccepted = React.useCallback((item: ActionTreeItem) => { + return setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime }); + }, [setSelectedTime]); + + const render = React.useCallback((item: ActionTreeItem) => { + return renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true }); + }, [isLive, revealConsole, sdkLanguage]); + + const isVisible = React.useCallback((item: ActionTreeItem) => { + return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum); + }, [selectedTime]); + + const onSelectedAction = React.useCallback((item: ActionTreeItem) => { + onSelected?.(item.action!); + }, [onSelected]); + + const onHighlightedAction = React.useCallback((item: ActionTreeItem | undefined) => { + onHighlighted?.(item?.action); + }, [onHighlighted]); + return
          {selectedTime &&
          setSelectedTime(undefined)}>Show all
          } = ({ treeState={treeState} setTreeState={setTreeState} selectedItem={selectedItem} - onSelected={item => onSelected?.(item.action!)} - onHighlighted={item => onHighlighted?.(item?.action)} - onAccepted={item => setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime })} - isError={item => !!item.action?.error?.message} - isVisible={item => !selectedTime || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum)} - render={item => renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true })} + onSelected={onSelectedAction} + onHighlighted={onHighlightedAction} + onAccepted={onAccepted} + isError={isError} + isVisible={isVisible} + render={render} />
          ; }; diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index 96fbaadbf7..e0ef2a8bca 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -161,7 +161,7 @@ export const TestListView: React.FC<{ render={treeItem => { return
          - {treeItem.title} + {treeItem.title} {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
          {!!treeItem.duration && treeItem.status !== 'skipped' &&
          {msToString(treeItem.duration)}
          } @@ -179,6 +179,7 @@ export const TestListView: React.FC<{
          ; }} icon={treeItem => testStatusIcon(treeItem.status)} + title={treeItem => treeItem.title} selectedItem={selectedTreeItem} onAccepted={runTreeItem} onSelected={treeItem => { diff --git a/packages/web/src/components/toolbarButton.tsx b/packages/web/src/components/toolbarButton.tsx index 184642b395..2cdd85b9b7 100644 --- a/packages/web/src/components/toolbarButton.tsx +++ b/packages/web/src/components/toolbarButton.tsx @@ -52,7 +52,7 @@ export const ToolbarButton: React.FC disabled={!!disabled} style={style} data-testid={testId} - aria-label={ariaLabel} + aria-label={ariaLabel || title} > {icon && } {children} diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 6ad7221455..9af8609f3b 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -32,6 +32,7 @@ export type TreeViewProps = { name: string, rootItem: T, render: (item: T) => React.ReactNode, + title?: (item: T) => string, icon?: (item: T) => string | undefined, isError?: (item: T) => boolean, isVisible?: (item: T) => boolean, @@ -52,6 +53,7 @@ export function TreeView({ name, rootItem, render, + title, icon, isError, isVisible, @@ -66,40 +68,12 @@ export function TreeView({ autoExpandDepth, }: TreeViewProps) { const treeItems = React.useMemo(() => { - return flattenTree(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0); - }, [rootItem, selectedItem, treeState, autoExpandDepth]); - - // Filter visible items. - const visibleItems = React.useMemo(() => { - if (!isVisible) - return [...treeItems.keys()]; - const cachedVisible = new Map(); - const visit = (item: TreeItem): boolean => { - const cachedResult = cachedVisible.get(item); - if (cachedResult !== undefined) - return cachedResult; - - let hasVisibleChildren = item.children.some(child => visit(child)); - for (const child of item.children) { - const result = visit(child); - hasVisibleChildren = hasVisibleChildren || result; - } - const result = isVisible(item as T) || hasVisibleChildren; - cachedVisible.set(item, result); - return result; - }; - for (const item of treeItems.keys()) - visit(item); - const result: T[] = []; - for (const item of treeItems.keys()) { - if (isVisible(item)) - result.push(item); - } - return result; - }, [treeItems, isVisible]); + return indexTree(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0, isVisible); + }, [rootItem, selectedItem, treeState, autoExpandDepth, isVisible]); const itemListRef = React.useRef(null); const [highlightedItem, setHighlightedItem] = React.useState(); + const [isKeyboardNavigation, setIsKeyboardNavigation] = React.useState(false); React.useEffect(() => { onHighlighted?.(highlightedItem); @@ -171,45 +145,55 @@ export function TreeView({ return; } - const index = selectedItem ? visibleItems.indexOf(selectedItem) : -1; - let newIndex = index; + let newSelectedItem: T | undefined = selectedItem; if (event.key === 'ArrowDown') { - if (index === -1) - newIndex = 0; - else - newIndex = Math.min(index + 1, visibleItems.length - 1); + if (selectedItem) { + const itemData = treeItems.get(selectedItem)!; + newSelectedItem = itemData.next as T; + } else if (treeItems.size) { + const itemList = [...treeItems.keys()]; + newSelectedItem = itemList[0]; + } } if (event.key === 'ArrowUp') { - if (index === -1) - newIndex = visibleItems.length - 1; - else - newIndex = Math.max(index - 1, 0); + if (selectedItem) { + const itemData = treeItems.get(selectedItem)!; + newSelectedItem = itemData.prev as T; + } else if (treeItems.size) { + const itemList = [...treeItems.keys()]; + newSelectedItem = itemList[itemList.length - 1]; + } } - const element = itemListRef.current?.children.item(newIndex); - scrollIntoViewIfNeeded(element || undefined); + // scrollIntoViewIfNeeded(element || undefined); onHighlighted?.(undefined); - onSelected?.(visibleItems[newIndex]); + if (newSelectedItem) { + setIsKeyboardNavigation(true); + onSelected?.(newSelectedItem); + } setHighlightedItem(undefined); }} ref={itemListRef} > - {noItemsMessage && visibleItems.length === 0 &&
          {noItemsMessage}
          } - {visibleItems.map(item => { - return
          - -
          ; + {noItemsMessage && treeItems.size === 0 &&
          {noItemsMessage}
          } + {rootItem.children.map(child => { + const itemData = treeItems.get(child as T); + return itemData && ; })}
          ; @@ -217,7 +201,7 @@ export function TreeView({ type TreeItemHeaderProps = { item: T, - itemData: TreeItemData, + treeItems: Map, selectedItem: T | undefined, onSelected?: (item: T) => void, toggleExpanded: (item: T) => void, @@ -226,12 +210,15 @@ type TreeItemHeaderProps = { onAccepted?: (item: T) => void, setHighlightedItem: (item: T | undefined) => void, render: (item: T) => React.ReactNode, + title?: (item: T) => string, icon?: (item: T) => string | undefined, + isKeyboardNavigation: boolean, + setIsKeyboardNavigation: (value: boolean) => void, }; export function TreeItemHeader({ item, - itemData, + treeItems, selectedItem, onSelected, highlightedItem, @@ -240,68 +227,122 @@ export function TreeItemHeader({ onAccepted, toggleExpanded, render, - icon }: TreeItemHeaderProps) { + title, + icon, + isKeyboardNavigation, + setIsKeyboardNavigation }: TreeItemHeaderProps) { + const itemRef = React.useRef(null); + React.useEffect(() => { + if (selectedItem === item && isKeyboardNavigation && itemRef.current) { + scrollIntoViewIfNeeded(itemRef.current); + setIsKeyboardNavigation(false); + } + }, [item, selectedItem, isKeyboardNavigation, setIsKeyboardNavigation]); + + const itemData = treeItems.get(item)!; const indentation = itemData.depth; const expanded = itemData.expanded; let expandIcon = 'codicon-blank'; if (typeof expanded === 'boolean') expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'; const rendered = render(item); + const children = expanded && item.children.length ? item.children as T[] : []; + const titled = title?.(item); - return
          onAccepted?.(item)} - className={clsx( - 'tree-view-entry', - selectedItem === item && 'selected', - highlightedItem === item && 'highlighted', - isError?.(item) && 'error')} - onClick={() => onSelected?.(item)} - onMouseEnter={() => setHighlightedItem(item)} - onMouseLeave={() => setHighlightedItem(undefined)} - > - {indentation ? new Array(indentation).fill(0).map((_, i) =>
          ) : undefined} + return
          { - e.preventDefault(); - e.stopPropagation(); - }} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - toggleExpanded(item); - }} - /> - {icon &&
          } - {typeof rendered === 'string' ?
          {rendered}
          : rendered} + onDoubleClick={() => onAccepted?.(item)} + className={clsx( + 'tree-view-entry', + selectedItem === item && 'selected', + highlightedItem === item && 'highlighted', + isError?.(item) && 'error')} + onClick={() => onSelected?.(item)} + onMouseEnter={() => setHighlightedItem(item)} + onMouseLeave={() => setHighlightedItem(undefined)} + > + {indentation ? new Array(indentation).fill(0).map((_, i) =>
          ) : undefined} + + {!!children.length &&
          + {children.map(child => { + const itemData = treeItems.get(child); + return itemData && ; + })} +
          }
          ; } type TreeItemData = { - depth: number, - expanded: boolean | undefined, - parent: TreeItem | null, + depth: number; + expanded: boolean | undefined; + parent: TreeItem | null; + next: TreeItem | null; + prev: TreeItem | null; }; -function flattenTree( +function indexTree( rootItem: T, selectedItem: T | undefined, expandedItems: Map, - autoExpandDepth: number): Map { + autoExpandDepth: number, + isVisible?: (item: T) => boolean): Map { const result = new Map(); const temporaryExpanded = new Set(); for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) temporaryExpanded.add(item.id); + let lastItem: T | null = null; const appendChildren = (parent: T, depth: number) => { + if (isVisible && !isVisible(parent)) + return; for (const item of parent.children as T[]) { const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id); const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false; const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined; - result.set(item, { depth, expanded, parent: rootItem === parent ? null : parent }); + const itemData: TreeItemData = { + depth, + expanded, + parent: rootItem === parent ? null : parent, + next: null, + prev: lastItem, + }; + if (lastItem) + result.get(lastItem)!.next = item; + lastItem = item; + result.set(item, itemData); if (expanded) appendChildren(item, depth + 1); } diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index fa573f2704..3de7b6a9d4 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -181,14 +181,12 @@ test('expected formatter', async ({ page }) => { expect(stripAnsi(error.message)).toContain(` Locator: locator('body') -- Expected - 4 +- Expected - 2 + Received string + 3 -- +- - heading "todos" + - banner: -- - heading "todos" + - heading "todos" -- - textbox "Wrong text" -- +- - textbox "Wrong text" + - textbox "What needs to be done?"`); }); diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index 1e3b11a03a..7ae98bb662 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -68,14 +68,15 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () = const result: string[] = []; const treeItems = treeElement.querySelectorAll('[role=treeitem]'); for (const treeItem of treeItems) { - const iconElements = treeItem.querySelectorAll('.codicon'); + const treeItemHeader = treeItem.querySelector('.tree-view-entry'); + const iconElements = treeItemHeader.querySelectorAll('.codicon'); const treeIcon = iconName(iconElements[0]); const statusIcon = iconName(iconElements[1]); - const indent = treeItem.querySelectorAll('.tree-view-indent').length; - const watch = treeItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; + const indent = treeItemHeader.querySelectorAll('.tree-view-indent').length; + const watch = treeItemHeader.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : ''; - const title = treeItem.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent; - const timeElement = options.time ? treeItem.querySelector('.ui-mode-tree-item-time') : undefined; + const title = treeItemHeader.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent; + const timeElement = options.time ? treeItemHeader.querySelector('.ui-mode-tree-item-time') : undefined; const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : ''; result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected); } diff --git a/tests/playwright-test/ui-mode-test-annotations.spec.ts b/tests/playwright-test/ui-mode-test-annotations.spec.ts index f32d43aecf..eeff6a5aca 100644 --- a/tests/playwright-test/ui-mode-test-annotations.spec.ts +++ b/tests/playwright-test/ui-mode-test-annotations.spec.ts @@ -33,7 +33,7 @@ test('should display annotations', async ({ runUITest }) => { }); await page.getByTitle('Run all').click(); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); - await page.getByRole('treeitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click(); + await page.getByRole('treeitem', { name: 'suite' }).locator('.codicon-chevron-right').click(); await page.getByText('annotation test').click(); await page.getByText('Annotations', { exact: true }).click(); diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 24731bcbb2..1120cede6b 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -93,7 +93,7 @@ test('should run on hover', async ({ runUITest }) => { }); await page.getByText('passes').hover(); - await page.getByRole('treeitem').filter({ hasText: 'passes' }).getByTitle('Run').click(); + await page.getByRole('treeitem', { name: 'passes' }).getByRole('button', { name: 'Run' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts @@ -275,7 +275,7 @@ test('should run folder', async ({ runUITest }) => { }); await page.getByText('folder-b').hover(); - await page.getByRole('treeitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click(); + await page.getByRole('treeitem', { name: 'folder-b' }).getByRole('button', { name: 'Run' }).click(); await expect.poll(dumpTestTree(page)).toContain(` ▼ ✅ folder-b <= @@ -421,8 +421,8 @@ test('should show proper total when using deps', async ({ runUITest }) => { await page.getByText('Status:').click(); - await page.getByLabel('setup').setChecked(true); - await page.getByLabel('chromium').setChecked(true); + await page.getByRole('checkbox', { name: 'setup' }).setChecked(true); + await page.getByRole('checkbox', { name: 'chromium' }).setChecked(true); await expect.poll(dumpTestTree(page)).toContain(` ▼ ◯ a.test.ts diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index f8de9e262a..65c6aa2533 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -140,9 +140,9 @@ const testsWithSetup = { test('should run setup and teardown projects (1)', async ({ runUITest }) => { const { page } = await runUITest(testsWithSetup); await page.getByText('Status:').click(); - await page.getByLabel('setup').setChecked(false); - await page.getByLabel('teardown').setChecked(false); - await page.getByLabel('test').setChecked(false); + await page.getByRole('checkbox', { name: 'setup' }).setChecked(false); + await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false); + await page.getByRole('checkbox', { name: 'test' }).setChecked(false); await page.getByTitle('Run all').click(); @@ -164,9 +164,9 @@ test('should run setup and teardown projects (1)', async ({ runUITest }) => { test('should run setup and teardown projects (2)', async ({ runUITest }) => { const { page } = await runUITest(testsWithSetup); await page.getByText('Status:').click(); - await page.getByLabel('setup').setChecked(false); - await page.getByLabel('teardown').setChecked(true); - await page.getByLabel('test').setChecked(true); + await page.getByRole('checkbox', { name: 'setup' }).setChecked(false); + await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true); + await page.getByRole('checkbox', { name: 'test' }).setChecked(true); await page.getByTitle('Run all').click(); @@ -186,9 +186,9 @@ test('should run setup and teardown projects (2)', async ({ runUITest }) => { test('should run setup and teardown projects (3)', async ({ runUITest }) => { const { page } = await runUITest(testsWithSetup); await page.getByText('Status:').click(); - await page.getByLabel('setup').setChecked(false); - await page.getByLabel('teardown').setChecked(false); - await page.getByLabel('test').setChecked(true); + await page.getByRole('checkbox', { name: 'setup' }).setChecked(false); + await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false); + await page.getByRole('checkbox', { name: 'test' }).setChecked(true); await page.getByTitle('Run all').click(); @@ -206,12 +206,12 @@ test('should run setup and teardown projects (3)', async ({ runUITest }) => { test('should run part of the setup only', async ({ runUITest }) => { const { page } = await runUITest(testsWithSetup); await page.getByText('Status:').click(); - await page.getByLabel('setup').setChecked(true); - await page.getByLabel('teardown').setChecked(true); - await page.getByLabel('test').setChecked(true); + await page.getByRole('checkbox', { name: 'setup' }).setChecked(true); + await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true); + await page.getByRole('checkbox', { name: 'test' }).setChecked(true); await page.getByText('setup.ts').hover(); - await page.getByRole('treeitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click(); + await page.getByRole('treeitem', { name: 'setup.ts' }).getByRole('button', { name: 'Run' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ✅ setup.ts <= diff --git a/tests/playwright-test/ui-mode-test-update.spec.ts b/tests/playwright-test/ui-mode-test-update.spec.ts index ae5752c3f0..67d0626a5f 100644 --- a/tests/playwright-test/ui-mode-test-update.spec.ts +++ b/tests/playwright-test/ui-mode-test-update.spec.ts @@ -215,7 +215,7 @@ test('should update test locations', async ({ runUITest, writeFiles }) => { const messages: any[] = []; await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg)); - const passesItemLocator = page.getByRole('treeitem').filter({ hasText: 'passes' }); + const passesItemLocator = page.getByRole('treeitem', { name: 'passes' }); await passesItemLocator.hover(); await passesItemLocator.getByTitle('Show source').click(); await page.getByTitle('Open in VS Code').click(); diff --git a/tests/playwright-test/ui-mode-test-watch.spec.ts b/tests/playwright-test/ui-mode-test-watch.spec.ts index 893a0ef7ac..9bbbdc0ec0 100644 --- a/tests/playwright-test/ui-mode-test-watch.spec.ts +++ b/tests/playwright-test/ui-mode-test-watch.spec.ts @@ -28,14 +28,14 @@ test('should watch files', async ({ runUITest, writeFiles }) => { }); await page.getByText('fails').click(); - await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Watch' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts ◯ passes ◯ fails 👁 <= `); - await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Run').click(); + await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Run' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ❌ a.test.ts @@ -75,7 +75,7 @@ test('should watch e2e deps', async ({ runUITest, writeFiles }) => { }); await page.getByText('answer').click(); - await page.getByRole('treeitem').filter({ hasText: 'answer' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'answer' }).getByRole('button', { name: 'Watch' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts ◯ answer 👁 <= @@ -102,13 +102,13 @@ test('should batch watch updates', async ({ runUITest, writeFiles }) => { }); await page.getByText('a.test.ts').click(); - await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'a.test.ts' }).getByRole('button', { name: 'Watch' }).click(); await page.getByText('b.test.ts').click(); - await page.getByRole('treeitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'b.test.ts' }).getByRole('button', { name: 'Watch' }).click(); await page.getByText('c.test.ts').click(); - await page.getByRole('treeitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'c.test.ts' }).getByRole('button', { name: 'Watch' }).click(); await page.getByText('d.test.ts').click(); - await page.getByRole('treeitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'd.test.ts' }).getByRole('button', { name: 'Watch' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts 👁 @@ -229,7 +229,7 @@ test('should run added test in watched file', async ({ runUITest, writeFiles }) }); await page.getByText('a.test.ts').click(); - await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click(); + await page.getByRole('treeitem', { name: 'a.test.ts' }).getByRole('button', { name: 'Watch' }).click(); await expect.poll(dumpTestTree(page)).toBe(` ▼ ◯ a.test.ts 👁 <= From b1fb4f16a75994330e38ea14fa38423d66197f40 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 18 Oct 2024 23:00:05 +0200 Subject: [PATCH 24/35] chore: hide 'markdown' reporter (#33140) --- .github/workflows/merge.config.ts | 2 +- packages/playwright/src/common/config.ts | 2 +- packages/playwright/src/runner/reporters.ts | 2 -- tests/playwright-test/reporter-markdown.spec.ts | 10 ++++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/merge.config.ts b/.github/workflows/merge.config.ts index b39944bc80..e8582ed521 100644 --- a/.github/workflows/merge.config.ts +++ b/.github/workflows/merge.config.ts @@ -1,4 +1,4 @@ export default { testDir: '../../tests', - reporter: [['markdown'], ['html']] + reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']] }; \ No newline at end of file diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index fd78f0c8d9..031a5215f2 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -276,7 +276,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | return reporters; } -export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const; +export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob'] as const; export type BuiltInReporter = typeof builtInReporters[number]; export type ContextReuseMode = 'none' | 'when-possible'; diff --git a/packages/playwright/src/runner/reporters.ts b/packages/playwright/src/runner/reporters.ts index 2f7b16f2a6..2bab152f08 100644 --- a/packages/playwright/src/runner/reporters.ts +++ b/packages/playwright/src/runner/reporters.ts @@ -25,7 +25,6 @@ import JSONReporter from '../reporters/json'; import JUnitReporter from '../reporters/junit'; import LineReporter from '../reporters/line'; import ListReporter from '../reporters/list'; -import MarkdownReporter from '../reporters/markdown'; import type { Suite } from '../common/test'; import type { BuiltInReporter, FullConfigInternal } from '../common/config'; import { loadReporter } from './loadUtils'; @@ -45,7 +44,6 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' | junit: JUnitReporter, null: EmptyReporter, html: HtmlReporter, - markdown: MarkdownReporter, }; const reporters: ReporterV2[] = []; descriptions ??= config.config.reporter; diff --git a/tests/playwright-test/reporter-markdown.spec.ts b/tests/playwright-test/reporter-markdown.spec.ts index d24f2561c1..076e28d66e 100644 --- a/tests/playwright-test/reporter-markdown.spec.ts +++ b/tests/playwright-test/reporter-markdown.spec.ts @@ -18,12 +18,14 @@ import fs from 'fs'; import path from 'path'; import { expect, test } from './playwright-test-fixtures'; +const markdownReporter = require.resolve('../../packages/playwright/lib/reporters/markdown'); + test('simple report', async ({ runInlineTest }) => { const files = { 'playwright.config.ts': ` module.exports = { retries: 1, - reporter: 'markdown', + reporter: ${JSON.stringify(markdownReporter)}, }; `, 'dir1/a.test.js': ` @@ -83,7 +85,7 @@ test('custom report file', async ({ runInlineTest }) => { const files = { 'playwright.config.ts': ` module.exports = { - reporter: [['markdown', { outputFile: 'my-report.md' }]], + reporter: [[${JSON.stringify(markdownReporter)}, { outputFile: 'my-report.md' }]], }; `, 'a.test.js': ` @@ -107,7 +109,7 @@ test('report error without snippet', async ({ runInlineTest }) => { 'playwright.config.ts': ` module.exports = { retries: 1, - reporter: 'markdown', + reporter: ${JSON.stringify(markdownReporter)}, }; `, 'a.test.js': ` @@ -135,7 +137,7 @@ test('report with worker error', async ({ runInlineTest }) => { 'playwright.config.ts': ` module.exports = { retries: 1, - reporter: 'markdown', + reporter: ${JSON.stringify(markdownReporter)}, }; `, 'a.test.js': ` From 64bf1bc1072a48e0def6785c4885a58497452fc5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 18 Oct 2024 20:18:18 -0700 Subject: [PATCH 25/35] chore: support basic aria attributes (#33182) --- .../src/server/ariaSnapshot.ts | 136 ++++++++++-------- .../src/server/injected/ariaSnapshot.ts | 134 ++++++++++++----- .../src/server/injected/roleUtils.ts | 62 ++++---- tests/page/page-aria-snapshot.spec.ts | 15 +- tests/page/to-match-aria-snapshot.spec.ts | 18 ++- 5 files changed, 236 insertions(+), 129 deletions(-) diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts index e450e5b15d..47ed4066b7 100644 --- a/packages/playwright-core/src/server/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -16,63 +16,87 @@ import type { AriaTemplateNode } from './injected/ariaSnapshot'; import { yaml } from '../utilsBundle'; +import type { AriaRole } from '@injected/roleUtils'; export function parseAriaSnapshot(text: string): AriaTemplateNode { - type YamlNode = Record | string>; - - const parseKey = (key: string): AriaTemplateNode => { - if (!key) - return { role: '' }; - - const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/); - - if (!match) - throw new Error(`Invalid key ${key}`); - - const role = match[1]; - if (role && role !== 'text' && !allRoles.includes(role)) - throw new Error(`Invalid role ${role}`); - - if (match[2]) - return { role, name: match[2] }; - if (match[3]) - return { role, name: new RegExp(match[3]) }; - return { role }; - }; - - const normalizeWhitespace = (text: string) => { - return text.replace(/[\r\n\s\t]+/g, ' ').trim(); - }; - - const valueOrRegex = (value: string): string | RegExp => { - return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value); - }; - - const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => { - const key = typeof object === 'string' ? object : Object.keys(object)[0]; - const value = typeof object === 'string' ? undefined : object[key]; - const parsed = parseKey(key); - if (parsed.role === 'text') { - if (typeof value !== 'string') - throw new Error(`Generic role must have a text value`); - return valueOrRegex(value as string); - } - if (Array.isArray(value)) - parsed.children = value.map(convert); - else if (value) - parsed.children = [valueOrRegex(value)]; - return parsed; - }; - const fragment = yaml.parse(text) as YamlNode[]; - return convert({ '': fragment }) as AriaTemplateNode; + const fragment = yaml.parse(text) as any[]; + const result: AriaTemplateNode = { role: 'fragment' }; + populateNode(result, fragment); + return result; } -const allRoles = [ - 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command', - 'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', - 'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu', - 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', - 'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider', - 'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', - 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window' -]; +function populateNode(node: AriaTemplateNode, container: any[]) { + for (const object of container) { + if (typeof object === 'string') { + const { role, name } = parseKey(object); + node.children = node.children || []; + node.children.push({ role, name }); + continue; + } + for (const key of Object.keys(object)) { + if (key === 'checked') { + node.checked = object[key]; + continue; + } + if (key === 'disabled') { + node.disabled = object[key]; + continue; + } + if (key === 'expanded') { + node.expanded = object[key]; + continue; + } + if (key === 'level') { + node.level = object[key]; + continue; + } + if (key === 'pressed') { + node.pressed = object[key]; + continue; + } + if (key === 'selected') { + node.selected = object[key]; + continue; + } + + const { role, name } = parseKey(key); + const value = object[key]; + node.children = node.children || []; + + if (role === 'text') { + node.children.push(valueOrRegex(value)); + continue; + } + + if (typeof value === 'string') { + node.children.push({ role, name, children: [valueOrRegex(value)] }); + continue; + } + + const childNode = { role, name }; + node.children.push(childNode); + populateNode(childNode, value); + } + } +} + +function parseKey(key: string) { + const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/); + if (!match) + throw new Error(`Invalid key ${key}`); + + const role = match[1] as AriaRole | 'text'; + if (match[2]) + return { role, name: match[2] }; + if (match[3]) + return { role, name: new RegExp(match[3]) }; + return { role }; +} + +function normalizeWhitespace(text: string) { + return text.replace(/[\r\n\s\t]+/g, ' ').trim(); +} + +function valueOrRegex(value: string): string | RegExp { + return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value); +} diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 907006ce0a..16d8dc6070 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -15,38 +15,32 @@ */ import { escapeWithQuotes } from '@isomorphic/stringUtils'; -import { accumulatedElementText, beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, getPseudoContent, isElementIgnoredForAria } from './roleUtils'; +import * as roleUtils from './roleUtils'; import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils'; +import type { AriaRole } from './roleUtils'; -type AriaNode = { - role: string; - name?: string; +type AriaProps = { + checked?: boolean | 'mixed'; + disabled?: boolean; + expanded?: boolean | 'none', + level?: number, + pressed?: boolean | 'mixed'; + selected?: boolean; +}; + +type AriaNode = AriaProps & { + role: AriaRole | 'fragment' | 'text'; + name: string; children: (AriaNode | string)[]; }; -export type AriaTemplateNode = { - role: string; +export type AriaTemplateNode = AriaProps & { + role: AriaRole | 'fragment' | 'text'; name?: RegExp | string; children?: (AriaTemplateNode | string | RegExp)[]; }; export function generateAriaTree(rootElement: Element): AriaNode { - const toAriaNode = (element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null => { - const role = getAriaRole(element); - if (!role) - return null; - - const name = role ? getElementAccessibleName(element, false) || undefined : undefined; - const isLeaf = leafRoles.has(role); - const result: AriaNode = { role, name, children: [] }; - if (isLeaf && !name) { - const text = accumulatedElementText(element); - if (text) - result.children = [text]; - } - return { isLeaf, ariaNode: result }; - }; - const visit = (ariaNode: AriaNode, node: Node) => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { const text = node.nodeValue; @@ -59,7 +53,7 @@ export function generateAriaTree(rootElement: Element): AriaNode { return; const element = node as Element; - if (isElementIgnoredForAria(element)) + if (roleUtils.isElementIgnoredForAria(element)) return; const visible = isElementVisible(element); @@ -87,7 +81,7 @@ export function generateAriaTree(rootElement: Element): AriaNode { if (treatAsBlock) ariaNode.children.push(treatAsBlock); - ariaNode.children.push(getPseudoContent(element, '::before')); + ariaNode.children.push(roleUtils.getPseudoContent(element, '::before')); const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; if (assignedNodes.length) { for (const child of assignedNodes) @@ -103,24 +97,59 @@ export function generateAriaTree(rootElement: Element): AriaNode { } } - ariaNode.children.push(getPseudoContent(element, '::after')); + ariaNode.children.push(roleUtils.getPseudoContent(element, '::after')); if (treatAsBlock) ariaNode.children.push(treatAsBlock); } - beginAriaCaches(); - const ariaRoot: AriaNode = { role: '', children: [] }; + roleUtils.beginAriaCaches(); + const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [] }; try { visit(ariaRoot, rootElement); } finally { - endAriaCaches(); + roleUtils.endAriaCaches(); } normalizeStringChildren(ariaRoot); return ariaRoot; } +function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null { + const role = roleUtils.getAriaRole(element); + if (!role) + return null; + + const name = roleUtils.getElementAccessibleName(element, false) || ''; + const isLeaf = leafRoles.has(role); + const result: AriaNode = { role, name, children: [] }; + if (isLeaf && !name) { + const text = roleUtils.accumulatedElementText(element); + if (text) + result.children = [text]; + } + + if (roleUtils.kAriaCheckedRoles.includes(role)) + result.checked = roleUtils.getAriaChecked(element); + + if (roleUtils.kAriaDisabledRoles.includes(role)) + result.disabled = roleUtils.getAriaDisabled(element); + + if (roleUtils.kAriaExpandedRoles.includes(role)) + result.expanded = roleUtils.getAriaExpanded(element); + + if (roleUtils.kAriaLevelRoles.includes(role)) + result.level = roleUtils.getAriaLevel(element); + + if (roleUtils.kAriaPressedRoles.includes(role)) + result.pressed = roleUtils.getAriaPressed(element); + + if (roleUtils.kAriaSelectedRoles.includes(role)) + result.selected = roleUtils.getAriaSelected(element); + + return { isLeaf, ariaNode: result }; +} + export function renderedAriaTree(rootElement: Element): string { return renderAriaTree(generateAriaTree(rootElement)); } @@ -155,7 +184,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) { const hiddenContainerRoles = new Set(['none', 'presentation']); -const leafRoles = new Set([ +const leafRoles = new Set([ 'alert', 'blockquote', 'button', 'caption', 'checkbox', 'code', 'columnheader', 'definition', 'deletion', 'emphasis', 'generic', 'heading', 'img', 'insertion', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option', @@ -178,7 +207,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { const root = generateAriaTree(rootElement); - const matches = nodeMatches(root, template); + const matches = matchesNodeDeep(root, template); return { matches, received: renderAriaTree(root) }; } @@ -187,7 +216,19 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx return matchesText(node, template); if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) { - if (template.role && template.role !== node.role) + if (template.role !== 'fragment' && template.role !== node.role) + return false; + if (template.checked !== undefined && template.checked !== node.checked) + return false; + if (template.disabled !== undefined && template.disabled !== node.disabled) + return false; + if (template.expanded !== undefined && template.expanded !== node.expanded) + return false; + if (template.level !== undefined && template.level !== node.level) + return false; + if (template.pressed !== undefined && template.pressed !== node.pressed) + return false; + if (template.selected !== undefined && template.selected !== node.selected) return false; if (!matchesText(node.name, template.name)) return false; @@ -216,7 +257,7 @@ function containsList(children: (AriaNode | string)[], template: (AriaTemplateNo return true; } -function nodeMatches(root: AriaNode, template: AriaTemplateNode): boolean { +function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean { const results: (AriaNode | string)[] = []; const visit = (node: AriaNode | string): boolean => { if (matchesNode(node, template, 0)) { @@ -245,19 +286,36 @@ export function renderAriaTree(ariaNode: AriaNode): string { let line = `${indent}- ${ariaNode.role}`; if (ariaNode.name) line += ` ${escapeWithQuotes(ariaNode.name, '"')}`; - const noChild = !ariaNode.name && !ariaNode.children?.length; - const oneChild = !ariaNode.name && ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'; - if (noChild || oneChild) { - if (oneChild) + const stringValue = !ariaNode.checked + && !ariaNode.disabled + && (!ariaNode.expanded || ariaNode.expanded === 'none') + && !ariaNode.level + && !ariaNode.pressed + && !ariaNode.selected + && (!ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string')); + if (stringValue) { + if (ariaNode.children.length) line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); lines.push(line); return; } - lines.push(line + (ariaNode.children.length ? ':' : '')); + + lines.push(line + ':'); + if (ariaNode.checked) + lines.push(`${indent} - checked: ${ariaNode.checked}`); + if (ariaNode.disabled) + lines.push(`${indent} - disabled: ${ariaNode.disabled}`); + if (ariaNode.expanded && ariaNode.expanded !== 'none') + lines.push(`${indent} - expanded: ${ariaNode.expanded}`); + if (ariaNode.level) + lines.push(`${indent} - level: ${ariaNode.level}`); + if (ariaNode.pressed) + lines.push(`${indent} - pressed: ${ariaNode.pressed}`); for (const child of ariaNode.children || []) visit(child, indent + ' '); }; - if (ariaNode.role === '') { + + if (ariaNode.role === 'fragment') { // Render fragment. for (const child of ariaNode.children || []) visit(child, ''); diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index d085a8e36d..9c5712311e 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -82,7 +82,7 @@ function isNativelyFocusable(element: Element) { // https://w3c.github.io/html-aam/#html-element-role-mappings // https://www.w3.org/TR/html-aria/#docconformance -const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null } = { +const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => AriaRole | null } = { 'A': (e: Element) => { return e.hasAttribute('href') ? 'link' : null; }, @@ -127,17 +127,8 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox'; } if (type === 'hidden') - return ''; - return { - 'button': 'button', - 'checkbox': 'checkbox', - 'image': 'button', - 'number': 'spinbutton', - 'radio': 'radio', - 'range': 'slider', - 'reset': 'button', - 'submit': 'button', - }[type] || 'textbox'; + return null; + return inputTypeToRole[type] || 'textbox'; }, 'INS': () => 'insertion', 'LI': () => 'listitem', @@ -200,7 +191,7 @@ const kPresentationInheritanceParents: { [tagName: string]: string[] } = { 'TR': ['THEAD', 'TBODY', 'TFOOT', 'TABLE'], }; -function getImplicitAriaRole(element: Element): string | null { +function getImplicitAriaRole(element: Element): AriaRole | null { const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || ''; if (!implicitRole) return null; @@ -221,23 +212,29 @@ function getImplicitAriaRole(element: Element): string | null { } // https://www.w3.org/TR/wai-aria-1.2/#role_definitions -const allRoles = [ - 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command', - 'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', - 'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu', - 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', - 'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider', - 'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', - 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window' -]; // https://www.w3.org/TR/wai-aria-1.2/#abstract_roles -const abstractRoles = ['command', 'composite', 'input', 'landmark', 'range', 'roletype', 'section', 'sectionhead', 'select', 'structure', 'widget', 'window']; -const validRoles = allRoles.filter(role => !abstractRoles.includes(role)); +// type AbstractRoles = 'command' | 'composite' | 'input' | 'landmark' | 'range' | 'roletype' | 'section' | 'sectionhead' | 'select' | 'structure' | 'widget' | 'window'; -function getExplicitAriaRole(element: Element): string | null { +export type AriaRole = 'alert' | 'alertdialog' | 'application' | 'article' | 'banner' | 'blockquote' | 'button' | 'caption' | 'cell' | 'checkbox' | 'code' | 'columnheader' | 'combobox' | + 'complementary' | 'contentinfo' | 'definition' | 'deletion' | 'dialog' | 'directory' | 'document' | 'emphasis' | 'feed' | 'figure' | 'form' | 'generic' | 'grid' | + 'gridcell' | 'group' | 'heading' | 'img' | 'insertion' | 'link' | 'list' | 'listbox' | 'listitem' | 'log' | 'main' | 'mark' | 'marquee' | 'math' | 'meter' | 'menu' | + 'menubar' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'navigation' | 'none' | 'note' | 'option' | 'paragraph' | 'presentation' | 'progressbar' | 'radio' | 'radiogroup' | + 'region' | 'row' | 'rowgroup' | 'rowheader' | 'scrollbar' | 'search' | 'searchbox' | 'separator' | 'slider' | + 'spinbutton' | 'status' | 'strong' | 'subscript' | 'superscript' | 'switch' | 'tab' | 'table' | 'tablist' | 'tabpanel' | 'term' | 'textbox' | 'time' | 'timer' | + 'toolbar' | 'tooltip' | 'tree' | 'treegrid' | 'treeitem'; + +const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', + 'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', + 'gridcell', 'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'meter', 'menu', + 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', + 'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider', + 'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', + 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem']; + +function getExplicitAriaRole(element: Element): AriaRole | null { // https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim()); - return roles.find(role => validRoles.includes(role)) || null; + return roles.find(role => validRoles.includes(role as any)) as AriaRole || null; } function hasPresentationConflictResolution(element: Element, role: string | null) { @@ -245,7 +242,7 @@ function hasPresentationConflictResolution(element: Element, role: string | null return hasGlobalAriaAttribute(element, role) || isFocusable(element); } -export function getAriaRole(element: Element): string | null { +export function getAriaRole(element: Element): AriaRole | null { const explicitRole = getExplicitAriaRole(element); if (!explicitRole) return getImplicitAriaRole(element); @@ -994,3 +991,14 @@ export function endAriaCaches() { cachePseudoContentAfter = undefined; } } + +const inputTypeToRole: Record = { + 'button': 'button', + 'checkbox': 'checkbox', + 'image': 'button', + 'number': 'spinbutton', + 'radio': 'radio', + 'range': 'slider', + 'reset': 'button', + 'submit': 'button', +}; diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index 878f48439e..cf71611320 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -40,7 +40,8 @@ async function checkAndMatchSnapshot(locator: Locator, snapshot: string) { it('should snapshot', async ({ page }) => { await page.setContent(`

          title

          `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "title" + - heading "title": + - level: 1 `); }); @@ -50,8 +51,10 @@ it('should snapshot list', async ({ page }) => {

          title 2

          `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "title" - - heading "title 2" + - heading "title": + - level: 1 + - heading "title 2": + - level: 1 `); }); @@ -91,7 +94,8 @@ it('should allow text nodes', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "Microsoft" + - heading "Microsoft": + - level: 1 - text: Open source projects and samples from Microsoft `); }); @@ -144,7 +148,8 @@ it('should snapshot integration', async ({ page }) => {
        `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "Microsoft" + - heading "Microsoft": + - level: 1 - text: Open source projects and samples from Microsoft - list: - listitem: diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index 3de7b6a9d4..d4be35fb8a 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -107,6 +107,17 @@ test('details visibility', async ({ page }) => { `); }); +test('checked state', async ({ page }) => { + await page.setContent(` + + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox: + - checked: true + `); +}); + test('integration test', async ({ page }) => { await page.setContent(`

        Microsoft

        @@ -182,11 +193,12 @@ test('expected formatter', async ({ page }) => { expect(stripAnsi(error.message)).toContain(` Locator: locator('body') - Expected - 2 -+ Received string + 3 ++ Received string + 4 - - heading "todos" -+ - banner: -+ - heading "todos" - - textbox "Wrong text" ++ - banner: ++ - heading "todos": ++ - level: 1 + - textbox "What needs to be done?"`); }); From 97d26e8166b0519756344afbfc33277dbfd530af Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 19 Oct 2024 14:23:08 -0700 Subject: [PATCH 26/35] chore: add aria attribute tests (#33184) --- .../src/server/ariaSnapshot.ts | 118 ++++++---- .../src/server/injected/ariaSnapshot.ts | 38 ++-- .../src/server/injected/roleUtils.ts | 6 +- tests/page/page-aria-snapshot.spec.ts | 15 +- tests/page/to-match-aria-snapshot.spec.ts | 215 +++++++++++++++++- .../playwright-test/ui-mode-test-run.spec.ts | 17 ++ 6 files changed, 331 insertions(+), 78 deletions(-) diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts index 47ed4066b7..03b5a05e92 100644 --- a/packages/playwright-core/src/server/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -17,6 +17,7 @@ import type { AriaTemplateNode } from './injected/ariaSnapshot'; import { yaml } from '../utilsBundle'; import type { AriaRole } from '@injected/roleUtils'; +import { assert } from '../utils'; export function parseAriaSnapshot(text: string): AriaTemplateNode { const fragment = yaml.parse(text) as any[]; @@ -28,69 +29,106 @@ export function parseAriaSnapshot(text: string): AriaTemplateNode { function populateNode(node: AriaTemplateNode, container: any[]) { for (const object of container) { if (typeof object === 'string') { - const { role, name } = parseKey(object); + const childNode = parseKey(object); node.children = node.children || []; - node.children.push({ role, name }); + node.children.push(childNode); continue; } - for (const key of Object.keys(object)) { - if (key === 'checked') { - node.checked = object[key]; - continue; - } - if (key === 'disabled') { - node.disabled = object[key]; - continue; - } - if (key === 'expanded') { - node.expanded = object[key]; - continue; - } - if (key === 'level') { - node.level = object[key]; - continue; - } - if (key === 'pressed') { - node.pressed = object[key]; - continue; - } - if (key === 'selected') { - node.selected = object[key]; - continue; - } - const { role, name } = parseKey(key); + for (const key of Object.keys(object)) { + const childNode = parseKey(key); const value = object[key]; node.children = node.children || []; - if (role === 'text') { + if (childNode.role === 'text') { node.children.push(valueOrRegex(value)); continue; } if (typeof value === 'string') { - node.children.push({ role, name, children: [valueOrRegex(value)] }); + node.children.push({ ...childNode, children: [valueOrRegex(value)] }); continue; } - const childNode = { role, name }; node.children.push(childNode); populateNode(childNode, value); } } } -function parseKey(key: string) { - const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/); - if (!match) +function applyAttribute(node: AriaTemplateNode, key: string, value: string) { + if (key === 'checked') { + assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "disabled" attribute must be a boolean or "mixed"'); + node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed'; + return; + } + if (key === 'disabled') { + assert(value === 'true' || value === 'false', 'Value of "disabled" attribute must be a boolean'); + node.disabled = value === 'true'; + return; + } + if (key === 'expanded') { + assert(value === 'true' || value === 'false', 'Value of "expanded" attribute must be a boolean'); + node.expanded = value === 'true'; + return; + } + if (key === 'level') { + assert(!isNaN(Number(value)), 'Value of "level" attribute must be a number'); + node.level = Number(value); + return; + } + if (key === 'pressed') { + assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "pressed" attribute must be a boolean or "mixed"'); + node.pressed = value === 'true' ? true : value === 'false' ? false : 'mixed'; + return; + } + if (key === 'selected') { + assert(value === 'true' || value === 'false', 'Value of "selected" attribute must be a boolean'); + node.selected = value === 'true'; + return; + } + throw new Error(`Unsupported attribute [${key}] `); +} + +function parseKey(key: string): AriaTemplateNode { + const tokenRegex = /\s*([a-z]+|"(?:[^"]*)"|\/(?:[^\/]*)\/|\[.*?\])/g; + let match; + const tokens = []; + while ((match = tokenRegex.exec(key)) !== null) + tokens.push(match[1]); + + if (tokens.length === 0) throw new Error(`Invalid key ${key}`); - const role = match[1] as AriaRole | 'text'; - if (match[2]) - return { role, name: match[2] }; - if (match[3]) - return { role, name: new RegExp(match[3]) }; - return { role }; + const role = tokens[0] as AriaRole | 'text'; + + let name: string | RegExp = ''; + let index = 1; + if (tokens.length > 1 && (tokens[1].startsWith('"') || tokens[1].startsWith('/'))) { + const nameToken = tokens[1]; + if (nameToken.startsWith('"')) { + name = nameToken.slice(1, -1); + } else { + const pattern = nameToken.slice(1, -1); + name = new RegExp(pattern); + } + index = 2; + } + + const result: AriaTemplateNode = { role, name }; + for (; index < tokens.length; index++) { + const attrToken = tokens[index]; + if (attrToken.startsWith('[') && attrToken.endsWith(']')) { + const attrContent = attrToken.slice(1, -1).trim(); + const [attrName, attrValue] = attrContent.split('=', 2); + const value = attrValue !== undefined ? attrValue.trim() : 'true'; + applyAttribute(result, attrName, value); + } else { + throw new Error(`Invalid attribute token ${attrToken} in key ${key}`); + } + } + + return result; } function normalizeWhitespace(text: string) { diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 16d8dc6070..56044a2027 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -22,8 +22,8 @@ import type { AriaRole } from './roleUtils'; type AriaProps = { checked?: boolean | 'mixed'; disabled?: boolean; - expanded?: boolean | 'none', - level?: number, + expanded?: boolean; + level?: number; pressed?: boolean | 'mixed'; selected?: boolean; }; @@ -286,13 +286,23 @@ export function renderAriaTree(ariaNode: AriaNode): string { let line = `${indent}- ${ariaNode.role}`; if (ariaNode.name) line += ` ${escapeWithQuotes(ariaNode.name, '"')}`; - const stringValue = !ariaNode.checked - && !ariaNode.disabled - && (!ariaNode.expanded || ariaNode.expanded === 'none') - && !ariaNode.level - && !ariaNode.pressed - && !ariaNode.selected - && (!ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string')); + + if (ariaNode.checked === 'mixed') + line += ` [checked=mixed]`; + if (ariaNode.checked === true) + line += ` [checked]`; + if (ariaNode.disabled) + line += ` [disabled]`; + if (ariaNode.expanded) + line += ` [expanded]`; + if (ariaNode.level) + line += ` [level=${ariaNode.level}]`; + if (ariaNode.pressed === 'mixed') + line += ` [pressed=mixed]`; + if (ariaNode.pressed === true) + line += ` [pressed]`; + + const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'); if (stringValue) { if (ariaNode.children.length) line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); @@ -301,16 +311,6 @@ export function renderAriaTree(ariaNode: AriaNode): string { } lines.push(line + ':'); - if (ariaNode.checked) - lines.push(`${indent} - checked: ${ariaNode.checked}`); - if (ariaNode.disabled) - lines.push(`${indent} - disabled: ${ariaNode.disabled}`); - if (ariaNode.expanded && ariaNode.expanded !== 'none') - lines.push(`${indent} - expanded: ${ariaNode.expanded}`); - if (ariaNode.level) - lines.push(`${indent} - level: ${ariaNode.level}`); - if (ariaNode.pressed) - lines.push(`${indent} - pressed: ${ariaNode.pressed}`); for (const child of ariaNode.children || []) visit(child, indent + ' '); }; diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 9c5712311e..df52329b1e 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -881,7 +881,7 @@ export function getAriaPressed(element: Element): boolean | 'mixed' { } export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch']; -export function getAriaExpanded(element: Element): boolean | 'none' { +export function getAriaExpanded(element: Element): boolean | undefined { // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (elementSafeTagName(element) === 'DETAILS') @@ -889,12 +889,12 @@ export function getAriaExpanded(element: Element): boolean | 'none' { if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { const expanded = element.getAttribute('aria-expanded'); if (expanded === null) - return 'none'; + return undefined; if (expanded === 'true') return true; return false; } - return 'none'; + return undefined; } export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem']; diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index cf71611320..88306a1e43 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -40,8 +40,7 @@ async function checkAndMatchSnapshot(locator: Locator, snapshot: string) { it('should snapshot', async ({ page }) => { await page.setContent(`

        title

        `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "title": - - level: 1 + - heading "title" [level=1] `); }); @@ -51,10 +50,8 @@ it('should snapshot list', async ({ page }) => {

        title 2

        `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "title": - - level: 1 - - heading "title 2": - - level: 1 + - heading "title" [level=1] + - heading "title 2" [level=1] `); }); @@ -94,8 +91,7 @@ it('should allow text nodes', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "Microsoft": - - level: 1 + - heading "Microsoft" [level=1] - text: Open source projects and samples from Microsoft `); }); @@ -148,8 +144,7 @@ it('should snapshot integration', async ({ page }) => {
      `); await checkAndMatchSnapshot(page.locator('body'), ` - - heading "Microsoft": - - level: 1 + - heading "Microsoft" [level=1] - text: Open source projects and samples from Microsoft - list: - listitem: diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index d4be35fb8a..1335be5a59 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -107,15 +107,219 @@ test('details visibility', async ({ page }) => { `); }); -test('checked state', async ({ page }) => { +test('checked attribute', async ({ page }) => { await page.setContent(` `); await expect(page.locator('body')).toMatchAriaSnapshot(` - - checkbox: - - checked: true + - checkbox `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox [checked] + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox [checked=true] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox [checked=false] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox [checked=mixed] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - checkbox [checked=5] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a boolean or "mixed"'); + } +}); + +test('disabled attribute', async ({ page }) => { + await page.setContent(` + + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [disabled] + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [disabled=true] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [disabled=false] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [disabled=invalid] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a boolean'); + } +}); + +test('expanded attribute', async ({ page }) => { + await page.setContent(` + + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [expanded] + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [expanded=true] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [expanded=false] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [expanded=invalid] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a boolean'); + } +}); + +test('level attribute', async ({ page }) => { + await page.setContent(` +

      Section Title

      + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - heading + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - heading [level=2] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - heading [level=3] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - heading [level=two] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a number'); + } +}); + +test('pressed attribute', async ({ page }) => { + await page.setContent(` + + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed] + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed=true] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed=false] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + // Test for 'mixed' state + await page.setContent(` + + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed=mixed] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed=true] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - button [pressed=5] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a boolean or "mixed"'); + } +}); + +test('selected attribute', async ({ page }) => { + await page.setContent(` + + + + +
      Row
      + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - row + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - row [selected] + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - row [selected=true] + `); + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - row [selected=false] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect'); + } + + { + const e = await expect(page.locator('body')).toMatchAriaSnapshot(` + - row [selected=invalid] + `, { timeout: 1000 }).catch(e => e); + expect(stripAnsi(e.message)).toContain(' attribute must be a boolean'); + } }); test('integration test', async ({ page }) => { @@ -193,12 +397,11 @@ test('expected formatter', async ({ page }) => { expect(stripAnsi(error.message)).toContain(` Locator: locator('body') - Expected - 2 -+ Received string + 4 ++ Received string + 3 - - heading "todos" - - textbox "Wrong text" + - banner: -+ - heading "todos": -+ - level: 1 ++ - heading "todos" [level=1] + - textbox "What needs to be done?"`); }); diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 1120cede6b..f60a6dde86 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -61,6 +61,23 @@ test('should run visible', async ({ runUITest }) => { ⊘ skipped `); + // await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + // - tree: + // - treeitem "a.test.ts" [expanded]: + // - treeitem "passes" + // - treeitem "fails" [selected]: + // - button "Run" + // - button "Show source" + // - button "Watch" + // - treeitem "suite" + // - treeitem "b.test.ts" [expanded]: + // - treeitem "passes" + // - treeitem "fails" + // - treeitem "c.test.ts" [expanded]: + // - treeitem "passes" + // - treeitem "skipped" + // `); + await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); From 4b187107ee341fb1576f790f8377d356acc4fa87 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 20 Oct 2024 19:44:48 +0300 Subject: [PATCH 27/35] devops: fix 'client side changes bot' PR links in PRs (#33189) --- .github/workflows/pr_check_client_side_changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_check_client_side_changes.yml b/.github/workflows/pr_check_client_side_changes.yml index 17a8da9ca3..6612845f47 100644 --- a/.github/workflows/pr_check_client_side_changes.yml +++ b/.github/workflows/pr_check_client_side_changes.yml @@ -27,7 +27,7 @@ jobs: repo: context.repo.repo, commit_sha: context.sha, }); - const commitHeader = data.message.split('\n')[0]; + const commitHeader = data.message.split('\n')[0].replace(/#(\d+)/g, 'https://github.com/microsoft/playwright/pull/$1'); const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion; for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) { From 40d5a1cb4af61fe6b670540abc9f0889ab0d98f9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 21 Oct 2024 11:14:48 +0200 Subject: [PATCH 28/35] fix(ff): resource type for image sets should be `image` (#33195) --- .../src/server/firefox/ffNetworkManager.ts | 2 +- tests/page/page-event-request.spec.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 978eb30bd4..73b8e3589f 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -183,7 +183,7 @@ const causeToResourceType: {[key: string]: string} = { TYPE_XSLT: 'other', TYPE_BEACON: 'other', TYPE_FETCH: 'fetch', - TYPE_IMAGESET: 'images', + TYPE_IMAGESET: 'image', TYPE_WEB_MANIFEST: 'manifest', }; diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts index f32f224374..2c1d7a7eba 100644 --- a/tests/page/page-event-request.spec.ts +++ b/tests/page/page-event-request.spec.ts @@ -258,3 +258,18 @@ it('should finish 204 request', { page.evaluate(async url => { await fetch(url); }, server.PREFIX + '/204').catch(() => {}); expect(await reqPromise).toBe('requestfinished'); }); + +it(' resource should have type image', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33148' }); + const [request] = await Promise.all([ + page.waitForEvent('request'), + page.setContent(` + + + + + + `) + ]); + expect(request.resourceType()).toBe('image'); +}); \ No newline at end of file From 014577d345a7985bb716a9c856bac783660c7c5e Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:33:16 -0700 Subject: [PATCH 29/35] feat(webkit): roll to r2094 (#33188) --- packages/playwright-core/browsers.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 11a2b1fad1..b808a2fe5e 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2092", + "revision": "2094", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", @@ -35,7 +35,9 @@ "mac11": "1816", "mac11-arm64": "1816", "mac12": "2009", - "mac12-arm64": "2009" + "mac12-arm64": "2009", + "ubuntu20.04-x64": "2092", + "ubuntu20.04-arm64": "2092" }, "browserVersion": "18.0" }, From e866e3306ee7e07918733cefca33ce6451bbb3e1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 17:00:10 +0200 Subject: [PATCH 30/35] devops: stop publishing Ubuntu 20.04 (#33203) --- docs/src/docker.md | 4 +-- utils/docker/Dockerfile.focal | 51 ---------------------------------- utils/docker/build.sh | 6 ++-- utils/docker/publish_docker.sh | 22 +++------------ 4 files changed, 8 insertions(+), 75 deletions(-) delete mode 100644 utils/docker/Dockerfile.focal diff --git a/docs/src/docker.md b/docs/src/docker.md index 665904a144..bfcf5e73b5 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -5,7 +5,7 @@ title: "Docker" ## Introduction -[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. +[Dockerfile.noble] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. ## Usage @@ -111,7 +111,6 @@ We currently publish images with the following tags: - `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish). -- `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa). :::note It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables. @@ -122,7 +121,6 @@ It is recommended to always pin your Docker image to a specific version if possi We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions: - **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble` - **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy` -- **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal` #### Alpine diff --git a/utils/docker/Dockerfile.focal b/utils/docker/Dockerfile.focal deleted file mode 100644 index cd1d1d7c6e..0000000000 --- a/utils/docker/Dockerfile.focal +++ /dev/null @@ -1,51 +0,0 @@ -FROM ubuntu:focal - -ARG DEBIAN_FRONTEND=noninteractive -ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-focal" - -ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 - -# === INSTALL Node.js === - -RUN apt-get update && \ - # Install Node.js - apt-get install -y curl wget gpg ca-certificates && \ - mkdir -p /etc/apt/keyrings && \ - curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ - apt-get update && \ - apt-get install -y nodejs && \ - # Feature-parity with node.js base images. - apt-get install -y --no-install-recommends git openssh-client && \ - npm install -g yarn && \ - # clean apt cache - rm -rf /var/lib/apt/lists/* && \ - # Create the pwuser - adduser pwuser - -# === BAKE BROWSERS INTO IMAGE === - -ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright - -# 1. Add tip-of-tree Playwright package to install its browsers. -# The package should be built beforehand from tip-of-tree Playwright. -COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz - -# 2. Bake in Playwright Agent. -# Playwright Agent is used to bake in browsers and browser dependencies, -# and run docker server later on. -# Browsers will be downloaded in `/ms-playwright`. -# Note: make sure to set 777 to the registry so that any user can access -# registry. -RUN mkdir /ms-playwright && \ - mkdir /ms-playwright-agent && \ - cd /ms-playwright-agent && npm init -y && \ - npm i /tmp/playwright-core.tar.gz && \ - npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ - npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \ - rm /tmp/playwright-core.tar.gz && \ - rm -rf /ms-playwright-agent && \ - rm -rf ~/.npm/ && \ - chmod -R 777 /ms-playwright diff --git a/utils/docker/build.sh b/utils/docker/build.sh index 46e8d9e6b6..280727eac5 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -3,12 +3,12 @@ set -e set +x if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then - echo "usage: $(basename $0) {--arm64,--amd64} {focal,jammy} playwright:localbuild-focal" + echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble" echo - echo "Build Playwright docker image and tag it as 'playwright:localbuild-focal'." + echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'." echo "Once image is built, you can run it with" echo "" - echo " docker run --rm -it playwright:localbuild-focal /bin/bash" + echo " docker run --rm -it playwright:localbuild-noble /bin/bash" echo "" echo "NOTE: this requires on Playwright dependencies to be installed with 'npm install'" echo " and Playwright itself being built with 'npm run build'" diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh index a4892024a8..870da29a90 100755 --- a/utils/docker/publish_docker.sh +++ b/utils/docker/publish_docker.sh @@ -21,11 +21,6 @@ else exit 1 fi -# Ubuntu 20.04 -FOCAL_TAGS=( - "v${PW_VERSION}-focal" -) - # Ubuntu 22.04 JAMMY_TAGS=( "v${PW_VERSION}-jammy" @@ -69,14 +64,12 @@ install_oras_if_needed() { publish_docker_images_with_arch_suffix() { local FLAVOR="$1" local TAGS=() - if [[ "$FLAVOR" == "focal" ]]; then - TAGS=("${FOCAL_TAGS[@]}") - elif [[ "$FLAVOR" == "jammy" ]]; then + if [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") elif [[ "$FLAVOR" == "noble" ]]; then TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'" exit 1 fi local ARCH="$2" @@ -97,14 +90,12 @@ publish_docker_images_with_arch_suffix() { publish_docker_manifest () { local FLAVOR="$1" local TAGS=() - if [[ "$FLAVOR" == "focal" ]]; then - TAGS=("${FOCAL_TAGS[@]}") - elif [[ "$FLAVOR" == "jammy" ]]; then + if [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") elif [[ "$FLAVOR" == "noble" ]]; then TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'" exit 1 fi @@ -123,11 +114,6 @@ publish_docker_manifest () { done } -# Ubuntu 20.04 -publish_docker_images_with_arch_suffix focal amd64 -publish_docker_images_with_arch_suffix focal arm64 -publish_docker_manifest focal amd64 arm64 - # Ubuntu 22.04 publish_docker_images_with_arch_suffix jammy amd64 publish_docker_images_with_arch_suffix jammy arm64 From 36d3a6764ea8bcb42e29e84d00e4106b0d3f2827 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 18:41:27 +0200 Subject: [PATCH 31/35] docs: set minimal Ubuntu version to 22 and Debian to 12 (#33207) --- docs/src/intro-csharp.md | 4 ++-- docs/src/intro-java.md | 4 ++-- docs/src/intro-js.md | 4 ++-- docs/src/intro-python.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/intro-csharp.md b/docs/src/intro-csharp.md index d26874d456..e0491aa3cc 100644 --- a/docs/src/intro-csharp.md +++ b/docs/src/intro-csharp.md @@ -180,8 +180,8 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a - Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-java.md b/docs/src/intro-java.md index 4e1503f2a3..733fee7fdc 100644 --- a/docs/src/intro-java.md +++ b/docs/src/intro-java.md @@ -130,8 +130,8 @@ By default browsers launched with Playwright run headless, meaning no browser UI - Java 8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index 09c070aecf..8c29641bd9 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -288,8 +288,8 @@ pnpm exec playwright --version - Node.js 18+ - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-python.md b/docs/src/intro-python.md index 3d17c2077d..e44f8a2642 100644 --- a/docs/src/intro-python.md +++ b/docs/src/intro-python.md @@ -101,8 +101,8 @@ pip install pytest-playwright playwright -U - Python 3.8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next From aebceb345e7849804752842c57ce02af130e202f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 21 Oct 2024 11:15:55 -0700 Subject: [PATCH 32/35] chore: expose expect error details on TestError (#33183) --- docs/src/test-reporter-api/class-testerror.md | 36 +++++++++++ .../playwright/src/matchers/matcherHint.ts | 4 ++ .../playwright/src/matchers/toBeTruthy.ts | 29 +++++++-- packages/playwright/src/matchers/toEqual.ts | 46 +++++++++----- .../src/matchers/toMatchSnapshot.ts | 4 ++ .../playwright/src/matchers/toMatchText.ts | 62 ++++++++++++++----- packages/playwright/src/reporters/base.ts | 14 +++++ packages/playwright/src/worker/testInfo.ts | 7 ++- packages/playwright/src/worker/util.ts | 45 ++++++++++++++ packages/playwright/src/worker/workerMain.ts | 9 +-- packages/playwright/types/testReporter.d.ts | 30 +++++++++ tests/page/expect-matcher-result.spec.ts | 30 +++++++++ 12 files changed, 274 insertions(+), 42 deletions(-) create mode 100644 packages/playwright/src/worker/util.ts diff --git a/docs/src/test-reporter-api/class-testerror.md b/docs/src/test-reporter-api/class-testerror.md index 7a872c63fc..8e76d6f595 100644 --- a/docs/src/test-reporter-api/class-testerror.md +++ b/docs/src/test-reporter-api/class-testerror.md @@ -4,18 +4,54 @@ Information about an error thrown during test execution. +## property: TestError.expected +* since: v1.49 +- type: ?<[string]> + +Expected value formatted as a human-readable string. + +## property: TestError.locator +* since: v1.49 +- type: ?<[string]> + +Receiver's locator. + +## property: TestError.log +* since: v1.49 +- type: ?<[Array]<[string]>> + +Call log. + +## property: TestError.matcherName +* since: v1.49 +- type: ?<[string]> + +Expect matcher name. + ## property: TestError.message * since: v1.10 - type: ?<[string]> Error message. Set when [Error] (or its subclass) has been thrown. +## property: TestError.received +* since: v1.49 +- type: ?<[string]> + +Received value formatted as a human-readable string. + ## property: TestError.stack * since: v1.10 - type: ?<[string]> Error stack. Set when [Error] (or its subclass) has been thrown. +## property: TestError.timeout +* since: v1.49 +- type: ?<[int]> + +Timeout in milliseconds, if the error was caused by a timeout. + ## property: TestError.value * since: v1.10 - type: ?<[string]> diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 8a78932c68..5ffc745263 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -39,6 +39,10 @@ export type MatcherResult = { actual?: A; log?: string[]; timeout?: number; + locator?: string; + printedReceived?: string; + printedExpected?: string; + printedDiff?: string; }; export class ExpectError extends Error { diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 0941ab7a63..8902a14eea 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -39,22 +39,41 @@ export async function toBeTruthy( }; const timeout = options.timeout ?? this.timeout; - const { matches, log, timedOut, received } = await query(!!this.isNot, timeout); + const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } + const notFound = received === kNoElementsFoundError ? received : undefined; - const actual = matches ? expected : unexpected; + const actual = pass ? expected : unexpected; + let printedReceived: string | undefined; + let printedExpected: string | undefined; + if (pass) { + printedExpected = `Expected: not ${expected}`; + printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`; + } else { + printedExpected = `Expected: ${expected}`; + printedReceived = `Received: ${notFound ? kNoElementsFoundError : unexpected}`; + } const message = () => { const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined); const logText = callLogText(log); - return matches ? `${header}Expected: not ${expected}\nReceived: ${notFound ? kNoElementsFoundError : expected}${logText}` : - `${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`; + return `${header}${printedExpected}\n${printedReceived}${logText}`; }; return { message, - pass: matches, + pass, actual, name: matcherName, expected, log, timeout: timedOut ? timeout : undefined, + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), }; } diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 29d3fd4866..f75caf87f5 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -44,22 +44,35 @@ export async function toEqual( const timeout = options.timeout ?? this.timeout; const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } - const message = pass - ? () => - matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + - `Expected: not ${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(received)}` + callLogText(log) - : () => - matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + - this.utils.printDiffOrStringify( - expected, - received, - EXPECTED_LABEL, - RECEIVED_LABEL, - false, - ) + callLogText(log); - + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (pass) { + printedExpected = `Expected: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${this.utils.printReceived(received)}`; + } else { + printedDiff = this.utils.printDiffOrStringify( + expected, + received, + EXPECTED_LABEL, + RECEIVED_LABEL, + false, + ); + } + const message = () => { + const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); + const details = printedDiff || `${printedExpected}\n${printedReceived}`; + return `${header}${details}${callLogText(log)}`; + }; // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message @@ -70,5 +83,8 @@ export async function toEqual( pass, log, timeout: timedOut ? timeout : undefined, + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), + ...(printedDiff ? { printedDiff } : {}), }; } diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index b3fa3f556e..86504062d3 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -194,6 +194,10 @@ class SnapshotHelper { pass, message: () => message, log, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + ...(this.locator ? { locator: this.locator.toString() } : {}), + printedExpected: this.expectedPath, + printedReceived: this.actualPath, }; return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult; } diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index ebac8f8028..76ed48af1e 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -58,29 +58,56 @@ export async function toMatchText( const timeout = options.timeout ?? this.timeout; const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } + const stringSubstring = options.matchSubstring ? 'substring' : 'string'; const receivedString = received || ''; const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; - const message = () => { - if (pass) { - if (typeof expected === 'string') { - if (notFound) - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); + + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (pass) { + if (typeof expected === 'string') { + if (notFound) { + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; } else { - if (notFound) - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); + printedReceived = `Received string: ${formattedReceived}`; } } else { - const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; - if (notFound) - return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log); + if (notFound) { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); + printedReceived = `Received string: ${formattedReceived}`; + } } + } else { + const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; + if (notFound) { + printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); + } + } + + const message = () => { + const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived; + return messagePrefix + resultDetails + callLogText(log); }; return { @@ -91,5 +118,10 @@ export async function toMatchText( actual: received, log, timeout: timedOut ? timeout : undefined, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + locator: receiver.toString(), + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), + ...(printedDiff ? { printedDiff } : {}), }; } diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 4249429a36..138820baee 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -32,6 +32,13 @@ type Annotation = { type ErrorDetails = { message: string; location?: Location; + timeout?: number; + matcherName?: string; + locator?: string; + expected?: string; + received?: string; + log?: string[]; + snippet?: string; }; type TestSummary = { @@ -383,6 +390,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI errorDetails.push({ message: indent(formattedError.message, initialIndent), location: formattedError.location, + timeout: error.timeout, + matcherName: error.matcherName, + locator: error.locator, + expected: error.expected, + received: error.received, + log: error.log, + snippet: error.snippet, }); } return errorDetails; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 378b32524f..e41f1a9a52 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana import type { RunnableDescription } from './timeoutManager'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { FullConfig, Location } from '../../types/testReporter'; -import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util'; +import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; +import { serializeWorkerError } from './util'; export interface TestStepInternal { complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; @@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo { if (result.error) { if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) (result.error as any)[stepSymbol] = step; - const error = serializeError(result.error); + const error = serializeWorkerError(result.error); if (data.boxedStack) error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`; step.error = error; @@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo { _failWithError(error: Error | unknown) { if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; - const serialized = serializeError(error); + const serialized = serializeWorkerError(error); const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; if (step && step.boxedStack) serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; diff --git a/packages/playwright/src/worker/util.ts b/packages/playwright/src/worker/util.ts new file mode 100644 index 0000000000..d24d337191 --- /dev/null +++ b/packages/playwright/src/worker/util.ts @@ -0,0 +1,45 @@ +/** + * 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. + */ + +import type { TestError } from '../../types/testReporter'; +import type { TestInfoError } from '../../types/test'; +import type { MatcherResult } from '../matchers/matcherHint'; +import { serializeError } from '../util'; + + +type MatcherResultDetails = Pick; + +export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails { + return { + ...serializeError(error), + ...serializeExpectDetails(error), + }; +} + +function serializeExpectDetails(e: Error): MatcherResultDetails { + const matcherResult = (e as any).matcherResult as MatcherResult; + if (!matcherResult) + return {}; + return { + timeout: matcherResult.timeout, + matcherName: matcherResult.name, + locator: matcherResult.locator, + expected: matcherResult.printedExpected, + received: matcherResult.printedReceived, + log: matcherResult.log, + }; +} + diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index f180f3d08b..5680c3ddb3 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -15,7 +15,7 @@ */ import { colors } from 'playwright-core/lib/utilsBundle'; -import { debugTest, relativeFilePath, serializeError } from '../util'; +import { debugTest, relativeFilePath } from '../util'; import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { deserializeConfig } from '../common/configLoader'; @@ -32,6 +32,7 @@ import type { TestInfoError } from '../../types/test'; import type { Location } from '../../types/testReporter'; import { inheritFixtureNames } from '../common/fixtures'; import { type TimeSlot } from './timeoutManager'; +import { serializeWorkerError } from './util'; export class WorkerMain extends ProcessRunner { private _params: WorkerInitParams; @@ -112,7 +113,7 @@ export class WorkerMain extends ProcessRunner { await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); this._fatalErrors.push(...fakeTestInfo.errors); } catch (e) { - this._fatalErrors.push(serializeError(e)); + this._fatalErrors.push(serializeWorkerError(e)); } if (this._fatalErrors.length) { @@ -153,7 +154,7 @@ export class WorkerMain extends ProcessRunner { // No current test - fatal error. if (!this._currentTest) { if (!this._fatalErrors.length) - this._fatalErrors.push(serializeError(error)); + this._fatalErrors.push(serializeWorkerError(error)); void this._stop(); return; } @@ -224,7 +225,7 @@ export class WorkerMain extends ProcessRunner { // In theory, we should run above code without any errors. // However, in the case we screwed up, or loadTestFile failed in the worker // but not in the runner, let's do a fatal error. - this._fatalErrors.push(serializeError(e)); + this._fatalErrors.push(serializeWorkerError(e)); void this._stop(); } finally { const donePayload: DonePayload = { diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index a9d1f020ae..5663f92f98 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -554,16 +554,41 @@ export interface TestCase { * Information about an error thrown during test execution. */ export interface TestError { + /** + * Expected value formatted as a human-readable string. + */ + expected?: string; + /** * Error location in the source code. */ location?: Location; + /** + * Receiver's locator. + */ + locator?: string; + + /** + * Call log. + */ + log?: Array; + + /** + * Expect matcher name. + */ + matcherName?: string; + /** * Error message. Set when [Error] (or its subclass) has been thrown. */ message?: string; + /** + * Received value formatted as a human-readable string. + */ + received?: string; + /** * Source code snippet with highlighted error. */ @@ -574,6 +599,11 @@ export interface TestError { */ stack?: string; + /** + * Timeout in milliseconds, if the error was caused by a timeout. + */ + timeout?: number; + /** * The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown. */ diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts index 8f8a83bc83..7767ecf5f6 100644 --- a/tests/page/expect-matcher-result.spec.ts +++ b/tests/page/expect-matcher-result.spec.ts @@ -24,12 +24,16 @@ test('toMatchText-based assertions should have matcher result', async ({ page }) { const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff); expect.soft(e.matcherResult).toEqual({ actual: 'Text content', expected: /Text2/, message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`), name: 'toHaveText', pass: false, + locator: `locator('#node')`, + printedDiff: `Expected pattern: /Text2/ +Received string: \"Text content\"`, log: expect.any(Array), timeout: 1, }); @@ -46,12 +50,17 @@ Call log`); { const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected); + e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived); expect.soft(e.matcherResult).toEqual({ actual: 'Text content', expected: /Text/, message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`), name: 'toHaveText', pass: true, + locator: `locator('#node')`, + printedExpected: 'Expected pattern: not /Text/', + printedReceived: `Received string: \"Text content\"`, log: expect.any(Array), timeout: 1, }); @@ -79,6 +88,8 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page }) name: 'toBeVisible', pass: false, log: expect.any(Array), + printedExpected: 'Expected: visible', + printedReceived: 'Received: ', timeout: 1, }); @@ -101,6 +112,8 @@ Call log`); name: 'toBeVisible', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not visible', + printedReceived: 'Received: visible', timeout: 1, }); @@ -120,6 +133,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) => { const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff); expect.soft(e.matcherResult).toEqual({ actual: 0, expected: 1, @@ -127,6 +141,8 @@ test('toEqual-based assertions should have matcher result', async ({ page }) => name: 'toHaveCount', pass: false, log: expect.any(Array), + printedDiff: `Expected: 1 +Received: 0`, timeout: 1, }); @@ -141,6 +157,8 @@ Call log`); { const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected); + e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived); expect.soft(e.matcherResult).toEqual({ actual: 1, expected: 1, @@ -148,6 +166,8 @@ Call log`); name: 'toHaveCount', pass: true, log: expect.any(Array), + printedExpected: `Expected: not 1`, + printedReceived: `Received: 1`, timeout: 1, }); @@ -177,6 +197,8 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag name: 'toBeChecked', pass: false, log: expect.any(Array), + printedExpected: 'Expected: checked', + printedReceived: 'Received: unchecked', timeout: 1, }); @@ -199,6 +221,8 @@ Call log`); name: 'toBeChecked', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not checked', + printedReceived: 'Received: checked', timeout: 1, }); @@ -221,6 +245,8 @@ Call log`); name: 'toBeChecked', pass: false, log: expect.any(Array), + printedExpected: 'Expected: unchecked', + printedReceived: 'Received: checked', timeout: 1, }); @@ -243,6 +269,8 @@ Call log`); name: 'toBeChecked', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not unchecked', + printedReceived: 'Received: unchecked', timeout: 1, }); @@ -271,6 +299,8 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is name: 'toHaveScreenshot', pass: false, log: expect.any(Array), + printedExpected: expect.stringContaining('screenshot-sanity-'), + printedReceived: expect.stringContaining('screenshot-sanity-actual'), }); expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed: From 0351fd9401a70a6cd300329e7fa1869449cf0a03 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 21:21:30 +0200 Subject: [PATCH 33/35] docs: use WebSocketFrame abstraction for Java & .NET (#33211) --- docs/src/api/class-page.md | 8 ++--- docs/src/api/class-websocketroute.md | 52 ++++++++++++++-------------- docs/src/mock.md | 20 +++++------ docs/src/release-notes-csharp.md | 4 +-- docs/src/release-notes-java.md | 4 +-- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 373649154d..f65a904932 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3691,8 +3691,8 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -3722,8 +3722,8 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); diff --git a/docs/src/api/class-websocketroute.md b/docs/src/api/class-websocketroute.md index b827db25dd..e23316ebcb 100644 --- a/docs/src/api/class-websocketroute.md +++ b/docs/src/api/class-websocketroute.md @@ -18,8 +18,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -47,8 +47,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); @@ -70,8 +70,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - JsonObject json = new JsonParser().parse(message).getAsJsonObject(); + ws.onMessage(frame -> { + JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject(); if ("question".equals(json.get("request").getAsString())) { Map result = new HashMap(); result.put("response", "answer"); @@ -105,8 +105,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - using var jsonDoc = JsonDocument.Parse(message); + ws.OnMessage(frame => { + using var jsonDoc = JsonDocument.Parse(frame.Text); JsonElement root = jsonDoc.RootElement; if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question") { @@ -140,11 +140,11 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) server.send("request2"); else - server.send(message); + server.send(frame.text()); }); }); ``` @@ -180,11 +180,11 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") server.Send("request2"); else - server.Send(message); + server.Send(frame.Text); }); }); ``` @@ -215,13 +215,13 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if (!"blocked-from-the-page".equals(message)) - server.send(message); + ws.onMessage(frame -> { + if (!"blocked-from-the-page".equals(frame.text())) + server.send(frame.text()); }); - server.onMessage(message -> { - if (!"blocked-from-the-server".equals(message)) - ws.send(message); + server.onMessage(frame -> { + if (!"blocked-from-the-server".equals(frame.text())) + ws.send(frame.text()); }); }); ``` @@ -263,13 +263,13 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message != "blocked-from-the-page") - server.Send(message); + ws.OnMessage(frame => { + if (frame.Text != "blocked-from-the-page") + server.Send(frame.Text); }); - server.OnMessage(message => { - if (message != "blocked-from-the-server") - ws.Send(message); + server.OnMessage(frame => { + if (frame.Text != "blocked-from-the-server") + ws.Send(frame.Text); }); }); ``` diff --git a/docs/src/mock.md b/docs/src/mock.md index 468690904a..50bc3915ce 100644 --- a/docs/src/mock.md +++ b/docs/src/mock.md @@ -451,8 +451,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -480,8 +480,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); @@ -504,11 +504,11 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) server.send("request2"); else - server.send(message); + server.send(frame.text()); }); }); ``` @@ -544,11 +544,11 @@ page.route_web_socket("wss://example.com/ws", handler) ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") server.Send("request2"); else - server.Send(message); + server.Send(frame.Text); }); }); ``` diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index fee7d732fe..130b9d4a71 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -13,8 +13,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe ```csharp await page.RouteWebSocketAsync("/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 908a768357..a5d01668de 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -12,8 +12,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe ```java page.routeWebSocket("/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); From 2a3d67195dff6ece12bd36aab970a1fc95776998 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 21 Oct 2024 21:54:06 -0700 Subject: [PATCH 34/35] chore: use aria snapshots in some ui mode tests (#33212) --- .../src/server/injected/ariaSnapshot.ts | 11 +- .../playwright-core/src/utils/sequence.ts | 63 ++++ .../playwright-core/src/utils/stackTrace.ts | 20 +- .../src/ui/uiModeTestListView.tsx | 9 +- packages/web/src/components/treeView.tsx | 7 +- tests/library/sequence.spec.ts | 157 ++++++++++ .../stable-test-runner/package-lock.json | 46 +-- .../stable-test-runner/package.json | 2 +- .../playwright-test/ui-mode-test-run.spec.ts | 270 ++++++++++++++++-- 9 files changed, 534 insertions(+), 51 deletions(-) create mode 100644 packages/playwright-core/src/utils/sequence.ts create mode 100644 tests/library/sequence.spec.ts diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 56044a2027..682f48365a 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -208,7 +208,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { const root = generateAriaTree(rootElement); const matches = matchesNodeDeep(root, template); - return { matches, received: renderAriaTree(root) }; + return { matches, received: renderAriaTree(root, { noText: true }) }; } function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { @@ -276,11 +276,12 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean { return !!results.length; } -export function renderAriaTree(ariaNode: AriaNode): string { +export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string { const lines: string[] = []; const visit = (ariaNode: AriaNode | string, indent: string) => { if (typeof ariaNode === 'string') { - lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); + if (!options?.noText) + lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); return; } let line = `${indent}- ${ariaNode.role}`; @@ -301,10 +302,12 @@ export function renderAriaTree(ariaNode: AriaNode): string { line += ` [pressed=mixed]`; if (ariaNode.pressed === true) line += ` [pressed]`; + if (ariaNode.selected === true) + line += ` [selected]`; const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'); if (stringValue) { - if (ariaNode.children.length) + if (!options?.noText && ariaNode.children.length) line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); lines.push(line); return; diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts new file mode 100644 index 0000000000..2af5429bd6 --- /dev/null +++ b/packages/playwright-core/src/utils/sequence.ts @@ -0,0 +1,63 @@ +/** + * 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 findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] { + const n = s.length; + const result = []; + let i = 0; + + const arraysEqual = (a1: string[], a2: string[]) => { + if (a1.length !== a2.length) return false; + for (let j = 0; j < a1.length; j++) { + if (a1[j] !== a2[j]) return false; + } + return true; + }; + + while (i < n) { + let maxRepeatCount = 1; + let maxRepeatSubstr = [s[i]]; // Initialize with the element at index i + let maxRepeatLength = 1; + + // Try substrings of length from 1 to the remaining length of the array + for (let p = 1; p <= n - i; p++) { + const substr = s.slice(i, i + p); // Extract substring as array + let k = 1; + + // Count how many times the substring repeats consecutively + while ( + i + p * k <= n && + arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr) + ) { + k += 1; + } + k -= 1; // Adjust k since it increments one extra time in the loop + + // Update the maximal repeating substring if necessary + if (k > 1 && (k * p) > (maxRepeatCount * maxRepeatLength)) { + maxRepeatCount = k; + maxRepeatSubstr = substr; + maxRepeatLength = p; + } + } + + // Record the substring and its count + result.push({ sequence: maxRepeatSubstr, count: maxRepeatCount }); + i += maxRepeatLength * maxRepeatCount; // Move index forward + } + + return result; +} \ No newline at end of file diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 77e1365b3f..6f9a87578b 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -19,6 +19,7 @@ import { parseStackTraceLine } from '../utilsBundle'; import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; import { colors } from '../utilsBundle'; +import { findRepeatedSubsequences } from './sequence'; export function rewriteErrorMessage(e: E, newMessage: string): E { const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); @@ -132,9 +133,26 @@ export function splitErrorMessage(message: string): { name: string, message: str export function formatCallLog(log: string[] | undefined): string { if (!log || !log.some(l => !!l)) return ''; + + const lines: string[] = []; + + for (const block of findRepeatedSubsequences(log)) { + for (let i = 0; i < block.sequence.length; i++) { + const line = block.sequence[i]; + const leadingWhitespace = line.match(/^\s*/); + const whitespacePrefix = ' ' + leadingWhitespace?.[0] || ''; + const countPrefix = `${block.count} × `; + if (block.count > 1 && i === 0) + lines.push(whitespacePrefix + countPrefix + line.trim()); + else if (block.count > 1) + lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim()); + else + lines.push(whitespacePrefix + '- ' + line.trim()); + } + } return ` Call log: - ${colors.dim('- ' + (log || []).join('\n - '))} +${colors.dim(lines.join('\n'))} `; } diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index e0ef2a8bca..a6cb82fb8a 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -159,12 +159,15 @@ export const TestListView: React.FC<{ rootItem={testTree.rootItem} dataTestId='test-tree' render={treeItem => { - return
      -
      + const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-'); + const labelId = prefixId + '-label'; + const timeId = prefixId + '-time'; + return
      +
      {treeItem.title} {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
      - {!!treeItem.duration && treeItem.status !== 'skipped' &&
      {msToString(treeItem.duration)}
      } + {!!treeItem.duration && treeItem.status !== 'skipped' &&
      {msToString(treeItem.duration)}
      } runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}> diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 9af8609f3b..cb7ab7150d 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -249,8 +249,9 @@ export function TreeItemHeader({ const rendered = render(item); const children = expanded && item.children.length ? item.children as T[] : []; const titled = title?.(item); + const iconed = icon?.(item) || 'codicon-blank'; - return
      + return
      onAccepted?.(item)} className={clsx( @@ -277,10 +278,10 @@ export function TreeItemHeader({ toggleExpanded(item); }} /> - {icon && } + {icon &&
      } {typeof rendered === 'string' ?
      {rendered}
      : rendered}
      - {!!children.length &&
      + {!!children.length &&
      {children.map(child => { const itemData = treeItems.get(child); return itemData && { + const input = []; + const expectedOutput = []; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a single-element array', () => { + const input = ['a']; + const expectedOutput = [{ sequence: ['a'], count: 1 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle an array with no repeats', () => { + const input = ['a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle contiguous repeats of single elements', () => { + const input = ['a', 'a', 'a', 'b', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 3 }, + { sequence: ['b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should detect longer repeating substrings', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b']; + const expectedOutput = [{ sequence: ['a', 'b'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle multiple repeating substrings', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex cases with overlapping repeats', () => { + const input = ['a', 'a', 'a', 'a']; + const expectedOutput = [{ sequence: ['a'], count: 4 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex acceptance cases with multiple possible repeats', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle non-repeating sequences correctly', () => { + const input = ['a', 'b', 'c', 'd', 'e']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['d'], count: 1 }, + { sequence: ['e'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a case where the entire array is a repeating sequence', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y']; + const expectedOutput = [{ sequence: ['x', 'y'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly identify the maximal repeating substring', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 3 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle repeats with varying lengths', () => { + const input = ['a', 'a', 'b', 'b', 'b', 'b', 'a', 'a']; + const expectedOutput = [ + { sequence: ['a'], count: 2 }, + { sequence: ['b'], count: 4 }, + { sequence: ['a'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle a repeat count of one (k adjustment to zero)', () => { + const input = ['a', 'b', 'a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle repeats at the end of the array', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y', 'z']; + const expectedOutput = [ + { sequence: ['x', 'y'], count: 3 }, + { sequence: ['z'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should not overcount repeats when the last potential repeat is incomplete', () => { + const input = ['m', 'n', 'm', 'n', 'm']; + const expectedOutput = [ + { sequence: ['m', 'n'], count: 2 }, + { sequence: ['m'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle single repeats correctly when the substring length is greater than one', () => { + const input = ['a', 'b', 'c', 'a', 'b', 'd']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['d'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index df6792d59d..1ebdfb52cc 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,15 +5,15 @@ "packages": { "": { "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } }, "node_modules/@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "dependencies": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -36,11 +36,11 @@ } }, "node_modules/playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "dependencies": { - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -53,9 +53,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==", "bin": { "playwright-core": "cli.js" }, @@ -66,11 +66,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "requires": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" } }, "fsevents": { @@ -80,18 +80,18 @@ "optional": true }, "playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" } }, "playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==" + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 14625ebe6d..dbe21acd15 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } } diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index f60a6dde86..3673faab45 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -61,22 +61,25 @@ test('should run visible', async ({ runUITest }) => { ⊘ skipped `); - // await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` - // - tree: - // - treeitem "a.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" [selected]: - // - button "Run" - // - button "Show source" - // - button "Watch" - // - treeitem "suite" - // - treeitem "b.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" - // - treeitem "c.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "skipped" - // `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -117,6 +120,17 @@ test('should run on hover', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/}: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on double click', async ({ runUITest }) => { @@ -135,6 +149,17 @@ test('should run on double click', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on Enter', async ({ runUITest }) => { @@ -154,6 +179,17 @@ test('should run on Enter', async ({ runUITest }) => { ◯ passes ❌ fails <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); }); test('should run by project', async ({ runUITest }) => { @@ -185,6 +221,26 @@ test('should run by project', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await page.getByText('Status:').click(); await page.getByLabel('bar').setChecked(true); @@ -203,6 +259,29 @@ test('should run by project', async ({ runUITest }) => { ► ◯ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/}: + - group: + - treeitem ${/\[icon-error\] foo/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] bar" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-circle-outline] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-circle-outline\] skipped/} + `); + await page.getByText('Status:').click(); await page.getByTestId('test-tree').getByText('passes').first().click(); @@ -216,6 +295,20 @@ test('should run by project', async ({ runUITest }) => { ► ❌ fails `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-circle-outline\] bar/} + - treeitem ${/\[icon-error\] fails \d+ms/} + `); + await expect(page.getByText('Projects: foo bar')).toBeVisible(); await page.getByTitle('Run all').click(); @@ -235,6 +328,32 @@ test('should run by project', async ({ runUITest }) => { ► ✅ passes ► ⊘ skipped `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-check\] bar \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-error\] foo \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem ${/\[icon-error\] bar \d+ms/} + - treeitem ${/\[icon-error\] suite/} + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-circle-slash\] skipped/} + `); }); test('should stop', async ({ runUITest }) => { @@ -261,6 +380,16 @@ test('should stop', async ({ runUITest }) => { 🕦 test 3 `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-loading] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-loading\] test 2/} + - treeitem ${/\[icon-clock\] test 3/} + `); + await expect(page.getByTitle('Run all')).toBeDisabled(); await expect(page.getByTitle('Stop')).toBeEnabled(); @@ -273,6 +402,16 @@ test('should stop', async ({ runUITest }) => { ◯ test 2 ◯ test 3 `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-circle-outline\] test 2/} + - treeitem ${/\[icon-circle-outline\] test 3/} + `); }); test('should run folder', async ({ runUITest }) => { @@ -301,6 +440,17 @@ test('should run folder', async ({ runUITest }) => { ▼ ◯ in-a.test.ts ◯ passes `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] folder-b" [expanded] [selected]: + - group: + - treeitem "[icon-check] folder-c" + - treeitem "[icon-check] in-b.test.ts" + - treeitem "[icon-circle-outline] in-a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + `); }); test('should show time', async ({ runUITest }) => { @@ -324,6 +474,26 @@ test('should show time', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -348,6 +518,13 @@ test('should show test.fail as passing', async ({ runUITest }) => { ✅ should fail XXms `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should fail \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -377,6 +554,13 @@ test('should ignore repeatEach', async ({ runUITest }) => { ✅ should pass `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -404,6 +588,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('Run all').click(); @@ -411,6 +603,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -451,6 +651,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup <= ◯ run @chromium `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] run @chromium chromium" + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('run @chromium').dblclick(); @@ -459,6 +671,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup ✅ run @chromium <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} + - treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); + await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)'); }); @@ -518,6 +742,13 @@ test('should respect --tsconfig option', { ✅ test `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] test \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -539,4 +770,11 @@ test('should respect --ignore-snapshots option', { ▼ ✅ a.test.ts ✅ snapshot `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] snapshot \d+ms/} + `); }); From b275c1961237c61492ac6fe20402524cb88d368b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 22 Oct 2024 11:52:20 +0200 Subject: [PATCH 35/35] chore: update eslintignore to lint files in utils/ folders (#33218) --- .eslintignore | 9 +++------ packages/playwright-core/src/utils/crypto.ts | 11 +++++----- .../src/utils/happy-eyeballs.ts | 20 +++++++++---------- .../src/utils/isomorphic/locatorGenerators.ts | 2 +- packages/playwright-core/src/utils/network.ts | 2 +- .../playwright-core/src/utils/sequence.ts | 11 ++++++---- .../playwright-core/src/utils/stackTrace.ts | 2 -- packages/playwright-core/src/utils/zones.ts | 3 ++- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.eslintignore b/.eslintignore index 60b8fd360f..9d22f618d8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,14 +8,11 @@ test/assets/modernizr.js /packages/playwright-ct-core/src/generated/* /index.d.ts node_modules/ -browser_patches/*/checkout/ -browser_patches/chromium/output/ **/*.d.ts output/ test-results/ -tests/components/ -tests/installation/fixture-scripts/ -examples/ +/tests/components/ +/tests/installation/fixture-scripts/ DEPS .cache/ -utils/ +/utils/ diff --git a/packages/playwright-core/src/utils/crypto.ts b/packages/playwright-core/src/utils/crypto.ts index 5da56d4e9b..f538912d24 100644 --- a/packages/playwright-core/src/utils/crypto.ts +++ b/packages/playwright-core/src/utils/crypto.ts @@ -33,11 +33,12 @@ function encodeBase128(value: number): Buffer { do { let byte = value & 0x7f; value >>>= 7; - if (bytes.length > 0) byte |= 0x80; + if (bytes.length > 0) + byte |= 0x80; bytes.push(byte); } while (value > 0); return Buffer.from(bytes.reverse()); -}; +} // ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en class DER { @@ -49,13 +50,13 @@ class DER { return this._encode(0x02, Buffer.from([data])); } static encodeObjectIdentifier(oid: string): Buffer { - const parts = oid.split('.').map((v) => Number(v)); + const parts = oid.split('.').map(v => Number(v)); // Encode the second part, which could be large, using base-128 encoding if necessary const output = [encodeBase128(40 * parts[0] + parts[1])]; - for (let i = 2; i < parts.length; i++) { + for (let i = 2; i < parts.length; i++) output.push(encodeBase128(parts[i])); - } + return this._encode(0x06, Buffer.concat(output)); } diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts index 18de1a938c..02b78de4f0 100644 --- a/packages/playwright-core/src/utils/happy-eyeballs.ts +++ b/packages/playwright-core/src/utils/happy-eyeballs.ts @@ -29,8 +29,8 @@ import { monotonicTime } from './time'; // Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102) const connectionAttemptDelayMs = 300; -const kDNSLookupAt = Symbol('kDNSLookupAt') -const kTCPConnectionAt = Symbol('kTCPConnectionAt') +const kDNSLookupAt = Symbol('kDNSLookupAt'); +const kTCPConnectionAt = Symbol('kTCPConnectionAt'); class HttpHappyEyeballsAgent extends http.Agent { createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined { @@ -75,7 +75,7 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise { assert(options.host, 'host is required'); if (net.isIP(options.host)) { - const socket = tls.connect(options) + const socket = tls.connect(options); socket.on('secureConnect', () => resolve(socket)); socket.on('error', error => reject(error)); } else { @@ -92,20 +92,20 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: tls.TLSSocket) => void) | undefined, useTLS: true ): Promise; export async function createConnectionAsync( - options: http.ClientRequestArgs, - oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, useTLS: false ): Promise; export async function createConnectionAsync( - options: http.ClientRequestArgs, - oncreate: ((err: Error | null, socket?: any) => void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: any) => void) | undefined, useTLS: boolean ): Promise { const lookup = (options as any).__testHookLookup || lookupAddresses; @@ -202,5 +202,5 @@ export function timingForSocket(socket: net.Socket | tls.TLSSocket) { return { dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined, tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined, - } + }; } diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 56252d02d3..930abaaba6 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -163,7 +163,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram continue; } - let locatorType: LocatorType = 'default'; + const locatorType: LocatorType = 'default'; const nextPart = parts[index + 1]; diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts index 632c74fe3a..25d3a11156 100644 --- a/packages/playwright-core/src/utils/network.ts +++ b/packages/playwright-core/src/utils/network.ts @@ -124,7 +124,7 @@ export function createHttpsServer(...args: any[]): https.Server { return server; } -export function createHttp2Server( onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; +export function createHttp2Server(onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; export function createHttp2Server(options: http2.SecureServerOptions, onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; export function createHttp2Server(...args: any[]): http2.Http2SecureServer { const server = http2.createSecureServer(...args); diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts index 2af5429bd6..27756fabeb 100644 --- a/packages/playwright-core/src/utils/sequence.ts +++ b/packages/playwright-core/src/utils/sequence.ts @@ -20,10 +20,13 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou let i = 0; const arraysEqual = (a1: string[], a2: string[]) => { - if (a1.length !== a2.length) return false; + if (a1.length !== a2.length) + return false; for (let j = 0; j < a1.length; j++) { - if (a1[j] !== a2[j]) return false; + if (a1[j] !== a2[j]) + return false; } + return true; }; @@ -41,9 +44,9 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou while ( i + p * k <= n && arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr) - ) { + ) k += 1; - } + k -= 1; // Adjust k since it increments one extra time in the loop // Update the maximal repeating substring if necessary diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 6f9a87578b..84d08b0184 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -16,7 +16,6 @@ import path from 'path'; import { parseStackTraceLine } from '../utilsBundle'; -import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; import { colors } from '../utilsBundle'; import { findRepeatedSubsequences } from './sequence'; @@ -51,7 +50,6 @@ export function captureRawStack(): RawStack { export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } { const stack = captureRawStack(); - const isTesting = isUnderTest(); type ParsedFrame = { frame: StackFrame; frameText: string; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index 68cb6fa7fa..e6fabac0f1 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -46,8 +46,9 @@ class ZoneManager { if (zone.type === 'apiZone') str += `(${(zone.data as any).apiName})`; zones.push(str); - + } + // eslint-disable-next-line no-console console.log('zones: ', zones.join(' -> ')); } }