diff --git a/README.md b/README.md index 3dcad58f49..2f04b28a4f 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-131.0.6778.33-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-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-132.0.6834.6-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-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 131.0.6778.33 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 132.0.6834.6 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 132.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index f4a2fe6d7c..64c5873710 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -206,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot: - link "About" ``` +### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%% +* since: v1.49 + ### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%% * since: v1.49 diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 5d06824b47..3102ef085e 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -701,7 +701,7 @@ expect(locator).to_be_enabled() ```csharp var locator = Page.Locator("button.submit"); -await Expect(locator).toBeEnabledAsync(); +await Expect(locator).ToBeEnabledAsync(); ``` ### option: LocatorAssertions.toBeEnabled.enabled @@ -1181,7 +1181,7 @@ expect(locator).to_have_accessible_description("Save results to disk") ```csharp var locator = Page.GetByTestId("save-button"); -await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk"); +await Expect(locator).ToHaveAccessibleDescriptionAsync("Save results to disk"); ``` ### param: LocatorAssertions.toHaveAccessibleDescription.description @@ -1231,12 +1231,12 @@ expect(locator).to_have_accessible_name("Save to disk") ```csharp var locator = Page.GetByTestId("save-button"); -await Expect(locator).toHaveAccessibleNameAsync("Save to disk"); +await Expect(locator).ToHaveAccessibleNameAsync("Save to disk"); ``` ### param: LocatorAssertions.toHaveAccessibleName.name * since: v1.44 -- `name` <[string]|[RegExp]> +- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>> Expected accessible name. @@ -2159,3 +2159,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot(""" ### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%% * since: v1.49 + +### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.49 diff --git a/docs/src/ci-intro.md b/docs/src/ci-intro.md index 9e0ee0eb4f..ed7e2208fc 100644 --- a/docs/src/ci-intro.md +++ b/docs/src/ci-intro.md @@ -21,7 +21,7 @@ Playwright tests can be run on any CI provider. This guide covers one way of run ## Introduction * langs: python, java, csharp -Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration. +Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration. #### You will learn * langs: python, java, csharp diff --git a/docs/src/clock.md b/docs/src/clock.md index ea14941d82..2f846e4fd3 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -164,11 +164,11 @@ await Page.GotoAsync("http://localhost:3333"); await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0)); // Assert the page state. -await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); +await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); // Close the laptop lid again and open it at 10:30am. await Page.Clock.FastForwardAsync("30:00"); -await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM"); +await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` ## Test inactivity monitoring diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index 8c29641bd9..321629662b 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -80,7 +80,7 @@ The `tests` folder contains a basic example test to help you get started with te ## Running the Example Test -By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. +By default tests will be run on all 3 browsers, Chromium, Firefox and WebKit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. void, children?: any, dataTestId?: string, - targetRef?: React.RefObject, -}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId, targetRef }) => { +}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId }) => { const id = React.useId(); - return
+ return
, -}> = ({ header, initialExpanded, noInsets, children, dataTestId, targetRef }) => { - const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined); + revealOnAnchorId?: string, +}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => { + const [expanded, setExpanded] = React.useState(initialExpanded ?? true); + const onReveal = React.useCallback(() => setExpanded(true), []); + useAnchor(revealOnAnchorId, onReveal); return {children} ; diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 4b48090e0a..4beb7b65e0 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -113,3 +113,32 @@ export function generateTraceUrl(traces: TestAttachment[]) { } const kMissingContentType = 'x-playwright/missing'; + +type AnchorID = string | ((id: string | null) => boolean) | undefined; + +export function useAnchor(id: AnchorID, onReveal: () => void) { + React.useEffect(() => { + if (typeof id === 'undefined') + return; + + const listener = () => { + const params = new URLSearchParams(window.location.hash.slice(1)); + const anchor = params.get('anchor'); + const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id; + if (isRevealed) + onReveal(); + }; + window.addEventListener('popstate', listener); + return () => window.removeEventListener('popstate', listener); + }, [id, onReveal]); +} + +export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) { + const ref = React.useRef(null); + const onAnchorReveal = React.useCallback(() => { + requestAnimationFrame(() => ref.current?.scrollIntoView({ block: 'start', inline: 'start' })); + }, []); + useAnchor(id, onAnchorReveal); + + return
{children}
; +} diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index e8a5c3b250..cf0f5e5e56 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -101,7 +101,6 @@ const TestCaseViewLoader: React.FC<{ const searchParams = React.useContext(SearchParamsContext); const [test, setTest] = React.useState(); const testId = searchParams.get('testId'); - const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | ''; const run = +(searchParams.get('run') || '0'); const { prev, next } = React.useMemo(() => { @@ -133,7 +132,6 @@ const TestCaseViewLoader: React.FC<{ next={next} prev={prev} test={test} - anchor={anchor} run={run} />; }; diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 892ad51b7f..b7a9f9405b 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -63,7 +63,7 @@ const testCase: TestCase = { }; test('should render test case', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); await expect(component.getByText('Hidden annotation')).toBeHidden(); await component.getByText('Annotations').click(); @@ -79,7 +79,7 @@ test('should render test case', async ({ mount }) => { test('should render copy buttons for annotations', async ({ mount, page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - const component = await mount(); + const component = await mount(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); await component.getByText('Annotation text', { exact: false }).first().hover(); await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible(); @@ -108,7 +108,7 @@ const annotationLinkRenderingTestCase: TestCase = { }; test('should correctly render links in annotations', async ({ mount }) => { - const component = await mount(); + const component = await mount(); const firstLink = await component.getByText('https://playwright.dev/docs/intro').first(); await expect(firstLink).toBeVisible(); @@ -181,7 +181,7 @@ const testCaseSummary: TestCaseSummary = { test('should correctly render links in attachments', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await component.getByText('first attachment').click(); const body = await component.getByText('The body with https://playwright.dev/docs/intro link'); await expect(body).toBeVisible(); @@ -194,7 +194,7 @@ test('should correctly render links in attachments', async ({ mount }) => { }); test('should correctly render links in attachment name', async ({ mount }) => { - const component = await mount(); + const component = await mount(); const link = component.getByText('attachment with inline link').locator('a'); await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284'); @@ -204,7 +204,7 @@ test('should correctly render links in attachment name', async ({ mount }) => { }); test('should correctly render prev and next', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component).toMatchAriaSnapshot(` - text: group - link "« previous" diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 320722fa9b..4e9785ad8a 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -33,9 +33,8 @@ export const TestCaseView: React.FC<{ test: TestCase | undefined, next: TestCaseSummary | undefined, prev: TestCaseSummary | undefined, - anchor: 'video' | 'diff' | '', run: number, -}> = ({ projectNames, test, run, anchor, next, prev }) => { +}> = ({ projectNames, test, run, next, prev }) => { const [selectedResultIndex, setSelectedResultIndex] = React.useState(run); const searchParams = React.useContext(SearchParamsContext); const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : ''; @@ -79,7 +78,7 @@ export const TestCaseView: React.FC<{ test.results.map((result, index) => ({ id: String(index), title:
{statusIcon(result.status)} {retryLabel(index)}
, - render: () => + render: () => })) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
; }; diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx index d63f5d7945..ea402106a8 100644 --- a/packages/html-reporter/src/testErrorView.tsx +++ b/packages/html-reporter/src/testErrorView.tsx @@ -25,7 +25,7 @@ export const TestErrorView: React.FC<{ 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/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index 4d6890ad33..6b31d2ebe2 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -75,12 +75,12 @@ function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined { const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => { return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/); })); - return resultWithImageDiff ? {image()} : undefined; + return resultWithImageDiff ? {image()} : undefined; } function videoBadge(test: TestCaseSummary): JSX.Element | undefined { const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video')); - return resultWithVideo ? {video()} : undefined; + return resultWithVideo ? {video()} : undefined; } function traceBadge(test: TestCaseSummary): JSX.Element | undefined { diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 3a562f3fcf..9170f2023d 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -20,7 +20,7 @@ import { TreeItem } from './treeItem'; import { msToString } from './utils'; import { AutoChip } from './chip'; import { traceImage } from './images'; -import { AttachmentLink, generateTraceUrl } from './links'; +import { Anchor, AttachmentLink, generateTraceUrl } from './links'; import { statusIcon } from './statusIcon'; import type { ImageDiff } from '@web/shared/imageDiffView'; import { ImageDiffView } from '@web/shared/imageDiffView'; @@ -64,9 +64,7 @@ function groupImageDiffs(screenshots: Set): ImageDiff[] { export const TestResultView: React.FC<{ test: TestCase, result: TestResult, - anchor: 'video' | 'diff' | '', -}> = ({ result, anchor }) => { - +}> = ({ result }) => { const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => { const attachments = result?.attachments || []; const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); @@ -80,20 +78,6 @@ export const TestResultView: React.FC<{ return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls }; }, [result]); - const videoRef = React.useRef(null); - const imageDiffRef = React.useRef(null); - - const [scrolled, setScrolled] = React.useState(false); - React.useEffect(() => { - if (scrolled) - return; - setScrolled(true); - if (anchor === 'video') - videoRef.current?.scrollIntoView({ block: 'start', inline: 'start' }); - if (anchor === 'diff') - imageDiffRef.current?.scrollIntoView({ block: 'start', inline: 'start' }); - }, [scrolled, anchor, setScrolled, videoRef]); - return
{!!errors.length && {errors.map((error, index) => { @@ -107,9 +91,11 @@ export const TestResultView: React.FC<{ } {diffs.map((diff, index) => - - - + + + + + )} {!!screenshots.length && @@ -123,23 +109,23 @@ export const TestResultView: React.FC<{ })} } - {!!traces.length && + {!!traces.length && {
{traces.map((a, i) => )}
} -
} +
} - {!!videos.length && + {!!videos.length && {videos.map((a, i) =>
)} -
} +
} {!!(otherAttachments.size + htmls.length) && {[...htmls].map((a, i) => ( diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 81f1d3afd3..32d5e3b375 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,21 +3,21 @@ "browsers": [ { "name": "chromium", - "revision": "1148", + "revision": "1149", "installByDefault": true, - "browserVersion": "131.0.6778.33" + "browserVersion": "132.0.6834.6" }, { "name": "chromium-headless-shell", - "revision": "1148", + "revision": "1149", "installByDefault": true, - "browserVersion": "131.0.6778.33" + "browserVersion": "132.0.6834.6" }, { "name": "chromium-tip-of-tree", - "revision": "1277", + "revision": "1279", "installByDefault": false, - "browserVersion": "132.0.6834.0" + "browserVersion": "133.0.6846.0" }, { "name": "firefox", @@ -27,9 +27,9 @@ }, { "name": "firefox-beta", - "revision": "1465", + "revision": "1466", "installByDefault": false, - "browserVersion": "132.0b8" + "browserVersion": "133.0b9" }, { "name": "webkit", diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 74b8df9367..cb18681ccf 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -97,6 +97,7 @@ export class Request extends ChannelOwner implements ap constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RequestInitializer) { super(parent, type, guid, initializer); + this.markAsInternalType(); this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom); if (this._redirectedFrom) this._redirectedFrom._redirectedTo = this; @@ -645,6 +646,7 @@ export class Response extends ChannelOwner implements constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) { super(parent, type, guid, initializer); + this.markAsInternalType(); this._provisionalHeaders = new RawHeaders(initializer.headers); this._request = Request.from(this._initializer.request); Object.assign(this._request._timing, this._initializer.timing); diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 35aa6f2eb9..3a61fbca59 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -112,7 +112,7 @@ export module Protocol { - from 'checked' to 'selected': states which apply to widgets - from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling. */ - export type AXPropertyName = "busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; + export type AXPropertyName = "actions"|"busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; /** * A node in the accessibility tree. */ @@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations export interface AffectedFrame { frameId: Page.FrameId; } - export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; + export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; /** @@ -2183,12 +2183,17 @@ The array enumerates @scope at-rules starting with the innermost one, going outw * The array keeps the types of ancestor CSSRules from the innermost going outwards. */ ruleTypes?: CSSRuleType[]; + /** + * @starting-style CSS at-rule array. +The array enumerates @starting-style at-rules starting with the innermost one, going outwards. + */ + startingStyles?: CSSStartingStyle[]; } /** * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. This list only contains rule types that are collected during the ancestor rule collection. */ - export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"; + export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule"; /** * CSS coverage information. */ @@ -2424,6 +2429,10 @@ available). * Optional logical axes queried for the container. */ logicalAxes?: DOM.LogicalAxes; + /** + * true if the query contains scroll-state() queries. + */ + queriesScrollState?: boolean; } /** * CSS Supports at-rule descriptor. @@ -2475,6 +2484,20 @@ available). text: string; /** * The associated rule header range in the enclosing stylesheet (if +available). + */ + range?: SourceRange; + /** + * Identifier of the stylesheet containing this object (if exists). + */ + styleSheetId?: StyleSheetId; + } + /** + * CSS Starting Style at-rule descriptor. + */ + export interface CSSStartingStyle { + /** + * The associated rule header range in the enclosing stylesheet (if available). */ range?: SourceRange; @@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea */ styleSheetId: StyleSheetId; } + export type computedStyleUpdatedPayload = { + /** + * The node id that has updated computed styles. + */ + nodeId: DOM.NodeId; + } /** * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the @@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet. export type getLocationForSelectorReturnValue = { ranges: SourceRange[]; } + /** + * Starts tracking the given node for the computed style updates +and whenever the computed style is updated for node, it queues +a `computedStyleUpdated` event with throttling. +There can only be 1 node tracked for computed style updates +so passing a new node id removes tracking from the previous node. +Pass `undefined` to disable tracking. + */ + export type trackComputedStyleUpdatesForNodeParameters = { + nodeId?: DOM.NodeId; + } + export type trackComputedStyleUpdatesForNodeReturnValue = { + } /** * Starts tracking the given computed styles for updates. The specified array of properties replaces the one previously specified. Pass empty array to disable tracking. @@ -3561,7 +3603,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker"; + export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; /** * Shadow root type. */ @@ -4876,15 +4918,17 @@ $x functions). } /** * Returns the query container of the given node based on container query -conditions: containerName, physical, and logical axes. If no axes are -provided, the style container is returned, which is the direct parent or the -closest element with a matching container-name. +conditions: containerName, physical and logical axes, and whether it queries +scroll-state. If no axes are provided and queriesScrollState is false, the +style container is returned, which is the direct parent or the closest +element with a matching container-name. */ export type getContainerForNodeParameters = { nodeId: NodeId; containerName?: string; physicalAxes?: PhysicalAxes; logicalAxes?: LogicalAxes; + queriesScrollState?: boolean; } export type getContainerForNodeReturnValue = { /** @@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc. */ export type LoaderId = string; /** - * Unique request identifier. + * Unique network request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. */ export type RequestId = string; /** @@ -8830,6 +8876,7 @@ If the opcode isn't 1, then payloadData is a base64 encoded string representing type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; /** * Initiator JavaScript stack trace, set for Script only. +Requires the Debugger domain to be enabled. */ stack?: Runtime.StackTrace; /** @@ -8944,7 +8991,7 @@ This is a temporary ability and it will be removed in the future. /** * Types of reasons why a cookie may not be sent with a request. */ - export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"; + export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch"; /** * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. */ @@ -11498,7 +11545,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"|"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"; + 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"|"fenced-unpartitioned-storage-read"|"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. */ @@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi defaultPrompt?: string; } /** - * Fired for top level page lifecycle events such as navigation, load, paint, etc. + * Fired for lifecycle events (navigation, load, paint, etc) in the current +target (including local frames). */ export type lifecycleEventPayload = { /** @@ -14339,6 +14387,7 @@ int destinationLimitPriority: SignedInt64AsBase10; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; scopesData?: AttributionScopesData; + maxEventLevelReports: number; } export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; @@ -14386,7 +14435,7 @@ int scopes: string[]; } export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; - export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; + export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"insufficientNamedBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; /** * A single Related Website Set object. */ @@ -15920,6 +15969,8 @@ are ignored. export module Fetch { /** * Unique request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. */ export type RequestId = string; /** @@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/ /** * Enum of AudioContextState from the spec */ - export type ContextState = "suspended"|"running"|"closed"; + export type ContextState = "suspended"|"running"|"closed"|"interrupted"; /** * Enum of AudioNode types */ @@ -20213,6 +20264,7 @@ Error was thrown. "CSS.styleSheetAdded": CSS.styleSheetAddedPayload; "CSS.styleSheetChanged": CSS.styleSheetChangedPayload; "CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload; + "CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload; "Cast.sinksUpdated": Cast.sinksUpdatedPayload; "Cast.issueUpdated": Cast.issueUpdatedPayload; "DOM.attributeModified": DOM.attributeModifiedPayload; @@ -20464,6 +20516,7 @@ Error was thrown. "CSS.getStyleSheetText": CSS.getStyleSheetTextParameters; "CSS.getLayersForNode": CSS.getLayersForNodeParameters; "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters; + "CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters; @@ -21075,6 +21128,7 @@ Error was thrown. "CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue; "CSS.getLayersForNode": CSS.getLayersForNodeReturnValue; "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue; + "CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue; diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index 428ca493f8..e5f72ce122 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -117,8 +117,10 @@ 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)});`; + case 'assertSnapshot': { + const commentIfNeeded = this._isTest ? '' : '// '; + return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot, `${commentIfNeeded} `)});`; + } } } diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 4132dceec9..e82744b73c 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 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/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36 Edg/131.0.6778.33", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36 Edg/132.0.6834.6", "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/131.0.6778.33 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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/131.0.6778.33 Safari/537.36 Edg/131.0.6778.33", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36 Edg/132.0.6834.6", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 11aba2b1b2..81f04d94eb 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -296,7 +296,8 @@ export class FrameManager { if (request._documentId) frame.setPendingDocument({ documentId: request._documentId, request }); if (request._isFavicon) { - route?.continue({ isFallback: true }).catch(() => {}); + // Abort favicon requests to avoid network access in case of interception. + route?.abort('aborted').catch(() => {}); return; } this._page.emitOnContext(BrowserContext.Events.Request, request); diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index b2352a1b9c..d541646f0e 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -20,15 +20,36 @@ import { escapeRegExp, longestCommonSubstring } from '@isomorphic/stringUtils'; import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml'; import type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot'; -type AriaNode = AriaProps & { +export type AriaNode = AriaProps & { role: AriaRole | 'fragment'; name: string; children: (AriaNode | string)[]; element: Element; }; -export function generateAriaTree(rootElement: Element): AriaNode { +export type AriaSnapshot = { + root: AriaNode; + elements: Map; + ids: Map; +}; + +export function generateAriaTree(rootElement: Element): AriaSnapshot { const visited = new Set(); + + const snapshot: AriaSnapshot = { + root: { role: 'fragment', name: '', children: [], element: rootElement }, + elements: new Map(), + ids: new Map(), + }; + + const addElement = (element: Element) => { + const id = snapshot.elements.size + 1; + snapshot.elements.set(id, element); + snapshot.ids.set(element, id); + }; + + addElement(rootElement); + const visit = (ariaNode: AriaNode, node: Node) => { if (visited.has(node)) return; @@ -58,6 +79,7 @@ export function generateAriaTree(rootElement: Element): AriaNode { } } + addElement(element); const childAriaNode = toAriaNode(element); if (childAriaNode) ariaNode.children.push(childAriaNode); @@ -100,15 +122,14 @@ export function generateAriaTree(rootElement: Element): AriaNode { } roleUtils.beginAriaCaches(); - const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [], element: rootElement }; try { - visit(ariaRoot, rootElement); + visit(snapshot.root, rootElement); } finally { roleUtils.endAriaCaches(); } - normalizeStringChildren(ariaRoot); - return ariaRoot; + normalizeStringChildren(snapshot.root); + return snapshot; } function toAriaNode(element: Element): AriaNode | null { @@ -143,10 +164,6 @@ function toAriaNode(element: Element): AriaNode | null { return result; } -export function renderedAriaTree(rootElement: Element, options?: { mode?: 'raw' | 'regex' }): string { - return renderAriaTree(generateAriaTree(rootElement), options); -} - function normalizeStringChildren(rootA11yNode: AriaNode) { const flushChildren = (buffer: string[], normalizedChildren: (AriaNode | string)[]) => { if (!buffer.length) @@ -203,7 +220,7 @@ export type MatcherReceived = { }; export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } { - const root = generateAriaTree(rootElement); + const root = generateAriaTree(rootElement).root; const matches = matchesNodeDeep(root, template, false); return { matches, @@ -215,7 +232,7 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode } export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] { - const root = generateAriaTree(rootElement); + const root = generateAriaTree(rootElement).root; const matches = matchesNodeDeep(root, template, true); return matches.map(n => n.element); } @@ -285,7 +302,7 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll: return results; } -export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex' }): string { +export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', ids?: Map }): string { const lines: string[] = []; const includeText = options?.mode === 'regex' ? textContributesInfo : () => true; const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str; @@ -324,6 +341,11 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'r key += ` [pressed]`; if (ariaNode.selected === true) key += ` [selected]`; + if (options?.ids) { + const id = options?.ids.get(ariaNode.element); + if (id) + key += ` [id=${id}]`; + } const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key); if (!ariaNode.children.length) { diff --git a/packages/playwright-core/src/server/injected/highlight.ts b/packages/playwright-core/src/server/injected/highlight.ts index c06e58f529..5720ffc539 100644 --- a/packages/playwright-core/src/server/injected/highlight.ts +++ b/packages/playwright-core/src/server/injected/highlight.ts @@ -90,7 +90,8 @@ export class Highlight { } install() { - if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement)) + // NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078 + if (this._injectedScript.document.documentElement && !this._injectedScript.document.documentElement.contains(this._glassPaneElement)) this._injectedScript.document.documentElement.appendChild(this._glassPaneElement); } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index d74a1c1482..aa0eb1004f 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -34,7 +34,8 @@ 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, renderedAriaTree, getAllByAria } from './ariaSnapshot'; +import { matchesAriaTree, getAllByAria, generateAriaTree, renderAriaTree } from './ariaSnapshot'; +import type { AriaNode, AriaSnapshot } from './ariaSnapshot'; import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; import { parseYamlTemplate } from '@isomorphic/ariaSnapshot'; @@ -215,10 +216,27 @@ export class InjectedScript { return new Set(result.map(r => r.element)); } - ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex' }): string { + ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex', id?: boolean }): string { if (node.nodeType !== Node.ELEMENT_NODE) throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); - return renderedAriaTree(node as Element, options); + const ariaSnapshot = generateAriaTree(node as Element); + return renderAriaTree(ariaSnapshot.root, options); + } + + ariaSnapshotAsObject(node: Node): AriaSnapshot { + return generateAriaTree(node as Element); + } + + ariaSnapshotElement(snapshot: AriaSnapshot, elementId: number): Element | null { + return snapshot.elements.get(elementId) || null; + } + + renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', id?: boolean}): string { + return renderAriaTree(ariaNode, options); + } + + renderAriaSnapshotWithIds(ariaSnapshot: AriaSnapshot): string { + return renderAriaTree(ariaSnapshot.root, { ids: ariaSnapshot.ids }); } getAllByAria(document: Document, template: AriaTemplateNode): Element[] { @@ -1336,6 +1354,8 @@ export class InjectedScript { received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full); else if (expression === 'to.have.class.array') received = elements.map(e => e.classList.toString()); + else if (expression === 'to.have.accessible.name.array') + received = elements.map(e => getElementAccessibleName(e, false)); if (received && options.expectedText) { // "To match an array" is "to contain an array" + "equal length" diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 1828af3bd0..ddfb0386d9 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -492,9 +492,10 @@ class RecordActionTool implements RecorderTool { return; const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null; this._activeModel = result && result.selector ? result : null; - if (userGesture) + if (userGesture) { this._hoveredElement = activeElement as HTMLElement | null; - this._updateModelForHoveredElement(); + this._updateModelForHoveredElement(); + } } private _shouldIgnoreMouseEvent(event: MouseEvent): boolean { @@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool { } private _updateModelForHoveredElement() { + if (this._performingActions.size) + return; if (!this._hoveredElement || !this._hoveredElement.isConnected) { this._hoveredModel = null; this._hoveredElement = null; @@ -1018,7 +1021,7 @@ export class Recorder { private _listeners: (() => void)[] = []; private _currentTool: RecorderTool; private _tools: Record; - private _actionSelectorModel: HighlightModel | null = null; + private _lastHighlightedSelector: string | undefined = undefined; private _lastHighlightedAriaTemplateJSON: string = 'undefined'; readonly highlight: Highlight; readonly overlay: Overlay | undefined; @@ -1129,12 +1132,12 @@ export class Recorder { this._switchCurrentTool(); this.overlay?.setUIState(state); - // Race or scroll. - if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON) - this._actionSelectorModel = null; - - if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector) - this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document); + let highlight: HighlightModel | 'clear' | 'noop' = 'noop'; + if (state.actionSelector !== this._lastHighlightedSelector) { + this._lastHighlightedSelector = state.actionSelector; + const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null; + highlight = model?.elements.length ? model : 'clear'; + } const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { @@ -1142,16 +1145,15 @@ export class Recorder { const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined; const elements = template ? this.injectedScript.getAllByAria(this.document, template) : []; if (elements.length) - this._actionSelectorModel = { elements }; + highlight = { elements }; else - this._actionSelectorModel = null; + highlight = 'clear'; } - if (!state.actionSelector && !state.ariaTemplate) - this._actionSelectorModel = null; - - if (this.state.mode === 'none' || this.state.mode === 'standby') - this.updateHighlight(this._actionSelectorModel, false); + if (highlight === 'clear') + this.clearHighlight(); + else if (highlight !== 'noop') + this.updateHighlight(highlight, false); } clearHighlight() { @@ -1266,6 +1268,8 @@ export class Recorder { private _onScroll(event: Event) { if (!event.isTrusted) return; + this._lastHighlightedSelector = undefined; + this._lastHighlightedAriaTemplateJSON = 'undefined'; this.highlight.hideActionPoint(); this._currentTool.onScroll?.(event); } diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 50564fa31f..25035b6998 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -383,7 +383,11 @@ export function getAriaLabelledByElements(element: Element): Element[] | null { const ref = element.getAttribute('aria-labelledby'); if (ref === null) return null; - return getIdRefs(element, ref); + const refs = getIdRefs(element, ref); + // step 2b: + // "if the current node has an aria-labelledby attribute that contains at least one valid IDREF" + // Therefore, if none of the refs match an element, we consider aria-labelledby to be missing. + return refs.length ? refs : null; } function allowsNameFromContent(role: string, targetDescendant: boolean) { diff --git a/packages/playwright-core/src/server/launchApp.ts b/packages/playwright-core/src/server/launchApp.ts index 3d119f9d46..d8c81a3169 100644 --- a/packages/playwright-core/src/server/launchApp.ts +++ b/packages/playwright-core/src/server/launchApp.ts @@ -43,12 +43,12 @@ export async function launchApp(browserType: BrowserType, options: { } const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', { - channel: !options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined, - noDefaultViewport: true, ignoreDefaultArgs: ['--enable-automation'], - colorScheme: 'no-override', - acceptDownloads: isUnderTest() ? 'accept' : 'internal-browser-default', ...options?.persistentContextOptions, + channel: options.persistentContextOptions?.channel ?? (!options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined), + noDefaultViewport: options.persistentContextOptions?.noDefaultViewport ?? true, + acceptDownloads: options?.persistentContextOptions?.acceptDownloads ?? (isUnderTest() ? 'accept' : 'internal-browser-default'), + colorScheme: options?.persistentContextOptions?.colorScheme ?? 'no-override', args, }); const [page] = context.pages(); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 13dd3829b1..16f9d791e1 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -132,6 +132,10 @@ export class Recorder implements InstrumentationListener, IRecorder { this._contextRecorder.clearScript(); return; } + if (data.event === 'runTask') { + this._contextRecorder.runTask(data.params.task); + return; + } }); await Promise.all([ diff --git a/packages/playwright-core/src/server/recorder/chat.ts b/packages/playwright-core/src/server/recorder/chat.ts new file mode 100644 index 0000000000..5b3917c735 --- /dev/null +++ b/packages/playwright-core/src/server/recorder/chat.ts @@ -0,0 +1,184 @@ +/** + * 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 { WebSocketTransport } from '../transport'; +import type { ConnectionTransport, ProtocolResponse } from '../transport'; + +export type ChatMessage = { + content: string; + user: 'user' | 'assistant'; +}; + +export class Chat { + private _history: ChatMessage[] = []; + private _connectionPromise: Promise | undefined; + private _chatSinks = new Map void>(); + private _wsEndpoint: string; + + constructor(wsEndpoint: string) { + this._wsEndpoint = wsEndpoint; + } + + clearHistory() { + this._history = []; + } + + async post(prompt: string): Promise { + await this._append('user', prompt); + let text = await asString(await this._post()); + if (text.startsWith('```json') && text.endsWith('```')) + text = text.substring('```json'.length, text.length - '```'.length); + for (let i = 0; i < 3; ++i) { + try { + return JSON.parse(text); + } catch (e) { + await this._append('user', String(e)); + } + } + throw new Error('Failed to parse response: ' + text); + } + + private async _append(user: ChatMessage['user'], content: string) { + this._history.push({ user, content }); + } + + private async _connection(): Promise { + if (!this._connectionPromise) { + this._connectionPromise = WebSocketTransport.connect(undefined, this._wsEndpoint).then(transport => { + return new Connection(transport, (method, params) => this._dispatchEvent(method, params), () => {}); + }); + } + return this._connectionPromise; + } + + private _dispatchEvent(method: string, params: any) { + if (method === 'chatChunk') { + const { chatId, chunk } = params; + const chunkSink = this._chatSinks.get(chatId)!; + chunkSink(chunk); + if (!chunk) + this._chatSinks.delete(chatId); + } + } + + private async _post(): Promise> { + const connection = await this._connection(); + const result = await connection.send('chat', { history: this._history }); + const { chatId } = result; + const { iterable, addChunk } = iterablePump(); + this._chatSinks.set(chatId, addChunk); + return iterable; + } +} + +export async function asString(stream: AsyncIterable): Promise { + let result = ''; + for await (const chunk of stream) + result += chunk; + return result; +} + +type ChunkIterator = { + iterable: AsyncIterable; + addChunk: (chunk: string) => void; +}; + +function iterablePump(): ChunkIterator { + let controller: ReadableStreamDefaultController; + const stream = new ReadableStream({ start: c => controller = c }); + + const iterable = (async function* () { + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + yield value!; + } + })(); + + return { + iterable, + addChunk: chunk => { + if (chunk) + controller.enqueue(chunk); + else + controller.close(); + } + }; +} + +class Connection { + private readonly _transport: ConnectionTransport; + private _lastId = 0; + private _closed = false; + private _pending = new Map void; reject: (error: any) => void; }>(); + private _onEvent: (method: string, params: any) => void; + private _onClose: () => void; + + constructor(transport: ConnectionTransport, onEvent: (method: string, params: any) => void, onClose: () => void) { + this._transport = transport; + this._onEvent = onEvent; + this._onClose = onClose; + this._transport.onmessage = this._dispatchMessage.bind(this); + this._transport.onclose = this._close.bind(this); + } + + send(method: string, params: any): Promise { + const id = this._lastId++; + const message = { id, method, params }; + this._transport.send(message); + return new Promise((resolve, reject) => { + this._pending.set(id, { resolve, reject }); + }); + } + + private _dispatchMessage(message: ProtocolResponse) { + if (message.id === undefined) { + this._onEvent(message.method!, message.params); + return; + } + + const callback = this._pending.get(message.id); + this._pending.delete(message.id); + if (!callback) + return; + + if (message.error) { + callback.reject(new Error(message.error.message)); + return; + } + callback.resolve(message.result); + } + + _close() { + this._closed = true; + this._transport.onmessage = undefined; + this._transport.onclose = undefined; + for (const { reject } of this._pending.values()) + reject(new Error('Connection closed')); + this._onClose(); + } + + isClosed() { + return this._closed; + } + + close() { + if (!this._closed) + this._transport.close(); + } +} diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 933a036233..d7a3c908e8 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -208,6 +208,10 @@ export class ContextRecorder extends EventEmitter { } } + runTask(task: string): void { + // TODO: implement + } + private _describeMainFrame(page: Page): actions.FrameDescription { return { pageAlias: this._pageAliases.get(page)!, diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 6b6b73e241..f8971531cf 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -88,7 +88,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { }); const mainFrame = this._page.mainFrame(); - await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); + await mainFrame.goto(serverSideCallMetadata(), process.env.PW_HMR ? 'http://localhost:44225' : 'https://playwright/index.html'); } static factory(context: BrowserContext): IRecorderAppFactory { diff --git a/packages/playwright-core/src/server/registry/browserFetcher.ts b/packages/playwright-core/src/server/registry/browserFetcher.ts index 6f8da6e825..3a6e36b42d 100644 --- a/packages/playwright-core/src/server/registry/browserFetcher.ts +++ b/packages/playwright-core/src/server/registry/browserFetcher.ts @@ -36,7 +36,7 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec const zipPath = path.join(os.tmpdir(), downloadFileName); try { - const retryCount = 3; + const retryCount = 5; for (let attempt = 1; attempt <= retryCount; ++attempt) { debugLogger.log('install', `downloading ${title} - attempt #${attempt}`); const url = downloadURLs[(attempt - 1) % downloadURLs.length]; diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 709829621c..cd2f2f1d32 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -37,16 +37,23 @@ const PACKAGE_PATH = path.join(__dirname, '..', '..', '..'); const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin'); const PLAYWRIGHT_CDN_MIRRORS = [ + 'https://playwright.azureedge.net/dbazure/download/playwright', // ESRP CDN + 'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // Directly hit ESRP CDN + + // Old endpoints which hit the Storage Bucket directly: 'https://playwright.azureedge.net', - 'https://playwright-akamai.azureedge.net', - 'https://playwright-verizon.azureedge.net', + 'https://playwright-akamai.azureedge.net', // Actually Edgio which will be retired Q4 2025. + 'https://playwright-verizon.azureedge.net', // Actually Edgio which will be retired Q4 2025. ]; if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { for (let i = 0; i < PLAYWRIGHT_CDN_MIRRORS.length; i++) { const cdn = PLAYWRIGHT_CDN_MIRRORS[i]; - if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK) - PLAYWRIGHT_CDN_MIRRORS[i] = cdn + '.does-not-resolve.playwright.dev'; + if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { + const parsedCDN = new URL(cdn); + parsedCDN.hostname = parsedCDN.hostname + '.does-not-resolve.playwright.dev'; + PLAYWRIGHT_CDN_MIRRORS[i] = parsedCDN.toString(); + } } } diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 3650ba88a7..127f84f369 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -125,11 +125,11 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ for (const reporter of options.reporter || []) params.append('reporter', reporter); - params.set('server', server.urlPrefix('precise')); - let baseUrl = '.'; - if (process.env.PW_HMR) + if (process.env.PW_HMR) { baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js + params.set('server', server.urlPrefix('precise') + '/trace/'); + } const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`; server.routePath('/', (_, response) => { diff --git a/packages/playwright-core/src/utils/debugLogger.ts b/packages/playwright-core/src/utils/debugLogger.ts index a5196da896..d50180a2ed 100644 --- a/packages/playwright-core/src/utils/debugLogger.ts +++ b/packages/playwright-core/src/utils/debugLogger.ts @@ -29,7 +29,8 @@ const debugLoggerColorMap = { 'channel': 33, // blue 'server': 45, // cyan 'server:channel': 34, // green - 'server:metadata': 33, // blue + 'server:metadata': 33, // blue, + 'recorder': 45, // cyan }; export type LogName = keyof typeof debugLoggerColorMap; diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 35aa6f2eb9..3a61fbca59 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -112,7 +112,7 @@ export module Protocol { - from 'checked' to 'selected': states which apply to widgets - from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling. */ - export type AXPropertyName = "busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; + export type AXPropertyName = "actions"|"busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; /** * A node in the accessibility tree. */ @@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations export interface AffectedFrame { frameId: Page.FrameId; } - export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; + export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; /** @@ -2183,12 +2183,17 @@ The array enumerates @scope at-rules starting with the innermost one, going outw * The array keeps the types of ancestor CSSRules from the innermost going outwards. */ ruleTypes?: CSSRuleType[]; + /** + * @starting-style CSS at-rule array. +The array enumerates @starting-style at-rules starting with the innermost one, going outwards. + */ + startingStyles?: CSSStartingStyle[]; } /** * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. This list only contains rule types that are collected during the ancestor rule collection. */ - export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"; + export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule"; /** * CSS coverage information. */ @@ -2424,6 +2429,10 @@ available). * Optional logical axes queried for the container. */ logicalAxes?: DOM.LogicalAxes; + /** + * true if the query contains scroll-state() queries. + */ + queriesScrollState?: boolean; } /** * CSS Supports at-rule descriptor. @@ -2475,6 +2484,20 @@ available). text: string; /** * The associated rule header range in the enclosing stylesheet (if +available). + */ + range?: SourceRange; + /** + * Identifier of the stylesheet containing this object (if exists). + */ + styleSheetId?: StyleSheetId; + } + /** + * CSS Starting Style at-rule descriptor. + */ + export interface CSSStartingStyle { + /** + * The associated rule header range in the enclosing stylesheet (if available). */ range?: SourceRange; @@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea */ styleSheetId: StyleSheetId; } + export type computedStyleUpdatedPayload = { + /** + * The node id that has updated computed styles. + */ + nodeId: DOM.NodeId; + } /** * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the @@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet. export type getLocationForSelectorReturnValue = { ranges: SourceRange[]; } + /** + * Starts tracking the given node for the computed style updates +and whenever the computed style is updated for node, it queues +a `computedStyleUpdated` event with throttling. +There can only be 1 node tracked for computed style updates +so passing a new node id removes tracking from the previous node. +Pass `undefined` to disable tracking. + */ + export type trackComputedStyleUpdatesForNodeParameters = { + nodeId?: DOM.NodeId; + } + export type trackComputedStyleUpdatesForNodeReturnValue = { + } /** * Starts tracking the given computed styles for updates. The specified array of properties replaces the one previously specified. Pass empty array to disable tracking. @@ -3561,7 +3603,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker"; + export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; /** * Shadow root type. */ @@ -4876,15 +4918,17 @@ $x functions). } /** * Returns the query container of the given node based on container query -conditions: containerName, physical, and logical axes. If no axes are -provided, the style container is returned, which is the direct parent or the -closest element with a matching container-name. +conditions: containerName, physical and logical axes, and whether it queries +scroll-state. If no axes are provided and queriesScrollState is false, the +style container is returned, which is the direct parent or the closest +element with a matching container-name. */ export type getContainerForNodeParameters = { nodeId: NodeId; containerName?: string; physicalAxes?: PhysicalAxes; logicalAxes?: LogicalAxes; + queriesScrollState?: boolean; } export type getContainerForNodeReturnValue = { /** @@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc. */ export type LoaderId = string; /** - * Unique request identifier. + * Unique network request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. */ export type RequestId = string; /** @@ -8830,6 +8876,7 @@ If the opcode isn't 1, then payloadData is a base64 encoded string representing type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; /** * Initiator JavaScript stack trace, set for Script only. +Requires the Debugger domain to be enabled. */ stack?: Runtime.StackTrace; /** @@ -8944,7 +8991,7 @@ This is a temporary ability and it will be removed in the future. /** * Types of reasons why a cookie may not be sent with a request. */ - export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"; + export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch"; /** * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. */ @@ -11498,7 +11545,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"|"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"; + 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"|"fenced-unpartitioned-storage-read"|"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. */ @@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi defaultPrompt?: string; } /** - * Fired for top level page lifecycle events such as navigation, load, paint, etc. + * Fired for lifecycle events (navigation, load, paint, etc) in the current +target (including local frames). */ export type lifecycleEventPayload = { /** @@ -14339,6 +14387,7 @@ int destinationLimitPriority: SignedInt64AsBase10; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; scopesData?: AttributionScopesData; + maxEventLevelReports: number; } export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; @@ -14386,7 +14435,7 @@ int scopes: string[]; } export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; - export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; + export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"insufficientNamedBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; /** * A single Related Website Set object. */ @@ -15920,6 +15969,8 @@ are ignored. export module Fetch { /** * Unique request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. */ export type RequestId = string; /** @@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/ /** * Enum of AudioContextState from the spec */ - export type ContextState = "suspended"|"running"|"closed"; + export type ContextState = "suspended"|"running"|"closed"|"interrupted"; /** * Enum of AudioNode types */ @@ -20213,6 +20264,7 @@ Error was thrown. "CSS.styleSheetAdded": CSS.styleSheetAddedPayload; "CSS.styleSheetChanged": CSS.styleSheetChangedPayload; "CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload; + "CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload; "Cast.sinksUpdated": Cast.sinksUpdatedPayload; "Cast.issueUpdated": Cast.issueUpdatedPayload; "DOM.attributeModified": DOM.attributeModifiedPayload; @@ -20464,6 +20516,7 @@ Error was thrown. "CSS.getStyleSheetText": CSS.getStyleSheetTextParameters; "CSS.getLayersForNode": CSS.getLayersForNodeParameters; "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters; + "CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters; @@ -21075,6 +21128,7 @@ Error was thrown. "CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue; "CSS.getLayersForNode": CSS.getLayersForNodeReturnValue; "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue; + "CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index a08f3790d9..27f1e9320b 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -618,9 +618,10 @@ class ArtifactsRecorder { if (captureScreenshots) await this._screenshotOnTestFailure(); - const leftoverContexts: BrowserContext[] = []; + let leftoverContexts: BrowserContext[] = []; for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) leftoverContexts.push(...(browserType as any)._contexts); + leftoverContexts = leftoverContexts.filter(context => !this._reusedContexts.has(context)); const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set); // Collect traces/screenshots for remaining contexts. diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index f7c68b4544..8a8089e91e 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -181,7 +181,7 @@ export function toHaveAccessibleDescription( options?: { timeout?: number, ignoreCase?: boolean }, ) { return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout }); }, expected, options); } @@ -189,13 +189,20 @@ export function toHaveAccessibleDescription( export function toHaveAccessibleName( this: ExpectMatcherState, locator: LocatorEx, - expected: string | RegExp, - options?: { timeout?: number, ignoreCase?: boolean }, + expected: string | RegExp | (string | RegExp)[], + options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: boolean } = {} ) { - return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); - return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); - }, expected, options); + if (Array.isArray(expected)) { + return toEqual.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues(expected, { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); + return await locator._expect('to.have.accessible.name.array', { expectedText, isNot, timeout }); + }, expected, options); + } else { + return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); + return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); + }, expected, options); + } } export function toHaveAttribute( diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index f3e7d47a6e..0bb600f7b0 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -126,7 +126,6 @@ function unshift(snapshot: string): string { 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/playwright/src/runner/rebase.ts b/packages/playwright/src/runner/rebase.ts index 24f37ca7f7..bc59e8374a 100644 --- a/packages/playwright/src/runner/rebase.ts +++ b/packages/playwright/src/runner/rebase.ts @@ -83,6 +83,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0]; const newText = replacement.code.replace(/\{indent\}/g, indent); ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText }); + // We can have multiple, hopefully equal, replacements for the same location, + // for example when a single test runs multiple times because of projects or retries. + // Do not apply multiple replacements for the same assertion. + break; } } }); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index b3d66a7f6d..715762cffe 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -7899,7 +7899,7 @@ interface LocatorAssertions { * @param name Expected accessible name. * @param options */ - toHaveAccessibleName(name: string|RegExp, options?: { + toHaveAccessibleName(name: string|RegExp|ReadonlyArray, options?: { /** * Whether to perform case-insensitive match. * [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case) diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 50fc5174ec..a34131a2c8 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -27,7 +27,7 @@ import { CallLogView } from './callLog'; import './recorder.css'; import { asLocator } from '@isomorphic/locatorGenerators'; import { toggleTheme } from '@web/theme'; -import { copy } from '@web/uiUtils'; +import { copy, useSetting } from '@web/uiUtils'; import yaml from 'yaml'; import { parseAriaKey } from '@isomorphic/ariaSnapshot'; import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot'; @@ -47,7 +47,7 @@ export const Recorder: React.FC = ({ }) => { const [selectedFileId, setSelectedFileId] = React.useState(); const [runningFileId, setRunningFileId] = React.useState(); - const [selectedTab, setSelectedTab] = React.useState('log'); + const [selectedTab, setSelectedTab] = useSetting('recorderPropertiesTab', 'log'); const [ariaSnapshot, setAriaSnapshot] = React.useState(); const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState(); @@ -67,6 +67,7 @@ export const Recorder: React.FC = ({ const language = source.language; setLocator(asLocator(language, elementInfo.selector)); setAriaSnapshot(elementInfo.ariaSnapshot); + setAriaSnapshotErrors([]); if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria') setSelectedTab('locator'); @@ -122,9 +123,6 @@ export const Recorder: React.FC = ({ if (!errors.length) window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } }); }, [mode]); - const isRecording = mode === 'recording' || mode === 'recording-inspecting'; - const locatorPlaceholder = isRecording ? '// Unavailable while recording' : (locator ? undefined : '// Pick element or type locator'); - const ariaPlaceholder = isRecording ? '# Unavailable while recording' : (ariaSnapshot ? undefined : '# Pick element or type snapshot'); return
@@ -191,7 +189,7 @@ export const Recorder: React.FC = ({ { id: 'locator', title: 'Locator', - render: () => + render: () => }, { id: 'log', @@ -200,8 +198,8 @@ export const Recorder: React.FC = ({ }, { id: 'aria', - title: 'Aria snapshot', - render: () => + title: 'Aria', + render: () => }, ]} selectedTab={selectedTab} diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/embedded.html deleted file mode 100644 index 7d0fd2f175..0000000000 --- a/packages/trace-viewer/embedded.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - Playwright Trace Viewer for VS Code - - -
- - - diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx deleted file mode 100644 index 4f1503dcf2..0000000000 --- a/packages/trace-viewer/src/embedded.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '@web/common.css'; -import { applyTheme } from '@web/theme'; -import '@web/third_party/vscode/codicon.css'; -import * as ReactDOM from 'react-dom/client'; -import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; - -(async () => { - applyTheme(); - - // workaround to send keystrokes back to vscode webview to keep triggering key bindings there - const handleKeyEvent = (e: KeyboardEvent) => { - if (!e.isTrusted) - return; - window.parent?.postMessage({ - type: e.type, - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat, - }, '*'); - }; - window.addEventListener('keydown', handleKeyEvent); - window.addEventListener('keyup', handleKeyEvent); - - if (window.location.protocol !== 'file:') { - if (!navigator.serviceWorker) - throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); - if (!navigator.serviceWorker.controller) { - await new Promise(f => { - navigator.serviceWorker.oncontrollerchange = () => f(); - }); - } - - // Keep SW running. - setInterval(function() { fetch('ping'); }, 10000); - } - - ReactDOM.createRoot(document.querySelector('#root')!).render(); -})(); diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index d977b0d7b7..4d01ef2a61 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -43,12 +43,9 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: const clientId = client?.id ?? ''; let data = clientIdToTraceUrls.get(clientId); if (!data) { - let traceViewerServerBaseUrl = self.registration.scope; - if (client?.url) { - const clientUrl = new URL(client.url); - if (clientUrl.searchParams.has('server')) - traceViewerServerBaseUrl = clientUrl.searchParams.get('server')!; - } + let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope); + if (traceViewerServerBaseUrl.searchParams.has('server')) + traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl); data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; clientIdToTraceUrls.set(clientId, data); diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index ec367575e3..95efffd502 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -146,7 +146,7 @@ function formatUrl(trace: string, server: TraceViewerServer) { } export class TraceViewerServer { - constructor(private readonly baseUrl: string) {} + constructor(private readonly baseUrl: URL) {} getFileURL(path: string): URL { const url = new URL('trace/file', this.baseUrl); diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css deleted file mode 100644 index 2274355322..0000000000 --- a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -.empty-state { - display: flex; - align-items: center; - justify-content: center; - flex: auto; - flex-direction: column; - background-color: var(--vscode-editor-background); - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 100; - line-height: 24px; -} - -body .empty-state { - background: rgba(255, 255, 255, 0.8); -} - -body.dark-mode .empty-state { - background: rgba(0, 0, 0, 0.8); -} - -.empty-state .title { - font-size: 24px; - font-weight: bold; - margin-bottom: 30px; -} - -.progress { - flex: none; - width: 100%; - height: 3px; - z-index: 10; -} - -.inner-progress { - background-color: var(--vscode-progressBar-background); - height: 100%; -} - -.workbench-loader { - contain: size; -} - -/* Limit to a reasonable minimum viewport */ -html, body { - min-width: 550px; - min-height: 450px; - overflow: auto; -} diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx deleted file mode 100644 index c8b8aa216c..0000000000 --- a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import * as React from 'react'; -import type { ContextEntry } from '../types/entries'; -import { MultiTraceModel } from './modelUtil'; -import './embeddedWorkbenchLoader.css'; -import { Workbench } from './workbench'; -import { currentTheme, toggleTheme } from '@web/theme'; -import type { SourceLocation } from './modelUtil'; - -function openPage(url: string, target?: string) { - if (url) - window.parent!.postMessage({ method: 'openExternal', params: { url, target } }, '*'); -} - -function openSourceLocation({ file, line, column }: SourceLocation) { - window.parent!.postMessage({ method: 'openSourceLocation', params: { file, line, column } }, '*'); -} - -export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { - const [traceURLs, setTraceURLs] = React.useState([]); - const [model, setModel] = React.useState(emptyModel); - const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); - const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); - - React.useEffect(() => { - window.addEventListener('message', async ({ data: { method, params } }) => { - if (method === 'loadTraceRequested') { - setTraceURLs(params.traceUrl ? [params.traceUrl] : []); - setProcessingErrorMessage(null); - } else if (method === 'applyTheme') { - if (currentTheme() !== params.theme) - toggleTheme(); - } - }); - // notify vscode that it is now listening to its messages - window.parent!.postMessage({ type: 'loaded' }, '*'); - }, []); - - React.useEffect(() => { - (async () => { - if (traceURLs.length) { - const swListener = (event: any) => { - if (event.data.method === 'progress') - setProgress(event.data.params); - }; - navigator.serviceWorker.addEventListener('message', swListener); - setProgress({ done: 0, total: 1 }); - const contextEntries: ContextEntry[] = []; - for (let i = 0; i < traceURLs.length; i++) { - const url = traceURLs[i]; - const params = new URLSearchParams(); - params.set('trace', url); - params.set('limit', String(traceURLs.length)); - const response = await fetch(`contexts?${params.toString()}`); - if (!response.ok) { - setProcessingErrorMessage((await response.json()).error); - return; - } - contextEntries.push(...(await response.json())); - } - navigator.serviceWorker.removeEventListener('message', swListener); - const model = new MultiTraceModel(contextEntries); - setProgress({ done: 0, total: 0 }); - setModel(model); - } else { - setModel(emptyModel); - } - })(); - }, [traceURLs]); - - React.useEffect(() => { - if (processingErrorMessage) - window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*'); - }, [processingErrorMessage]); - - return
-
-
-
- - {!traceURLs.length &&
-
Select test to see the trace
-
} -
; -}; - -export const emptyModel = new MultiTraceModel([]); diff --git a/packages/trace-viewer/src/ui/settingsView.tsx b/packages/trace-viewer/src/ui/settingsView.tsx index 883e4abca5..0a4340b2b6 100644 --- a/packages/trace-viewer/src/ui/settingsView.tsx +++ b/packages/trace-viewer/src/ui/settingsView.tsx @@ -30,7 +30,7 @@ export const SettingsView: React.FunctionComponent<{ {settings.map(({ value, set, title }) => { return
; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 2e8bf483ba..8383890a2e 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -40,8 +40,7 @@ export const SnapshotTabsView: React.FunctionComponent<{ setIsInspecting: (isInspecting: boolean) => void, highlightedLocator: string, setHighlightedLocator: (locator: string) => void, - openPage?: (url: string, target?: string) => Window | any, -}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { +}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => { const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const snapshots = React.useMemo(() => { @@ -66,9 +65,7 @@ export const SnapshotTabsView: React.FunctionComponent<{ })}
{ - if (!openPage) - openPage = window.open; - const win = openPage(snapshotUrls?.popoutUrl || '', '_blank'); + const win = window.open(snapshotUrls?.popoutUrl || '', '_blank'); win?.addEventListener('DOMContentLoaded', () => { const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); new ConsoleAPI(injectedScript); diff --git a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx index 314fce3a96..8ed0f47982 100644 --- a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx +++ b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx @@ -62,7 +62,7 @@ export const FiltersView: React.FC<{ {[...statusFilters.entries()].map(([status, value]) => { return