diff --git a/docs/src/test-assertions-csharp-java-python.md b/docs/src/test-assertions-csharp-java-python.md index 4d19503115fa0..d81762849e266 100644 --- a/docs/src/test-assertions-csharp-java-python.md +++ b/docs/src/test-assertions-csharp-java-python.md @@ -56,7 +56,7 @@ expect.soft(page.get_by_role("heading", name="Make another order")).to_be_visibl Note that soft assertions only work with the [`pytest-playwright`](https://pypi.org/project/pytest-playwright/) (or [`pytest-playwright-asyncio`](https://pypi.org/project/pytest-playwright-asyncio/)) -plugin, version `0.7.3` or newer. +plugin, version `0.8.0` or newer. ## Custom Expect Message * langs: python, csharp diff --git a/packages/injected/src/ariaSnapshotDistiller.ts b/packages/injected/src/ariaSnapshotDistiller.ts index de12ac05597d4..02ab6c0f1ff5d 100644 --- a/packages/injected/src/ariaSnapshotDistiller.ts +++ b/packages/injected/src/ariaSnapshotDistiller.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { hasPointerCursor } from '@isomorphic/ariaSnapshot'; import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; import type * as aria from '@isomorphic/ariaSnapshot'; @@ -102,6 +103,11 @@ function isLeafGeneric(node: aria.AriaNode): boolean { return node.role === 'generic' && node.children.every(child => typeof child === 'string'); } +// Removing the click target root would hide an actionable element from the snapshot. +function isClickTargetRoot(node: aria.AriaNode, ctx: DistillerContext): boolean { + return !!node.ref && hasPointerCursor(node) && !ctx.ancestors.some(ancestor => !!ancestor.ref && hasPointerCursor(ancestor)); +} + // The tree builder emits raw text tokens - text nodes, CSS content, block spacing markers - as // string children. Coalesce the adjacent ones, normalize whitespace and drop the empties, then // drop a lone text child that merely repeats the node's accessible name. Runs on `exit`, so the @@ -137,22 +143,27 @@ const mergeStringChildren: DistillerPlugin = { // Only unwrap a generic that encloses at most one element, logical grouping still makes sense, // even if it is not ref-able. The decision is made on `exit` - whether the node encloses a single // ref-bearing child is only known after its own descendants were unwrapped - so nested wrappers -// collapse bottom-up. +// collapse bottom-up. A generic emptied by the other plugins is dropped, unless it is the +// click target root, for example an icon-only button. const unwrapSingleChildGenerics: DistillerPlugin = { name: 'unwrapSingleChildGenerics', - exit(node: aria.AriaNode): 'unwrap' | void { - if (node.role === 'generic' && !node.name && node.children.length <= 1 && node.children.every(child => typeof child !== 'string' && !!child.ref)) - return 'unwrap'; + exit(node: aria.AriaNode, ctx: DistillerContext): 'unwrap' | void { + if (node.role !== 'generic' || node.name || node.children.length > 1 || !node.children.every(child => typeof child !== 'string' && !!child.ref)) + return; + if (!node.children.length && isClickTargetRoot(node, ctx)) + return; + return 'unwrap'; }, }; // A decorative image - role `img` with no accessible name and no content - carries no // information. The decision is made on `exit` - whether the node has content is only known after -// `mergeStringChildren` dropped the empty text tokens. +// `mergeStringChildren` dropped the empty text tokens. A clickable image outside of any clickable +// container is not decorative though - e.g. a bare svg icon acting as a button - and is kept. const removeNamelessImages: DistillerPlugin = { name: 'removeNamelessImages', - exit(node: aria.AriaNode): 'remove' | void { - if (node.role === 'img' && !node.name && !node.children.length) + exit(node: aria.AriaNode, ctx: DistillerContext): 'remove' | void { + if (node.role === 'img' && !node.name && !node.children.length && !isClickTargetRoot(node, ctx)) return 'remove'; }, }; diff --git a/packages/isomorphic/selectorParser.ts b/packages/isomorphic/selectorParser.ts index 17f1edced4e14..74818985bc640 100644 --- a/packages/isomorphic/selectorParser.ts +++ b/packages/isomorphic/selectorParser.ts @@ -92,8 +92,8 @@ export function parseSelector(selector: string): ParsedSelector { } // Splits a selector into per-frame chunks separated by "enter-frame" boundaries. When the selector -// starts with the "pierce-frames" token, `pierce` is set globally and no "enter-frame" boundaries -// are allowed (piercing already searches every descendant frame), so `chunks` holds a single chunk. +// starts with the "pierce-frames" token, `pierce` is set globally and "enter-frame" tokens are +// preserved, so `chunks` holds a single chunk. export function splitSelectorByFrame(selectorText: string): { pierce: boolean, chunks: ParsedSelector[] } { const selector = parseSelector(selectorText); const chunks: ParsedSelector[] = []; @@ -113,10 +113,13 @@ export function splitSelectorByFrame(selectorText: string): { pierce: boolean, c continue; } if (part.name === 'internal:control' && part.body === 'enter-frame') { - if (pierce) - throw new InvalidSelectorError(`Entering frames is not allowed while piercing frames, while parsing selector ${selectorText}`); - if (!chunk.parts.length) + const lastPart = chunk.parts[chunk.parts.length - 1]; + if (!lastPart || (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame')) throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first'); + if (pierce) { + chunk.parts.push(part); + continue; + } chunks.push(chunk); chunk = { parts: [] }; chunkStartIndex = i + 1; @@ -131,6 +134,9 @@ export function splitSelectorByFrame(selectorText: string): { pierce: boolean, c throw new InvalidSelectorError(`Selector cannot be empty when piercing frames, while parsing selector ${selectorText}`); throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`); } + const lastPart = chunk.parts[chunk.parts.length - 1]; + if (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame') + throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`); chunks.push(chunk); if (typeof selector.capture === 'number' && typeof chunks[chunks.length - 1].capture !== 'number') throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`); diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2999e0c14dac8..4e49c41d1fdb0 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,20 +15,6 @@ "browserVersion": "151.0.7922.47", "title": "Chrome Headless Shell" }, - { - "name": "chromium-tip-of-tree", - "revision": "1433", - "installByDefault": false, - "browserVersion": "151.0.7893.0", - "title": "Chrome Canary for Testing" - }, - { - "name": "chromium-tip-of-tree-headless-shell", - "revision": "1433", - "installByDefault": false, - "browserVersion": "151.0.7893.0", - "title": "Chrome Canary Headless Shell" - }, { "name": "firefox", "revision": "1539", @@ -36,22 +22,13 @@ "browserVersion": "153.0", "title": "Firefox" }, - { - "name": "firefox-beta", - "revision": "1526", - "installByDefault": false, - "browserVersion": "152.0b1", - "title": "Firefox Beta" - }, { "name": "webkit", "revision": "2340", "installByDefault": true, "revisionOverrides": { "mac14": "2251", - "mac14-arm64": "2251", - "ubuntu20.04-x64": "2092", - "ubuntu20.04-arm64": "2092" + "mac14-arm64": "2251" }, "browserVersion": "26.5", "title": "WebKit" diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index dd58b18101734..fe48cab8bd178 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -207,8 +207,6 @@ export class Locator implements api.Locator { } frameLocator(selector: string): FrameLocator { - if (selectorPiercesFrames(this._selector)) - throw new Error(`Entering frames is not allowed while piercing frames.`); return new FrameLocator(this._frame, this._selector + ' >> ' + selector); } @@ -485,8 +483,6 @@ export class FrameLocator implements api.FrameLocator { } frameLocator(selector: string): FrameLocator { - if (selectorPiercesFrames(this._frameSelector)) - throw new Error(`Entering frames is not allowed while piercing frames.`); return new FrameLocator(this._frame, this._childSelector(selector)); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 68d0a9096e21b..fca1112b91256 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -417,8 +417,6 @@ export class Chromium extends BrowserType { override getExecutableName(options: types.LaunchOptions): string { if (options.channel && registry.isChromiumAlias(options.channel)) return 'chromium'; - if (options.channel === 'chromium-tip-of-tree') - return options.headless ? 'chromium-tip-of-tree-headless-shell' : 'chromium-tip-of-tree'; if (options.channel) return options.channel; return options.headless ? 'chromium-headless-shell' : 'chromium'; diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 7233ac85a7422..14397b88e8623 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -136,7 +136,7 @@ export class FrameSelectors { } if (pierce) { - const parsed = chunks[0]; // Only one chunk is allowed with pierce. + const parsed = chunks[0]; // Only one chunk is allowed with pierce, it may contain enter-frame parts. if (parsed.parts.some((part, index) => part.name === 'nth' && index !== parsed.parts.length - 1)) { const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector); throw new InvalidSelectorError(`nth can only be the last locator when piercing frames, while querying "${locator}"`); @@ -186,6 +186,9 @@ export class FrameSelectors { for (const [frame, startIndexes] of candidates) { for (const startIndex of startIndexes) { const suffix = infos.slice(startIndex); + // A leftover "enter-frame" token means we are not going to match anything in this frame. + if (suffix.some(info => isEnterFramePart(info.parsed.parts[0]))) + continue; const partialInfo: SelectorInfo = { parsed: { parts: suffix.map(info => info.parsed.parts[0]) }, world: suffix.some(info => info.world === 'main') ? 'main' : 'utility', @@ -225,8 +228,17 @@ export class FrameSelectors { for (const element of all) next.add(element); } + const nextPart = index + 1 < infos.length ? infos[index + 1].parsed.parts[0] : undefined; + if (nextPart && nextPart.name === 'internal:control' && nextPart.body === 'enter-frame') { + // We must enter the iframe now, so stop matching any further. + for (const { frameElement, nextIndexes } of result) { + if (next.has(frameElement)) + nextIndexes.push(index + 2); + } + break; + } roots = [...next]; - if (index + 1 < infos.length && !['nth', 'visible'].includes(infos[index + 1].parsed.parts[0].name)) { + if (nextPart && !['nth', 'visible'].includes(nextPart.name)) { for (const { frameElement, nextIndexes } of result) { if (roots.some(root => injected.utils.isInsideScope(root, frameElement))) nextIndexes.push(index + 1); @@ -342,6 +354,10 @@ export class FrameSelectors { } } +function isEnterFramePart(part: ParsedSelector['parts'][0]): boolean { + return part.name === 'internal:control' && part.body === 'enter-frame'; +} + async function adoptIfNeeded(handle: ElementHandle, context: FrameExecutionContext): Promise> { if (handle._context === context) return handle; diff --git a/packages/playwright-core/src/server/recorder/recorderSignalProcessor.ts b/packages/playwright-core/src/server/recorder/recorderSignalProcessor.ts index 6c970ef831278..8624cf18632c5 100644 --- a/packages/playwright-core/src/server/recorder/recorderSignalProcessor.ts +++ b/packages/playwright-core/src/server/recorder/recorderSignalProcessor.ts @@ -133,6 +133,12 @@ export class RecorderSignalProcessor { const lastAction = this._lastAction; const signalThreshold = isUnderTest() ? 500 : 5000; + // A duplicate navigation signal to the URL we already recorded a goto for + // (e.g. a second commit for the same about:blank navigation) must not + // produce a second identical goto. + if (lastAction?.action.name === 'navigate' && lastAction.pageGuid === frame._page.guid && lastAction.action.url === frame.url()) + return; + let generateGoto = false; if (!lastAction) generateGoto = true; diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 9b1583e3f3a9a..9552aee0b661c 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -78,22 +78,6 @@ const EXECUTABLE_PATHS = { 'mac-arm64': ['chrome-headless-shell-mac-arm64', 'chrome-headless-shell'], 'win-x64': ['chrome-headless-shell-win64', 'chrome-headless-shell.exe'], }, - 'chromium-tip-of-tree': { - '': undefined, - 'linux-x64': ['chrome-linux64', 'chrome'], - 'linux-arm64': ['chrome-linux', 'chrome'], // non-cft build - 'mac-x64': ['chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'], - 'mac-arm64': ['chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'], - 'win-x64': ['chrome-win64', 'chrome.exe'], - }, - 'chromium-tip-of-tree-headless-shell': { - '': undefined, - 'linux-x64': ['chrome-headless-shell-linux64', 'chrome-headless-shell'], - 'linux-arm64': ['chrome-linux', 'headless_shell'], // non-cft build - 'mac-x64': ['chrome-headless-shell-mac-x64', 'chrome-headless-shell'], - 'mac-arm64': ['chrome-headless-shell-mac-arm64', 'chrome-headless-shell'], - 'win-x64': ['chrome-headless-shell-win64', 'chrome-headless-shell.exe'], - }, 'firefox': { '': undefined, 'linux-x64': ['firefox', 'firefox'], @@ -146,12 +130,12 @@ const DOWNLOAD_PATHS: Record = { 'chromium': { '': undefined, 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': cftUrl('linux64/chrome-linux64.zip'), + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': cftUrl('linux64/chrome-linux64.zip'), 'ubuntu24.04-x64': cftUrl('linux64/chrome-linux64.zip'), 'ubuntu26.04-x64': cftUrl('linux64/chrome-linux64.zip'), 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'ubuntu24.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', 'ubuntu26.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip', @@ -181,12 +165,12 @@ const DOWNLOAD_PATHS: Record = { 'chromium-headless-shell': { '': undefined, 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), 'ubuntu24.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), 'ubuntu26.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', 'ubuntu24.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', 'ubuntu26.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip', @@ -213,85 +197,15 @@ const DOWNLOAD_PATHS: Record = { 'mac26-arm64': cftUrl('mac-arm64/chrome-headless-shell-mac-arm64.zip'), 'win64': cftUrl('win64/chrome-headless-shell-win64.zip'), }, - 'chromium-tip-of-tree': { - '': undefined, - 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': cftUrl('linux64/chrome-linux64.zip'), - 'ubuntu22.04-x64': cftUrl('linux64/chrome-linux64.zip'), - 'ubuntu24.04-x64': cftUrl('linux64/chrome-linux64.zip'), - 'ubuntu26.04-x64': cftUrl('linux64/chrome-linux64.zip'), - 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'ubuntu26.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'debian11-x64': undefined, - 'debian11-arm64': undefined, - 'debian12-x64': cftUrl('linux64/chrome-linux64.zip'), - 'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'debian13-x64': cftUrl('linux64/chrome-linux64.zip'), - 'debian13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip', - 'mac10.13': undefined, - 'mac10.14': undefined, - 'mac10.15': undefined, - 'mac11': undefined, - 'mac11-arm64': undefined, - 'mac12': undefined, - 'mac12-arm64': undefined, - 'mac13': undefined, - 'mac13-arm64': undefined, - 'mac14': cftUrl('mac-x64/chrome-mac-x64.zip'), - 'mac14-arm64': cftUrl('mac-arm64/chrome-mac-arm64.zip'), - 'mac15': cftUrl('mac-x64/chrome-mac-x64.zip'), - 'mac15-arm64': cftUrl('mac-arm64/chrome-mac-arm64.zip'), - 'mac26': cftUrl('mac-x64/chrome-mac-x64.zip'), - 'mac26-arm64': cftUrl('mac-arm64/chrome-mac-arm64.zip'), - 'win64': cftUrl('win64/chrome-win64.zip'), - }, - 'chromium-tip-of-tree-headless-shell': { - '': undefined, - 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'ubuntu22.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'ubuntu24.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'ubuntu26.04-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'ubuntu26.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'debian11-x64': undefined, - 'debian11-arm64': undefined, - 'debian12-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'debian13-x64': cftUrl('linux64/chrome-headless-shell-linux64.zip'), - 'debian13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip', - 'mac10.13': undefined, - 'mac10.14': undefined, - 'mac10.15': undefined, - 'mac11': undefined, - 'mac11-arm64': undefined, - 'mac12': undefined, - 'mac12-arm64': undefined, - 'mac13': undefined, - 'mac13-arm64': undefined, - 'mac14': cftUrl('mac-x64/chrome-headless-shell-mac-x64.zip'), - 'mac14-arm64': cftUrl('mac-arm64/chrome-headless-shell-mac-arm64.zip'), - 'mac15': cftUrl('mac-x64/chrome-headless-shell-mac-x64.zip'), - 'mac15-arm64': cftUrl('mac-arm64/chrome-headless-shell-mac-arm64.zip'), - 'mac26': cftUrl('mac-x64/chrome-headless-shell-mac-x64.zip'), - 'mac26-arm64': cftUrl('mac-arm64/chrome-headless-shell-mac-arm64.zip'), - 'win64': cftUrl('win64/chrome-headless-shell-win64.zip'), - }, 'firefox': { '': undefined, 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': 'builds/firefox/%s/firefox-ubuntu-20.04.zip', + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': 'builds/firefox/%s/firefox-ubuntu-22.04.zip', 'ubuntu24.04-x64': 'builds/firefox/%s/firefox-ubuntu-24.04.zip', 'ubuntu26.04-x64': 'builds/firefox/%s/firefox-ubuntu-24.04.zip', 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/firefox/%s/firefox-ubuntu-20.04-arm64.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/firefox/%s/firefox-ubuntu-22.04-arm64.zip', 'ubuntu24.04-arm64': 'builds/firefox/%s/firefox-ubuntu-24.04-arm64.zip', 'ubuntu26.04-arm64': 'builds/firefox/%s/firefox-ubuntu-24.04-arm64.zip', @@ -318,50 +232,15 @@ const DOWNLOAD_PATHS: Record = { 'mac26-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip', 'win64': 'builds/firefox/%s/firefox-win64.zip', }, - 'firefox-beta': { - '': undefined, - 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-20.04.zip', - 'ubuntu22.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04.zip', - 'ubuntu24.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04.zip', - 'ubuntu26.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04.zip', - 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': undefined, - 'ubuntu22.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip', - 'ubuntu24.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip', - 'ubuntu26.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip', - 'debian11-x64': undefined, - 'debian11-arm64': undefined, - 'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip', - 'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip', - 'debian13-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip', - 'debian13-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip', - 'mac10.13': undefined, - 'mac10.14': undefined, - 'mac10.15': undefined, - 'mac11': undefined, - 'mac11-arm64': undefined, - 'mac12': undefined, - 'mac12-arm64': undefined, - 'mac13': undefined, - 'mac13-arm64': undefined, - 'mac14': 'builds/firefox-beta/%s/firefox-beta-mac.zip', - 'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', - 'mac15': 'builds/firefox-beta/%s/firefox-beta-mac.zip', - 'mac15-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', - 'mac26': 'builds/firefox-beta/%s/firefox-beta-mac.zip', - 'mac26-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip', - 'win64': 'builds/firefox-beta/%s/firefox-beta-win64.zip', - }, 'webkit': { '': undefined, 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': 'builds/webkit/%s/webkit-ubuntu-20.04.zip', + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': 'builds/webkit/%s/webkit-ubuntu-22.04.zip', 'ubuntu24.04-x64': 'builds/webkit/%s/webkit-ubuntu-24.04.zip', 'ubuntu26.04-x64': 'builds/webkit/%s/webkit-ubuntu-26.04.zip', 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/webkit/%s/webkit-ubuntu-20.04-arm64.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/webkit/%s/webkit-ubuntu-22.04-arm64.zip', 'ubuntu24.04-arm64': 'builds/webkit/%s/webkit-ubuntu-24.04-arm64.zip', 'ubuntu26.04-arm64': 'builds/webkit/%s/webkit-ubuntu-26.04-arm64.zip', @@ -391,12 +270,12 @@ const DOWNLOAD_PATHS: Record = { 'ffmpeg': { '': undefined, 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'ubuntu24.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'ubuntu26.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip', 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'ubuntu24.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', 'ubuntu26.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip', @@ -461,12 +340,12 @@ const DOWNLOAD_PATHS: Record = { 'android': { '': 'builds/android/%s/android.zip', 'ubuntu18.04-x64': undefined, - 'ubuntu20.04-x64': 'builds/android/%s/android.zip', + 'ubuntu20.04-x64': undefined, 'ubuntu22.04-x64': 'builds/android/%s/android.zip', 'ubuntu24.04-x64': 'builds/android/%s/android.zip', 'ubuntu26.04-x64': 'builds/android/%s/android.zip', 'ubuntu18.04-arm64': undefined, - 'ubuntu20.04-arm64': 'builds/android/%s/android.zip', + 'ubuntu20.04-arm64': undefined, 'ubuntu22.04-arm64': 'builds/android/%s/android.zip', 'ubuntu24.04-arm64': 'builds/android/%s/android.zip', 'ubuntu26.04-arm64': 'builds/android/%s/android.zip', @@ -717,44 +596,6 @@ export class Registry { _isHermeticInstallation: true, }); - const chromiumTipOfTreeHeadlessShell = descriptors.find(d => d.name === 'chromium-tip-of-tree-headless-shell')!; - const chromiumTipOfTreeHeadlessShellExecutable = findExecutablePath(chromiumTipOfTreeHeadlessShell.dir, 'chromium-tip-of-tree-headless-shell'); - this._executables.push({ - name: 'chromium-tip-of-tree-headless-shell', - browserName: 'chromium', - directory: chromiumTipOfTreeHeadlessShell.dir, - executablePath: () => chromiumTipOfTreeHeadlessShellExecutable, - executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumTipOfTreeHeadlessShellExecutable, chromiumTipOfTreeHeadlessShell.installByDefault, sdkLanguage), - installType: chromiumTipOfTreeHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand', - _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumTipOfTreeHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']), - downloadURLs: this._downloadURLs(chromiumTipOfTreeHeadlessShell), - title: chromiumTipOfTreeHeadlessShell.title, - revision: chromiumTipOfTreeHeadlessShell.revision, - browserVersion: chromiumTipOfTreeHeadlessShell.browserVersion, - _install: force => this._downloadExecutable(chromiumTipOfTreeHeadlessShell, force, chromiumTipOfTreeHeadlessShellExecutable), - _dependencyGroup: 'chromium', - _isHermeticInstallation: true, - }); - - const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!; - const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium-tip-of-tree'); - this._executables.push({ - name: 'chromium-tip-of-tree', - browserName: 'chromium', - directory: chromiumTipOfTree.dir, - executablePath: () => chromiumTipOfTreeExecutable, - executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-tip-of-tree', chromiumTipOfTreeExecutable, chromiumTipOfTree.installByDefault, sdkLanguage), - installType: chromiumTipOfTree.installByDefault ? 'download-by-default' : 'download-on-demand', - _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumTipOfTree.dir, ['chrome-linux'], [], ['chrome-win']), - downloadURLs: this._downloadURLs(chromiumTipOfTree), - title: chromiumTipOfTree.title, - revision: chromiumTipOfTree.revision, - browserVersion: chromiumTipOfTree.browserVersion, - _install: force => this._downloadExecutable(chromiumTipOfTree, force, chromiumTipOfTreeExecutable), - _dependencyGroup: 'chromium', - _isHermeticInstallation: true, - }); - this._executables.push(this._createChromiumChannel('chrome', { 'linux': '/opt/google/chrome/chrome', 'darwin': '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', @@ -858,25 +699,6 @@ export class Registry { _isHermeticInstallation: true, }); - const firefoxBeta = descriptors.find(d => d.name === 'firefox-beta')!; - const firefoxBetaExecutable = findExecutablePath(firefoxBeta.dir, 'firefox'); - this._executables.push({ - name: 'firefox-beta', - browserName: 'firefox', - directory: firefoxBeta.dir, - executablePath: () => firefoxBetaExecutable, - executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('firefox-beta', firefoxBetaExecutable, firefoxBeta.installByDefault, sdkLanguage), - installType: firefoxBeta.installByDefault ? 'download-by-default' : 'download-on-demand', - _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, firefoxBeta.dir, ['firefox'], [], ['firefox']), - downloadURLs: this._downloadURLs(firefoxBeta), - title: firefoxBeta.title, - revision: firefoxBeta.revision, - browserVersion: firefoxBeta.browserVersion, - _install: force => this._downloadExecutable(firefoxBeta, force, firefoxBetaExecutable), - _dependencyGroup: 'firefox', - _isHermeticInstallation: true, - }); - const webkit = descriptors.find(d => d.name === 'webkit')!; const webkitExecutable = findExecutablePath(webkit.dir, 'webkit'); const webkitLinuxLddDirectories = [ @@ -1439,9 +1261,9 @@ export class Registry { private _defaultBrowsersToInstall(options: { shell?: 'no' | 'only' }): Executable[] { let executables = this.defaultExecutables(); if (options.shell === 'no') - executables = executables.filter(e => e.name !== 'chromium-headless-shell' && e.name !== 'chromium-tip-of-tree-headless-shell'); + executables = executables.filter(e => e.name !== 'chromium-headless-shell'); if (options.shell === 'only') - executables = executables.filter(e => e.name !== 'chromium' && e.name !== 'chromium-tip-of-tree'); + executables = executables.filter(e => e.name !== 'chromium'); return executables; } @@ -1477,11 +1299,6 @@ export class Registry { handleArgument('chromium'); if (options.shell !== 'no') handleArgument('chromium-headless-shell'); - } else if (alias === 'chromium-tip-of-tree') { - if (options.shell !== 'only') - handleArgument('chromium-tip-of-tree'); - if (options.shell !== 'no') - handleArgument('chromium-tip-of-tree-headless-shell'); } else { handleArgument(alias); } diff --git a/packages/playwright-core/src/server/registry/nativeDeps.ts b/packages/playwright-core/src/server/registry/nativeDeps.ts index 5e02aabbafd57..1a75f17cb92d7 100644 --- a/packages/playwright-core/src/server/registry/nativeDeps.ts +++ b/packages/playwright-core/src/server/registry/nativeDeps.ts @@ -16,224 +16,9 @@ // - This file is used to execute 'npx playwright install-deps' // - The reverse mappings "lib2package" are generated with the following script: -// ./utils/linux-browser-dependencies/run.sh ubuntu:20.04 +// ./utils/linux-browser-dependencies/run.sh ubuntu:22.04 export const deps: any = { - 'ubuntu20.04-x64': { - tools: [ - 'xvfb', - 'fonts-noto-color-emoji', - 'ttf-unifont', - 'libfontconfig', - 'libfreetype6', - 'xfonts-cyrillic', - 'xfonts-scalable', - 'fonts-liberation', - 'fonts-ipafont-gothic', - 'fonts-wqy-zenhei', - 'fonts-tlwg-loma-otf', - 'ttf-ubuntu-font-family', - ], - chromium: [ - 'fonts-liberation', - 'libasound2', - 'libatk-bridge2.0-0', - 'libatk1.0-0', - 'libatspi2.0-0', - 'libcairo2', - 'libcups2', - 'libdbus-1-3', - 'libdrm2', - 'libegl1', - 'libgbm1', - 'libglib2.0-0', - 'libgtk-3-0', - 'libnspr4', - 'libnss3', - 'libpango-1.0-0', - 'libx11-6', - 'libx11-xcb1', - 'libxcb1', - 'libxcomposite1', - 'libxdamage1', - 'libxext6', - 'libxfixes3', - 'libxrandr2', - 'libxshmfence1', - ], - firefox: [ - 'ffmpeg', - 'libatk1.0-0', - 'libcairo-gobject2', - 'libcairo2', - 'libdbus-1-3', - 'libdbus-glib-1-2', - 'libfontconfig1', - 'libfreetype6', - 'libgdk-pixbuf2.0-0', - 'libglib2.0-0', - 'libgtk-3-0', - 'libpango-1.0-0', - 'libpangocairo-1.0-0', - 'libpangoft2-1.0-0', - 'libx11-6', - 'libx11-xcb1', - 'libxcb-shm0', - 'libxcb1', - 'libxcomposite1', - 'libxcursor1', - 'libxdamage1', - 'libxext6', - 'libxfixes3', - 'libxi6', - 'libxrender1', - 'libxt6', - 'libxtst6' - ], - webkit: [ - 'libenchant-2-2', - 'libflite1', - 'libx264-155', - 'libatk-bridge2.0-0', - 'libatk1.0-0', - 'libcairo2', - 'libegl1', - 'libenchant1c2a', - 'libepoxy0', - 'libevdev2', - 'libfontconfig1', - 'libfreetype6', - 'libgdk-pixbuf2.0-0', - 'libgl1', - 'libgles2', - 'libglib2.0-0', - 'libgtk-3-0', - 'libgudev-1.0-0', - 'libharfbuzz-icu0', - 'libharfbuzz0b', - 'libhyphen0', - 'libicu66', - 'libjpeg-turbo8', - 'libnghttp2-14', - 'libnotify4', - 'libopengl0', - 'libopenjp2-7', - 'libopus0', - 'libpango-1.0-0', - 'libpng16-16', - 'libsecret-1-0', - 'libvpx6', - 'libwayland-client0', - 'libwayland-egl1', - 'libwayland-server0', - 'libwebp6', - 'libwebpdemux2', - 'libwoff1', - 'libx11-6', - 'libxcomposite1', - 'libxdamage1', - 'libxkbcommon0', - 'libxml2', - 'libxslt1.1', - 'libatomic1', - 'libevent-2.1-7', - ], - lib2package: { - 'libflite.so.1': 'libflite1', - 'libflite_usenglish.so.1': 'libflite1', - 'libflite_cmu_grapheme_lang.so.1': 'libflite1', - 'libflite_cmu_grapheme_lex.so.1': 'libflite1', - 'libflite_cmu_indic_lang.so.1': 'libflite1', - 'libflite_cmu_indic_lex.so.1': 'libflite1', - 'libflite_cmulex.so.1': 'libflite1', - 'libflite_cmu_time_awb.so.1': 'libflite1', - 'libflite_cmu_us_awb.so.1': 'libflite1', - 'libflite_cmu_us_kal16.so.1': 'libflite1', - 'libflite_cmu_us_kal.so.1': 'libflite1', - 'libflite_cmu_us_rms.so.1': 'libflite1', - 'libflite_cmu_us_slt.so.1': 'libflite1', - 'libx264.so': 'libx264-155', - 'libasound.so.2': 'libasound2', - 'libatk-1.0.so.0': 'libatk1.0-0', - 'libatk-bridge-2.0.so.0': 'libatk-bridge2.0-0', - 'libatspi.so.0': 'libatspi2.0-0', - 'libcairo-gobject.so.2': 'libcairo-gobject2', - 'libcairo.so.2': 'libcairo2', - 'libcups.so.2': 'libcups2', - 'libdbus-1.so.3': 'libdbus-1-3', - 'libdbus-glib-1.so.2': 'libdbus-glib-1-2', - 'libdrm.so.2': 'libdrm2', - 'libEGL.so.1': 'libegl1', - 'libenchant.so.1': 'libenchant1c2a', - 'libevdev.so.2': 'libevdev2', - 'libepoxy.so.0': 'libepoxy0', - 'libfontconfig.so.1': 'libfontconfig1', - 'libfreetype.so.6': 'libfreetype6', - 'libgbm.so.1': 'libgbm1', - 'libgdk_pixbuf-2.0.so.0': 'libgdk-pixbuf2.0-0', - 'libgdk-3.so.0': 'libgtk-3-0', - 'libgdk-x11-2.0.so.0': 'libgtk2.0-0', - 'libgio-2.0.so.0': 'libglib2.0-0', - 'libGL.so.1': 'libgl1', - 'libGLESv2.so.2': 'libgles2', - 'libglib-2.0.so.0': 'libglib2.0-0', - 'libgmodule-2.0.so.0': 'libglib2.0-0', - 'libgobject-2.0.so.0': 'libglib2.0-0', - 'libgthread-2.0.so.0': 'libglib2.0-0', - 'libgtk-3.so.0': 'libgtk-3-0', - 'libgtk-x11-2.0.so.0': 'libgtk2.0-0', - 'libgudev-1.0.so.0': 'libgudev-1.0-0', - 'libharfbuzz-icu.so.0': 'libharfbuzz-icu0', - 'libharfbuzz.so.0': 'libharfbuzz0b', - 'libhyphen.so.0': 'libhyphen0', - 'libicui18n.so.66': 'libicu66', - 'libicuuc.so.66': 'libicu66', - 'libjpeg.so.8': 'libjpeg-turbo8', - 'libnotify.so.4': 'libnotify4', - 'libnspr4.so': 'libnspr4', - 'libnss3.so': 'libnss3', - 'libnssutil3.so': 'libnss3', - 'libOpenGL.so.0': 'libopengl0', - 'libopenjp2.so.7': 'libopenjp2-7', - 'libopus.so.0': 'libopus0', - 'libpango-1.0.so.0': 'libpango-1.0-0', - 'libpangocairo-1.0.so.0': 'libpangocairo-1.0-0', - 'libpangoft2-1.0.so.0': 'libpangoft2-1.0-0', - 'libpng16.so.16': 'libpng16-16', - 'libsecret-1.so.0': 'libsecret-1-0', - 'libsmime3.so': 'libnss3', - 'libvpx.so.6': 'libvpx6', - 'libwayland-client.so.0': 'libwayland-client0', - 'libwayland-egl.so.1': 'libwayland-egl1', - 'libwayland-server.so.0': 'libwayland-server0', - 'libwebp.so.6': 'libwebp6', - 'libwebpdemux.so.2': 'libwebpdemux2', - 'libwoff2dec.so.1.0.2': 'libwoff1', - 'libX11-xcb.so.1': 'libx11-xcb1', - 'libX11.so.6': 'libx11-6', - 'libxcb-dri3.so.0': 'libxcb-dri3-0', - 'libxcb-shm.so.0': 'libxcb-shm0', - 'libxcb.so.1': 'libxcb1', - 'libXcomposite.so.1': 'libxcomposite1', - 'libXcursor.so.1': 'libxcursor1', - 'libXdamage.so.1': 'libxdamage1', - 'libXext.so.6': 'libxext6', - 'libXfixes.so.3': 'libxfixes3', - 'libXi.so.6': 'libxi6', - 'libxkbcommon.so.0': 'libxkbcommon0', - 'libxml2.so.2': 'libxml2', - 'libXrandr.so.2': 'libxrandr2', - 'libXrender.so.1': 'libxrender1', - 'libxslt.so.1': 'libxslt1.1', - 'libXt.so.6': 'libxt6', - 'libXtst.so.6': 'libxtst6', - 'libxshmfence.so.1': 'libxshmfence1', - 'libatomic.so.1': 'libatomic1', - 'libenchant-2.so.2': 'libenchant-2-2', - 'libevent-2.1.so.7': 'libevent-2.1-7', - }, - }, - 'ubuntu22.04-x64': { tools: [ 'xvfb', @@ -1204,20 +989,6 @@ export const deps: any = { }, }; -deps['ubuntu20.04-arm64'] = { - tools: [...deps['ubuntu20.04-x64'].tools], - chromium: [...deps['ubuntu20.04-x64'].chromium], - firefox: [ - ...deps['ubuntu20.04-x64'].firefox, - ], - webkit: [ - ...deps['ubuntu20.04-x64'].webkit, - ], - lib2package: { - ...deps['ubuntu20.04-x64'].lib2package, - }, -}; - deps['ubuntu22.04-arm64'] = { tools: [...deps['ubuntu22.04-x64'].tools], chromium: [...deps['ubuntu22.04-x64'].chromium], diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index c694d28404bcb..cd2b1689cbbae 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -958,13 +958,8 @@ export class WKPage implements PageDelegate { const buffer = Buffer.from(event.data, 'base64'); void this._page.screencast.onScreencastFrame({ buffer, - frameSwapWallTime: event.timestamp - // timestamp is in seconds, we need to convert to milliseconds. - ? event.timestamp * 1000 - // Fallback for Ubuntu 20.04 where WebKit is frozen on an older - // version that did not send timestamp. - // TODO: remove this fallback when Ubuntu 20.04 is EOL. - : Date.now(), + // timestamp is in seconds, we need to convert to milliseconds. + frameSwapWallTime: event.timestamp * 1000, viewportWidth: event.deviceWidth, viewportHeight: event.deviceHeight, }).then(() => { diff --git a/packages/utils/hostPlatform.ts b/packages/utils/hostPlatform.ts index 82974b982c7c6..d633ca383aee2 100644 --- a/packages/utils/hostPlatform.ts +++ b/packages/utils/hostPlatform.ts @@ -88,7 +88,7 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte if (major < 20) return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; if (major < 22) - return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' }; + return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; if (major < 24) return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' }; if (major < 26) diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index c350201b64f3a..651d14c602a11 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -99,8 +99,7 @@ const test = baseTest.extend { const isShell = channel === 'chromium-headless-shell' || (!channel && headless); - const isToTShell = channel === 'chromium-tip-of-tree-headless-shell' || (channel === 'chromium-tip-of-tree' && headless); - await use(browserName === 'chromium' && (isShell || isToTShell)); + await use(browserName === 'chromium' && isShell); }, { scope: 'worker' }], isFrozenWebkit: [async ({ browserName, isMac, macVersion }, use) => { diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts index 0a961f53945ca..249611fd43a00 100644 --- a/tests/library/browsercontext-cookies.spec.ts +++ b/tests/library/browsercontext-cookies.spec.ts @@ -356,7 +356,6 @@ it('should add cookies with an expiration', async ({ context }) => { it('should support requestStorageAccess', async ({ page, server, channel, browserName, isMac, isLinux, isWindows, macVersion }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17285' }); it.skip(browserName === 'chromium', 'requestStorageAccess API is not available in Chromium'); - it.skip(channel === 'firefox-beta', 'hasStorageAccess returns true, but no cookie is sent'); server.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', 'name=value; Path=/'); diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index bc9707ba69a06..bb437b33e6e54 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -21,7 +21,7 @@ import { expect, playwrightTest as it } from '../config/browserTest'; const { compare } = utils; it.skip(({ headless }) => headless, 'avoid popping windows in headless mode'); -it.skip(({ channel }) => channel === 'chromium-headless-shell' || channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); +it.skip(({ channel }) => channel === 'chromium-headless-shell', 'shell is never headed'); it('should have default url when launching browser @smoke', async ({ launchPersistent }) => { const { context } = await launchPersistent(); diff --git a/tests/library/launcher.spec.ts b/tests/library/launcher.spec.ts index ba29451d5c4f0..1bbe08616625a 100644 --- a/tests/library/launcher.spec.ts +++ b/tests/library/launcher.spec.ts @@ -45,7 +45,6 @@ it('should kill browser process on timeout after close', async ({ browserType, m it('should throw a friendly error if its headed and there is no xserver on linux running', async ({ mode, browserType, platform, channel }) => { it.skip(platform !== 'linux'); it.skip(channel === 'chromium-headless-shell', 'shell is never headed'); - it.skip(channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); const error: Error = await browserType.launch({ headless: false, diff --git a/tests/page/locator-pierce-frames.spec.ts b/tests/page/locator-pierce-frames.spec.ts index 5ad3bd4a5be8a..7939a48b38e1c 100644 --- a/tests/page/locator-pierce-frames.spec.ts +++ b/tests/page/locator-pierce-frames.spec.ts @@ -299,27 +299,121 @@ it('should pierce only frames inside the starting frame', async ({ page, server await expect(middleFrame.pierceFrames().locator('button')).toHaveText('deep'); }); +it('should enter a frame found in a nested frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().frameLocator('#target').locator('button')).toHaveText('inside'); +}); + +it('should click inside an entered frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await page.pierceFrames().frameLocator('#target').getByRole('button', { name: 'Click me' }).click(); + const frame = page.frames().find(f => f.url().includes('b.html'))!; + expect(await frame.evaluate(() => (window as any).__clicked)).toBe(true); +}); + +it('should pierce frames inside the entered frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().frameLocator('#target').locator('button')).toHaveText('deep'); +}); + +it('should support two frameLocators while piercing', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().frameLocator('#x').frameLocator('#y').locator('button')).toHaveText('bottom'); +}); + +it('should support locator before frameLocator while piercing', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
`); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().locator('section').frameLocator('iframe').locator('button')).toHaveText('in-section'); +}); + +it('should support owner of a frameLocator while piercing', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + expect(await page.pierceFrames().frameLocator('#target').owner().getAttribute('id')).toBe('target'); +}); + +it('should wait for the frame to enter to appear', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Nothing yet
`); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(2); + await page.frames()[1].evaluate(() => { + window.builtins.setTimeout(() => { + const iframe = document.createElement('iframe'); + iframe.id = 'late'; + iframe.src = 'b.html'; + document.body.appendChild(iframe); + }, 3000); + }); + await expect(page.pierceFrames().frameLocator('#late').locator('button')).toHaveText('late'); +}); + +it('should fail when the frame to enter matches in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(5); + for (const frame of page.frames()) { + if (frame.url().includes('c.html')) + await frame.waitForSelector('button', { state: 'attached' }); + } + const error = await page.pierceFrames().frameLocator('.inner').locator('button').click({ timeout: 3000 }).catch(e => e); + expect(error.message).toContain('Pierce-frame mode matched elements from multiple frames'); +}); + +it('should support contentFrame while piercing', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().locator('#target').contentFrame().locator('button')).toHaveText('inside'); +}); + +it('should enter only the frame element when the selector matches other elements too', async ({ page, server }) => { + await routePage(page, 'empty.html', `
not a frame
`); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', `
found
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.pierceFrames().locator('.foo').contentFrame().locator('#target')).toHaveText('found'); +}); + +it('should render frameLocator while piercing in the locator description', async ({ page }) => { + expect(String(page.pierceFrames().frameLocator('#x').locator('button'))).toBe(`pierceFrames().locator('#x').contentFrame().locator('button')`); + expect(String(page.pierceFrames().locator('section').frameLocator('iframe').getByText('foo'))).toBe(`pierceFrames().locator('section').locator('iframe').contentFrame().getByText('foo')`); +}); + it('should not allow nth in the middle', async ({ page }) => { const error = await page.pierceFrames().locator('div').first().locator('span').count().catch(e => e); expect(error.message).toContain(`nth can only be the last locator when piercing frames, while querying "pierceFrames().locator('div').first().locator('span')"`); }); -it('should not allow frameLocator after pierceFrames', async ({ page }) => { - expect(() => page.pierceFrames().frameLocator('iframe')).toThrow('Entering frames is not allowed while piercing frames'); - expect(() => page.pierceFrames().locator('div').frameLocator('iframe')).toThrow('Entering frames is not allowed while piercing frames'); -}); - it('should not allow first/last/nth after pierceFrames', async ({ page }) => { expect(() => page.pierceFrames().first()).toThrow('Selecting the nth frame is not allowed while piercing frames'); expect(() => page.pierceFrames().last()).toThrow('Selecting the nth frame is not allowed while piercing frames'); expect(() => page.pierceFrames().nth(1)).toThrow('Selecting the nth frame is not allowed while piercing frames'); -}); - -it('should not allow chaining pierce-frames and enter-frame selectors', async ({ page }) => { - const error1 = await page.locator('internal:control=pierce-frames >> iframe >> internal:control=enter-frame >> button').count().catch(e => e); - expect(error1.message).toContain('Entering frames is not allowed while piercing frames'); - const error2 = await page.locator('iframe >> internal:control=enter-frame >> internal:control=pierce-frames >> button').count().catch(e => e); - expect(error2.message).toContain('"pierce-frames" is only allowed as the first selector token'); + expect(() => page.pierceFrames().frameLocator('#x').first()).toThrow('Selecting the nth frame is not allowed while piercing frames'); }); it('should not allow composite locators', async ({ page }) => { diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index d2a85e53bb3d5..e0051d11afe21 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -440,13 +440,13 @@ it('should omit images without an accessible name', async ({ page }) => { `); const snapshot = await snapshotForAI(page); - // A nameless image carries no information and is omitted, whether or not it is clickable. Only - // the named image is kept - and the body wrapper, left with a single child, is unwrapped. + // A nameless image that cannot be clicked carries no information and is omitted. expect(snapshot).toContainYaml(` - - img "A cat" [ref=e3] + - generic [active] [ref=e1]: + - img "A cat" [ref=e3] + - img [ref=e4] [cursor=pointer] `); expect(snapshot).not.toContain('[ref=e2]'); - expect(snapshot).not.toContain('[ref=e4]'); }); it('should omit a nameless image nested inside a link', async ({ page }) => { @@ -463,6 +463,27 @@ it('should omit a nameless image nested inside a link', async ({ page }) => { expect(snapshot).not.toContain('img'); }); +it('should keep icon-only clickable elements', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42013' }, +}, async ({ page }) => { + const icon = ``; + await page.setContent(` +
${icon}
+ + ${icon} +
${icon}
+ `); + + const snapshot = await snapshotForAI(page); + expect(snapshot).toContainYaml(` + - generic [active] [ref=e1]: + - generic [ref=e2] [cursor=pointer] + - img [ref=e5] [cursor=pointer] + - link [ref=e6] [cursor=pointer]: + - /url: /target + `); +}); + it('should omit leaf generic whose text is already in an ancestor name', async ({ page }) => { // The inner element is block so it survives as its own generic node (an inline single-text span // would be collapsed into the link instead). It inherits the link's pointer cursor. diff --git a/tests/page/selectors-frame.spec.ts b/tests/page/selectors-frame.spec.ts index 8a305157bbc91..c12a6c38ff57b 100644 --- a/tests/page/selectors-frame.spec.ts +++ b/tests/page/selectors-frame.spec.ts @@ -388,9 +388,20 @@ it('should not allow pierce-frames in the middle of a selector', async ({ page, expect(error.message).toContain('"pierce-frames" is only allowed as the first selector token'); }); -it('should not allow entering frames while piercing', async ({ page, server }) => { +it('should allow entering frames while piercing', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const error = await page.locator('internal:control=pierce-frames >> iframe >> internal:control=enter-frame >> div').waitFor().catch(e => e); - expect(error.message).toContain('Entering frames is not allowed while piercing frames'); + const button = page.locator('internal:control=pierce-frames >> iframe[src="iframe-2.html"] >> internal:control=enter-frame >> button'); + await button.waitFor(); + expect(await button.innerText()).toBe('Hello nested iframe'); +}); + +it('should not allow pierce-frames after entering a frame', async ({ page }) => { + const error = await page.locator('iframe >> internal:control=enter-frame >> internal:control=pierce-frames >> button').count().catch(e => e); + expect(error.message).toContain('"pierce-frames" is only allowed as the first selector token'); +}); + +it('should not allow dangling enter-frame while piercing', async ({ page }) => { + const error = await page.locator('internal:control=pierce-frames >> iframe >> internal:control=enter-frame').count().catch(e => e); + expect(error.message).toContain('Selector cannot end with entering frame'); }); diff --git a/utils/roll_browser.js b/utils/roll_browser.js index a177d44dc8e06..f04030c4a7f6c 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -32,9 +32,9 @@ usage: ${SCRIPT_NAME} [version] Roll the to a specific and generate new protocol. Version is required for chromium-based browsers. -Supported browsers: chromium, firefox, webkit, ffmpeg, firefox-beta. +Supported browsers: chromium, firefox, webkit, ffmpeg. -Rolling firefox or firefox-beta requires a playwright-browsers checkout +Rolling firefox requires a playwright-browsers checkout next to the playwright checkout, to roll browser patches from upstream. Set PW_BROWSERS_CHECKOUT to point to a checkout in a custom location. @@ -63,7 +63,6 @@ Example: const browserName = { 'cr': 'chromium', 'ff': 'firefox', - 'ff-beta': 'firefox-beta', 'wk': 'webkit', }[args[0].toLowerCase()] ?? args[0].toLowerCase(); const browserTypeName = browserName.split('-')[0];