Compare commits

...

13 commits

Author SHA1 Message Date
Max Schmitt a70a96ab25
chore: mark v1.49.0 (#33649) 2024-11-18 19:31:20 +01:00
Pavel Feldman 53f51a8cf1 cherry-pick(#33638): chore: clear highlight when performing action 2024-11-16 07:57:30 -08:00
Pavel Feldman 2a00ca8453 cherry-pick(#33635): chore: add cm placeholder text 2024-11-15 16:59:49 -08:00
Pavel Feldman 0e6434013b cherry-pick(#33632): chore: highlight edited locator while recording 2024-11-15 14:22:08 -08:00
Dmitry Gozman cb0f456e46
cherry-pick(#33629): fix(rebase): do not apply multiple rebaselines to the same assertion (#33630) 2024-11-15 12:49:54 -08:00
Max Schmitt 698823a78e cherry-pick(#33627): fix(codegen): document.documentElement is null on early navigation 2024-11-15 17:16:41 +01:00
Dmitry Gozman c0fa804367
cherry-pick(#33619): fix(aria): normalize whitespace in toMatchAccessible{Name,Description} (#33621) 2024-11-15 04:06:44 -08:00
Yury Semikhatsky 7a32228aed
cherry-pick(#33614): docs: add ariaSnapshot.timeout for language ports (#33615) 2024-11-14 12:40:44 -08:00
Simon Knott 0e31acea8f
cherry-pick(#33575): fix(canvas snapshots): position mismatch in headless mode 2024-11-14 15:43:48 +01:00
Dmitry Gozman b2a39ffc61 cherry-pick(#33604): docs: update docs about headless shell 2024-11-14 13:40:37 +00:00
Dmitry Gozman 1eea46bd66 cherry-pick(#33603): chore: update headless shell treatment 2024-11-14 13:39:03 +00:00
Dmitry Gozman 4c53e56cb4
cherry-pick(#33583): fix(merge): update error.cause location (#33601) 2024-11-14 03:03:34 -08:00
Pavel Feldman 3f36d7ff51 cherry-pick(#33594): chore: allow highlighting aria template from extension 2024-11-13 21:34:50 -08:00
61 changed files with 472 additions and 298 deletions

View file

@ -268,29 +268,8 @@ jobs:
- run: npx playwright install-deps
- run: utils/build/build-playwright-driver.sh
test_linux_chromium_headless_shell:
name: Chromium Headless Shell
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
runs-on: [ubuntu-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium chromium-headless-shell
command: npm run ctest
bot-name: "headless-shell-${{ matrix.runs-on }}"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chromium-headless-shell
test_chromium_next:
name: Test chromium-next channel
test_channel_chromium:
name: Test channel=chromium
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
@ -301,11 +280,13 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
# TODO: this should pass --no-shell.
# However, codegen tests do not inherit the channel and try to launch headless shell.
browsers-to-install: chromium
command: npm run ctest
bot-name: "chromium-next-${{ matrix.runs-on }}"
bot-name: "channel-chromium-${{ matrix.runs-on }}"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chromium-next
PWTEST_CHANNEL: chromium

View file

@ -206,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot:
- link "About"
```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
* since: v1.49
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49

View file

@ -2159,3 +2159,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49

View file

@ -338,30 +338,85 @@ dotnet test --settings:webkit.runsettings
For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later.
Playwright ships a regular Chromium build for headed operations and a separate [Chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. These two behave differently in some edge cases, but the majority of testing scenarios are not affected. Note this behavior has changed in Playwright version 1.49, see [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
#### Save on download size
#### Optimize download size on CI
If you are only running tests in headless, for example on CI, you can avoid downloading a headed version of Chromium by specifying `chromium-headless-shell` during installation.
If you are only running tests in headless mode, for example on CI, you can avoid downloading a regular version of Chromium by passing `--only-shell` during installation.
```bash js
# When only running tests headlessly
npx playwright install chromium-headless-shell firefox webkit
# only running tests headlessly
npx playwright install --with-deps --only-shell
```
```bash java
# When only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium-headless-shell firefox webkit"
# only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --only-shell"
```
```bash python
# When only running tests headlessly
playwright install chromium-headless-shell firefox webkit
# only running tests headlessly
playwright install --with-deps --only-shell
```
```bash csharp
# When only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install chromium-headless-shell firefox webkit
# only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell
```
#### Opt-in to new headless mode
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
```js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
},
],
});
```
```java
import com.microsoft.playwright.*;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
Page page = browser.newPage();
// ...
}
}
}
```
```bash python
pytest test_login.py --browser-channel chromium
```
```xml csharp
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
<LaunchOptions>
<Channel>chromium</Channel>
</LaunchOptions>
</Playwright>
</RunSettings>
```
```bash csharp
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
```
### Google Chrome & Microsoft Edge

View file

@ -40,51 +40,13 @@ Learn more in the [aria snapshots guide](./aria-snapshots).
### Breaking: channels `chrome`, `msedge` and similar switch to new headless
Prior to this release, Playwright was running the old established implementation of [Chromium headless mode](https://developer.chrome.com/docs/chromium/headless). However, Chromium had entirely **switched to the new headless mode**, and **removed the old one**.
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
![Chromium Headless](https://github.com/user-attachments/assets/2829e86a-dfe2-4743-a6d4-2aa65beea890)
#### What do I need to do?
If you are using a browser channel, for example `'chrome'` or `'msedge'`, the headless mode switch **will affect you**. Most likely, you will have to update some of your tests and all of your screenshot expectations. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
#### Chromium headless shell
Starting with this release, Playwright downloads and runs two different browser builds - one is a regular headed chromium and the other is a chromium headless shell. This should be transparent to you, **no action is needed**. You can learn more in [issue #33566](https://github.com/microsoft/playwright/issues/33566).
If you are only running tests in headless, for example on CI, you can avoid downloading a headed version of Chromium by specifying `chromium-headless-shell` during installation.
```bash
# only running tests headlessly
npx playwright install chromium-headless-shell firefox webkit
```
Playwright will skip downloading headed chromium build, and will use `chromium-headless-shell` when running headless.
#### Opt-in to new headless
We encourage everyone to try and switch to the new headless by using the `chromium-next` channel.
First, install this channel prior to running tests. Make sure to list all the browsers that you use.
```bash
npx playwright install chromium-next firefox webkit
```
Then update your config file to specify `'chromium-next'` channel.
```js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
channel: 'chromium-next',
},
},
],
});
```
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
### Other breaking changes
@ -92,6 +54,27 @@ export default defineConfig({
- Package `@playwright/experimental-ct-vue2` will no longer be updated.
- Package `@playwright/experimental-ct-solid` will no longer be updated.
### Try new Chromium headless
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
```js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
},
],
});
```
### Miscellaneous
- `<canvas>` elements inside a snapshot now draw a preview.

60
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.49.0-next",
"version": "1.49.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -7733,10 +7733,10 @@
"version": "0.0.0"
},
"packages/playwright": {
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"bin": {
"playwright": "cli.js"
@ -7750,11 +7750,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"engines": {
"node": ">=18"
@ -7762,11 +7762,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"engines": {
"node": ">=18"
@ -7774,22 +7774,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"engines": {
"node": ">=18"
}
},
"packages/playwright-chromium": {
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"bin": {
"playwright": "cli.js"
@ -7799,7 +7799,7 @@
}
},
"packages/playwright-core": {
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -7810,11 +7810,11 @@
},
"packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.49.0-next",
"playwright-core": "1.49.0-next",
"playwright": "1.49.0",
"playwright-core": "1.49.0",
"vite": "^5.2.8"
},
"engines": {
@ -7823,10 +7823,10 @@
},
"packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -7838,10 +7838,10 @@
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -7853,10 +7853,10 @@
},
"packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
@ -7871,10 +7871,10 @@
},
"packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
@ -7885,11 +7885,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"bin": {
"playwright": "cli.js"
@ -7900,10 +7900,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.49.0-next",
"version": "1.49.0",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.49.0-next"
"playwright": "1.49.0"
},
"bin": {
"playwright": "cli.js"
@ -7913,11 +7913,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.49.0-next",
"version": "1.49.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"bin": {
"playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -24,4 +24,4 @@ try {
}
if (install)
install(['chromium', 'ffmpeg']);
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-chromium",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright package that automatically installs Chromium",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-firefox",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright package that automatically installs Firefox",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-webkit",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright package that automatically installs WebKit",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -24,4 +24,4 @@ try {
}
if (install)
install(['chromium', 'ffmpeg']);
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);

View file

@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate Chromium",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-core",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -96,16 +96,42 @@ function suggestedBrowsersToInstall() {
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
}
function checkBrowsersToInstall(args: string[]): Executable[] {
function defaultBrowsersToInstall(options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
let executables = registry.defaultExecutables();
if (options.noShell)
executables = executables.filter(e => e.name !== 'chromium-headless-shell');
if (options.onlyShell)
executables = executables.filter(e => e.name !== 'chromium');
return executables;
}
function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
if (options.noShell && options.onlyShell)
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
const faultyArguments: string[] = [];
const executables: Executable[] = [];
for (const arg of args) {
const handleArgument = (arg: string) => {
const executable = registry.findExecutable(arg);
if (!executable || executable.installType === 'none')
faultyArguments.push(arg);
else
executables.push(executable);
if (executable?.browserName === 'chromium')
executables.push(registry.findExecutable('ffmpeg')!);
};
for (const arg of args) {
if (arg === 'chromium') {
if (!options.onlyShell)
handleArgument('chromium');
if (!options.noShell)
handleArgument('chromium-headless-shell');
} else {
handleArgument(arg);
}
}
if (faultyArguments.length)
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
return executables;
@ -118,7 +144,12 @@ program
.option('--with-deps', 'install system dependencies for browsers')
.option('--dry-run', 'do not execute installation, only print information')
.option('--force', 'force reinstall of stable browser channels')
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean }) {
.option('--only-shell', 'only install headless shell when installing chromium')
.option('--no-shell', 'do not install chromium headless shell')
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean, shell?: boolean, noShell?: boolean, onlyShell?: boolean }) {
// For '--no-shell' option, commander sets `shell: false` instead.
if (options.shell === false)
options.noShell = true;
if (isLikelyNpxGlobal()) {
console.error(wrapInASCIIBox([
`WARNING: It looks like you are running 'npx playwright install' without first`,
@ -141,7 +172,7 @@ program
}
try {
const hasNoArguments = !args.length;
const executables = hasNoArguments ? registry.defaultExecutables() : checkBrowsersToInstall(args);
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
if (options.withDeps)
await registry.installDeps(executables, !!options.dryRun);
if (options.dryRun) {
@ -199,9 +230,9 @@ program
.action(async function(args: string[], options: { dryRun?: boolean }) {
try {
if (!args.length)
await registry.installDeps(registry.defaultExecutables(), !!options.dryRun);
await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
else
await registry.installDeps(checkBrowsersToInstall(args), !!options.dryRun);
await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
} catch (e) {
console.log(`Failed to install browser dependencies\n${e}`);
gracefullyProcessExitDoNotHang(1);

View file

@ -422,7 +422,8 @@ scheme.DebugControllerSetRecorderModeParams = tObject({
});
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
scheme.DebugControllerHighlightParams = tObject({
selector: tString,
selector: tOptional(tString),
ariaTemplate: tOptional(tString),
});
scheme.DebugControllerHighlightResult = tOptional(tObject({}));
scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));

View file

@ -15,12 +15,16 @@
*/
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import type { AriaTemplateNode, ParsedYaml } from '@isomorphic/ariaSnapshot';
import { yaml } from '../utilsBundle';
export function parseAriaSnapshot(text: string): AriaTemplateNode {
const fragment = yaml.parse(text);
if (!Array.isArray(fragment))
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
return parseYamlTemplate(fragment);
return parseYamlTemplate(parseYamlForAriaSnapshot(text));
}
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
const parsed = yaml.parse(text);
if (!Array.isArray(parsed))
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
return parsed;
}

View file

@ -24,6 +24,7 @@ import type { Playwright } from './playwright';
import { Recorder } from './recorder';
import { EmptyRecorderApp } from './recorder/recorderApp';
import { asLocator, type Language } from '../utils';
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
const internalMetadata = serverSideCallMetadata();
@ -142,9 +143,13 @@ export class DebugController extends SdkObject {
this._autoCloseTimer = setTimeout(heartBeat, 30000);
}
async highlight(selector: string) {
for (const recorder of await this._allRecorders())
recorder.setHighlightedSelector(this._sdkLanguage, selector);
async highlight(params: { selector?: string, ariaTemplate?: string }) {
for (const recorder of await this._allRecorders()) {
if (params.ariaTemplate)
recorder.setHighlightedAriaTemplate(parseYamlForAriaSnapshot(params.ariaTemplate));
else if (params.selector)
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
}
}
async hideHighlight() {

View file

@ -68,7 +68,7 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
}
async highlight(params: channels.DebugControllerHighlightParams) {
await this._object.highlight(params.selector);
await this._object.highlight(params);
}
async hideHighlight() {

View file

@ -19,7 +19,6 @@ export {
registry,
registryDirectory,
Registry,
installDefaultBrowsersForNpmInstall,
installBrowsersForNpmInstall,
writeDockerVersion } from './registry';

View file

@ -90,7 +90,8 @@ export class Highlight {
}
install() {
if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement))
// NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078
if (this._injectedScript.document.documentElement && !this._injectedScript.document.documentElement.contains(this._glassPaneElement))
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
}

View file

@ -492,10 +492,11 @@ class RecordActionTool implements RecorderTool {
return;
const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
this._activeModel = result && result.selector ? result : null;
if (userGesture)
if (userGesture) {
this._hoveredElement = activeElement as HTMLElement | null;
this._updateModelForHoveredElement();
}
}
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
const target = this._recorder.deepEventTarget(event);
@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool {
}
private _updateModelForHoveredElement() {
if (this._performingActions.size)
return;
if (!this._hoveredElement || !this._hoveredElement.isConnected) {
this._hoveredModel = null;
this._hoveredElement = null;
@ -1018,7 +1021,7 @@ export class Recorder {
private _listeners: (() => void)[] = [];
private _currentTool: RecorderTool;
private _tools: Record<Mode, RecorderTool>;
private _actionSelectorModel: HighlightModel | null = null;
private _lastHighlightedSelector: string | undefined = undefined;
private _lastHighlightedAriaTemplateJSON: string = 'undefined';
readonly highlight: Highlight;
readonly overlay: Overlay | undefined;
@ -1129,12 +1132,12 @@ export class Recorder {
this._switchCurrentTool();
this.overlay?.setUIState(state);
// Race or scroll.
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON)
this._actionSelectorModel = null;
if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector)
this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document);
let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
if (state.actionSelector !== this._lastHighlightedSelector) {
this._lastHighlightedSelector = state.actionSelector;
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
highlight = model?.elements.length ? model : 'clear';
}
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
@ -1142,16 +1145,15 @@ export class Recorder {
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
if (elements.length)
this._actionSelectorModel = { elements };
highlight = { elements };
else
this._actionSelectorModel = null;
highlight = 'clear';
}
if (!state.actionSelector && !state.ariaTemplate)
this._actionSelectorModel = null;
if (this.state.mode === 'none' || this.state.mode === 'standby')
this.updateHighlight(this._actionSelectorModel, false);
if (highlight === 'clear')
this.clearHighlight();
else if (highlight !== 'noop')
this.updateHighlight(highlight, false);
}
clearHighlight() {
@ -1266,6 +1268,8 @@ export class Recorder {
private _onScroll(event: Event) {
if (!event.isTrusted)
return;
this._lastHighlightedSelector = undefined;
this._lastHighlightedAriaTemplateJSON = 'undefined';
this.highlight.hideActionPoint();
this._currentTool.onScroll?.(event);
}

View file

@ -40,7 +40,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
readonly handleSIGINT: boolean | undefined;
private _context: BrowserContext;
private _mode: Mode;
private _highlightedElement: { selector?: string, ariaSnapshot?: ParsedYaml } = {};
private _highlightedElement: { selector?: string, ariaTemplate?: ParsedYaml } = {};
private _overlayState: OverlayState = { offsetX: 0 };
private _recorderApp: IRecorderApp | null = null;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
@ -107,8 +107,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
if (data.event === 'highlightRequested') {
if (data.params.selector)
this.setHighlightedSelector(this._currentLanguage, data.params.selector);
if (data.params.ariaSnapshot)
this.setHighlightedAriaSnapshot(data.params.ariaSnapshot);
if (data.params.ariaTemplate)
this.setHighlightedAriaTemplate(data.params.ariaTemplate);
return;
}
if (data.event === 'step') {
@ -169,7 +169,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
mode: this._mode,
actionPoint,
actionSelector,
ariaTemplate: this._highlightedElement.ariaSnapshot,
ariaTemplate: this._highlightedElement.ariaTemplate,
language: this._currentLanguage,
testIdAttributeName: this._contextRecorder.testIdAttributeName(),
overlay: this._overlayState,
@ -245,8 +245,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._refreshOverlay();
}
setHighlightedAriaSnapshot(ariaSnapshot: ParsedYaml) {
this._highlightedElement = { ariaSnapshot };
setHighlightedAriaTemplate(ariaTemplate: ParsedYaml) {
this._highlightedElement = { ariaTemplate };
this._refreshOverlay();
}

View file

@ -79,7 +79,7 @@ const EXECUTABLE_PATHS = {
};
type DownloadPaths = Record<HostPlatform, string | undefined>;
const DOWNLOAD_PATHS: Record<BrowserName | InternalTool | 'chromium-headless-shell', DownloadPaths> = {
const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
'chromium': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
@ -403,9 +403,9 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
}
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'android';
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'chromium-headless-shell' | 'chromium-next' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell'];
export interface Executable {
@ -488,21 +488,6 @@ export class Registry {
_dependencyGroup: 'chromium',
_isHermeticInstallation: true,
});
this._executables.push({
type: 'channel',
name: 'chromium-next',
browserName: 'chromium',
directory: chromium.dir,
executablePath: () => chromiumExecutable,
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-next', chromiumExecutable, chromium.installByDefault, sdkLanguage),
installType: 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromium.dir, ['chrome-linux'], [], ['chrome-win']),
downloadURLs: this._downloadURLs(chromium),
browserVersion: chromium.browserVersion,
_install: () => this._downloadExecutable(chromium, chromiumExecutable),
_dependencyGroup: 'chromium',
_isHermeticInstallation: true,
});
const chromiumHeadlessShell = descriptors.find(d => d.name === 'chromium-headless-shell')!;
const chromiumHeadlessShellExecutable = findExecutablePath(chromiumHeadlessShell.dir, 'chromium-headless-shell');
@ -512,7 +497,7 @@ export class Registry {
browserName: 'chromium',
directory: chromiumHeadlessShell.dir,
executablePath: () => chromiumHeadlessShellExecutable,
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-headless-shell', chromiumHeadlessShellExecutable, false, sdkLanguage),
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumHeadlessShellExecutable, chromiumHeadlessShell.installByDefault, sdkLanguage),
installType: chromiumHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']),
downloadURLs: this._downloadURLs(chromiumHeadlessShell),
@ -894,16 +879,8 @@ export class Registry {
return this._executables.filter(e => e.installType === 'download-by-default');
}
private _addRequirementsAndDedupe(executables: Executable[]): ExecutableImpl[] {
const set = new Set<ExecutableImpl>();
for (const executable of executables as ExecutableImpl[]) {
set.add(executable);
if (executable.browserName === 'chromium')
set.add(this.findExecutable('ffmpeg')!);
if (executable.name === 'chromium')
set.add(this.findExecutable('chromium-headless-shell')!);
}
return Array.from(set);
private _dedupe(executables: Executable[]): ExecutableImpl[] {
return Array.from(new Set(executables as ExecutableImpl[]));
}
private async _validateHostRequirements(sdkLanguage: string, browserDirectory: string, linuxLddDirectories: string[], dlOpenLibraries: string[], windowsExeAndDllDirectories: string[]) {
@ -914,7 +891,7 @@ export class Registry {
}
async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) {
const executables = this._addRequirementsAndDedupe(executablesToInstallDeps);
const executables = this._dedupe(executablesToInstallDeps);
const targets = new Set<DependencyGroup>();
for (const executable of executables) {
if (executable._dependencyGroup)
@ -928,7 +905,7 @@ export class Registry {
}
async install(executablesToInstall: Executable[], forceReinstall: boolean) {
const executables = this._addRequirementsAndDedupe(executablesToInstall);
const executables = this._dedupe(executablesToInstall);
await fs.promises.mkdir(registryDirectory, { recursive: true });
const lockfilePath = path.join(registryDirectory, '__dirlock');
const linksDir = path.join(registryDirectory, '.links');
@ -1224,11 +1201,6 @@ export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: strin
}
}
export async function installDefaultBrowsersForNpmInstall() {
const defaultBrowserNames = registry.defaultExecutables().map(e => e.name);
return installBrowsersForNpmInstall(defaultBrowserNames);
}
export async function installBrowsersForNpmInstall(browsers: string[]) {
// PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD should have a value of 0 or 1
if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD')) {

View file

@ -47,6 +47,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
const kTargetAttribute = '__playwright_target__';
const kCustomElementsAttribute = '__playwright_custom_elements__';
const kCurrentSrcAttribute = '__playwright_current_src__';
const kBoundingRectAttribute = '__playwright_bounding_rect__';
// Symbols for our own info on Nodes/StyleSheets.
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
@ -436,6 +437,18 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
expectValue(value);
attrs[kSelectedAttribute] = value;
}
if (nodeName === 'CANVAS') {
const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect();
const value = JSON.stringify({
left: boundingRect.left / window.innerWidth,
top: boundingRect.top / window.innerHeight,
right: boundingRect.right / window.innerWidth,
bottom: boundingRect.bottom / window.innerHeight
});
expectValue(kBoundingRectAttribute);
expectValue(value);
attrs[kBoundingRectAttribute] = value;
}
if (element.scrollTop) {
expectValue(kScrollTopAttribute);
expectValue(element.scrollTop);

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-core",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@ -26,8 +26,8 @@
}
},
"dependencies": {
"playwright-core": "1.49.0-next",
"playwright-core": "1.49.0",
"vite": "^5.2.8",
"playwright": "1.49.0-next"
"playwright": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react17",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-svelte",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright Component Testing for Svelte",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "Playwright Component Testing for Vue",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next",
"@playwright/experimental-ct-core": "1.49.0",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "playwright-firefox",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate Firefox",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/test",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
},
"scripts": {},
"dependencies": {
"playwright": "1.49.0-next"
"playwright": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-webkit",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate WebKit",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright",
"version": "1.49.0-next",
"version": "1.49.0",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@ -56,7 +56,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-next"
"playwright-core": "1.49.0"
},
"optionalDependencies": {
"fsevents": "2.3.2"

View file

@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
options?: { timeout?: number, ignoreCase?: boolean },
) {
return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout });
}, expected, options);
}
@ -193,7 +193,7 @@ export function toHaveAccessibleName(
options?: { timeout?: number, ignoreCase?: boolean },
) {
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options);
}

View file

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path';
import type { ReporterDescription } from '../../types/test';
import type { FullConfigInternal } from '../common/config';
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart, JsonTestStepEnd } from '../isomorphic/teleReceiver';
import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters';
@ -471,7 +471,7 @@ class PathSeparatorPatcher {
}
if (jsonEvent.method === 'onTestEnd') {
const testResult = jsonEvent.params.result as JsonTestResultEnd;
testResult.errors.forEach(error => this._updateLocation(error.location));
testResult.errors.forEach(error => this._updateErrorLocations(error));
testResult.attachments.forEach(attachment => {
if (attachment.path)
attachment.path = this._updatePath(attachment.path);
@ -483,6 +483,11 @@ class PathSeparatorPatcher {
this._updateLocation(step.location);
return;
}
if (jsonEvent.method === 'onStepEnd') {
const step = jsonEvent.params.step as JsonTestStepEnd;
this._updateErrorLocations(step.error);
return;
}
}
private _updateProject(project: JsonProject) {
@ -504,6 +509,13 @@ class PathSeparatorPatcher {
}
}
private _updateErrorLocations(error: TestError | undefined) {
while (error) {
this._updateLocation(error.location);
error = error.cause;
}
}
private _updateLocation(location?: JsonLocation) {
if (location)
location.file = this._updatePath(location.file);

View file

@ -83,6 +83,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
// We can have multiple, hopefully equal, replacements for the same location,
// for example when a single test runs multiple times because of projects or retries.
// Do not apply multiple replacements for the same assertion.
break;
}
}
});

View file

@ -741,10 +741,12 @@ export type DebugControllerSetRecorderModeOptions = {
};
export type DebugControllerSetRecorderModeResult = void;
export type DebugControllerHighlightParams = {
selector: string,
selector?: string,
ariaTemplate?: string,
};
export type DebugControllerHighlightOptions = {
selector?: string,
ariaTemplate?: string,
};
export type DebugControllerHighlightResult = void;
export type DebugControllerHideHighlightParams = {};

View file

@ -791,7 +791,8 @@ DebugController:
highlight:
parameters:
selector: string
selector: string?
ariaTemplate: string?
hideHighlight:

View file

@ -27,7 +27,7 @@ import { CallLogView } from './callLog';
import './recorder.css';
import { asLocator } from '@isomorphic/locatorGenerators';
import { toggleTheme } from '@web/theme';
import { copy } from '@web/uiUtils';
import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml';
import { parseAriaKey } from '@isomorphic/ariaSnapshot';
import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot';
@ -47,7 +47,7 @@ export const Recorder: React.FC<RecorderProps> = ({
}) => {
const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
const [runningFileId, setRunningFileId] = React.useState<string | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('log');
const [selectedTab, setSelectedTab] = useSetting<string>('recorderPropertiesTab', 'log');
const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
@ -67,6 +67,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const language = source.language;
setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator');
@ -120,11 +121,8 @@ export const Recorder: React.FC<RecorderProps> = ({
setAriaSnapshotErrors(errors);
setAriaSnapshot(ariaSnapshot);
if (!errors.length)
window.dispatch({ event: 'highlightRequested', params: { ariaSnapshot: fragment } });
window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
}, [mode]);
const isRecording = mode === 'recording' || mode === 'recording-inspecting';
const locatorPlaceholder = isRecording ? '// Unavailable while recording' : (locator ? undefined : '// Pick element or type locator');
const ariaPlaceholder = isRecording ? '# Unavailable while recording' : (ariaSnapshot ? undefined : '# Pick element or type snapshot');
return <div className='recorder'>
<Toolbar>
@ -191,7 +189,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{
id: 'locator',
title: 'Locator',
render: () => <CodeMirrorWrapper text={locatorPlaceholder || locator} language={source.language} readOnly={isRecording} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
render: () => <CodeMirrorWrapper text={locator} placeholder='Type locator to inspect' language={source.language} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
},
{
id: 'log',
@ -200,8 +198,8 @@ export const Recorder: React.FC<RecorderProps> = ({
},
{
id: 'aria',
title: 'Aria snapshot',
render: () => <CodeMirrorWrapper text={ariaPlaceholder || ariaSnapshot || ''} language={'yaml'} readOnly={isRecording} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
title: 'Aria',
render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} placeholder='Type aria template to match' language={'yaml'} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
},
]}
selectedTab={selectedTab}

View file

@ -427,14 +427,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
for (const canvas of canvasElements) {
const context = canvas.getContext('2d')!;
const boundingRect = canvas.getBoundingClientRect();
const xStart = boundingRect.left / window.innerWidth;
const yStart = boundingRect.top / window.innerHeight;
const xEnd = boundingRect.right / window.innerWidth;
const yEnd = boundingRect.bottom / window.innerHeight;
const boundingRectAttribute = canvas.getAttribute('__playwright_bounding_rect__');
canvas.removeAttribute('__playwright_bounding_rect__');
if (!boundingRectAttribute)
continue;
const partiallyUncaptured = xEnd > 1 || yEnd > 1;
const fullyUncaptured = xStart > 1 || yStart > 1;
let boundingRect: { left: number, top: number, right: number, bottom: number };
try {
boundingRect = JSON.parse(boundingRectAttribute);
} catch (e) {
continue;
}
const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1;
const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1;
if (fullyUncaptured) {
canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`;
continue;
@ -442,10 +448,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
drawCheckerboard(context, canvas);
context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height);
context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height);
if (isUnderTest)
// eslint-disable-next-line no-console
console.log(`canvas drawn:`, JSON.stringify([xStart, yStart, xEnd, yEnd].map(v => Math.floor(v * 100))));
console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, (boundingRect.right - boundingRect.left), (boundingRect.bottom - boundingRect.top)].map(v => Math.floor(v * 100))));
if (partiallyUncaptured)
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;

View file

@ -24,6 +24,7 @@ import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/mode/simple';
import 'codemirror/mode/yaml/yaml';

View file

@ -181,3 +181,7 @@ body.dark-mode .CodeMirror span.cm-type {
text-decoration-color: var(--vscode-errorForeground);
text-decoration-style: wavy;
}
.CodeMirror-placeholder {
color: var(--vscode-input-placeholderForeground) !important;
}

View file

@ -46,6 +46,7 @@ export interface SourceProps {
wrapLines?: boolean;
onChange?: (text: string) => void;
dataTestId?: string;
placeholder?: string;
}
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
@ -62,6 +63,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
wrapLines,
onChange,
dataTestId,
placeholder,
}) => {
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
@ -89,7 +91,8 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
&& mode === codemirrorRef.current.cm.getOption('mode')
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers')
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) {
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')
&& placeholder === codemirrorRef.current.cm.getOption('placeholder')) {
// No need to re-create codemirror.
return;
}
@ -102,6 +105,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
readOnly: !!readOnly,
lineNumbers,
lineWrapping: wrapLines,
placeholder,
});
codemirrorRef.current = { cm };
if (isFocused)
@ -109,7 +113,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
setCodemirror(cm);
return cm;
})();
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]);
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused, placeholder]);
React.useEffect(() => {
if (codemirrorRef.current)

View file

@ -35,6 +35,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
browserType: BrowserType;
isAndroid: boolean;
isElectron: boolean;
isHeadlessShell: boolean;
nodeVersion: { major: number, minor: number, patch: number };
bidiTestSkipPredicate: (info: TestInfo) => boolean;
};
@ -97,6 +98,10 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
electronMajorVersion: [0, { scope: 'worker' }],
isWebView2: [false, { scope: 'worker' }],
isHeadlessShell: [async ({ browserName, channel, headless }, use) => {
await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || (!channel && headless)));
}, { scope: 'worker' }],
contextFactory: async ({ _contextFactory }: any, run) => {
await run(_contextFactory);
},

View file

@ -76,15 +76,24 @@ test(`playwright should work`, async ({ exec, installedSoftwareOnDisk }) => {
await exec('node esm-playwright.mjs');
});
test(`playwright should work with chromium-next`, async ({ exec, installedSoftwareOnDisk }) => {
test(`playwright should work with chromium --no-shell`, async ({ exec, installedSoftwareOnDisk }) => {
const result1 = await exec('npm i --foreground-scripts playwright');
expect(result1).toHaveLoggedSoftwareDownload([]);
expect(await installedSoftwareOnDisk()).toEqual([]);
const result2 = await exec('npx playwright install chromium-next');
const result2 = await exec('npx playwright install chromium --no-shell');
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'ffmpeg']);
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'ffmpeg']);
});
test(`playwright should work with chromium --only-shell`, async ({ exec, installedSoftwareOnDisk }) => {
const result1 = await exec('npm i --foreground-scripts playwright');
expect(result1).toHaveLoggedSoftwareDownload([]);
expect(await installedSoftwareOnDisk()).toEqual([]);
const result2 = await exec('npx playwright install --only-shell');
expect(result2).toHaveLoggedSoftwareDownload(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
expect(await installedSoftwareOnDisk()).toEqual(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
});
test('@playwright/test should work', async ({ exec, installedSoftwareOnDisk }) => {
const result1 = await exec('npm i --foreground-scripts @playwright/test');
expect(result1).toHaveLoggedSoftwareDownload([]);

View file

@ -18,8 +18,7 @@
import { browserTest as base, expect } from '../config/browserTest';
const it = base.extend<{ failsOn401: boolean }>({
failsOn401: async ({ browserName, headless, channel }, use) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
failsOn401: async ({ browserName, isHeadlessShell }, use) => {
await use(browserName === 'chromium' && !isHeadlessShell);
},
});

View file

@ -52,8 +52,7 @@ it('should open devtools when "devtools: true" option is given', async ({ browse
await browser.close();
});
it('should return background pages', async ({ browserType, createUserDataDir, asset, headless, channel }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it('should return background pages', async ({ browserType, createUserDataDir, asset, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();
@ -78,8 +77,7 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
expect(context.backgroundPages().length).toBe(0);
});
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, headless, channel }, testInfo) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, isHeadlessShell }, testInfo) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();
@ -105,8 +103,7 @@ it('should return background pages when recording video', async ({ browserType,
await context.close();
});
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, headless, channel }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
server.setRoute('/empty.html', (req, res) => {
@ -157,8 +154,7 @@ it('should support request/response events when using backgroundPage()', async (
it('should report console messages from content script', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' }
}, async ({ browserType, createUserDataDir, asset, server, headless, channel }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
}, async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir();

View file

@ -20,6 +20,7 @@ import { createGuid } from '../../packages/playwright-core/lib/utils/crypto';
import { Backend } from '../config/debugControllerBackend';
import type { Browser, BrowserContext } from '@playwright/test';
import type * as channels from '@protocol/channels';
import { roundBox } from '../page/pageTest';
type BrowserWithReuse = Browser & { _newContextForReuse: () => Promise<BrowserContext> };
type Fixtures = {
@ -30,7 +31,8 @@ type Fixtures = {
};
const test = baseTest.extend<Fixtures>({
wsEndpoint: async ({ }, use) => {
wsEndpoint: async ({ headless }, use) => {
if (headless)
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
const wsEndpoint = await server.listen();
@ -279,3 +281,20 @@ test('should highlight inside iframe', async ({ backend, connectedBrowser }, tes
await expect(highlight).toHaveCount(1);
await expect(page.locator('x-pw-highlight')).toHaveCount(1);
});
test('should highlight aria template', async ({ backend, connectedBrowser }, testInfo) => {
const context = await connectedBrowser._newContextForReuse();
const page = await context.newPage();
await backend.navigate({ url: `data:text/html,<button>Submit</button>` });
const button = page.getByRole('button');
const highlight = page.locator('x-pw-highlight');
await backend.highlight({ ariaTemplate: `- button "Submit2"` });
await expect(highlight).toHaveCount(0);
await backend.highlight({ ariaTemplate: `- button "Submit"` });
const box1 = roundBox(await button.boundingBox());
const box2 = roundBox(await highlight.boundingBox());
expect(box1).toEqual(box2);
});

View file

@ -636,8 +636,7 @@ it('should be able to download a inline PDF file via response interception', asy
await page.close();
});
it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, channel, headless }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, isHeadlessShell }) => {
it.skip(browserName === 'chromium' && !isHeadlessShell, 'We expect PDF Viewer to open up in headed Chromium');
const page = await browser.newPage();

View file

@ -101,10 +101,9 @@ it('should change document.activeElement', async ({ page, server }) => {
expect(active).toEqual(['INPUT', 'TEXTAREA']);
});
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, channel }) => {
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, isHeadlessShell }) => {
it.skip(browserName === 'webkit' && isWindows && !headless, 'WebKit/Windows/headed has a larger minimal viewport. See https://github.com/microsoft/playwright/issues/22616');
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image');
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
const page2 = await page.context().newPage();

View file

@ -402,17 +402,6 @@ await page1.GotoAsync("about:blank?foo");`);
await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']);
});
test('should update hover model on action', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [models] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.hovered).toBe('#checkbox');
});
test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

View file

@ -64,11 +64,10 @@ test.describe(() => {
test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- textbox
- text: '- button "Submit"'
@ -85,12 +84,11 @@ test.describe(() => {
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
@ -128,13 +126,12 @@ test.describe(() => {
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);

View file

@ -0,0 +1,69 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
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();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
});
test('should update locator highlight', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
await recorder.recorderPage.locator('.tab-locator .CodeMirror').click();
for (let i = 0; i < `Submit' })`.length; i++)
await recorder.recorderPage.keyboard.press('Backspace');
{
// Different button.
await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`);
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await cancelButton.boundingBox());
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
}
});
});

View file

@ -145,7 +145,7 @@ it.describe('permissions', () => {
});
});
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, channel }) => {
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, isHeadlessShell }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27475' });
it.fail(browserName === 'firefox', 'No such permissions (requires flag) in Firefox');
it.fixme(browserName === 'webkit' && isWindows, 'WebPasteboardProxy::allPasteboardItemInfo not implemented for Windows.');
@ -156,8 +156,7 @@ it('should support clipboard read', async ({ page, context, server, browserName,
if (browserName !== 'webkit')
expect(await getPermission(page, 'clipboard-read')).toBe('prompt');
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
if (browserName === 'chromium' && isHeadlessShell) {
if (isHeadlessShell) {
// Chromium (but not headless-shell) shows a dialog and does not resolve the promise.
const error = await page.evaluate(() => navigator.clipboard.readText()).catch(e => e);
expect(error.toString()).toContain('denied');

View file

@ -22,8 +22,7 @@ import { verifyViewport } from '../config/utils';
browserTest.describe('page screenshot', () => {
browserTest.skip(({ browserName, headless }) => browserName === 'firefox' && !headless, 'Firefox headed produces a different image.');
browserTest('should run in parallel in multiple pages', async ({ server, contextFactory, browserName, headless, channel }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
browserTest('should run in parallel in multiple pages', async ({ server, contextFactory, browserName, isHeadlessShell }) => {
browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
const context = await contextFactory();

View file

@ -1510,7 +1510,7 @@ test('canvas clipping', async ({ runAndTrace, page, server }) => {
});
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
expect(msg.text()).toEqual('canvas drawn: [0,91,12,111]');
expect(msg.text()).toEqual('canvas drawn: [0,91,11,20]');
const snapshot = await traceViewer.snapshotFrame('page.goto');
await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`);

View file

@ -429,8 +429,7 @@ for (const params of [
height: 768,
}
]) {
browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, channel }, testInfo) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, isHeadlessShell }, testInfo) => {
browserTest.skip(browserName === 'chromium' && video === 'on', 'Same screencast resolution conflicts');
browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless-shell) screencast has a min width issue');
browserTest.fixme(params.id === 'fit' && browserName === 'chromium' && platform === 'darwin', 'High DPI maxes image at 600x600');

View file

@ -473,9 +473,8 @@ it.describe('screencast', () => {
expect(videoFiles.length).toBe(2);
});
it('should scale frames down to the requested size ', async ({ browser, browserName, server, headless, channel }, testInfo) => {
it('should scale frames down to the requested size ', async ({ browser, browserName, server, headless, isHeadlessShell }, testInfo) => {
it.fixme(!headless, 'Fails on headed');
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless shell) has a min width issue');
const context = await browser.newContext({
@ -723,9 +722,8 @@ it.describe('screencast', () => {
expect(files.length).toBe(1);
});
it('should capture full viewport', async ({ browserType, browserName, isWindows, headless, channel }, testInfo) => {
it('should capture full viewport', async ({ browserType, browserName, isWindows, headless, isHeadlessShell }, testInfo) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
const size = { width: 600, height: 400 };
@ -759,9 +757,8 @@ it.describe('screencast', () => {
expectAll(pixels, almostRed);
});
it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, channel }, testInfo) => {
it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell }, testInfo) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
it.fixme(browserName === 'webkit' && isLinux && !headless, 'https://github.com/microsoft/playwright/issues/22617');
@ -797,10 +794,9 @@ it.describe('screencast', () => {
expectAll(pixels, almostRed);
});
it('should work with video+trace', async ({ browser, trace, headless, browserName, channel }, testInfo) => {
it('should work with video+trace', async ({ browser, trace, headless, browserName, isHeadlessShell }, testInfo) => {
it.skip(trace === 'on');
it.fixme(!headless, 'different trace screencast image size on all browsers');
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'different trace screencast image size');
const size = { width: 500, height: 400 };

View file

@ -431,6 +431,9 @@ test('toHaveAccessibleName', async ({ page }) => {
await expect(page.locator('div')).toHaveAccessibleName(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleName(/hello/);
await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true });
await page.setContent(`<button>foo&nbsp;bar\nbaz</button>`);
await expect(page.locator('button')).toHaveAccessibleName('foo bar baz');
});
test('toHaveAccessibleDescription', async ({ page }) => {
@ -443,6 +446,12 @@ test('toHaveAccessibleDescription', async ({ page }) => {
await expect(page.locator('div')).toHaveAccessibleDescription(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/);
await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true });
await page.setContent(`
<div role="button" aria-describedby="desc"></div>
<span id="desc">foo&nbsp;bar\nbaz</span>
`);
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
});
test('toHaveRole', async ({ page }) => {

View file

@ -24,12 +24,15 @@ function trimPatch(patch: string) {
return patch.split('\n').map(line => line.trimEnd()).join('\n');
}
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
test('should update snapshot with the update-snapshots flag with multiple projects', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "world"
\`);
@ -43,12 +46,13 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -3,7 +3,7 @@
@@ -3,7 +3,8 @@
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- - heading "world"
+ - heading "hello" [level=1]
+ - heading "bye" [level=2]
\`);
});