diff --git a/README.md b/README.md index ca5c10d519..013ac177cf 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-132.0.6834.57-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-134.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-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-134.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 132.0.6834.57 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 133.0.6943.16 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 134.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index cb5f56027c..63b487208f 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1313,7 +1313,7 @@ await Expect(locator).ToHaveAccessibleNameAsync("Save to disk"); ### param: LocatorAssertions.toHaveAccessibleName.name * since: v1.44 -- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>> +- `name` <[string]|[RegExp]> Expected accessible name. @@ -1413,49 +1413,48 @@ Attribute name. * langs: - alias-java: hasClass -Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match -or using a relaxed regular expression. +Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: **Usage** ```html -
+
``` ```js const locator = page.locator('#component'); -await expect(locator).toHaveClass(/selected/); -await expect(locator).toHaveClass('selected row'); +await expect(locator).toHaveClass('middle selected row'); +await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); ``` ```java -assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); -assertThat(page.locator("#component")).hasClass("selected row"); +assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)")); +assertThat(page.locator("#component")).hasClass("middle selected row"); ``` ```python async from playwright.async_api import expect locator = page.locator("#component") -await expect(locator).to_have_class(re.compile(r"selected")) -await expect(locator).to_have_class("selected row") +await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)")) +await expect(locator).to_have_class("middle selected row") ``` ```python sync from playwright.sync_api import expect locator = page.locator("#component") -expect(locator).to_have_class(re.compile(r"selected")) -expect(locator).to_have_class("selected row") +expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)")) +expect(locator).to_have_class("middle selected row") ``` ```csharp var locator = Page.Locator("#component"); -await Expect(locator).ToHaveClassAsync(new Regex("selected")); -await Expect(locator).ToHaveClassAsync("selected row"); +await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); +await Expect(locator).ToHaveClassAsync("middle selected row"); ``` -Note that if array is passed as an expected value, entire lists of elements can be asserted: +When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array: ```js const locator = page.locator('list > .component'); @@ -2242,8 +2241,7 @@ assertThat(page.locator("body")).matchesAriaSnapshot(""" ## async method: LocatorAssertions.toMatchAriaSnapshot#2 * since: v1.50 -* langs: - - alias-java: matchesAriaSnapshot +* langs: js Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md). @@ -2264,7 +2262,7 @@ expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml' ``` ```csharp -await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" }); +await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" }); ``` ```java diff --git a/packages/playwright-core/bin/reinstall_msedge_beta_linux.sh b/packages/playwright-core/bin/reinstall_msedge_beta_linux.sh index ececd05ace..a1531a95d0 100755 --- a/packages/playwright-core/bin/reinstall_msedge_beta_linux.sh +++ b/packages/playwright-core/bin/reinstall_msedge_beta_linux.sh @@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then apt-get install -y curl fi +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + # 3. Add the GPG key, the apt repo, update the apt cache, and install the package curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ diff --git a/packages/playwright-core/bin/reinstall_msedge_dev_linux.sh b/packages/playwright-core/bin/reinstall_msedge_dev_linux.sh index 6ab84c3100..7fde34e5b8 100755 --- a/packages/playwright-core/bin/reinstall_msedge_dev_linux.sh +++ b/packages/playwright-core/bin/reinstall_msedge_dev_linux.sh @@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then apt-get install -y curl fi +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + # 3. Add the GPG key, the apt repo, update the apt cache, and install the package curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ diff --git a/packages/playwright-core/bin/reinstall_msedge_stable_linux.sh b/packages/playwright-core/bin/reinstall_msedge_stable_linux.sh index e66f85bbba..4acb1dbf1b 100755 --- a/packages/playwright-core/bin/reinstall_msedge_stable_linux.sh +++ b/packages/playwright-core/bin/reinstall_msedge_stable_linux.sh @@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then apt-get install -y curl fi +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + # 3. Add the GPG key, the apt repo, update the apt cache, and install the package curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 0803da3ff1..0895f005b1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1153", + "revision": "1155", "installByDefault": true, - "browserVersion": "132.0.6834.57" + "browserVersion": "133.0.6943.16" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 5cd941d5d4..a69414f2b2 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -595,7 +595,6 @@ async function codegen(options: Options & { target: string, output?: string, tes device: options.device, saveStorage: options.saveStorage, mode: 'recording', - codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, handleSIGINT: false, diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9b14551fb8..50e8b4f02a 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderParams = tObject({ language: tOptional(tString), mode: tOptional(tEnum(['inspecting', 'recording'])), - codegenMode: tOptional(tEnum(['actions', 'trace-events'])), pauseOnNextStatement: tOptional(tBoolean), testIdAttributeName: tOptional(tString), launchOptions: tOptional(tAny), diff --git a/packages/playwright-core/src/server/bidi/bidiInput.ts b/packages/playwright-core/src/server/bidi/bidiInput.ts index e149bb4a3f..cf6770a479 100644 --- a/packages/playwright-core/src/server/bidi/bidiInput.ts +++ b/packages/playwright-core/src/server/bidi/bidiInput.ts @@ -19,6 +19,7 @@ import type * as types from '../types'; import type { BidiSession } from './bidiConnection'; import * as bidi from './third_party/bidiProtocol'; import { getBidiKeyValue } from './third_party/bidiKeyboard'; +import { resolveSmartModifierString } from '../input'; export class RawKeyboardImpl implements input.RawKeyboard { private _session: BidiSession; @@ -32,12 +33,14 @@ export class RawKeyboardImpl implements input.RawKeyboard { } async keydown(modifiers: Set, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise { + keyName = resolveSmartModifierString(keyName); const actions: bidi.Input.KeySourceAction[] = []; actions.push({ type: 'keyDown', value: getBidiKeyValue(keyName) }); await this._performActions(actions); } async keyup(modifiers: Set, keyName: string, description: input.KeyDescription): Promise { + keyName = resolveSmartModifierString(keyName); const actions: bidi.Input.KeySourceAction[] = []; actions.push({ type: 'keyUp', value: getBidiKeyValue(keyName) }); await this._performActions(actions); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 8a835d3726..ce10daf013 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') - await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true }); + await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true }); // When paused, show inspector. if (this._debugger.isPaused()) diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 3a61fbca59..fa1d6121f9 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations /** * The unique request id. */ - requestId: Network.RequestId; - url?: string; + requestId?: Network.RequestId; + url: string; } /** * Information about the frame affected by an inspector issue. @@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations 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"; + /** + * Represents the category of insight that a cookie issue falls under. + */ + export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics"; + /** + * Information about the suggested solution to a cookie issue. + */ + export interface CookieIssueInsight { + type: InsightType; + /** + * Link to table entry in third-party cookie migration readiness list. + */ + tableEntryUrl?: string; + } /** * This information is currently necessary, as the front-end has a difficult time finding a specific cookie. With this, we can convey specific error @@ -721,6 +735,10 @@ may be used by the front-end as additional context. siteForCookies?: string; cookieUrl?: string; request?: AffectedRequest; + /** + * The recommended solution to the issue. + */ + insight?: CookieIssueInsight; } export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning"; export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT"; @@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls). * Enum indicating the reason a response has been blocked. These reasons are refinements of the net error BLOCKED_BY_RESPONSE. */ - export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"; + export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch"; /** * Details for a request that has been blocked with the BLOCKED_BY_RESPONSE code. Currently only used for COEP/COOP, but may be extended to include @@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance. failureMessage: string; requestId?: Network.RequestId; } + export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild"; + /** + * This isue warns about errors in the select element content model. + */ + export interface SelectElementAccessibilityIssueDetails { + nodeId: DOM.BackendNodeId; + selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason; + hasDisallowedAttributes: boolean; + } export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed"; /** * This issue warns when a referenced stylesheet couldn't be loaded. @@ -1005,7 +1032,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1033,6 +1060,7 @@ add a new optional field to this type. propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; + selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1534,7 +1562,7 @@ events afterwards if enabled and recording. */ windowState?: WindowState; } - export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement"; + export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; export type PermissionSetting = "granted"|"denied"|"prompt"; /** * Definition of PermissionDescriptor defined in the Permissions API: @@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets. */ matches: RuleMatch[]; } + /** + * CSS style coming from animations with the name of the animation. + */ + export interface CSSAnimationStyle { + /** + * The name of the animation. + */ + name?: string; + /** + * The style coming from the animation. + */ + style: CSSStyle; + } /** * Inherited CSS rule collection from ancestor node. */ @@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets. */ matchedCSSRules: RuleMatch[]; } + /** + * Inherited CSS style collection for animated styles from ancestor node. + */ + export interface InheritedAnimatedStyleEntry { + /** + * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. + */ + transitionsStyle?: CSSStyle; + } /** * Inherited pseudo element matches from pseudos of an ancestor node. */ @@ -2897,6 +2951,21 @@ the browser. } export type forcePseudoStateReturnValue = { } + /** + * Ensures that the given node is in its starting-style state. + */ + export type forceStartingStyleParameters = { + /** + * The element id for which to force the starting-style state. + */ + nodeId: DOM.NodeId; + /** + * Boolean indicating if this is on or off. + */ + forced: boolean; + } + export type forceStartingStyleReturnValue = { + } export type getBackgroundColorsParameters = { /** * Id of the node to get background colors for. @@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load). */ computedStyle: CSSComputedStyleProperty[]; } + /** + * Resolve the specified values in the context of the provided element. +For example, a value of '1em' is evaluated according to the computed +'font-size' of the element and a value 'calc(1px + 2px)' will be +resolved to '3px'. + */ + export type resolveValuesParameters = { + /** + * Substitution functions (var()/env()/attr()) and cascade-dependent +keywords (revert/revert-layer) do not work. + */ + values: string[]; + /** + * Id of the node in whose context the expression is evaluated + */ + nodeId: DOM.NodeId; + /** + * Only longhands and custom property names are accepted. + */ + propertyName?: string; + /** + * Pseudo element type, only works for pseudo elements that generate +elements in the tree, such as ::before and ::after. + */ + pseudoType?: DOM.PseudoType; + /** + * Pseudo element custom ident. + */ + pseudoIdentifier?: string; + } + export type resolveValuesReturnValue = { + results: string[]; + } + export type getLonghandPropertiesParameters = { + shorthandName: string; + value: string; + } + export type getLonghandPropertiesReturnValue = { + longhandProperties: CSSProperty[]; + } /** * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM attributes) for a DOM node identified by `nodeId`. @@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`. */ attributesStyle?: CSSStyle; } + /** + * Returns the styles coming from animations & transitions +including the animation & transition styles coming from inheritance chain. + */ + export type getAnimatedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getAnimatedStylesForNodeReturnValue = { + /** + * Styles coming from animations. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * Style coming from transitions. + */ + transitionsStyle?: CSSStyle; + /** + * Inherited style entries for animationsStyle and transitionsStyle from +the inheritance chain of the element. + */ + inherited?: InheritedAnimatedStyleEntry[]; + } /** * Returns requested styles for a DOM node identified by `nodeId`. */ @@ -3603,7 +3734,7 @@ front-end. /** * Pseudo element type. */ - 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"; + export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-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. */ @@ -8616,7 +8747,7 @@ applicable or not known. /** * The reason why request was blocked. */ - export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"; + export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; /** * The reason why request was blocked. */ @@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie. blockedCookies: BlockedSetCookieWithReason[]; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; /** @@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve requestId: RequestId; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; } @@ -9978,7 +10115,7 @@ or after the response was received. of the operation already exists und thus, the operation was abort preemptively (e.g. a cache hit). */ - status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"; + status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit"; type: TrustTokenOperationType; requestId: RequestId; /** @@ -10672,6 +10809,26 @@ should be omitted for worker targets. export type loadNetworkResourceReturnValue = { resource: LoadNetworkResourcePageResult; } + /** + * Sets Controls for third-party cookie access +Page reload is required before the new cookie bahavior will be observed + */ + export type setCookieControlsParameters = { + /** + * Whether 3pc restriction is enabled. + */ + enableThirdPartyCookieRestriction: boolean; + /** + * Whether 3pc grace period exception should be enabled; false by default. + */ + disableThirdPartyCookieMetadata: boolean; + /** + * Whether 3pc heuristics exceptions should be enabled; false by default. + */ + disableThirdPartyCookieHeuristics: boolean; + } + export type setCookieControlsReturnValue = { + } } /** @@ -11545,7 +11702,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"|"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"; + 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"|"deferred-fetch-minimal"|"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. */ @@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored. * The initial URL the page will be navigated to. An empty string indicates about:blank. */ url: string; + /** + * Frame left origin in DIP (headless chrome only). + */ + left?: number; + /** + * Frame top origin in DIP (headless chrome only). + */ + top?: number; /** * Frame width in DIP (headless chrome only). */ @@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt. ruleSetIds: RuleSetId[]; nodeIds: DOM.BackendNodeId[]; } + /** + * Chrome manages different types of preloads together using a +concept of preloading pipeline. For example, if a site uses a +SpeculationRules for prerender, Chrome first starts a prefetch and +then upgrades it to prerender. + +CDP events for them are emitted separately but they share +`PreloadPipelineId`. + */ + export type PreloadPipelineId = string; /** * List of FinalStatus reasons for Prerender2. */ @@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers. */ export type prefetchStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; /** * The frame id of the frame initiating prefetch. */ @@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers. */ export type prerenderStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; status: PreloadingStatus; prerenderStatus?: PrerenderFinalStatus; /** @@ -17922,6 +18099,10 @@ variables as its properties. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -17996,6 +18177,10 @@ scripts upon enabling debugger. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -20507,9 +20692,13 @@ Error was thrown. "CSS.disable": CSS.disableParameters; "CSS.enable": CSS.enableParameters; "CSS.forcePseudoState": CSS.forcePseudoStateParameters; + "CSS.forceStartingStyle": CSS.forceStartingStyleParameters; "CSS.getBackgroundColors": CSS.getBackgroundColorsParameters; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters; + "CSS.resolveValues": CSS.resolveValuesParameters; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters; "CSS.getMediaQueries": CSS.getMediaQueriesParameters; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters; @@ -20751,6 +20940,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters; "Network.enableReportingApi": Network.enableReportingApiParameters; "Network.loadNetworkResource": Network.loadNetworkResourceParameters; + "Network.setCookieControls": Network.setCookieControlsParameters; "Overlay.disable": Overlay.disableParameters; "Overlay.enable": Overlay.enableParameters; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters; @@ -21119,9 +21309,13 @@ Error was thrown. "CSS.disable": CSS.disableReturnValue; "CSS.enable": CSS.enableReturnValue; "CSS.forcePseudoState": CSS.forcePseudoStateReturnValue; + "CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue; "CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue; + "CSS.resolveValues": CSS.resolveValuesReturnValue; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue; "CSS.getMediaQueries": CSS.getMediaQueriesReturnValue; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue; @@ -21363,6 +21557,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue; "Network.enableReportingApi": Network.enableReportingApiReturnValue; "Network.loadNetworkResource": Network.loadNetworkResourceReturnValue; + "Network.setCookieControls": Network.setCookieControlsReturnValue; "Overlay.disable": Overlay.disableReturnValue; "Overlay.enable": Overlay.enableReturnValue; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 3380be8ac7..4a08f9d687 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "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/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 3579f4b3bb..0dcfb23e0a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -39,7 +39,6 @@ import type { Dialog } from '../dialog'; import type { ConsoleMessage } from '../console'; import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; -import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; @@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher { - if (params.codegenMode === 'trace-events') { - await this._context.tracing.start({ - name: 'trace', - snapshots: true, - screenshots: true, - live: true, - }); - await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params); - } else { - await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params); - } + await Recorder.show(this._context, RecorderApp.factory(this._context), params); } async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 4ffb8ba2d8..17380db170 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1437,8 +1437,6 @@ 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 5a3f3d9a14..3df43f751e 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -1138,25 +1138,28 @@ export class Recorder { 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'; + this._lastHighlightedSelector = model?.elements.length ? state.actionSelector : undefined; } const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { - this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON; const elements = state.ariaTemplate ? this.injectedScript.getAllByAria(this.document, state.ariaTemplate) : []; - if (elements.length) + if (elements.length) { highlight = { elements }; - else - highlight = 'clear'; + this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON; + } else { + if (!this._lastHighlightedSelector) + highlight = 'clear'; + this._lastHighlightedAriaTemplateJSON = 'undefined'; + } } if (highlight === 'clear') this.clearHighlight(); else if (highlight !== 'noop') - this.updateHighlight(highlight, false); + this._updateHighlight(highlight, false); } clearHighlight() { @@ -1299,6 +1302,12 @@ export class Recorder { } updateHighlight(model: HighlightModel | null, userGesture: boolean) { + this._lastHighlightedSelector = undefined; + this._lastHighlightedAriaTemplateJSON = 'undefined'; + this._updateHighlight(model, userGesture); + } + + private _updateHighlight(model: HighlightModel | null, userGesture: boolean) { let tooltipText = model?.tooltipText; if (tooltipText === undefined && !model?.tooltipList && model?.selector) tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 115639223c..f763d5491b 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -54,33 +54,33 @@ export class Recorder implements InstrumentationListener, IRecorder { static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) { if (isUnderTest()) params.language = process.env.TEST_INSPECTOR_LANGUAGE; - return await Recorder.show('actions', context, recorderAppFactory, params); + return await Recorder.show(context, recorderAppFactory, params); } static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {}); } - static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise { + static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params); + recorderPromise = Recorder._create(context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } - private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { - const recorder = new Recorder(codegenMode, context, params); + private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { + const recorder = new Recorder(context, params); const recorderApp = await recorderAppFactory(recorder); await recorder._install(recorderApp); return recorder; } - constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { + constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { this._mode = params.mode || 'none'; this.handleSIGINT = params.handleSIGINT; - this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); + this._contextRecorder = new ContextRecorder(context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index 85ae7c9152..b130c181dc 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -10,6 +10,3 @@ ../../utils/** ../../utilsBundle.ts ../../zipBundle.ts - -[recorderInTraceViewer.ts] -../trace/viewer/traceViewer.ts diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 9b641e96f8..292271acd5 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter { private _throttledOutputFile: ThrottledFile | null = null; private _orderedLanguages: LanguageGenerator[] = []; private _listeners: RegisteredListener[] = []; - private _codegenMode: 'actions' | 'trace-events'; - constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { + constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { super(); - this._codegenMode = codegenMode; this._context = context; this._params = params; this._delegate = delegate; @@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter { setEnabled(enabled: boolean) { this._collection.setEnabled(enabled); - if (this._codegenMode === 'trace-events') { - if (enabled) - this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {}); - else - this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {}); - } } dispose() { diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts deleted file mode 100644 index fcfd0a36c5..0000000000 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ /dev/null @@ -1,126 +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 path from 'path'; -import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes'; -import { EventEmitter } from 'events'; -import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFrontend'; -import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer'; -import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer'; -import type { BrowserContext } from '../browserContext'; -import type { HttpServer, Transport } from '../../utils/httpServer'; -import type { Page } from '../page'; -import { ManualPromise } from '../../utils/manualPromise'; -import type * as actions from '@recorder/actions'; - -export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { - readonly wsEndpointForTest: string | undefined; - private _transport: RecorderTransport; - private _tracePage: Page; - private _traceServer: HttpServer; - - static factory(context: BrowserContext): IRecorderAppFactory { - return async (recorder: IRecorder) => { - const transport = new RecorderTransport(); - const trace = path.join(context._browser.options.tracesDir, 'trace'); - const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful }); - return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest); - }; - } - - constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) { - super(); - this._transport = transport; - this._transport.eventSink.resolve(this); - this._tracePage = tracePage; - this._traceServer = traceServer; - this.wsEndpointForTest = wsEndpointForTest; - this._tracePage.once('close', () => { - this.close(); - }); - } - - async close(): Promise { - await this._tracePage.context().close({ reason: 'Recorder window closed' }); - await this._traceServer.stop(); - } - - async setPaused(paused: boolean): Promise { - this._transport.deliverEvent('setPaused', { paused }); - } - - async setMode(mode: Mode): Promise { - this._transport.deliverEvent('setMode', { mode }); - } - - async setRunningFile(file: string | undefined): Promise { - this._transport.deliverEvent('setRunningFile', { file }); - } - - async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise { - this._transport.deliverEvent('elementPicked', { elementInfo, userGesture }); - } - - async updateCallLogs(callLogs: CallLog[]): Promise { - this._transport.deliverEvent('updateCallLogs', { callLogs }); - } - - async setSources(sources: Source[]): Promise { - this._transport.deliverEvent('setSources', { sources }); - if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) { - if ((process as any)._didSetSourcesForTest(sources[0].text)) - this.close(); - } - } - - async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise { - this._transport.deliverEvent('setActions', { actions, sources }); - } -} - -async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> { - const traceServer = await startTraceViewerServer(options); - await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' }); - const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options); - return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer }; -} - -class RecorderTransport implements Transport { - private _connected = new ManualPromise(); - readonly eventSink = new ManualPromise(); - - constructor() { - } - - onconnect() { - this._connected.resolve(); - } - - async dispatch(method: string, params: any): Promise { - const eventSink = await this.eventSink; - eventSink.emit('event', { event: method, params }); - } - - onclose() { - } - - deliverEvent(method: string, params: any) { - this._connected.then(() => this.sendEvent?.(method, params)); - } - - sendEvent?: (method: string, params: any) => void; - close?: () => void; -} diff --git a/packages/playwright-core/src/server/registry/dependencies.ts b/packages/playwright-core/src/server/registry/dependencies.ts index 60ef80d846..4aa568a6f9 100644 --- a/packages/playwright-core/src/server/registry/dependencies.ts +++ b/packages/playwright-core/src/server/registry/dependencies.ts @@ -95,7 +95,7 @@ export async function installDependenciesLinux(targets: Set, dr for (const target of targets) { const info = deps[platform]; if (!info) { - console.warn(`Cannot install dependencies for ${platform}!`); // eslint-disable-line no-console + console.warn(`Cannot install dependencies for ${platform} with Playwright ${getPlaywrightVersion()}!`); // eslint-disable-line no-console return; } libraries.push(...info[target]); diff --git a/packages/playwright-core/src/utils/hostPlatform.ts b/packages/playwright-core/src/utils/hostPlatform.ts index 7d81f39da3..0563e4ab29 100644 --- a/packages/playwright-core/src/utils/hostPlatform.ts +++ b/packages/playwright-core/src/utils/hostPlatform.ts @@ -74,14 +74,18 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte // KDE Neon is ubuntu-based and has the same versions. // TUXEDO OS is ubuntu-based and has the same versions. if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') { - const isOfficiallySupportedPlatform = distroInfo?.id === 'ubuntu'; - if (parseInt(distroInfo.version, 10) <= 19) + const isUbuntu = distroInfo?.id === 'ubuntu'; + const version = distroInfo?.version; + const major = parseInt(distroInfo.version, 10); + if (major < 20) return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - if (parseInt(distroInfo.version, 10) <= 21) - return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - if (parseInt(distroInfo.version, 10) <= 22) - return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + if (major < 22) + return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' }; + if (major < 24) + return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' }; + if (major < 26) + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' }; + return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; } // Linux Mint is ubuntu-based but does not have the same versions if (distroInfo?.id === 'linuxmint') { diff --git a/packages/playwright-core/src/utils/isomorphic/cssParser.ts b/packages/playwright-core/src/utils/isomorphic/cssParser.ts index 58d4df243b..9a791a1da4 100644 --- a/packages/playwright-core/src/utils/isomorphic/cssParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/cssParser.ts @@ -43,7 +43,7 @@ export function parseCSS(selector: string, customNames: Set): { selector if (!(tokens[tokens.length - 1] instanceof css.EOFToken)) tokens.push(new css.EOFToken()); } catch (e) { - const newMessage = e.message + ` while parsing selector "${selector}"`; + const newMessage = e.message + ` while parsing css selector "${selector}". Did you mean to CSS.escape it?`; const index = (e.stack || '').indexOf(e.message); if (index !== -1) e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length); @@ -68,13 +68,13 @@ export function parseCSS(selector: string, customNames: Set): { selector (token instanceof css.PercentageToken); }); if (unsupportedToken) - throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`); + throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); let pos = 0; const names = new Set(); function unexpected() { - return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`); + return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); } function skipWhitespace() { @@ -246,7 +246,7 @@ export function parseCSS(selector: string, customNames: Set): { selector if (!isEOF()) throw unexpected(); if (result.some(arg => typeof arg !== 'object' || !('simples' in arg))) - throw new InvalidSelectorError(`Error while parsing selector "${selector}"`); + throw new InvalidSelectorError(`Error while parsing css selector "${selector}". Did you mean to CSS.escape it?`); return { selector: result as CSSComplexSelector[], names: Array.from(names) }; } diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 2e40968ebc..eba52a30f2 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -156,8 +156,3 @@ export function compressCallLog(log: string[]): string[] { } return lines; } - -export type ExpectZone = { - title: string; - stepId: string; -}; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index 75612c8938..32664c3898 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -16,7 +16,7 @@ import { AsyncLocalStorage } from 'async_hooks'; -export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone'; +export type ZoneType = 'apiZone' | 'stepZone'; class ZoneManager { private readonly _asyncLocalStorage = new AsyncLocalStorage(); diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 3a61fbca59..fa1d6121f9 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations /** * The unique request id. */ - requestId: Network.RequestId; - url?: string; + requestId?: Network.RequestId; + url: string; } /** * Information about the frame affected by an inspector issue. @@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations 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"; + /** + * Represents the category of insight that a cookie issue falls under. + */ + export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics"; + /** + * Information about the suggested solution to a cookie issue. + */ + export interface CookieIssueInsight { + type: InsightType; + /** + * Link to table entry in third-party cookie migration readiness list. + */ + tableEntryUrl?: string; + } /** * This information is currently necessary, as the front-end has a difficult time finding a specific cookie. With this, we can convey specific error @@ -721,6 +735,10 @@ may be used by the front-end as additional context. siteForCookies?: string; cookieUrl?: string; request?: AffectedRequest; + /** + * The recommended solution to the issue. + */ + insight?: CookieIssueInsight; } export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning"; export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT"; @@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls). * Enum indicating the reason a response has been blocked. These reasons are refinements of the net error BLOCKED_BY_RESPONSE. */ - export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"; + export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch"; /** * Details for a request that has been blocked with the BLOCKED_BY_RESPONSE code. Currently only used for COEP/COOP, but may be extended to include @@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance. failureMessage: string; requestId?: Network.RequestId; } + export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild"; + /** + * This isue warns about errors in the select element content model. + */ + export interface SelectElementAccessibilityIssueDetails { + nodeId: DOM.BackendNodeId; + selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason; + hasDisallowedAttributes: boolean; + } export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed"; /** * This issue warns when a referenced stylesheet couldn't be loaded. @@ -1005,7 +1032,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1033,6 +1060,7 @@ add a new optional field to this type. propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; + selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1534,7 +1562,7 @@ events afterwards if enabled and recording. */ windowState?: WindowState; } - export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement"; + export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; export type PermissionSetting = "granted"|"denied"|"prompt"; /** * Definition of PermissionDescriptor defined in the Permissions API: @@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets. */ matches: RuleMatch[]; } + /** + * CSS style coming from animations with the name of the animation. + */ + export interface CSSAnimationStyle { + /** + * The name of the animation. + */ + name?: string; + /** + * The style coming from the animation. + */ + style: CSSStyle; + } /** * Inherited CSS rule collection from ancestor node. */ @@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets. */ matchedCSSRules: RuleMatch[]; } + /** + * Inherited CSS style collection for animated styles from ancestor node. + */ + export interface InheritedAnimatedStyleEntry { + /** + * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. + */ + transitionsStyle?: CSSStyle; + } /** * Inherited pseudo element matches from pseudos of an ancestor node. */ @@ -2897,6 +2951,21 @@ the browser. } export type forcePseudoStateReturnValue = { } + /** + * Ensures that the given node is in its starting-style state. + */ + export type forceStartingStyleParameters = { + /** + * The element id for which to force the starting-style state. + */ + nodeId: DOM.NodeId; + /** + * Boolean indicating if this is on or off. + */ + forced: boolean; + } + export type forceStartingStyleReturnValue = { + } export type getBackgroundColorsParameters = { /** * Id of the node to get background colors for. @@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load). */ computedStyle: CSSComputedStyleProperty[]; } + /** + * Resolve the specified values in the context of the provided element. +For example, a value of '1em' is evaluated according to the computed +'font-size' of the element and a value 'calc(1px + 2px)' will be +resolved to '3px'. + */ + export type resolveValuesParameters = { + /** + * Substitution functions (var()/env()/attr()) and cascade-dependent +keywords (revert/revert-layer) do not work. + */ + values: string[]; + /** + * Id of the node in whose context the expression is evaluated + */ + nodeId: DOM.NodeId; + /** + * Only longhands and custom property names are accepted. + */ + propertyName?: string; + /** + * Pseudo element type, only works for pseudo elements that generate +elements in the tree, such as ::before and ::after. + */ + pseudoType?: DOM.PseudoType; + /** + * Pseudo element custom ident. + */ + pseudoIdentifier?: string; + } + export type resolveValuesReturnValue = { + results: string[]; + } + export type getLonghandPropertiesParameters = { + shorthandName: string; + value: string; + } + export type getLonghandPropertiesReturnValue = { + longhandProperties: CSSProperty[]; + } /** * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM attributes) for a DOM node identified by `nodeId`. @@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`. */ attributesStyle?: CSSStyle; } + /** + * Returns the styles coming from animations & transitions +including the animation & transition styles coming from inheritance chain. + */ + export type getAnimatedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getAnimatedStylesForNodeReturnValue = { + /** + * Styles coming from animations. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * Style coming from transitions. + */ + transitionsStyle?: CSSStyle; + /** + * Inherited style entries for animationsStyle and transitionsStyle from +the inheritance chain of the element. + */ + inherited?: InheritedAnimatedStyleEntry[]; + } /** * Returns requested styles for a DOM node identified by `nodeId`. */ @@ -3603,7 +3734,7 @@ front-end. /** * Pseudo element type. */ - 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"; + export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-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. */ @@ -8616,7 +8747,7 @@ applicable or not known. /** * The reason why request was blocked. */ - export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"; + export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; /** * The reason why request was blocked. */ @@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie. blockedCookies: BlockedSetCookieWithReason[]; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; /** @@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve requestId: RequestId; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; } @@ -9978,7 +10115,7 @@ or after the response was received. of the operation already exists und thus, the operation was abort preemptively (e.g. a cache hit). */ - status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"; + status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit"; type: TrustTokenOperationType; requestId: RequestId; /** @@ -10672,6 +10809,26 @@ should be omitted for worker targets. export type loadNetworkResourceReturnValue = { resource: LoadNetworkResourcePageResult; } + /** + * Sets Controls for third-party cookie access +Page reload is required before the new cookie bahavior will be observed + */ + export type setCookieControlsParameters = { + /** + * Whether 3pc restriction is enabled. + */ + enableThirdPartyCookieRestriction: boolean; + /** + * Whether 3pc grace period exception should be enabled; false by default. + */ + disableThirdPartyCookieMetadata: boolean; + /** + * Whether 3pc heuristics exceptions should be enabled; false by default. + */ + disableThirdPartyCookieHeuristics: boolean; + } + export type setCookieControlsReturnValue = { + } } /** @@ -11545,7 +11702,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"|"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"; + 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"|"deferred-fetch-minimal"|"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. */ @@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored. * The initial URL the page will be navigated to. An empty string indicates about:blank. */ url: string; + /** + * Frame left origin in DIP (headless chrome only). + */ + left?: number; + /** + * Frame top origin in DIP (headless chrome only). + */ + top?: number; /** * Frame width in DIP (headless chrome only). */ @@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt. ruleSetIds: RuleSetId[]; nodeIds: DOM.BackendNodeId[]; } + /** + * Chrome manages different types of preloads together using a +concept of preloading pipeline. For example, if a site uses a +SpeculationRules for prerender, Chrome first starts a prefetch and +then upgrades it to prerender. + +CDP events for them are emitted separately but they share +`PreloadPipelineId`. + */ + export type PreloadPipelineId = string; /** * List of FinalStatus reasons for Prerender2. */ @@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers. */ export type prefetchStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; /** * The frame id of the frame initiating prefetch. */ @@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers. */ export type prerenderStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; status: PreloadingStatus; prerenderStatus?: PrerenderFinalStatus; /** @@ -17922,6 +18099,10 @@ variables as its properties. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -17996,6 +18177,10 @@ scripts upon enabling debugger. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -20507,9 +20692,13 @@ Error was thrown. "CSS.disable": CSS.disableParameters; "CSS.enable": CSS.enableParameters; "CSS.forcePseudoState": CSS.forcePseudoStateParameters; + "CSS.forceStartingStyle": CSS.forceStartingStyleParameters; "CSS.getBackgroundColors": CSS.getBackgroundColorsParameters; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters; + "CSS.resolveValues": CSS.resolveValuesParameters; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters; "CSS.getMediaQueries": CSS.getMediaQueriesParameters; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters; @@ -20751,6 +20940,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters; "Network.enableReportingApi": Network.enableReportingApiParameters; "Network.loadNetworkResource": Network.loadNetworkResourceParameters; + "Network.setCookieControls": Network.setCookieControlsParameters; "Overlay.disable": Overlay.disableParameters; "Overlay.enable": Overlay.enableParameters; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters; @@ -21119,9 +21309,13 @@ Error was thrown. "CSS.disable": CSS.disableReturnValue; "CSS.enable": CSS.enableReturnValue; "CSS.forcePseudoState": CSS.forcePseudoStateReturnValue; + "CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue; "CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue; + "CSS.resolveValues": CSS.resolveValuesReturnValue; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue; "CSS.getMediaQueries": CSS.getMediaQueriesReturnValue; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue; @@ -21363,6 +21557,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue; "Network.enableReportingApi": Network.enableReportingApiReturnValue; "Network.loadNetworkResource": Network.loadNetworkResourceReturnValue; + "Network.setCookieControls": Network.setCookieControlsReturnValue; "Overlay.disable": Overlay.disableReturnValue; "Overlay.enable": Overlay.enableReturnValue; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue; diff --git a/packages/playwright/src/common/esmLoaderHost.ts b/packages/playwright/src/common/esmLoaderHost.ts index 9b5b4f9b8c..9daa71107c 100644 --- a/packages/playwright/src/common/esmLoaderHost.ts +++ b/packages/playwright/src/common/esmLoaderHost.ts @@ -30,7 +30,6 @@ export function registerESMLoader() { const { port1, port2 } = new MessageChannel(); // register will wait until the loader is initialized. require('node:module').register(url.pathToFileURL(require.resolve('../transform/esmLoader')), { - parentURL: url.pathToFileURL(__filename), data: { port: port2 }, transferList: [port2], }); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 29fabb22e8..83913c18dc 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -19,7 +19,6 @@ import * as path from 'path'; import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; import * as playwrightLibrary from 'playwright-core'; import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { TestInfoImpl, TestStepInternal } from './worker/testInfo'; import { rootTestType } from './common/testType'; @@ -264,12 +263,12 @@ const playwrightFixtures: Fixtures = ({ // Some special calls do not get into steps. if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') return; - const expectZone = zones.zoneData('expectZone'); - if (expectZone) { + const zone = zones.zoneData('stepZone'); + if (zone && zone.category === 'expect') { // Display the internal locator._expect call under the name of the enclosing expect call, // and connect it to the existing expect step. - data.apiName = expectZone.title; - data.stepId = expectZone.stepId; + data.apiName = zone.title; + data.stepId = zone.stepId; return; } // In the general case, create a step for each api call and connect them through the stepId. diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index d4c3287d33..d382a4dbfd 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -19,7 +19,6 @@ import { createGuid, isString, pollAgainstDeadline } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import { toBeAttached, toBeChecked, @@ -315,9 +314,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { // out all the frames that belong to the test runner from caught runtime errors. const stackFrames = filteredStackTrace(captureRawStack()); - // Enclose toPass in a step to maintain async stacks, toPass matcher is always async. + // toPass and poll matchers can contain other steps, expects and API calls, + // so they behave like a retriable step. const stepInfo = { - category: 'expect', + category: (matcherName === 'toPass' || this._info.poll) ? 'step' : 'expect', title: trimLongString(title, 1024), params: args[0] ? { expected: args[0] } : undefined, infectParentStepsWithError: this._info.isSoft, @@ -345,11 +345,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { try { const callback = () => matcher.call(target, ...args); - // toPass and poll matchers can contain other steps, expects and API calls, - // so they behave like a retriable step. - const result = (matcherName === 'toPass' || this._info.poll) ? - zones.run('stepZone', step, callback) : - zones.run('expectZone', { title, stepId: step.stepId }, callback); + const result = zones.run('stepZone', step, callback); if (result instanceof Promise) return result.then(finalizer).catch(reportStepError); finalizer(); diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index c942fef246..9b43999cf9 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -196,20 +196,13 @@ export function toHaveAccessibleDescription( export function toHaveAccessibleName( this: ExpectMatcherState, locator: LocatorEx, - expected: string | RegExp | (string | RegExp)[], - options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: boolean } = {} + expected: string | RegExp, + options?: { timeout?: number, ignoreCase?: boolean }, ) { - 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); - } + 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 toHaveAccessibleErrorMessage( diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 569e72c5dd..3eba797853 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,6 @@ import fs from 'fs'; import path from 'path'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import type { TestInfo, TestStatus, FullProject } from '../../types/test'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; @@ -35,7 +34,7 @@ export interface TestStepInternal { attachmentIndices: number[]; stepId: string; title: string; - category: 'hook' | 'fixture' | 'test.step' | 'test.step.skip' | 'expect' | 'attach' | string; + category: string; location?: Location; boxedStack?: StackFrame[]; steps: TestStepInternal[]; @@ -195,7 +194,7 @@ export class TestInfoImpl implements TestInfo { this._attachmentsPush = this.attachments.push.bind(this.attachments); this.attachments.push = (...attachments: TestInfo['attachments']) => { for (const a of attachments) - this._attach(a, this._expectStepId() ?? this._parentStep()?.stepId); + this._attach(a, this._parentStep()?.stepId); return this.attachments.length; }; @@ -245,10 +244,6 @@ export class TestInfoImpl implements TestInfo { ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. } - private _expectStepId() { - return zones.zoneData('expectZone')?.stepId; - } - _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 8e1ca2f447..ce5b78959a 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8165,7 +8165,7 @@ interface LocatorAssertions { * @param name Expected accessible name. * @param options */ - toHaveAccessibleName(name: string|RegExp|ReadonlyArray, options?: { + toHaveAccessibleName(name: string|RegExp, options?: { /** * Whether to perform case-insensitive match. * [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case) @@ -8230,21 +8230,24 @@ interface LocatorAssertions { /** * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes. - * This needs to be a full match or using a relaxed regular expression. + * When a string is provided, it must fully match the element's `class` attribute. To match individual classes or + * perform partial matches, use a regular expression: * * **Usage** * * ```html - *
+ *
* ``` * * ```js * const locator = page.locator('#component'); - * await expect(locator).toHaveClass(/selected/); - * await expect(locator).toHaveClass('selected row'); + * await expect(locator).toHaveClass('middle selected row'); + * await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); * ``` * - * Note that if array is passed as an expected value, entire lists of elements can be asserted: + * When an array is passed, the method asserts that the list of elements located matches the corresponding list of + * expected class values. Each element's class attribute is matched against the corresponding string or regular + * expression in the array: * * ```js * const locator = page.locator('list > .component'); diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 6f9e36f0c3..526cc599ab 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void; export type BrowserContextEnableRecorderParams = { language?: string, mode?: 'inspecting' | 'recording', - codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = { export type BrowserContextEnableRecorderOptions = { language?: string, mode?: 'inspecting' | 'recording', - codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index e8b9746b41..df54dcbe1c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1198,11 +1198,6 @@ BrowserContext: literals: - inspecting - recording - codegenMode: - type: enum? - literals: - - actions - - trace-events pauseOnNextStatement: boolean? testIdAttributeName: string? launchOptions: json? diff --git a/packages/trace-viewer/recorder.html b/packages/trace-viewer/recorder.html deleted file mode 100644 index c33d6586e5..0000000000 --- a/packages/trace-viewer/recorder.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - Playwright Recorder - - -
- - - diff --git a/packages/trace-viewer/src/DEPS.list b/packages/trace-viewer/src/DEPS.list index 3d486b5452..f52c0a024e 100644 --- a/packages/trace-viewer/src/DEPS.list +++ b/packages/trace-viewer/src/DEPS.list @@ -6,7 +6,3 @@ ui/ [sw-main.ts] sw/** - - -[recorder.tsx] -ui/recorder/** diff --git a/packages/trace-viewer/src/recorder.tsx b/packages/trace-viewer/src/recorder.tsx deleted file mode 100644 index 5e6b9764e3..0000000000 --- a/packages/trace-viewer/src/recorder.tsx +++ /dev/null @@ -1,41 +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 { RecorderView } from './ui/recorder/recorderView'; - -(async () => { - applyTheme(); - - if (window.location.protocol !== 'file:') { - if (!navigator.serviceWorker) - throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${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/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 10e3c39f98..0cb3cb5f54 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -70,13 +70,20 @@ flex: none; } -.action-selector { +.action-parameter { display: inline; flex: none; padding-left: 5px; +} + +.action-locator-parameter { color: var(--vscode-charts-orange); } +.action-generic-parameter { + color: var(--vscode-charts-purple); +} + .action-url { display: inline; flex: none; diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 1deb8ecd88..87e78416b0 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -19,8 +19,7 @@ import { msToString } from '@web/uiUtils'; import * as React from 'react'; import './actionList.css'; import * as modelUtil from './modelUtil'; -import { asLocator } from '@isomorphic/locatorGenerators'; -import type { Language } from '@isomorphic/locatorGenerators'; +import { asLocator, type Language } from '@isomorphic/locatorGenerators'; import type { TreeState } from '@web/components/treeView'; import { TreeView } from '@web/components/treeView'; import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil'; @@ -116,9 +115,10 @@ export const renderAction = ( }) => { const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options; const { errors, warnings } = modelUtil.stats(action); - const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined; const showAttachments = !!action.attachments?.length && !!revealAttachment; + const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); + let time: string = ''; if (action.endTime) time = msToString(action.endTime - action.startTime); @@ -129,7 +129,23 @@ export const renderAction = ( return <>
{action.apiName} - {locator &&
{locator}
} + {parameterString && + (parameterString.type === 'locator' ? ( + <> + + {parameterString.value} + + {parameterString.childDisplayString && ( + + {parameterString.childDisplayString.value} + + )} + + ) : ( + + {parameterString.value} + + ))} {action.method === 'goto' && action.params.url &&
{action.params.url}
} {action.class === 'APIRequestContext' && action.params.url &&
{excludeOrigin(action.params.url)}
}
@@ -151,3 +167,154 @@ function excludeOrigin(url: string): string { return url; } } + +type ActionParameterDisplayString = + | { + type: 'generic'; + value: string; + } + | { + type: 'locator'; + value: string; + childDisplayString?: ActionParameterDisplayString; + }; + +const clockDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'clockPauseAt': + case 'clockSetFixedTime': + case 'clockSetSystemTime': { + if ( + action.params.timeString === undefined && + action.params.timeNumber === undefined + ) + return undefined; + return { + type: 'generic', + value: new Date( + action.params.timeString ?? action.params.timeNumber, + ).toLocaleString(undefined, { timeZone: 'UTC' }), + }; + } + case 'clockFastForward': + case 'clockRunFor': { + if ( + action.params.ticksNumber === undefined && + action.params.ticksString === undefined + ) + return undefined; + return { + type: 'generic', + value: action.params.ticksString ?? `${action.params.ticksNumber}ms`, + }; + } + } + + return undefined; +}; + +const keyboardDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'press': + case 'keyboardPress': + case 'keyboardDown': + case 'keyboardUp': { + if (action.params.key === undefined) + return undefined; + return { type: 'generic', value: action.params.key }; + } + case 'type': + case 'fill': + case 'keyboardType': + case 'keyboardInsertText': { + const string = action.params.text ?? action.params.value; + if (string === undefined) + return undefined; + return { type: 'generic', value: `"${string}"` }; + } + } +}; + +const mouseDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'click': + case 'dblclick': + case 'mouseClick': + case 'mouseMove': { + if (action.params.x === undefined || action.params.y === undefined) + return undefined; + return { + type: 'generic', + value: `(${action.params.x}, ${action.params.y})`, + }; + } + case 'mouseWheel': { + if ( + action.params.deltaX === undefined || + action.params.deltaY === undefined + ) + return undefined; + return { + type: 'generic', + value: `(${action.params.deltaX}, ${action.params.deltaY})`, + }; + } + } +}; + +const touchscreenDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'tap': { + if (action.params.x === undefined || action.params.y === undefined) + return undefined; + return { + type: 'generic', + value: `(${action.params.x}, ${action.params.y})`, + }; + } + } +}; + +const actionParameterDisplayString = ( + action: ActionTraceEvent, + sdkLanguage: Language, + ignoreLocator: boolean = false, +): ActionParameterDisplayString | undefined => { + const params = action.params; + + // Locators have many possible classes, so follow existing logic and use `selector` presence + if (!ignoreLocator && params.selector !== undefined) { + return { + type: 'locator', + value: asLocator(sdkLanguage, params.selector), + childDisplayString: actionParameterDisplayString( + action, + sdkLanguage, + true, + ), + }; + } + + switch (action.class.toLowerCase()) { + case 'browsercontext': + return clockDisplayString(action); + case 'page': + case 'frame': + case 'elementhandle': + return ( + keyboardDisplayString(action) ?? + mouseDisplayString(action) ?? + touchscreenDisplayString(action) + ); + } + + return undefined; +}; diff --git a/packages/trace-viewer/src/ui/inspectorTab.tsx b/packages/trace-viewer/src/ui/inspectorTab.tsx index b6882a6a04..4278cdbc28 100644 --- a/packages/trace-viewer/src/ui/inspectorTab.tsx +++ b/packages/trace-viewer/src/ui/inspectorTab.tsx @@ -47,17 +47,28 @@ export const InspectorTab: React.FunctionComponent<{ setIsInspecting(false); }, [highlightedElement, setHighlightedElement, setIsInspecting]); - return
-
Locator
-
+ return
+
+
Locator
+ { + copy(highlightedElement.locator || ''); + }}> +
+
{ // Updating text needs to go first - react can squeeze a render between the state updates. setHighlightedElement({ ...highlightedElement, locator: text, lastEdited: 'locator' }); setIsInspecting(false); }} />
-
Aria
-
+ +
+
Aria snapshot
+ { + copy(highlightedElement.ariaSnapshot || ''); + }}> +
+
-
- { - copy(highlightedElement.locator || ''); - }}> -
; }; diff --git a/packages/trace-viewer/src/ui/recorder/DEPS.list b/packages/trace-viewer/src/ui/recorder/DEPS.list deleted file mode 100644 index a504a7dba1..0000000000 --- a/packages/trace-viewer/src/ui/recorder/DEPS.list +++ /dev/null @@ -1,5 +0,0 @@ -[*] -@isomorphic/** -@trace/** -@web/** -../** diff --git a/packages/trace-viewer/src/ui/recorder/actionListView.tsx b/packages/trace-viewer/src/ui/recorder/actionListView.tsx deleted file mode 100644 index 8e9fa0df45..0000000000 --- a/packages/trace-viewer/src/ui/recorder/actionListView.tsx +++ /dev/null @@ -1,62 +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 type * as actionTypes from '@recorder/actions'; -import { ListView } from '@web/components/listView'; -import * as React from 'react'; -import '../actionList.css'; -import { traceParamsForAction } from '@isomorphic/recorderUtils'; -import { asLocator } from '@isomorphic/locatorGenerators'; -import type { Language } from '@isomorphic/locatorGenerators'; - -const ActionList = ListView; - -export const ActionListView: React.FC<{ - sdkLanguage: Language, - actions: actionTypes.ActionInContext[], - selectedAction: actionTypes.ActionInContext | undefined, - onSelectedAction: (action: actionTypes.ActionInContext | undefined) => void, -}> = ({ - sdkLanguage, - actions, - selectedAction, - onSelectedAction, -}) => { - const render = React.useCallback((action: actionTypes.ActionInContext) => { - return renderAction(sdkLanguage, action); - }, [sdkLanguage]); - return
- -
; -}; - -export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => { - const { method, apiName, params } = traceParamsForAction(action); - const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined; - - return <> -
- {apiName} - {locator &&
{locator}
} - {method === 'goto' && params.url &&
{params.url}
} -
- ; -}; diff --git a/packages/trace-viewer/src/ui/recorder/backendContext.tsx b/packages/trace-viewer/src/ui/recorder/backendContext.tsx deleted file mode 100644 index 312281001e..0000000000 --- a/packages/trace-viewer/src/ui/recorder/backendContext.tsx +++ /dev/null @@ -1,118 +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 type * as actionTypes from '@recorder/actions'; -import type { Mode, Source } from '@recorder/recorderTypes'; -import * as React from 'react'; - -export const BackendContext = React.createContext(undefined); - -export const BackendProvider: React.FunctionComponent> = ({ guid, children }) => { - const [connection, setConnection] = React.useState(undefined); - const [mode, setMode] = React.useState('none'); - const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] }); - const callbacks = React.useRef({ setMode, setActions }); - - React.useEffect(() => { - const wsURL = new URL(`../${guid}`, window.location.toString()); - wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); - const webSocket = new WebSocket(wsURL.toString()); - setConnection(new Connection(webSocket, callbacks.current)); - return () => { - webSocket.close(); - }; - }, [guid]); - - const backend = React.useMemo(() => { - return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined; - }, [actions, mode, connection]); - - return - {children} - ; -}; - -export type Backend = { - actions: actionTypes.ActionInContext[], - sources: Source[], - connection: Connection, -}; - -type ConnectionCallbacks = { - setMode: (mode: Mode) => void; - setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void; -}; - -class Connection { - private _lastId = 0; - private _webSocket: WebSocket; - private _callbacks = new Map void, reject: (arg: Error) => void }>(); - private _options: ConnectionCallbacks; - - constructor(webSocket: WebSocket, options: ConnectionCallbacks) { - this._webSocket = webSocket; - this._callbacks = new Map(); - this._options = options; - - this._webSocket.addEventListener('message', event => { - const message = JSON.parse(event.data); - const { id, result, error, method, params } = message; - if (id) { - const callback = this._callbacks.get(id); - if (!callback) - return; - this._callbacks.delete(id); - if (error) - callback.reject(new Error(error)); - else - callback.resolve(result); - } else { - this._dispatchEvent(method, params); - } - }); - } - - setMode(mode: Mode) { - this._sendMessageNoReply('setMode', { mode }); - } - - private async _sendMessage(method: string, params?: any): Promise { - const id = ++this._lastId; - const message = { id, method, params }; - this._webSocket.send(JSON.stringify(message)); - return new Promise((resolve, reject) => { - this._callbacks.set(id, { resolve, reject }); - }); - } - - private _sendMessageNoReply(method: string, params?: any) { - this._sendMessage(method, params).catch(() => { }); - } - - private _dispatchEvent(method: string, params?: any) { - if (method === 'setMode') { - const { mode } = params as { mode: Mode }; - this._options.setMode(mode); - } - if (method === 'setActions') { - const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] }; - this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources }); - (window as any).playwrightSourcesEchoForTest = sources; - } - } -} diff --git a/packages/trace-viewer/src/ui/recorder/modelContext.tsx b/packages/trace-viewer/src/ui/recorder/modelContext.tsx deleted file mode 100644 index 98f450361b..0000000000 --- a/packages/trace-viewer/src/ui/recorder/modelContext.tsx +++ /dev/null @@ -1,71 +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 { sha1 } from '@web/uiUtils'; -import * as React from 'react'; -import type { ContextEntry } from '../../types/entries'; -import { MultiTraceModel } from '../modelUtil'; - -export const ModelContext = React.createContext(undefined); - -export const ModelProvider: React.FunctionComponent> = ({ trace, children }) => { - const [model, setModel] = React.useState<{ model: MultiTraceModel, sha1: string } | undefined>(); - const [counter, setCounter] = React.useState(0); - const pollTimer = React.useRef(null); - - React.useEffect(() => { - if (pollTimer.current) - clearTimeout(pollTimer.current); - - // Start polling running test. - pollTimer.current = setTimeout(async () => { - try { - const result = await loadSingleTraceFile(trace); - if (result.sha1 !== model?.sha1) - setModel(result); - } catch { - setModel(undefined); - } finally { - setCounter(counter + 1); - } - }, 500); - return () => { - if (pollTimer.current) - clearTimeout(pollTimer.current); - }; - }, [counter, model, trace]); - - return - {children} - ; -}; - -async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> { - const params = new URLSearchParams(); - params.set('trace', url); - params.set('limit', '1'); - const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - - const tokens: string[] = []; - for (const entry of contextEntries) { - entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime)); - entry.events.forEach(e => tokens.push(e.type + '@' + e.time)); - } - return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) }; -} diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.css b/packages/trace-viewer/src/ui/recorder/recorderView.css deleted file mode 100644 index ad03e78e7d..0000000000 --- a/packages/trace-viewer/src/ui/recorder/recorderView.css +++ /dev/null @@ -1,15 +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. -*/ diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.tsx b/packages/trace-viewer/src/ui/recorder/recorderView.tsx deleted file mode 100644 index 93db2b917d..0000000000 --- a/packages/trace-viewer/src/ui/recorder/recorderView.tsx +++ /dev/null @@ -1,299 +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 type * as actionTypes from '@recorder/actions'; -import { SourceChooser } from '@web/components/sourceChooser'; -import { SplitView } from '@web/components/splitView'; -import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; -import { TabbedPane } from '@web/components/tabbedPane'; -import { Toolbar } from '@web/components/toolbar'; -import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton'; -import { copy, useSetting } from '@web/uiUtils'; -import * as React from 'react'; -import { ConsoleTab, useConsoleTabModel } from '../consoleTab'; -import type { Boundaries } from '../geometry'; -import { InspectorTab } from '../inspectorTab'; -import type * as modelUtil from '../modelUtil'; -import type { SourceLocation } from '../modelUtil'; -import { NetworkTab, useNetworkTabModel } from '../networkTab'; -import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab'; -import { SourceTab } from '../sourceTab'; -import { ModelContext, ModelProvider } from './modelContext'; -import './recorderView.css'; -import { ActionListView } from './actionListView'; -import { BackendContext, BackendProvider } from './backendContext'; -import type { Language } from '@isomorphic/locatorGenerators'; -import { SettingsToolbarButton } from '../settingsToolbarButton'; -import type { HighlightedElement } from '../snapshotTab'; - -export const RecorderView: React.FunctionComponent = () => { - const searchParams = new URLSearchParams(window.location.search); - const guid = searchParams.get('ws')!; - const trace = searchParams.get('trace') + '.json'; - - return - - - - ; -}; - -export const Workbench: React.FunctionComponent = () => { - const backend = React.useContext(BackendContext); - const model = React.useContext(ModelContext); - const [fileId, setFileId] = React.useState(); - const [selectedStartTime, setSelectedStartTime] = React.useState(undefined); - const [isInspecting, setIsInspecting] = React.useState(false); - const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState({ lastEdited: 'none' }); - const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState({ lastEdited: 'none' }); - const [traceCallId, setTraceCallId] = React.useState(); - - const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => { - setSelectedStartTime(action?.startTime); - }, []); - - const selectedAction = React.useMemo(() => { - return backend?.actions.find(a => a.startTime === selectedStartTime); - }, [backend?.actions, selectedStartTime]); - - React.useEffect(() => { - const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId; - if (callId) - setTraceCallId(callId); - }, [model, selectedAction]); - - const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]); - const sourceLocation = React.useMemo(() => { - if (!source) - return undefined; - const sourceLocation: SourceLocation = { - file: '', - line: 0, - column: 0, - source: { - errors: [], - content: source.text - } - }; - return sourceLocation; - }, [source]); - - const sdkLanguage: Language = source?.language || 'javascript'; - - const { boundaries } = React.useMemo(() => { - const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 }; - if (boundaries.minimum > boundaries.maximum) { - boundaries.minimum = 0; - boundaries.maximum = 30000; - } - // Leave some nice free space on the right hand side. - boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; - return { boundaries }; - }, [model]); - - const elementPickedInTrace = React.useCallback((element: HighlightedElement) => { - setHighlightedElementInProperties(element); - setHighlightedElementInTrace({ lastEdited: 'none' }); - setIsInspecting(false); - }, []); - - const elementTypedInProperties = React.useCallback((element: HighlightedElement) => { - setHighlightedElementInTrace(element); - setHighlightedElementInProperties(element); - }, []); - - const actionList = ; - - const actionsTab: TabbedPaneTabModel = { - id: 'actions', - title: 'Actions', - component: actionList, - }; - - const toolbar = -
- { - setIsInspecting(!isInspecting); - }} /> - { - }} /> - { - }} /> - { - }} /> - - { - if (source?.text) - copy(source.text); - }}> -
-
Target:
- { - setFileId(fileId); - }} /> - -
; - - const sidebarTabbedPane = ; - const traceView = ; - const propertiesView = ; - - return
- - {toolbar} - {traceView} -
} - sidebar={propertiesView} - />} - sidebar={sidebarTabbedPane} - /> -
; -}; - -const PropertiesView: React.FunctionComponent<{ - sdkLanguage: Language, - boundaries: Boundaries, - setIsInspecting: (value: boolean) => void, - highlightedElement: HighlightedElement, - setHighlightedElement: (element: HighlightedElement) => void, - sourceLocation: modelUtil.SourceLocation | undefined, -}> = ({ - sdkLanguage, - boundaries, - setIsInspecting, - highlightedElement, - setHighlightedElement, - sourceLocation, -}) => { - const model = React.useContext(ModelContext); - const consoleModel = useConsoleTabModel(model, boundaries); - const networkModel = useNetworkTabModel(model, boundaries); - const sourceModel = React.useRef(new Map()); - const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('recorderPropertiesTab', 'source'); - - const inspectorTab: TabbedPaneTabModel = { - id: 'inspector', - title: 'Locator', - render: () => , - }; - - const sourceTab: TabbedPaneTabModel = { - id: 'source', - title: 'Source', - render: () => - }; - const consoleTab: TabbedPaneTabModel = { - id: 'console', - title: 'Console', - count: consoleModel.entries.length, - render: () => - }; - const networkTab: TabbedPaneTabModel = { - id: 'network', - title: 'Network', - count: networkModel.resources.length, - render: () => - }; - - const tabs: TabbedPaneTabModel[] = [ - sourceTab, - inspectorTab, - consoleTab, - networkTab, - ]; - - return ; -}; - -const TraceView: React.FunctionComponent<{ - sdkLanguage: Language, - callId: string | undefined, - isInspecting: boolean; - setIsInspecting: (value: boolean) => void; - highlightedElement: HighlightedElement; - setHighlightedElement: (element: HighlightedElement) => void; -}> = ({ - sdkLanguage, - callId, - isInspecting, - setIsInspecting, - highlightedElement, - setHighlightedElement, -}) => { - const model = React.useContext(ModelContext); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false); - - const action = React.useMemo(() => { - return model?.actions.find(a => a.callId === callId); - }, [model, callId]); - - const snapshot = React.useMemo(() => { - const snapshot = collectSnapshots(action); - return snapshot.action || snapshot.after || snapshot.before; - }, [action]); - const snapshotUrls = React.useMemo(() => { - return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined; - }, [snapshot, shouldPopulateCanvasFromScreenshot]); - - return ; -}; diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index c26e020fde..00b367bbc8 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -46,7 +46,6 @@ export default defineConfig({ input: { index: path.resolve(__dirname, 'index.html'), uiMode: path.resolve(__dirname, 'uiMode.html'), - recorder: path.resolve(__dirname, 'recorder.html'), snapshot: path.resolve(__dirname, 'snapshot.html'), }, output: { diff --git a/tests/config/testModeFixtures.ts b/tests/config/testModeFixtures.ts index 1231a78260..6b6feff7c2 100644 --- a/tests/config/testModeFixtures.ts +++ b/tests/config/testModeFixtures.ts @@ -21,7 +21,6 @@ import * as playwrightLibrary from 'playwright-core'; export type TestModeWorkerOptions = { mode: TestModeName; - codegenMode: 'trace-events' | 'actions'; }; export type TestModeTestFixtures = { @@ -49,7 +48,6 @@ export const testModeTest = test.extend { await use((playwright as any)._toImpl); diff --git a/tests/library/css-parser.spec.ts b/tests/library/css-parser.spec.ts index 3bf8a742e6..e6ff046579 100644 --- a/tests/library/css-parser.spec.ts +++ b/tests/library/css-parser.spec.ts @@ -77,7 +77,8 @@ it('should throw on malformed css', async () => { } catch (e) { error = e; } - expect(error.message).toContain(`while parsing selector "${selector}"`); + expect(error.message).toContain(`while parsing css selector "${selector}"`); + expect(error.message).toContain(`Did you mean to CSS.escape it?`); } expectError(''); diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index 0fd5f69d0b..6936aeee41 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -19,7 +19,6 @@ import type { ConsoleMessage } from 'playwright'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should click', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); @@ -413,7 +412,7 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`); expect(messages[0].text()).toBe('press'); }); - test('should update selected element after pressing Tab', async ({ openRecorder, browserName, codegenMode }) => { + test('should update selected element after pressing Tab', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); await recorder.setContentAndWait(` diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 47afcc2fab..920ecbdf76 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -20,7 +20,6 @@ import fs from 'fs'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should contain open page', async ({ openRecorder }) => { const { recorder } = await openRecorder(); @@ -310,8 +309,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync(); } }); - test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => { - test.skip(codegenMode === 'trace-events'); + test('should record open in a new tab with url', async ({ openRecorder, browserName }) => { const { page, recorder } = await openRecorder(); await recorder.setContentAndWait(`link`); @@ -453,8 +451,7 @@ await page1.GotoAsync("about:blank?foo");`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`); }); - test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => { - test.skip(codegenMode === 'trace-events'); + test('should --save-trace', async ({ runCLI }, testInfo) => { const traceFileName = testInfo.outputPath('trace.zip'); const cli = runCLI([`--save-trace=${traceFileName}`], { autoExitWhen: ' ', @@ -463,8 +460,7 @@ await page1.GotoAsync("about:blank?foo");`); expect(fs.existsSync(traceFileName)).toBeTruthy(); }); - test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => { - test.skip(codegenMode === 'trace-events'); + test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { test.skip(platform === 'win32', 'SIGINT not supported on Windows'); const traceFileName = testInfo.outputPath('trace.zip'); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 87c7e7bfec..8af5a76472 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -21,7 +21,6 @@ import type { Page } from '@playwright/test'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should click locator.first', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts index 354ca6495a..a11cf33342 100644 --- a/tests/library/inspector/cli-codegen-aria.spec.ts +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest'; test.describe(() => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should generate aria snapshot', async ({ openRecorder }) => { const { recorder } = await openRecorder(); diff --git a/tests/library/inspector/cli-codegen-pick-locator.spec.ts b/tests/library/inspector/cli-codegen-pick-locator.spec.ts index 53d106b4c9..bd1a612842 100644 --- a/tests/library/inspector/cli-codegen-pick-locator.spec.ts +++ b/tests/library/inspector/cli-codegen-pick-locator.spec.ts @@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest'; test.describe(() => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should inspect locator', async ({ openRecorder }) => { const { recorder } = await openRecorder(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index a90f73fcdf..b94bfc09a0 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -67,7 +67,7 @@ export const test = contextTest.extend({ }); }, - runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => { + runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => { testInfo.skip(mode.startsWith('service')); await run((cliArgs, { autoExitWhen } = {}) => { @@ -78,17 +78,15 @@ export const test = contextTest.extend({ args: cliArgs, executablePath: launchOptions.executablePath, autoExitWhen, - codegenMode }); }); }, - openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => { + openRecorder: async ({ context, recorderPageGetter }, run) => { await run(async (options?: { testIdAttributeName?: string }) => { await (context as any)._enableRecorder({ language: 'javascript', mode: 'recording', - codegenMode, ...options }); const page = await context.newPage(); @@ -235,7 +233,7 @@ export class Recorder { class CLIMock { process: TestChildProcess; - constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) { + constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined}) { const nodeArgs = [ 'node', path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'), @@ -248,7 +246,6 @@ class CLIMock { this.process = childProcess({ command: nodeArgs, env: { - PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined, PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen, PWTEST_CLI_IS_UNDER_TEST: '1', PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined, diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts index 399eec1b6b..f588ee2f45 100644 --- a/tests/library/playwright.config.ts +++ b/tests/library/playwright.config.ts @@ -147,18 +147,6 @@ for (const browserName of browserNames) { testDir: path.join(testDir, 'page'), ...projectTemplate, }); - - // TODO: figure out reporting to flakiness dashboard (Problem: they get merged, we want to keep them separate) - // config.projects.push({ - // name: `${browserName}-codegen-mode-trace`, - // testDir: path.join(testDir, 'library'), - // testMatch: '**/cli-codegen-*.spec.ts', - // ...projectTemplate, - // use: { - // ...projectTemplate.use, - // codegenMode: 'trace-events', - // } - // }); } export default config; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 71ecfe0764..9bb67884f5 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -166,6 +166,61 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => { ]); }); +test('should show action context on locators and other common actions', async ({ + runAndTrace, + page, +}) => { + const traceViewer = await runAndTrace(async () => { + await page.setContent(''); + await page.locator('input').click({ button: 'right' }); + await page.getByRole('textbox').click(); + await expect(page.locator('input')).toHaveText(''); + await page.locator('input').press('Enter'); + await page.keyboard.type( + 'Hello world this is a very long string what happens when it overflows?', + ); + await page.keyboard.press('Control+c'); + await page.keyboard.down('Shift'); + await page.keyboard.insertText('Hello world'); + await page.keyboard.up('Shift'); + await page.mouse.move(0, 0); + await page.mouse.down(); + await page.mouse.move(100, 200); + await page.mouse.wheel(5, 7); + await page.mouse.up(); + await page.clock.fastForward(1000); + await page.clock.fastForward('30:00'); + await page.clock.pauseAt(new Date('2020-02-02T00:00:00Z')); + await page.clock.runFor(10); + await page.clock.setFixedTime(new Date('2020-02-02T00:00:00Z')); + await page.clock.setSystemTime(new Date('2020-02-02T00:00:00Z')); + }); + + await expect(traceViewer.actionTitles).toHaveText([ + /page.setContent/, + /locator.clicklocator\('input'\)/, + /locator.clickgetByRole\('textbox'\)/, + /expect.toHaveTextlocator\('input'\)/, + /locator.presslocator\('input'\)Enter/, + /keyboard.type\"Hello world this is a very long string what happens when it overflows\?\"/, + /keyboard.pressControl\+c/, + /keyboard.downShift/, + /keyboard.insertText\"Hello world\"/, + /keyboard.upShift/, + /mouse.move\(0, 0\)/, + /mouse.down/, + /mouse.move\(100, 200\)/, + /mouse.wheel\(5, 7\)/, + /mouse.up/, + /clock.fastForward1000ms/, + /clock.fastForward30:00/, + /clock.pauseAt2\/2\/2020, 12:00:00 AM/, + /clock.runFor10ms/, + /clock.setFixedTime2\/2\/2020, 12:00:00 AM/, + /clock.setSystemTime2\/2\/2020, 12:00:00 AM/, + ]); +}); + test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => { const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]); await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible(); diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index 49b06b7747..d5b4c4d3b5 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -479,7 +479,7 @@ test('should print unknown engine error', async ({ page }) => { test('should print selector syntax error', async ({ page }) => { const error = await expect(page.locator('row]')).toBeVisible().catch(e => e); - expect(error.message).toContain(`Unexpected token "]" while parsing selector "row]"`); + expect(error.message).toContain(`Unexpected token "]" while parsing css selector "row]"`); }); test.describe(() => { diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index a1fb6637b1..8bd668dfd7 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -436,43 +436,6 @@ test('toHaveAccessibleName', async ({ page }) => { await expect(page.locator('button')).toHaveAccessibleName('foo bar baz'); }); -test('toHaveAccessibleName should accept array of names for multiple elements', async ({ page }) => { - await page.setContent(` - - - - - - - - - - - - - - - - -
Cell A1Cell B1Cell C1
Cell A2Cell B2Cell C2
Cell A3Cell B3Cell C3
- `); - await expect(page.getByRole('row')).toHaveAccessibleName([ - 'Cell A1 Cell B1 Cell C1', - 'Cell A2 Cell B2 Cell C2', - 'Cell A3 Cell B3 Cell C3', - ]); - await expect(page.getByRole('row')).toHaveAccessibleName(['cell a1 cell b1 cell C1', - 'cell A2 Cell b2 Cell c2', - 'Cell a3 Cell b3 cell C3',], { ignoreCase: true }); - - await expect(page.getByRole('row')).not.toHaveAccessibleName([ - 'Cel A4 Cell B4 Cell C4', - 'Cell A5 Cell B5 Cell C5', - 'Cell A6 Cell B6 Cell C6', - ]); -}); - - test('toHaveAccessibleDescription', async ({ page }) => { await page.setContent(`
diff --git a/tests/page/page-check.spec.ts b/tests/page/page-check.spec.ts index 01b00ddc55..42946ce555 100644 --- a/tests/page/page-check.spec.ts +++ b/tests/page/page-check.spec.ts @@ -18,9 +18,10 @@ import { test as it, expect } from './pageTest'; it('should check the box @smoke', async ({ page }) => { - await page.setContent(``); - await page.check('input'); - expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true); + await page.setContent(`
`); + const locator = page.locator('#component'); + await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); + await expect(locator).toHaveClass('middle selected row'); }); it('should not check the checked box', async ({ page }) => { diff --git a/tests/page/selectors-misc.spec.ts b/tests/page/selectors-misc.spec.ts index 605ff6c5a0..f656e9d512 100644 --- a/tests/page/selectors-misc.spec.ts +++ b/tests/page/selectors-misc.spec.ts @@ -415,7 +415,7 @@ it('should work with internal:has=', async ({ page, server }) => { const error3 = await page.$(`div >> internal:has=33`).catch(e => e); expect(error3.message).toContain('Malformed selector: internal:has=33'); const error4 = await page.$(`div >> internal:has="span!"`).catch(e => e); - expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"'); + expect(error4.message).toContain('Unexpected token "!" while parsing css selector "span!"'); }); it('should work with internal:has-not=', async ({ page }) => { diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index ec04ef19e8..648cbeb52e 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -616,7 +616,7 @@ test('should not propagate errors from within toPass', async ({ runInlineTest }) expect(result.exitCode).toBe(0); expect(result.output).toBe(` hook |Before Hooks -expect |expect.toPass @ a.test.ts:7 +step |expect.toPass @ a.test.ts:7 expect | expect.toBe @ a.test.ts:6 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:6 @@ -643,8 +643,8 @@ test('should show final toPass error', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); expect(stripAnsi(result.output)).toBe(` hook |Before Hooks -expect |expect.toPass @ a.test.ts:6 -expect |↪ error: Error: expect(received).toBe(expected) // Object.is equality +step |expect.toPass @ a.test.ts:6 +step |↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:5 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality hook |After Hooks @@ -909,7 +909,7 @@ test('step inside expect.toPass', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |step 1 @ a.test.ts:4 -expect | expect.toPass @ a.test.ts:11 +step | expect.toPass @ a.test.ts:11 test.step | step 2, attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:9 @@ -956,7 +956,7 @@ fixture | fixture: context pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage -expect |expect.toPass @ a.test.ts:11 +step |expect.toPass @ a.test.ts:11 pw:api | page.goto(about:blank) @ a.test.ts:6 test.step | inner step attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @@ -1007,7 +1007,7 @@ fixture | fixture: context pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage -expect |expect.poll.toHaveLength @ a.test.ts:14 +step |expect.poll.toHaveLength @ a.test.ts:14 pw:api | page.goto(about:blank) @ a.test.ts:7 test.step | inner step attempt: 0 @ a.test.ts:8 expect | expect.toBe @ a.test.ts:10 @@ -1059,7 +1059,7 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage pw:api |page.setContent @ a.test.ts:4 -expect |expect.poll.toBe @ a.test.ts:13 +step |expect.poll.toBe @ a.test.ts:13 expect | expect.toHaveText @ a.test.ts:7 test.step | iteration 1 @ a.test.ts:9 expect | expect.toBeVisible @ a.test.ts:10 @@ -1565,3 +1565,66 @@ expect |expect.toBe @ a.test.ts:10 hook |After Hooks `); }); + +test('show api calls inside expects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepIndentReporter, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.test.ts': ` + import { test, expect as baseExpect } from '@playwright/test'; + + const expect = baseExpect.extend({ + async toBeInvisible(locator: Locator) { + try { + await expect.poll(() => locator.isVisible()).toBe(false); + return { name: 'toBeInvisible', pass: true, message: '' }; + } catch (e) { + return { name: 'toBeInvisible', pass: false, message: () => 'Expected to be invisible, got visible!' }; + } + }, + }); + + test('test', async ({ page }) => { + await page.setContent('
hello
'); + const promise = expect(page.locator('div')).toBeInvisible(); + await page.waitForTimeout(1100); + await page.setContent('
hello
'); + await promise; + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.report.stats.expected).toBe(1); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +fixture | fixture: browser +pw:api | browserType.launch +fixture | fixture: context +pw:api | browser.newContext +fixture | fixture: page +pw:api | browserContext.newPage +pw:api |page.setContent @ a.test.ts:16 +expect |expect.toBeInvisible @ a.test.ts:17 +step | expect.poll.toBe @ a.test.ts:7 +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +pw:api |page.waitForTimeout @ a.test.ts:18 +pw:api |page.setContent @ a.test.ts:19 +hook |After Hooks +fixture | fixture: page +fixture | fixture: context +`); +});