Merge branch 'microsoft:main' into feature/annotation-hyperlinks

This commit is contained in:
오소현 2024-05-05 20:43:32 +09:00 committed by GitHub
commit 9f2c68ef9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
434 changed files with 10886 additions and 6902 deletions

View file

@ -7,8 +7,6 @@ test/assets/modernizr.js
/packages/playwright-core/types/*
/packages/playwright-ct-core/src/generated/*
/index.d.ts
utils/generate_types/overrides.d.ts
utils/generate_types/test/test.ts
node_modules/
browser_patches/*/checkout/
browser_patches/chromium/output/

View file

@ -34,7 +34,7 @@ runs:
artifact_id: artifact.id,
archive_format: 'zip'
});
console.log('downloaded artifact', result);
console.log(`Downloaded ${artifact.name}.zip (${result.data.byteLength} bytes)`);
fs.writeFileSync(`${{ inputs.path }}/artifacts/${artifact.name}.zip`, Buffer.from(result.data));
}
- name: Unzip artifacts

View file

@ -1,26 +0,0 @@
name: 'Download blob report from Azure'
description: 'Download blob report from Azure blob storage'
inputs:
blob_prefix:
description: 'Name of the Azure blob storage directory containing blob report'
required: true
output_dir:
description: 'Output directory where downloaded blobs will be stored'
required: true
default: 'blob-report'
connection_string:
description: 'Azure connection string'
required: true
runs:
using: "composite"
steps:
- name: Download Blob Reports from Azure Blob Storage
shell: bash
run: |
OUTPUT_DIR='${{ inputs.output_dir }}'
mkdir -p $OUTPUT_DIR
LIST=$(az storage blob list -c '$web' --prefix ${{ inputs.blob_prefix }} --connection-string "${{ inputs.connection_string }}")
for name in $(echo $LIST | jq --raw-output '.[].name | select(test("report-.*\\.zip$"))');
do
az storage blob download -c '$web' --name $name -f $OUTPUT_DIR/$(basename $name) --connection-string "${{ inputs.connection_string }}"
done

87
.github/actions/run-test/action.yml vendored Normal file
View file

@ -0,0 +1,87 @@
name: 'Run browser tests'
description: 'Run browser tests'
inputs:
command:
description: 'Command to run tests'
required: true
node-version:
description: 'Node.js version to use'
required: false
default: '18'
browsers-to-install:
description: 'Browser to install. Default is all browsers.'
required: false
default: ''
bot-name:
description: 'Bot name'
required: true
shell:
description: 'Shell to use'
required: false
default: 'bash'
flakiness-client-id:
description: 'Azure Flakiness Dashboard Client ID'
required: false
flakiness-tenant-id:
description: 'Azure Flakiness Dashboard Tenant ID'
required: false
flakiness-subscription-id:
description: 'Azure Flakiness Dashboard Subscription ID'
required: false
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
# https://github.com/actions/runner-images/issues/9330
- name: Allow microphone access to all apps (macOS 14)
shell: bash
run: |
if [[ "$(uname)" == "Darwin" && "$(sw_vers -productVersion | cut -d. -f1)" == "14" ]]; then
echo "Allowing microphone access to all apps"
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159,NULL,NULL,'UNUSED',1687786159);"
fi
- run: npm ci
shell: bash
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- run: npm run build
shell: bash
- run: npx playwright install --with-deps ${{ inputs.browsers-to-install }}
shell: bash
- name: Run tests
if: inputs.shell == 'bash'
run: |
if [[ "$(uname)" == "Linux" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- ${{ inputs.command }}
else
${{ inputs.command }}
fi
shell: bash
env:
PWTEST_BOT_NAME: ${{ inputs.bot-name }}
- name: Run tests
if: inputs.shell != 'bash'
run: ${{ inputs.command }}
shell: ${{ inputs.shell }}
env:
PWTEST_BOT_NAME: ${{ inputs.bot-name }}
- name: Azure Login
uses: azure/login@v2
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
with:
client-id: ${{ inputs.flakiness-client-id }}
tenant-id: ${{ inputs.flakiness-tenant-id }}
subscription-id: ${{ inputs.flakiness-subscription-id }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
if: ${{ !cancelled() }}
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ inputs.bot-name }}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-chromium",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-core",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-firefox",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "@playwright/test",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright-webkit",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -1,7 +0,0 @@
{
"name": "playwright",
"version": "1.0.0",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"license": "Apache-2.0"
}

View file

@ -9,6 +9,8 @@ jobs:
permissions:
pull-requests: write
checks: write
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
if: ${{ github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
steps:
@ -16,7 +18,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
env:
DEBUG: pw:install
@ -35,16 +36,20 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=4096
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }}
- name: Upload HTML report to Azure
run: |
REPORT_DIR='run-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}-${{ github.sha }}'
azcopy cp --recursive "./playwright-report/*" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR"
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html"
env:
AZCOPY_AUTO_LOGIN_TYPE: SPN
AZCOPY_SPA_APPLICATION_ID: '${{ secrets.AZCOPY_SPA_APPLICATION_ID }}'
AZCOPY_SPA_CLIENT_SECRET: '${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}'
AZCOPY_TENANT_ID: '${{ secrets.AZCOPY_TENANT_ID }}'
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
- name: Read pull request number
uses: ./.github/actions/download-artifact

View file

@ -17,8 +17,9 @@ jobs:
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
permissions:
contents: read
id-token: write
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -49,11 +50,15 @@ jobs:
utils/publish_all_packages.sh --beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_PW_CDN_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_PW_CDN_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }}
- name: build & publish driver
env:
AZ_UPLOAD_FOLDER: driver/next
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}
run: |
utils/build/build-playwright-driver.sh
utils/build/upload-playwright-driver.sh

View file

@ -18,18 +18,17 @@ jobs:
publish-docker-release:
name: "publish to DockerHub"
runs-on: ubuntu-22.04
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
if: github.repository == 'microsoft/playwright'
environment: allow-publishing-docker-to-acr
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- uses: azure/docker-login@v1
with:
login-server: playwright.azurecr.io
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v3
with:
@ -37,6 +36,14 @@ jobs:
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
- name: Login to ACR via OIDC
run: az acr login --name playwright
- run: ./utils/docker/publish_docker.sh stable
if: (github.event_name != 'workflow_dispatch' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release == 'true')
- run: ./utils/docker/publish_docker.sh canary

View file

@ -12,6 +12,10 @@ jobs:
name: "publish playwright driver to CDN"
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -22,8 +26,12 @@ jobs:
- run: npm run build
- run: npx playwright install-deps
- run: utils/build/build-playwright-driver.sh
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_PW_CDN_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_PW_CDN_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }}
- run: utils/build/upload-playwright-driver.sh
env:
AZ_UPLOAD_FOLDER: driver
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}

View file

@ -17,15 +17,18 @@ on:
env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
jobs:
test_electron:
name: ${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -41,8 +44,13 @@ jobs:
if: matrix.os == 'ubuntu-latest'
- run: npm run etest
if: matrix.os != 'ubuntu-latest'
- run: node tests/config/checkCoverage.js electron
if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
- name: Azure Login
uses: azure/login@v2
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
with:
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash

View file

@ -22,12 +22,12 @@ concurrency:
env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_linux:
name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }})
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
@ -42,67 +42,48 @@ jobs:
node-version: 20
browser: chromium
runs-on: ${{ matrix.os }}
env:
PWTEST_BOT_NAME: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: ./.github/actions/run-test
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
if: ${{ !cancelled() }}
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
browsers-to-install: ${{ matrix.browser }} chromium
command: npm run test -- --project=${{ matrix.browser }}-*
bot-name: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
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 }}
test_linux_chromium_tot:
name: ${{ matrix.os }} (chromium tip-of-tree)
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
runs-on: ${{ matrix.os }}
env:
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: ./.github/actions/run-test
with:
node-version: 18
- run: npm ci
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium-tip-of-tree
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium-*
browsers-to-install: chromium-tip-of-tree
command: npm run test -- --project=chromium-*
bot-name: "${{ matrix.os }}-chromium-tip-of-tree"
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-tip-of-tree
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
if: ${{ !cancelled() }}
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
test_test_runner:
name: Test Runner
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
@ -128,31 +109,21 @@ jobs:
shardIndex: 2
shardTotal: 2
runs-on: ${{ matrix.os }}
env:
PWTEST_BOT_NAME: "${{ matrix.os }}-node${{ matrix.node-version }}-${{ matrix.shardIndex }}"
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: ./.github/actions/run-test
with:
node-version: ${{matrix.node-version}}
- run: npm ci
command: npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
bot-name: "${{ matrix.os }}-node${{ matrix.node-version }}-${{ matrix.shardIndex }}"
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:
DEBUG: pw:install
- run: npm run build
- run: npx playwright install --with-deps
- run: npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
if: matrix.os != 'ubuntu-latest'
- run: xvfb-run npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
if: matrix.os == 'ubuntu-latest'
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
if: ${{ !cancelled() }}
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
PWTEST_CHANNEL: firefox-beta
test_web_components:
name: Web Components
@ -227,6 +198,7 @@ jobs:
test_package_installations:
name: "Installation Test ${{ matrix.os }}"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
@ -236,30 +208,19 @@ jobs:
- windows-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
PWTEST_BOT_NAME: "package-installations-${{ matrix.os }}"
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
env:
DEBUG: pw:install
- run: npm run build
- run: npx playwright install --with-deps
- run: npm install -g yarn@1
- run: npm install -g pnpm@8
- run: npm run itest
if: matrix.os != 'ubuntu-latest'
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
if: matrix.os == 'ubuntu-latest'
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
if: ${{ !cancelled() }}
uses: ./.github/actions/upload-blob-report
- uses: ./.github/actions/run-test
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
command: npm run itest
bot-name: "package-installations-${{ matrix.os }}"
# TODO: figure out why itest fails with 'bash' on Windows.
shell: ${{ matrix.os == 'windows-latest' && 'pwsh' || 'bash' }}
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 }}

File diff suppressed because it is too large Load diff

View file

@ -34,8 +34,6 @@ jobs:
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps
- run: npx playwright install firefox-asan
if: matrix.os != 'windows-latest'
- run: npm run stest contexts -- --project=chromium
if: ${{ !cancelled() }}
- run: npm run stest browsers -- --project=chromium

View file

@ -9,17 +9,20 @@ on:
env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
video_linux:
name: "Video Linux"
environment: allow-uploading-flakiness-results
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
os: [ubuntu-20.04, ubuntu-22.04]
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -35,6 +38,13 @@ jobs:
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
env:
PWTEST_VIDEO: 1
- name: Azure Login
uses: azure/login@v2
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
with:
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash

View file

@ -17,13 +17,16 @@ on:
env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_webview2:
name: WebView2
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-2022
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -45,6 +48,13 @@ jobs:
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- run: npm run webview2test
- name: Azure Login
uses: azure/login@v2
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
with:
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() }}
shell: bash

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ node_modules/
*.swp
*.pyc
.vscode
.mono
.idea
yarn.lock
/packages/playwright-core/src/generated

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-124.0.6367.29-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-124.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-125.0.6422.26-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-125.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->124.0.6367.29<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->125.0.6422.26<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->124.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->125.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

View file

@ -21,6 +21,8 @@ const ALL_PERMISSIONS = [
'desktop-notification',
];
let globalTabAndWindowActivationChain = Promise.resolve();
class DownloadInterceptor {
constructor(registry) {
this._registry = registry
@ -384,7 +386,7 @@ class PageTarget {
const tabBrowser = ownerWindow.gBrowser;
// Serialize all tab-switching commands per tabbed browser
// to disallow concurrent tab switching.
const result = (tabBrowser.__serializedChain ?? Promise.resolve()).then(async () => {
const result = globalTabAndWindowActivationChain.then(async () => {
this._window.focus();
if (tabBrowser.selectedTab !== this._tab) {
const promise = helper.awaitEvent(ownerWindow, 'TabSwitchDone');
@ -399,7 +401,7 @@ class PageTarget {
notificationsPopup?.style.removeProperty('pointer-events');
}
});
tabBrowser.__serializedChain = result.catch(error => { /* swallow errors to keep chain running */ });
globalTabAndWindowActivationChain = result.catch(error => { /* swallow errors to keep chain running */ });
return result;
}

View file

@ -382,6 +382,12 @@ class PageAgent {
throw new Error('Object is not input!');
const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
unsafeObject.mozSetFileArray(nsFiles);
const events = [
new (frame.domWindow().Event)('input', { bubbles: true, cancelable: true, composed: true }),
new (frame.domWindow().Event)('change', { bubbles: true, cancelable: true, composed: true }),
];
for (const event of events)
unsafeObject.dispatchEvent(event);
}
_getContentQuads({objectId, frameId}) {

View file

@ -2475,7 +2475,7 @@ index 6dfd07d6b676a99993408921de8dea9d561f201d..e3c6794363cd6336effbeac83a179f37
readonly attribute boolean securityCheckDisabled;
};
diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs
index 73c83e526be1a3a252f995d0718e3975d50bffa7..4b05141c8ab8b084d977341127f36ea6d226aa9a 100644
index 73c83e526be1a3a252f995d0718e3975d50bffa7..db5977c54221e19e107a8325a0834302c2e84546 100644
--- a/services/settings/Utils.sys.mjs
+++ b/services/settings/Utils.sys.mjs
@@ -95,7 +95,7 @@ function _isUndefined(value) {
@ -2487,6 +2487,16 @@ index 73c83e526be1a3a252f995d0718e3975d50bffa7..4b05141c8ab8b084d977341127f36ea6
? lazy.gServerURL
: AppConstants.REMOTE_SETTINGS_SERVER_URL;
},
@@ -108,6 +108,9 @@ export var Utils = {
log,
get shouldSkipRemoteActivityDueToTests() {
+ // Playwright does not set Cu.isInAutomation, hence we just return true
+ // here in order to disable the remote activity.
+ return true;
return (
(lazy.isRunningTests || Cu.isInAutomation) &&
this.SERVER_URL == "data:,#remote-settings-dummy/v1"
diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs
index c9ad30b28b069a122cf01c5e97b83dc68ef022ff..f32dbcfe26fe2f6b8a0d066d6dfd208f1afc510d 100644
--- a/servo/components/style/gecko/media_features.rs
@ -2925,7 +2935,7 @@ diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.
index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce8050630a1aa 100644
--- a/widget/cocoa/NativeKeyBindings.mm
+++ b/widget/cocoa/NativeKeyBindings.mm
@@ -528,6 +528,13 @@
@@ -528,6 +528,13 @@ void NativeKeyBindings::GetEditCommandsForTests(
break;
case KEY_NAME_INDEX_ArrowLeft:
if (aEvent.IsAlt()) {
@ -2939,7 +2949,7 @@ index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce805
break;
}
if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
@@ -550,6 +557,13 @@
@@ -550,6 +557,13 @@ void NativeKeyBindings::GetEditCommandsForTests(
break;
case KEY_NAME_INDEX_ArrowRight:
if (aEvent.IsAlt()) {
@ -2953,7 +2963,7 @@ index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce805
break;
}
if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
@@ -572,6 +586,10 @@
@@ -572,6 +586,10 @@ void NativeKeyBindings::GetEditCommandsForTests(
break;
case KEY_NAME_INDEX_ArrowUp:
if (aEvent.IsControl()) {
@ -2964,7 +2974,7 @@ index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce805
break;
}
if (aEvent.IsMeta()) {
@@ -582,7 +600,7 @@
@@ -582,7 +600,7 @@ void NativeKeyBindings::GetEditCommandsForTests(
!aEvent.IsShift()
? ToObjcSelectorPtr(@selector(moveToBeginningOfDocument:))
: ToObjcSelectorPtr(
@ -2973,7 +2983,7 @@ index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce805
aCommands);
break;
}
@@ -609,6 +627,10 @@
@@ -609,6 +627,10 @@ void NativeKeyBindings::GetEditCommandsForTests(
break;
case KEY_NAME_INDEX_ArrowDown:
if (aEvent.IsControl()) {
@ -2984,31 +2994,6 @@ index e4bdf715e2fb899e97a5bfeb2e147127460d6047..3554f919480278b7353617481c7ce805
break;
}
if (aEvent.IsMeta()) {
diff --git a/widget/cocoa/nsCocoaUtils.mm b/widget/cocoa/nsCocoaUtils.mm
index f3a760476290953df2179fa44bd68dd171d291ab..6cd3f8178d260904905372c1afc7ecbcc568fbc8 100644
--- a/widget/cocoa/nsCocoaUtils.mm
+++ b/widget/cocoa/nsCocoaUtils.mm
@@ -267,14 +267,17 @@ static float MenuBarScreenHeight() {
return nullptr;
}
- nsCOMPtr<nsIWidget> hiddenWindowWidget;
+ nsCOMPtr<nsIWidget> mainWindowWidget;
if (NS_FAILED(baseHiddenWindow->GetMainWidget(
- getter_AddRefs(hiddenWindowWidget)))) {
+ getter_AddRefs(mainWindowWidget)))) {
NS_WARNING("Couldn't get nsIWidget from hidden window (nsIBaseWindow)");
return nullptr;
}
- return hiddenWindowWidget;
+ // In the case of headless mode, it's a HeadlessWidget while the callee expects a nsCocoaWindow
+ if (gfxPlatform::IsHeadless())
+ return nullptr;
+ return mainWindowWidget;
}
BOOL nsCocoaUtils::WasLaunchedAtLogin() {
diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp
index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db5151dd90c1a 100644
--- a/widget/headless/HeadlessCompositorWidget.cpp

View file

@ -112,9 +112,20 @@ pref("prompts.contentPromptSubDialog", false);
// Do not use system colors - they are affected by themes.
pref("ui.use_standins_for_native_colors", true);
// Turn off the Push service.
pref("dom.push.serverURL", "");
// This setting breaks settings loading.
// Prevent Remote Settings (firefox.settings.services.mozilla.com) to issue non local connections.
pref("services.settings.server", "");
// Prevent location.services.mozilla.com to issue non local connections.
pref("browser.region.network.url", "");
pref("browser.pocket.enabled", false);
pref("browser.newtabpage.activity-stream.feeds.topsites", false);
// Disable sponsored tiles from "Mozilla Tiles Service"
pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
// required to prevent non-local access to push.services.mozilla.com
pref("dom.push.connection.enabled", false);
// Prevent contile.services.mozilla.com to issue non local connections.
pref("browser.topsites.contile.enabled", false);
pref("browser.safebrowsing.provider.mozilla.updateURL", "");
pref("browser.library.activity-stream.enabled", false);
pref("browser.search.geoSpecificDefaults", false);

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/WebKit/WebKit.git"
BASE_BRANCH="main"
BASE_REVISION="d477c762a9ecbbc8dedf3ca7a6a2a079577bf60c"
BASE_REVISION="e225c278f4c06f451ea92cc68b12986dd2a99979"

File diff suppressed because it is too large Load diff

View file

@ -159,7 +159,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.delete.multipart = %%-js-fetch-option-multipart-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-python-fetch-option-multipart-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-csharp-fetch-option-multipart-%%
@ -187,10 +190,12 @@ All responses returned by [`method: APIRequestContext.get`] and similar methods
- returns: <[APIResponse]>
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly to the request.
context cookies from the response. The method will automatically follow redirects.
**Usage**
JSON objects can be passed directly to the request:
```js
await request.fetch('https://example.com/api/createBook', {
method: 'post',
@ -224,28 +229,17 @@ var data = new Dictionary<string, object>() {
await Request.FetchAsync("https://example.com/api/createBook", new() { Method = "post", DataObject = data });
```
The common way to send file(s) in the body of a request is to encode it as form fields with `multipart/form-data` encoding. You can achieve that with Playwright API like this:
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding. Use [FormData] to construct request body and pass it to the request as [`option: multipart`] parameter:
```js
// Open file as a stream and pass it to the request:
const stream = fs.createReadStream('team.csv');
await request.fetch('https://example.com/api/uploadTeamList', {
method: 'post',
multipart: {
fileField: stream
}
});
// Or you can pass the file content directly as an object:
await request.fetch('https://example.com/api/uploadScript', {
method: 'post',
multipart: {
fileField: {
name: 'f.js',
mimeType: 'text/javascript',
buffer: Buffer.from('console.log(2022);')
}
}
const form = new FormData();
form.set('name', 'John');
form.append('name', 'Doe');
// Send two file fields with the same name.
form.append('file', new File(['console.log(2024);'], 'f1.js', { type: 'text/javascript' }));
form.append('file', new File(['hello'], 'f2.txt', { type: 'text/plain' }));
await request.fetch('https://example.com/api/uploadForm', {
multipart: form
});
```
@ -259,15 +253,14 @@ APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
// Or you can pass the file content directly as FilePayload object:
FilePayload filePayload = new FilePayload("f.js", "text/javascript",
"console.log(2022);".getBytes(StandardCharsets.UTF_8));
APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
APIResponse response = request.fetch("https://example.com/api/uploadScript",
RequestOptions.create().setMethod("post").setMultipart(
FormData.create().set("fileField", filePayload)));
```
```python
api_request_context.fetch(
"https://example.com/api/uploadScrip'",
method="post",
"https://example.com/api/uploadScript", method="post",
multipart={
"fileField": {
"name": "f.js",
@ -324,7 +317,10 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.fetch.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-csharp-fetch-option-multipart-%%
@ -410,7 +406,10 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.get.multipart = %%-js-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-python-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-csharp-fetch-option-multipart-%%
@ -460,7 +459,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.head.multipart = %%-js-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-python-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-csharp-fetch-option-multipart-%%
@ -510,7 +512,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.patch.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-csharp-fetch-option-multipart-%%
@ -566,7 +571,7 @@ api_request_context.post("https://example.com/api/createBook", data=data)
```csharp
var data = new Dictionary<string, object>() {
{ "firstNam", "John" },
{ "firstName", "John" },
{ "lastName", "Doe" }
};
await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data });
@ -604,26 +609,17 @@ formData.Set("body", "John Doe");
await request.PostAsync("https://example.com/api/findBook", new() { Form = formData });
```
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding. You can achieve that with Playwright API like this:
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding. Use [FormData] to construct request body and pass it to the request as `multipart` parameter:
```js
// Open file as a stream and pass it to the request:
const stream = fs.createReadStream('team.csv');
await request.post('https://example.com/api/uploadTeamList', {
multipart: {
fileField: stream
}
});
// Or you can pass the file content directly as an object:
await request.post('https://example.com/api/uploadScript', {
multipart: {
fileField: {
name: 'f.js',
mimeType: 'text/javascript',
buffer: Buffer.from('console.log(2022);')
}
}
const form = new FormData();
form.set('name', 'John');
form.append('name', 'Doe');
// Send two file fields with the same name.
form.append('file', new File(['console.log(2024);'], 'f1.js', { type: 'text/javascript' }));
form.append('file', new File(['hello'], 'f2.txt', { type: 'text/plain' }));
await request.post('https://example.com/api/uploadForm', {
multipart: form
});
```
@ -635,16 +631,16 @@ APIResponse response = request.post("https://example.com/api/uploadTeamList",
FormData.create().set("fileField", file)));
// Or you can pass the file content directly as FilePayload object:
FilePayload filePayload = new FilePayload("f.js", "text/javascript",
FilePayload filePayload1 = new FilePayload("f1.js", "text/javascript",
"console.log(2022);".getBytes(StandardCharsets.UTF_8));
APIResponse response = request.post("https://example.com/api/uploadTeamList",
APIResponse response = request.post("https://example.com/api/uploadScript",
RequestOptions.create().setMultipart(
FormData.create().set("fileField", filePayload)));
```
```python
api_request_context.post(
"https://example.com/api/uploadScrip'",
"https://example.com/api/uploadScript'",
multipart={
"fileField": {
"name": "f.js",
@ -690,7 +686,10 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.post.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-csharp-fetch-option-multipart-%%
@ -740,7 +739,10 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-js-python-fetch-option-multipart-%%
### option: APIRequestContext.put.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-csharp-fetch-option-multipart-%%

View file

@ -30,7 +30,7 @@ client.on("Animation.animationCreated", lambda: print("animation created!"))
response = await client.send("Animation.getPlaybackRate")
print("playback rate is " + str(response["playbackRate"]))
await client.send("Animation.setPlaybackRate", {
playbackRate: response["playbackRate"] / 2
"playbackRate": response["playbackRate"] / 2
})
```
@ -41,7 +41,7 @@ client.on("Animation.animationCreated", lambda: print("animation created!"))
response = client.send("Animation.getPlaybackRate")
print("playback rate is " + str(response["playbackRate"]))
client.send("Animation.setPlaybackRate", {
playbackRate: response["playbackRate"] / 2
"playbackRate": response["playbackRate"] / 2
})
```
```csharp

View file

@ -692,7 +692,7 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.

View file

@ -14,6 +14,77 @@ FormData form = FormData.create()
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
```
## method: FormData.append
* since: v1.44
- returns: <[FormData]>
Appends a new value onto an existing key inside a FormData object, or adds the key if it
does not already exist. File values can be passed either as `Path` or as `FilePayload`.
Multiple fields with the same name can be added.
The difference between [`method: FormData.set`] and [`method: FormData.append`] is that if the specified key already exists,
[`method: FormData.set`] will overwrite all existing values with the new one, whereas [`method: FormData.append`] will append
the new value onto the end of the existing set of values.
```java
import com.microsoft.playwright.options.FormData;
...
FormData form = FormData.create()
// Only name and value are set.
.append("firstName", "John")
// Name and value are set, filename and Content-Type are inferred from the file path.
.append("attachment", Paths.get("pic.jpg"))
// Name, value, filename and Content-Type are set.
.append("attachment", new FilePayload("table.csv", "text/csv", Files.readAllBytes(Paths.get("my-tble.csv"))));
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
```
```csharp
var multipart = Context.APIRequest.CreateFormData();
// Only name and value are set.
multipart.Append("firstName", "John");
// Name, value, filename and Content-Type are set.
multipart.Append("attachment", new FilePayload()
{
Name = "pic.jpg",
MimeType = "image/jpeg",
Buffer = File.ReadAllBytes("john.jpg")
});
// Name, value, filename and Content-Type are set.
multipart.Append("attachment", new FilePayload()
{
Name = "table.csv",
MimeType = "text/csv",
Buffer = File.ReadAllBytes("my-tble.csv")
});
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
```
### param: FormData.append.name
* since: v1.44
- `name` <[string]>
Field name.
### param: FormData.append.value
* since: v1.44
- `value` <[string]|[boolean]|[int]|[Path]|[Object]>
- `name` <[string]> File name
- `mimeType` <[string]> File type
- `buffer` <[Buffer]> File content
Field value.
### param: FormData.append.value
* since: v1.44
* langs: csharp
- `value` <[string]|[boolean]|[int]|[Object]>
- `name` <[string]> File name
- `mimeType` <[string]> File type
- `buffer` <[Buffer]> File content
Field value.
## method: FormData.create
* since: v1.18
* langs: java
@ -36,7 +107,7 @@ FormData form = FormData.create()
// Name and value are set, filename and Content-Type are inferred from the file path.
.set("profilePicture1", Paths.get("john.jpg"))
// Name, value, filename and Content-Type are set.
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))));
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))))
.set("age", 30);
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
```
@ -52,6 +123,7 @@ multipart.Set("profilePicture", new FilePayload()
MimeType = "image/jpeg",
Buffer = File.ReadAllBytes("john.jpg")
});
multipart.Set("age", 30);
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
```

View file

@ -1030,7 +1030,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Frame.getByRole.role = %%-locator-get-by-role-role-%%
### param: Frame.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -1393,7 +1394,8 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -1970,6 +1972,10 @@ Waits for the required load state to be reached.
This returns when the frame reaches a required load state, `load` by default. The navigation must have been committed
when this method is called. If current document has already reached the required state, resolves immediately.
:::note
Most of the time, this method is not needed because Playwright [auto-waits before every action](../actionability.md).
:::
**Usage**
```js

View file

@ -133,7 +133,8 @@ in that iframe.
%%-template-locator-get-by-role-%%
### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%%
### param: FrameLocator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27

View file

@ -151,7 +151,8 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -227,7 +228,8 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.

View file

@ -1173,7 +1173,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Locator.getByRole.role = %%-locator-get-by-role-role-%%
### param: Locator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -1760,7 +1761,8 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.

View file

@ -211,11 +211,8 @@ The opposite of [`method: LocatorAssertions.toContainText`].
Expected substring or RegExp or a list of those.
### option: LocatorAssertions.NotToContainText.ignoreCase
### option: LocatorAssertions.NotToContainText.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.23
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToContainText.useInnerText
* since: v1.18
@ -226,6 +223,45 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
### option: LocatorAssertions.NotToContainText.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.NotToHaveAccessibleDescription
* since: v1.44
* langs: python
The opposite of [`method: LocatorAssertions.toHaveAccessibleDescription`].
### param: LocatorAssertions.NotToHaveAccessibleDescription.name
* since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name.
### option: LocatorAssertions.NotToHaveAccessibleDescription.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.44
### option: LocatorAssertions.NotToHaveAccessibleDescription.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.NotToHaveAccessibleName
* since: v1.44
* langs: python
The opposite of [`method: LocatorAssertions.toHaveAccessibleName`].
### param: LocatorAssertions.NotToHaveAccessibleName.name
* since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name.
### option: LocatorAssertions.NotToHaveAccessibleName.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.44
### option: LocatorAssertions.NotToHaveAccessibleName.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.NotToHaveAttribute
* since: v1.20
* langs: python
@ -244,11 +280,8 @@ Attribute name.
Expected attribute value.
### option: LocatorAssertions.NotToHaveAttribute.ignoreCase
### option: LocatorAssertions.NotToHaveAttribute.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.40
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
@ -340,6 +373,23 @@ Property value.
### option: LocatorAssertions.NotToHaveJSProperty.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.NotToHaveRole
* since: v1.44
* langs: python
The opposite of [`method: LocatorAssertions.toHaveRole`].
### param: LocatorAssertions.NotToHaveRole.name
* since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name.
### option: LocatorAssertions.NotToHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.NotToHaveText
* since: v1.20
* langs: python
@ -352,11 +402,8 @@ The opposite of [`method: LocatorAssertions.toHaveText`].
Expected string or RegExp or a list of those.
### option: LocatorAssertions.NotToHaveText.ignoreCase
### option: LocatorAssertions.NotToHaveText.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.23
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToHaveText.useInnerText
* since: v1.18
@ -1089,11 +1136,8 @@ Expected substring or RegExp or a list of those.
Expected substring or RegExp or a list of those.
### option: LocatorAssertions.toContainText.ignoreCase
### option: LocatorAssertions.toContainText.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.23
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.toContainText.useInnerText
* since: v1.18
@ -1107,6 +1151,107 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
### option: LocatorAssertions.toContainText.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.toHaveAccessibleDescription
* since: v1.44
* langs:
- alias-java: hasAccessibleDescription
Ensures the [Locator] points to an element with a given [accessible description](https://w3c.github.io/accname/#dfn-accessible-description).
**Usage**
```js
const locator = page.getByTestId('save-button');
await expect(locator).toHaveAccessibleDescription('Save results to disk');
```
```java
Locator locator = page.getByTestId("save-button");
assertThat(locator).hasAccessibleDescription("Save results to disk");
```
```python async
locator = page.get_by_test_id("save-button")
await expect(locator).to_have_accessible_description("Save results to disk")
```
```python sync
locator = page.get_by_test_id("save-button")
expect(locator).to_have_accessible_description("Save results to disk")
```
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk");
```
### param: LocatorAssertions.toHaveAccessibleDescription.description
* since: v1.44
- `description` <[string]|[RegExp]>
Expected accessible description.
### option: LocatorAssertions.toHaveAccessibleDescription.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveAccessibleDescription.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveAccessibleDescription.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.44
## async method: LocatorAssertions.toHaveAccessibleName
* since: v1.44
* langs:
- alias-java: hasAccessibleName
Ensures the [Locator] points to an element with a given [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
**Usage**
```js
const locator = page.getByTestId('save-button');
await expect(locator).toHaveAccessibleName('Save to disk');
```
```java
Locator locator = page.getByTestId("save-button");
assertThat(locator).hasAccessibleName("Save to disk");
```
```python async
locator = page.get_by_test_id("save-button")
await expect(locator).to_have_accessible_name("Save to disk")
```
```python sync
locator = page.get_by_test_id("save-button")
expect(locator).to_have_accessible_name("Save to disk")
```
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).toHaveAccessibleNameAsync("Save to disk");
```
### param: LocatorAssertions.toHaveAccessibleName.name
* since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name.
### option: LocatorAssertions.toHaveAccessibleName.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveAccessibleName.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveAccessibleName.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.44
## async method: LocatorAssertions.toHaveAttribute
* since: v1.20
* langs:
@ -1162,11 +1307,8 @@ Expected attribute value.
### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
### option: LocatorAssertions.toHaveAttribute.ignoreCase
### option: LocatorAssertions.toHaveAttribute.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.40
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
## async method: LocatorAssertions.toHaveAttribute#2
* since: v1.39
@ -1504,6 +1646,53 @@ Property value.
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.toHaveRole
* since: v1.44
* langs:
- alias-java: hasRole
Ensures the [Locator] points to an element with a given [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles).
Note that role is matched as a string, disregarding the ARIA role hierarchy. For example, asserting a superclass role `"checkbox"` on an element with a subclass role `"switch"` will fail.
**Usage**
```js
const locator = page.getByTestId('save-button');
await expect(locator).toHaveRole('button');
```
```java
Locator locator = page.getByTestId("save-button");
assertThat(locator).hasRole(AriaRole.BUTTON);
```
```python async
locator = page.get_by_test_id("save-button")
await expect(locator).to_have_role("button")
```
```python sync
locator = page.get_by_test_id("save-button")
expect(locator).to_have_role("button")
```
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).ToHaveRoleAsync(AriaRole.Button);
```
### param: LocatorAssertions.toHaveRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.44
### option: LocatorAssertions.toHaveRole.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.toHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.toHaveScreenshot#1
* since: v1.23
* langs: js
@ -1769,11 +1958,8 @@ Expected string or RegExp or a list of those.
Expected string or RegExp or a list of those.
### option: LocatorAssertions.toHaveText.ignoreCase
### option: LocatorAssertions.toHaveText.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.23
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.toHaveText.useInnerText
* since: v1.18

View file

@ -290,9 +290,7 @@ Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` o
**Usage**
```js
page.on('dialog', dialog => {
dialog.accept();
});
page.on('dialog', dialog => dialog.accept());
```
```java
@ -2342,7 +2340,8 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Page.getByRole.role = %%-locator-get-by-role-role-%%
### param: Page.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### option: Page.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -3014,7 +3013,8 @@ generate the text for. A superset of the [`param: key`] values can be found
`F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
`Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -3146,10 +3146,6 @@ return value resolves to `[]`.
## async method: Page.addLocatorHandler
* since: v1.42
:::warning[Experimental]
This method is experimental and its behavior may change in the upcoming releases.
:::
When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them tricky to handle in automated tests.
This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.
@ -3157,6 +3153,7 @@ This method lets you set up a special function, called a handler, that activates
Things to keep in mind:
* When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`].
* Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered.
* After executing the handler, Playwright will ensure that overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with [`option: noWaitAfter`].
* The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts.
* You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler.
@ -3286,13 +3283,13 @@ await page.GotoAsync("https://example.com");
await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync();
```
An example with a custom callback on every actionability check. It uses a `<body>` locator that is always visible, so the handler is called before every actionability check:
An example with a custom callback on every actionability check. It uses a `<body>` locator that is always visible, so the handler is called before every actionability check. It is important to specify [`option: noWaitAfter`], because the handler does not hide the `<body>` element.
```js
// Setup the handler.
await page.addLocatorHandler(page.locator('body'), async () => {
await page.evaluate(() => window.removeObstructionsForTestIfNeeded());
});
}, { noWaitAfter: true });
// Write the test as usual.
await page.goto('https://example.com');
@ -3303,7 +3300,7 @@ await page.getByRole('button', { name: 'Start here' }).click();
// Setup the handler.
page.addLocatorHandler(page.locator("body")), () => {
page.evaluate("window.removeObstructionsForTestIfNeeded()");
});
}, new Page.AddLocatorHandlerOptions.setNoWaitAfter(true));
// Write the test as usual.
page.goto("https://example.com");
@ -3314,7 +3311,7 @@ page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
# Setup the handler.
def handler():
page.evaluate("window.removeObstructionsForTestIfNeeded()")
page.add_locator_handler(page.locator("body"), handler)
page.add_locator_handler(page.locator("body"), handler, no_wait_after=True)
# Write the test as usual.
page.goto("https://example.com")
@ -3325,7 +3322,7 @@ page.get_by_role("button", name="Start here").click()
# Setup the handler.
def handler():
await page.evaluate("window.removeObstructionsForTestIfNeeded()")
await page.add_locator_handler(page.locator("body"), handler)
await page.add_locator_handler(page.locator("body"), handler, no_wait_after=True)
# Write the test as usual.
await page.goto("https://example.com")
@ -3336,13 +3333,45 @@ await page.get_by_role("button", name="Start here").click()
// Setup the handler.
await page.AddLocatorHandlerAsync(page.Locator("body"), async () => {
await page.EvaluateAsync("window.removeObstructionsForTestIfNeeded()");
});
}, new() { NoWaitAfter = true });
// Write the test as usual.
await page.GotoAsync("https://example.com");
await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync();
```
Handler takes the original locator as an argument. You can also automatically remove the handler after a number of invocations by setting [`option: times`]:
```js
await page.addLocatorHandler(page.getByLabel('Close'), async locator => {
await locator.click();
}, { times: 1 });
```
```java
page.addLocatorHandler(page.getByLabel("Close"), locator => {
locator.click();
}, new Page.AddLocatorHandlerOptions().setTimes(1));
```
```python sync
def handler(locator):
locator.click()
page.add_locator_handler(page.get_by_label("Close"), handler, times=1)
```
```python async
def handler(locator):
await locator.click()
await page.add_locator_handler(page.get_by_label("Close"), handler, times=1)
```
```csharp
await page.AddLocatorHandlerAsync(page.GetByText("Sign up to the newsletter"), async locator => {
await locator.ClickAsync();
}, new() { Times = 1 });
```
### param: Page.addLocatorHandler.locator
* since: v1.42
- `locator` <[Locator]>
@ -3352,24 +3381,49 @@ Locator that triggers the handler.
### param: Page.addLocatorHandler.handler
* langs: js, python
* since: v1.42
- `handler` <[function]>
- `handler` <[function]\([Locator]\): [Promise<any>]>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### param: Page.addLocatorHandler.handler
* langs: csharp
* since: v1.42
- `handler` <[function](): [Promise<any>]>
- `handler` <[function]\([Locator]\)>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### param: Page.addLocatorHandler.handler
* langs: java
* since: v1.42
- `handler` <[Runnable]>
- `handler` <[function]\([Locator]\)>
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
### option: Page.addLocatorHandler.times
* since: v1.44
- `times` <[int]>
Specifies the maximum number of times this handler should be called. Unlimited by default.
### option: Page.addLocatorHandler.noWaitAfter
* since: v1.44
- `noWaitAfter` <[boolean]>
By default, after calling the handler Playwright will wait until the overlay becomes hidden, and only then Playwright will continue with the action/assertion that triggered the handler. This option allows to opt-out of this behavior, so that overlay can stay visible after the handler has run.
## async method: Page.removeLocatorHandler
* since: v1.44
Removes all locator handlers added by [`method: Page.addLocatorHandler`] for a specific locator.
### param: Page.removeLocatorHandler.locator
* since: v1.44
- `locator` <[Locator]>
Locator passed to [`method: Page.addLocatorHandler`].
## async method: Page.reload
* since: v1.8
- returns: <[null]|[Response]>
@ -4465,6 +4519,10 @@ Returns when the required load state has been reached.
This resolves when the page reaches a required load state, `load` by default. The navigation must have been committed
when this method is called. If current document has already reached the required state, resolves immediately.
:::note
Most of the time, this method is not needed because Playwright [auto-waits before every action](../actionability.md).
:::
**Usage**
```js

View file

@ -334,7 +334,6 @@ Expected URL string or RegExp.
### option: PageAssertions.toHaveURL.ignoreCase
* since: v1.44
* langs: js
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.

View file

@ -97,10 +97,11 @@ A point to use relative to the top-left corner of element padding box. If not sp
element.
## input-modifiers
- `modifiers` <[Array]<[KeyboardModifier]<"Alt"|"Control"|"Meta"|"Shift">>>
- `modifiers` <[Array]<[KeyboardModifier]<"Alt"|"Control"|"ControlOrMeta"|"Meta"|"Shift">>>
Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
modifiers back. If not specified, currently pressed modifiers are used.
modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
and Linux and to "Meta" on macOS.
## input-button
- `button` <[MouseButton]<"left"|"right"|"middle">>
@ -403,9 +404,9 @@ unless explicitly provided.
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
## js-python-fetch-option-multipart
* langs: js, python
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
## js-fetch-option-multipart
* langs: js
- `multipart` <[FormData]|[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
- `name` <[string]> File name
- `mimeType` <[string]> File type
- `buffer` <[Buffer]> File content
@ -415,14 +416,24 @@ this request body. If this parameter is specified `content-type` header will be
unless explicitly provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
or as file-like object containing file name, mime-type and its content.
## python-fetch-option-multipart
* langs: python
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
- `name` <[string]> File name
- `mimeType` <[string]> File type
- `buffer` <[Buffer]> File content
Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as
this request body. If this parameter is specified `content-type` header will be set to `multipart/form-data`
unless explicitly provided. File values can be passed as file-like object containing file name, mime-type and its content.
## csharp-fetch-option-multipart
* langs: csharp
- `multipart` <[FormData]>
Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as
this request body. If this parameter is specified `content-type` header will be set to `multipart/form-data`
unless explicitly provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
or as file-like object containing file name, mime-type and its content.
unless explicitly provided. File values can be passed as file-like object containing file name, mime-type and its content.
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
@ -560,6 +571,7 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about
- `username` <[string]>
- `password` <[string]>
- `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port).
- `sendImmediately` ?<[boolean]> Whether to send `Authorization` header with the first API request. By deafult, the credentials are sent only when 401 (Unauthorized) response with `WWW-Authenticate` header is received. This option does not affect requests sent from the browser.
Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
If no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@ -846,6 +858,11 @@ Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestC
Time to retry the assertion for in milliseconds. Defaults to `5000`.
## assertions-ignore-case
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
## assertions-max-diff-pixels
* langs: js
- `maxDiffPixels` <[int]>
@ -1188,8 +1205,7 @@ Text to locate the element for.
Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular expression. Note that exact match still trims whitespace.
## locator-get-by-role-role
* since: v1.27
## get-by-role-to-have-role-role
- `role` <[AriaRole]<"alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem">>
Required aria role.
@ -1716,13 +1732,17 @@ test.describe('suite', () => {
The list of supported tokens:
* `{testDir}` - Project's [`property: TestConfig.testDir`].
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* Value: `foo/bar/baz`
* `{ext}` - snapshot extension (with dots)
* Value: `.png`
* `{platform}` - The value of `process.platform`.
* `{projectName}` - Project's file-system-sanitized name, if any.
* Value: `''` (empty string).
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{testDir}` - Project's [`property: TestConfig.testDir`].
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* Value: `page`
* `{testFileName}` - Test file name with extension.
@ -1731,10 +1751,6 @@ The list of supported tokens:
* Value: `page/page-click.spec.ts`
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* Value: `suite-test-should-work`
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* Value: `foo/bar/baz`
* `{ext}` - snapshot extension (with dots)
* Value: `.png`
Each token can be preceded with a single character that will be used **only if** this token has non-empty value.

View file

@ -296,15 +296,16 @@ context = browser.new_context(storage_state="state.json")
```csharp
// Save storage state into the file.
// Tests are executed in <TestProject>\bin\Debug\netX.0\ therefore relative path is used to reference playwright/.auth created in project root
await context.StorageStateAsync(new()
{
Path = "state.json"
Path = "../../../playwright/.auth/state.json"
});
// Create a new context with the saved storage state.
var context = await browser.NewContextAsync(new()
{
StorageStatePath = "state.json"
StorageStatePath = "../../../playwright/.auth/state.json"
});
```

View file

@ -148,7 +148,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
@ -266,7 +266,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
@ -370,7 +370,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
@ -388,23 +388,49 @@ jobs:
Once you have your [GitHub actions workflow](#setting-up-github-actions) setup then all you need to do is [Create a repo on GitHub](https://docs.github.com/en/get-started/quickstart/create-a-repo) or push your code to an existing repository. Follow the instructions on GitHub and don't forget to [initialize a git repository](https://github.com/git-guides/git-init) using the `git init` command so you can [add](https://github.com/git-guides/git-add), [commit](https://github.com/git-guides/git-commit) and [push](https://github.com/git-guides/git-push) your code.
######
* langs: js, java, python
<img width="861" alt="Create a Repo and Push to GitHub" src="https://user-images.githubusercontent.com/13063165/183423254-d2735278-a2ab-4d63-bb99-48d8e5e447bc.png"/>
######
* langs: csharp
![dotnet repo on github](https://github.com/microsoft/playwright/assets/13063165/4f1b4cc3-b850-4d60-a99e-24057eaf91ad)
## Opening the Workflows
Click on the **Actions** tab to see the workflows. Here you will see if your tests have passed or failed.
<img width="847" alt="Opening the Workflows" src="https://user-images.githubusercontent.com/13063165/183423584-2ea18038-cd49-4daa-a20c-2205352f0933.png"/>
######
* langs: js, python, java
![opening the workflow](https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png)
######
* langs: csharp
![opening the workflow](https://github.com/microsoft/playwright/assets/13063165/71793c09-0815-4faa-866b-85684a1f87e5)
On Pull Requests you can also click on the **Details** link in the [PR status check](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks).
<img width="645" alt="pr status checked" src="https://user-images.githubusercontent.com/13063165/183722462-17a985db-0e10-4205-b16c-8aaac36117b9.png" />
## Viewing Test Logs
Clicking on the workflow run will show you the all the actions that GitHub performed and clicking on **Run Playwright tests** will show the error messages, what was expected and what was received as well as the call log.
<img width="839" alt="Viewing Test Logs" src="https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png"/>
######
* langs: js, python, java
![Viewing Test Logs](https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png)
######
* langs: csharp
![viewing the test logs](https://github.com/microsoft/playwright/assets/13063165/ba2d8d7b-ffce-42de-95e0-bcb35c421975)
## HTML Report
@ -441,12 +467,22 @@ Once you have served the report using `npx playwright show-report`, click on the
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274)
## Viewing the Trace
* langs: python, java, csharp
* langs: python, java
[trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/6d5885dc-d511-4c20-b728-040a7ef6cea4)
## Viewing the Trace
* langs: csharp
You can upload Traces which get created on your CI like GitHub Actions as artifacts. This requires [starting and stopping the trace](./trace-viewer-intro#recording-a-trace). We recommend only recording traces for failing tests. Once your traces have been uploaded to CI, they can then be downloaded and opened using [trace.playwright.dev](https://trace.playwright.dev), which is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
######
* langs: csharp
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/84150084-5019-470a-8449-b61d206bfbb0)
## Publishing report on the web
* langs: js

View file

@ -29,7 +29,7 @@ playwright codegen demo.playwright.dev/todomvc
```
```bash csharp
pwsh bin/Debug/netX/playwright.ps1 codegen demo.playwright.dev/todomvc
pwsh bin/Debug/net8.0/playwright.ps1 codegen demo.playwright.dev/todomvc
```
### Recording a test

View file

@ -139,7 +139,7 @@ To learn more about the trace viewer see our [Trace Viewer guide](./trace-viewer
CodeGen will auto generate your tests for you as you perform actions in the browser and is a great way to quickly get started. The viewport for the browser window is set to a specific width and height. See the [configuration guide](./test-configuration.md) to change the viewport or emulate different environments.
<LiteYouTube
id="LM4yqrOzmFE"
id="5XIZPqKkdBA"
title="Generating Playwright tests in VS Code"
/>

View file

@ -217,6 +217,10 @@ await page.getByText('Item').click({ button: 'right' });
// Shift + click
await page.getByText('Item').click({ modifiers: ['Shift'] });
// Ctrl + click or Windows and Linux
// Meta + click on macOS
await page.getByText('Item').click({ modifiers: ['ControlOrMeta'] });
// Hover over element
await page.getByText('Item').hover();
@ -237,6 +241,10 @@ page.getByText("Item").click(new Locator.ClickOptions().setButton(MouseButton.RI
// Shift + click
page.getByText("Item").click(new Locator.ClickOptions().setModifiers(Arrays.asList(KeyboardModifier.SHIFT)));
// Ctrl + click or Windows and Linux
// Meta + click on macOS
page.getByText("Item").click(new Locator.ClickOptions().setModifiers(Arrays.asList(KeyboardModifier.CONTROL_OR_META)));
// Hover over element
page.getByText("Item").hover();
@ -257,6 +265,10 @@ await page.get_by_text("Item").click(button="right")
# Shift + click
await page.get_by_text("Item").click(modifiers=["Shift"])
# Ctrl + click or Windows and Linux
# Meta + click on macOS
await page.get_by_text("Item").click(modifiers=["ControlOrMeta"])
# Hover over element
await page.get_by_text("Item").hover()
@ -297,6 +309,10 @@ await page.GetByText("Item").ClickAsync(new() { Button = MouseButton.Right });
// Shift + click
await page.GetByText("Item").ClickAsync(new() { Modifiers = new[] { KeyboardModifier.Shift } });
// Ctrl + click or Windows and Linux
// Meta + click on macOS
await page.GetByText("Item").ClickAsync(new() { Modifiers = new[] { KeyboardModifier.ControlOrMeta } });
// Hover over element
await page.GetByText("Item").HoverAsync();

View file

@ -69,13 +69,13 @@ dotnet add package Microsoft.Playwright.MSTest
dotnet build
```
4. Install required browsers by replacing `netX` with the actual output folder name, e.g. `net8.0`:
1. Install required browsers. This example uses `net8.0`, if you are using a different version of .NET you will need to adjust the command and change `net8.0` to your version.
```bash
pwsh bin/Debug/netX/playwright.ps1 install
pwsh bin/Debug/net8.0/playwright.ps1 install
```
If `pwsh` is not available, you have to [install PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell).
If `pwsh` is not available, you will have to [install PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell).
## Add Example Tests
@ -102,28 +102,28 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
public class ExampleTest : PageTest
{
[Test]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage()
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
[Test]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await getStarted.ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro"));
}
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
```
@ -140,28 +140,28 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestMethod]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage()
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
[TestMethod]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await getStarted.ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro"));
}
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
```
@ -170,33 +170,13 @@ public class UnitTest1 : PageTest
## Running the Example Tests
By default tests will be run on Chromium. This can be configured via the `BROWSER` environment variable, or by adjusting the [launch configuration options](./test-runners.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
By default tests will be run on Chromium. This can be configured via the `BROWSER` environment variable, or by adjusting the [launch configuration options](./running-tests.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
```bash
dotnet test -- NUnit.NumberOfTestWorkers=5
dotnet test
```
</TabItem>
<TabItem value="mstest">
```bash
dotnet test -- MSTest.Parallelize.Workers=5
```
</TabItem>
</Tabs>
See our doc on [Test Runners](./test-runners.md) to learn more about running tests in headed mode, running multiple tests, running specific configurations etc.
See our doc on [Running and Debugging Tests](./running-tests.md) to learn more about running tests in headed mode, running multiple tests, running specific configurations etc.
## System requirements
@ -209,7 +189,7 @@ See our doc on [Test Runners](./test-runners.md) to learn more about running tes
- [Write tests using web first assertions, page fixtures and locators](./writing-tests.md)
- [Run single test, multiple tests, headed mode](./running-tests.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)
- [Generate tests with Codegen](./codegen.md)
- [See a trace of your tests](./trace-viewer-intro.md)
- [Using Playwright as library](./library.md)
- [Run tests on CI](./ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

View file

@ -17,7 +17,7 @@ Playwright Test was created specifically to accommodate the needs of end-to-end
## Installing Playwright
Get started by installing Playwright using npm or yarn. Alternatively you can also get started and run your tests using the [VS Code Extension](./getting-started-vscode.md).
Get started by installing Playwright using npm, yarn or pnpm. Alternatively you can also get started and run your tests using the [VS Code Extension](./getting-started-vscode.md).
<Tabs
defaultValue="npm"

View file

@ -109,7 +109,7 @@ Sometimes, it is essential to make an API request, but the response needs to be
allow for reproducible testing. In that case, instead of mocking the request, one
can perform the request and fulfill it with the modified response.
In the example below we intercept the call to the fruit API and add a new fruit called 'playwright', to the data. We then go to the url and assert that this data is there:
In the example below we intercept the call to the fruit API and add a new fruit called 'Loquat', to the data. We then go to the url and assert that this data is there:
```js

View file

@ -143,7 +143,8 @@ handle new pages opened by `target="_blank"` links.
const pagePromise = context.waitForEvent('page');
await page.getByText('open new tab').click();
const newPage = await pagePromise;
await newPage.waitForLoadState();
// Interact with the new page normally.
await newPage.getByRole('button').click();
console.log(await newPage.title());
```
@ -152,7 +153,8 @@ console.log(await newPage.title());
Page newPage = context.waitForPage(() -> {
page.getByText("open new tab").click(); // Opens a new tab
});
newPage.waitForLoadState();
// Interact with the new page normally
newPage.getByRole(AriaRole.BUTTON).click();
System.out.println(newPage.title());
```
@ -162,7 +164,8 @@ async with context.expect_page() as new_page_info:
await page.get_by_text("open new tab").click() # Opens a new tab
new_page = await new_page_info.value
await new_page.wait_for_load_state()
# Interact with the new page normally
await new_page.get_by_role("button").click()
print(await new_page.title())
```
@ -172,7 +175,8 @@ with context.expect_page() as new_page_info:
page.get_by_text("open new tab").click() # Opens a new tab
new_page = new_page_info.value
new_page.wait_for_load_state()
# Interact with the new page normally
new_page.get_by_role("button").click()
print(new_page.title())
```
@ -182,7 +186,8 @@ var newPage = await context.RunAndWaitForPageAsync(async () =>
{
await page.GetByText("open new tab").ClickAsync();
});
await newPage.WaitForLoadStateAsync();
// Interact with the new page normally
await newPage.GetByRole(AriaRole.Button).ClickAsync();
Console.WriteLine(await newPage.TitleAsync());
```
@ -242,8 +247,8 @@ This event is emitted in addition to the `browserContext.on('page')` event, but
const popupPromise = page.waitForEvent('popup');
await page.getByText('open the popup').click();
const popup = await popupPromise;
// Wait for the popup to load.
await popup.waitForLoadState();
// Interact with the new popup normally.
await popup.getByRole('button').click();
console.log(await popup.title());
```
@ -252,7 +257,8 @@ console.log(await popup.title());
Page popup = page.waitForPopup(() -> {
page.getByText("open the popup").click();
});
popup.waitForLoadState();
// Interact with the popup normally
popup.getByRole(AriaRole.BUTTON).click();
System.out.println(popup.title());
```
@ -262,7 +268,8 @@ async with page.expect_popup() as popup_info:
await page.get_by_text("open the popup").click()
popup = await popup_info.value
await popup.wait_for_load_state()
# Interact with the popup normally
await popup.get_by_role("button").click()
print(await popup.title())
```
@ -272,7 +279,8 @@ with page.expect_popup() as popup_info:
page.get_by_text("open the popup").click()
popup = popup_info.value
popup.wait_for_load_state()
# Interact with the popup normally
popup.get_by_role("button").click()
print(popup.title())
```
@ -282,7 +290,8 @@ var popup = await page.RunAndWaitForPopupAsync(async () =>
{
await page.GetByText("open the popup").ClickAsync();
});
await popup.WaitForLoadStateAsync();
// Interact with the popup normally
await popup.GetByRole(AriaRole.Button).ClickAsync();
Console.WriteLine(await popup.TitleAsync());
```

View file

@ -6,6 +6,104 @@ toc_max_heading_level: 2
import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.44
### New APIs
**Accessibility assertions**
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
```js
const locator = page.getByRole('button');
await expect(locator).toHaveAccessibleName('Submit');
```
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
```js
const locator = page.getByRole('button');
await expect(locator).toHaveAccessibleName('Upload the photo');
```
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
```js
const locator = page.getByTestId('save-button');
await expect(locator).toHaveRole('button');
```
**Locator handler**
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `noWaitAfter` option.
- You can use new `times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
```js
const locator = page.getByText('This interstitial covers the button');
await page.addLocatorHandler(locator, async overlay => {
await overlay.locator('#close').click();
}, { times: 3, noWaitAfter: true });
// Run your tests that can be interrupted by the overlay.
// ...
await page.removeLocatorHandler(locator);
```
**Miscellaneous options**
- [`multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `apiRequestContext.fetch()` now accepts [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and supports repeating fields with the same name.
```js
const formData = new FormData();
formData.append('file', new File(['let x = 2024;'], 'f1.js', { type: 'text/javascript' }));
formData.append('file', new File(['hello'], 'f2.txt', { type: 'text/plain' }));
context.request.post('https://example.com/uploadFiles', {
multipart: formData
});
```
- `expect(callback).toPass({ intervals })` can now be configured by `expect.toPass.inervals` option globally in [`property: TestConfig.expect`] or per project in [`property: TestProject.expect`].
- `expect(page).toHaveURL(url)` now supports `ignoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
- [`property: TestProject.ignoreSnapshots`](./api/class-testproject#test-project-ignore-snapshots) allows to configure per project whether to skip screenshot expectations.
**Reporter API**
- New method [`method: Suite.entries`] returns child test suites and test cases in their declaration order. [`property: Suite.type`] and [`property: TestCase.type`] can be used to tell apart test cases and suites in the list.
- [Blob](./test-reporters#blob-reporter) reporter now allows overriding report file path with a single option `outputFile`. The same option can also be specified as `PLAYWRIGHT_BLOB_OUTPUT_FILE` environment variable that might be more convenient on CI/CD.
- [JUnit](./test-reporters#junit-reporter) reporter now supports `includeProjectInTestName` option.
**Command line**
- `--last-failed` CLI option to for running only tests that failed in the previous run.
First run all tests:
```sh
$ npx playwright test
Running 103 tests using 5 workers
...
2 failed
[chromium] my-test.spec.ts:8:5 two ─────────────────────────────────────────────────────────
[chromium] my-test.spec.ts:13:5 three ──────────────────────────────────────────────────────
101 passed (30.0s)
```
Now fix the failing tests and run Playwright again with `--last-failed` option:
```sh
$ npx playwright test --last-failed
Running 2 tests using 2 workers
2 passed (1.2s)
```
### Browser Versions
* Chromium 125.0.6422.14
* Mozilla Firefox 125.0.1
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 124
* Microsoft Edge 124
## Version 1.43
### New APIs

View file

@ -7,57 +7,130 @@ title: "Running and debugging tests"
You can run a single test, a set of tests or all tests. Tests can be run on different browsers. By default, tests are run in a headless manner, meaning no browser window will be opened while running the tests and results will be seen in the terminal. If you prefer, you can run your tests in headed mode by using the `headless` test run parameter.
- Running all tests
**You will learn**
```bash
dotnet test
```
- [How to run tests](#running-tests)
- [How to debug tests](#debugging-tests)
- Running a single test file
## Running tests
```bash
dotnet test --filter "MyClassName"
```
### Run all tests
- Run a set of test files
Use the following command to run all tests.
```bash
dotnet test --filter "MyClassName1|MyClassName2"
```
```bash
dotnet test
```
- Run the test with the title
### Run your tests in headed mode
```bash
dotnet test --filter "Name~TestMethod1"
```
Use the following command to run your tests in headed mode opening a browser window for each test.
- Running Tests on specific browsers
```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```bash
dotnet test -- Playwright.BrowserName=webkit
```
```batch tab=bash-batch lang=csharp
set HEADED=1
dotnet test
```
- Running Tests on multiple browsers
```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
To run your test on multiple browsers or configurations, you need to invoke the `dotnet test` command multiple times. There you can then either specify the `BROWSER` environment variable or set the `Playwright.BrowserName` via the runsettings file:
### Run tests on different browsers: Browser env
```bash
dotnet test --settings:chromium.runsettings
dotnet test --settings:firefox.runsettings
dotnet test --settings:webkit.runsettings
```
Specify which browser you would like to run your tests on via the `BROWSER` environment variable.
```xml
<?xml version="1.0" encoding="utf-8"?>
```bash tab=bash-bash lang=csharp
BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
### Run tests on different browsers: launch configuration
Specify which browser you would like to run your tests on by adjusting the launch configuration options:
```bash
dotnet test -- Playwright.BrowserName=webkit
```
To run your test on multiple browsers or configurations, you need to invoke the `dotnet test` command multiple times. There you can then either specify the `BROWSER` environment variable or set the `Playwright.BrowserName` via the runsettings file:
```bash
dotnet test --settings:chromium.runsettings
dotnet test --settings:firefox.runsettings
dotnet test --settings:webkit.runsettings
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
</Playwright>
</RunSettings>
```
```
For more information see [selective unit tests](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest) in the Microsoft docs.
### Run specific tests
To run a single test file, use the filter flag followed by the class name of the test you want to run.
```bash
dotnet test --filter "ExampleTest"
```
To run a set of test files, use the filter flag followed by the class names of the tests you want to run.
```bash
dotnet test --filter "ExampleTest1|ExampleTest2"
```
To run a test with a specific title use the filter flag followed by *Name~* and the title of the test.
```bash
dotnet test --filter "Name~GetStartedLink"
```
### Run tests with multiple workers:
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```bash
dotnet test -- NUnit.NumberOfTestWorkers=5
```
</TabItem>
<TabItem value="mstest">
```bash
dotnet test -- MSTest.Parallelize.Workers=5
```
</TabItem>
</Tabs>
## Debugging Tests
Since Playwright runs in .NET, you can debug it with your debugger of choice in e.g. Visual Studio Code or Visual Studio. Playwright comes with the Playwright Inspector which allows you to step through Playwright API calls, see their debug logs and explore [locators](./locators.md).
@ -76,7 +149,7 @@ $env:PWDEBUG=1
dotnet test
```
<img width="712" alt="Playwright Inspector" src="https://user-images.githubusercontent.com/883973/108614092-8c478a80-73ac-11eb-9597-67dfce110e00.png"></img>
![debugging tests with playwright inspector](https://github.com/microsoft/playwright/assets/13063165/a1e758d3-d379-414f-be0b-7339f12bb635)
Check out our [debugging guide](./debug.md) to learn more about the [Playwright Inspector](./debug.md#playwright-inspector) as well as debugging with [Browser Developer tools](./debug.md#browser-developer-tools).

View file

@ -1,135 +0,0 @@
# class: ConfigInWorker
* since: v1.10
* langs: js
Resolved configuration available via [`property: TestInfo.config`] and [`property: WorkerInfo.config`].
## property: ConfigInWorker.configFile
* since: v1.20
- type: ?<[string]>
Path to the configuration file (if any) used to run the tests.
## property: ConfigInWorker.forbidOnly
* since: v1.10
- type: <[boolean]>
See [`property: TestConfig.forbidOnly`].
## property: ConfigInWorker.fullyParallel
* since: v1.20
- type: <[boolean]>
See [`property: TestConfig.fullyParallel`].
## property: ConfigInWorker.globalSetup
* since: v1.10
- type: <[null]|[string]>
See [`property: TestConfig.globalSetup`].
## property: ConfigInWorker.globalTeardown
* since: v1.10
- type: <[null]|[string]>
See [`property: TestConfig.globalTeardown`].
## property: ConfigInWorker.globalTimeout
* since: v1.10
- type: <[int]>
See [`property: TestConfig.globalTimeout`].
## property: ConfigInWorker.grep
* since: v1.10
- type: <[RegExp]|[Array]<[RegExp]>>
See [`property: TestConfig.grep`].
## property: ConfigInWorker.grepInvert
* since: v1.10
- type: <[null]|[RegExp]|[Array]<[RegExp]>>
See [`property: TestConfig.grepInvert`].
## property: ConfigInWorker.maxFailures
* since: v1.10
- type: <[int]>
See [`property: TestConfig.maxFailures`].
## property: ConfigInWorker.metadata
* since: v1.10
- type: <[Metadata]>
See [`property: TestConfig.metadata`].
## property: ConfigInWorker.preserveOutput
* since: v1.10
- type: <[PreserveOutput]<"always"|"never"|"failures-only">>
See [`property: TestConfig.preserveOutput`].
## property: ConfigInWorker.projects
* since: v1.10
- type: <[Array]<[ProjectInWorker]>>
List of resolved projects.
## property: ConfigInWorker.quiet
* since: v1.10
- type: <[boolean]>
See [`property: TestConfig.quiet`].
## property: ConfigInWorker.reporter
* since: v1.10
- type: <[string]|[Array]<[Object]>|[BuiltInReporter]<"list"|"dot"|"line"|"github"|"json"|"junit"|"null"|"html">>
- `0` <[string]> Reporter name or module or file path
- `1` <[Object]> An object with reporter options if any
See [`property: TestConfig.reporter`].
## property: ConfigInWorker.reportSlowTests
* since: v1.10
- type: <[null]|[Object]>
- `max` <[int]> The maximum number of slow test files to report. Defaults to `5`.
- `threshold` <[float]> Test duration in milliseconds that is considered slow. Defaults to 15 seconds.
See [`property: TestConfig.reportSlowTests`].
## property: ConfigInWorker.rootDir
* since: v1.20
- type: <[string]>
## property: ConfigInWorker.shard
* since: v1.10
- type: <[null]|[Object]>
- `total` <[int]> The total number of shards.
- `current` <[int]> The index of the shard to execute, one-based.
See [`property: TestConfig.shard`].
## property: ConfigInWorker.updateSnapshots
* since: v1.10
- type: <[UpdateSnapshots]<"all"|"none"|"missing">>
See [`property: TestConfig.updateSnapshots`].
## property: ConfigInWorker.version
* since: v1.20
- type: <[string]>
Playwright version.
## property: ConfigInWorker.webServer
* since: v1.10
- type: <[null]|[Object]>
See [`property: TestConfig.webServer`].
## property: ConfigInWorker.workers
* since: v1.10
- type: <[int]>
See [`property: TestConfig.workers`].

View file

@ -2,13 +2,13 @@
* since: v1.10
* langs: js
Resolved configuration passed to [`method: Reporter.onBegin`].
Resolved configuration which is accessible via [`property: TestInfo.config`] and is passed to the test reporters. To see the format of Playwright configuration file, please see [TestConfig] instead.
## property: FullConfig.configFile
* since: v1.20
- type: ?<[string]>
Path to the configuration file (if any) used to run the tests.
Path to the configuration file used to run the tests. The value is an empty string if no config file was used.
## property: FullConfig.forbidOnly
* since: v1.10
@ -102,6 +102,8 @@ See [`property: TestConfig.reportSlowTests`].
* since: v1.20
- type: <[string]>
Base directory for all relative paths used in the reporters.
## property: FullConfig.shard
* since: v1.10
- type: <[null]|[Object]>

View file

@ -2,10 +2,7 @@
* since: v1.10
* langs: js
Runtime representation of the test project configuration that is passed
to [Reporter]. It exposes some of the resolved fields declared in
[TestProject]. You can get [FullProject] instance from [`property: FullConfig.projects`]
or [`method: Suite.project`].
Runtime representation of the test project configuration. It is accessible in the tests via [`property: TestInfo.project`] and [`property: WorkerInfo.project`] and is passed to the test reporters. To see the format of the project in the Playwright configuration file please see [TestProject] instead.
## property: FullProject.dependencies
* since: v1.31

View file

@ -1,96 +0,0 @@
# class: ProjectInWorker
* since: v1.10
* langs: js
Runtime representation of the test project configuration that can be accessed
in the tests via [`property: TestInfo.project`] and [`property: WorkerInfo.project`].
## property: ProjectInWorker.dependencies
* since: v1.31
- type: <[Array]<[string]>>
See [`property: TestProject.dependencies`].
## property: ProjectInWorker.grep
* since: v1.10
- type: <[RegExp]|[Array]<[RegExp]>>
See [`property: TestProject.grep`].
## property: ProjectInWorker.grepInvert
* since: v1.10
- type: <[null]|[RegExp]|[Array]<[RegExp]>>
See [`property: TestProject.grepInvert`].
## property: ProjectInWorker.metadata
* since: v1.10
- type: <[Metadata]>
See [`property: TestProject.metadata`].
## property: ProjectInWorker.name
* since: v1.10
- type: <[string]>
See [`property: TestProject.name`].
## property: ProjectInWorker.snapshotDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.snapshotDir`].
## property: ProjectInWorker.outputDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.outputDir`].
## property: ProjectInWorker.repeatEach
* since: v1.10
- type: <[int]>
See [`property: TestProject.repeatEach`].
## property: ProjectInWorker.retries
* since: v1.10
- type: <[int]>
See [`property: TestProject.retries`].
## property: ProjectInWorker.teardown
* since: v1.34
- type: ?<[string]>
See [`property: TestProject.teardown`].
## property: ProjectInWorker.testDir
* since: v1.10
- type: <[string]>
See [`property: TestProject.testDir`].
## property: ProjectInWorker.testIgnore
* since: v1.10
- type: <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
See [`property: TestProject.testIgnore`].
## property: ProjectInWorker.testMatch
* since: v1.10
- type: <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
See [`property: TestProject.testMatch`].
## property: ProjectInWorker.timeout
* since: v1.10
- type: <[int]>
See [`property: TestProject.timeout`].
## property: ProjectInWorker.use
* since: v1.10
- type: <[Fixtures]>
See [`property: TestProject.use`].

View file

@ -2,7 +2,7 @@
* since: v1.10
* langs: js
Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or `testDir`. These options are described in the [TestConfig] object in the [configuration file](../test-configuration.md).
Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or `testDir`. These options are described in the [TestConfig] object in the [configuration file](../test-configuration.md). This type describes format of the configuration file, to access resolved configuration parameters at run time use [FullConfig].
Playwright Test supports running multiple test projects at the same time. Project-specific options should be put to [`property: TestConfig.projects`], but top-level [TestConfig] can also define base options shared between all projects.
@ -41,20 +41,20 @@ export default defineConfig({
- type: ?<[Object]>
- `timeout` ?<[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
- `toHaveScreenshot` ?<[Object]> Configuration for the [`method: PageAssertions.toHaveScreenshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`.
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: style`] in [`method: Page.screenshot`].
- `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
- `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `toPass` ?<[Object]> Configuration for the [expect(value).toPass()](../test-assertions.md#expecttopass) method.
- `timeout` ?<[int]> timeout for toPass method in milliseconds.
- `intervals` ?<[Array]<[int]>> probe intervals for toPass method in milliseconds.
- `intervals` ?<[Array]<[int]>> Probe intervals for toPass method in milliseconds.
- `timeout` ?<[int]> Timeout for toPass method in milliseconds.
Configuration for the `expect` assertion library. Learn more about [various timeouts](../test-timeouts.md).
@ -112,7 +112,7 @@ export default defineConfig({
* since: v1.10
- type: ?<[string]>
Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [`TestConfig`] argument.
Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument.
Learn more about [global setup and teardown](../test-global-setup-teardown.md).
@ -458,8 +458,8 @@ export default defineConfig({
## property: TestConfig.shard
* since: v1.10
- type: ?<[null]|[Object]>
- `total` <[int]> The total number of shards.
- `current` <[int]> The index of the shard to execute, one-based.
- `total` <[int]> The total number of shards.
Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`.
@ -589,15 +589,15 @@ export default defineConfig({
* since: v1.10
- type: ?<[Object]|[Array]<[Object]>>
- `command` <[string]> Shell command to start. For example `npm run start`..
- `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified.
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
- `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file.
- `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default.
- `ignoreHTTPSErrors` ?<[boolean]> Whether to ignore HTTPS errors when fetching the `url`. Defaults to `false`.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `port` ?<[int]> The port that your http server is expected to appear on. It does wait until it accepts connections. Either `port` or `url` should be specified.
- `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally.
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
- `cwd` ?<[string]> Current working directory of the spawned process, defaults to the directory of the configuration file.
- `env` ?<[Object]<[string], [string]>> Environment variables to set for the command, `process.env` by default.
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
Launch a development web server (or multiple) during the tests.

View file

@ -106,7 +106,7 @@ Column number where the currently running test is declared.
## property: TestInfo.config
* since: v1.10
- type: <[ConfigInWorker]>
- type: <[FullConfig]>
Processed configuration from the [configuration file](../test-configuration.md).
@ -279,7 +279,7 @@ Also available as `process.env.TEST_PARALLEL_INDEX`. Learn more about [paralleli
## property: TestInfo.project
* since: v1.10
- type: <[ProjectInWorker]>
- type: <[FullProject]>
Processed project configuration from the [configuration file](../test-configuration.md).

View file

@ -2,7 +2,7 @@
* since: v1.10
* langs: js
Playwright Test supports running multiple test projects at the same time. This is useful for running tests in multiple configurations. For example, consider running tests against multiple browsers.
Playwright Test supports running multiple test projects at the same time. This is useful for running tests in multiple configurations. For example, consider running tests against multiple browsers. This type describes format of a project in the configuration file, to access resolved configuration parameters at run time use [FullProject].
`TestProject` encapsulates configuration specific to a single project. Projects are configured in [`property: TestConfig.projects`] specified in the [configuration file](../test-configuration.md). Note that all properties of [TestProject] are available in the top-level [TestConfig], in which case they are shared between all projects.
@ -135,6 +135,39 @@ Filter to only run tests with a title **not** matching one of the patterns. This
`grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests).
## property: TestProject.ignoreSnapshots
* since: v1.44
- type: ?<[boolean]>
Whether to skip snapshot expectations, such as `expect(value).toMatchSnapshot()` and `await expect(page).toHaveScreenshot()`.
**Usage**
The following example will only perform screenshot assertions on Chromium.
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: devices['Desktop Chrome'],
},
{
name: 'firefox',
use: devices['Desktop Firefox'],
ignoreSnapshots: true,
},
{
name: 'webkit',
use: devices['Desktop Safari'],
ignoreSnapshots: true,
},
],
});
```
## property: TestProject.metadata
* since: v1.10
- type: ?<[Metadata]>

View file

@ -6,7 +6,7 @@
## property: WorkerInfo.config
* since: v1.10
- type: <[ConfigInWorker]>
- type: <[FullConfig]>
Processed configuration from the [configuration file](../test-configuration.md).
@ -22,7 +22,7 @@ Also available as `process.env.TEST_PARALLEL_INDEX`. Learn more about [paralleli
## property: WorkerInfo.project
* since: v1.10
- type: <[ProjectInWorker]>
- type: <[FullProject]>
Processed project configuration from the [configuration file](../test-configuration.md).

View file

@ -18,12 +18,15 @@ title: "Assertions"
| [`method: LocatorAssertions.toBeInViewport`] | Element intersects viewport |
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
| [`method: LocatorAssertions.toContainText`] | Element contains text |
| [`method: LocatorAssertions.toHaveAccessibleDescription`] | Element has a matching [accessible description](https://w3c.github.io/accname/#dfn-accessible-description) |
| [`method: LocatorAssertions.toHaveAccessibleName`] | Element has a matching [accessible name](https://w3c.github.io/accname/#dfn-accessible-name) |
| [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute |
| [`method: LocatorAssertions.toHaveClass`] | Element has a class property |
| [`method: LocatorAssertions.toHaveCount`] | List has exact number of children |
| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property |
| [`method: LocatorAssertions.toHaveId`] | Element has an ID |
| [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property |
| [`method: LocatorAssertions.toHaveRole`] | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) |
| [`method: LocatorAssertions.toHaveText`] | Element matches text |
| [`method: LocatorAssertions.toHaveValue`] | Input has a value |
| [`method: LocatorAssertions.toHaveValues`] | Select has options selected |

View file

@ -40,12 +40,15 @@ Note that retrying assertions are async, so you must `await` them.
| [await expect(locator).toBeInViewport()](./api/class-locatorassertions.md#locator-assertions-to-be-in-viewport) | Element intersects viewport |
| [await expect(locator).toBeVisible()](./api/class-locatorassertions.md#locator-assertions-to-be-visible) | Element is visible |
| [await expect(locator).toContainText()](./api/class-locatorassertions.md#locator-assertions-to-contain-text) | Element contains text |
| [await expect(locator).toHaveAccessibleDescription()](./api/class-locatorassertions.md#locator-assertions-to-have-accessible-description) | Element has a matching [accessible description](https://w3c.github.io/accname/#dfn-accessible-description) |
| [await expect(locator).toHaveAccessibleName()](./api/class-locatorassertions.md#locator-assertions-to-have-accessible-name) | Element has a matching [accessible name](https://w3c.github.io/accname/#dfn-accessible-name) |
| [await expect(locator).toHaveAttribute()](./api/class-locatorassertions.md#locator-assertions-to-have-attribute) | Element has a DOM attribute |
| [await expect(locator).toHaveClass()](./api/class-locatorassertions.md#locator-assertions-to-have-class) | Element has a class property |
| [await expect(locator).toHaveCount()](./api/class-locatorassertions.md#locator-assertions-to-have-count) | List has exact number of children |
| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css) | Element has CSS property |
| [await expect(locator).toHaveId()](./api/class-locatorassertions.md#locator-assertions-to-have-id) | Element has an ID |
| [await expect(locator).toHaveJSProperty()](./api/class-locatorassertions.md#locator-assertions-to-have-js-property) | Element has a JavaScript property |
| [await expect(locator).toHaveRole()](./api/class-locatorassertions.md#locator-assertions-to-have-role) | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) |
| [await expect(locator).toHaveScreenshot()](./api/class-locatorassertions.md#locator-assertions-to-have-screenshot-1) | Element has a screenshot |
| [await expect(locator).toHaveText()](./api/class-locatorassertions.md#locator-assertions-to-have-text) | Element matches text |
| [await expect(locator).toHaveValue()](./api/class-locatorassertions.md#locator-assertions-to-have-value) | Input has a value |

View file

@ -79,14 +79,15 @@ Complete set of Playwright Test options is available in the [configuration file]
| Option | Description |
| :- | :- |
| Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from the files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. |
| `--headed` | Run tests in headed browsers. Useful for debugging. |
|`--browser`| Run test in a specific browser. Available options are `"chromium"`, `"firefox"`, `"webkit"` or `"all"` to run tests in all three browsers at the same time. |
| `--debug`| Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.|
| `-c <file>` or `--config <file>`| Configuration file. If not passed, defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
| `--debug`| Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.|
| `--forbid-only` | Whether to disallow `test.only`. Useful on CI.|
| `--global-timeout <number>` | Total timeout for the whole test run in milliseconds. By default, there is no global timeout. Learn more about [various timeouts](./test-timeouts.md).|
| `-g <grep>` or `--grep <grep>` | Only run tests matching this regular expression. For example, this will run `'should add to cart'` when passed `-g "add to cart"`. The regular expression will be tested against the string that consists of the test file name, `test.describe` titles if any, test title and all test tags, separated by spaces, e.g. `my-test.spec.ts my-suite my-test @smoke`. The filter does not apply to the tests from dependcy projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies). |
| `--grep-invert <grep>` | Only run tests **not** matching this regular expression. The opposite of `--grep`. The filter does not apply to the tests from dependcy projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies).|
| `--global-timeout <number>` | Total timeout for the whole test run in milliseconds. By default, there is no global timeout. Learn more about [various timeouts](./test-timeouts.md).|
| `--headed` | Run tests in headed browsers. Useful for debugging. |
| `--ignore-snapshots` | Whether to ignore [snapshots](./test-snapshots.md). Use this when snapshot expectations are known to be different, e.g. running tests on Linux against Windows screenshots. |
| `--last-failed` | Only re-run the failures.|
| `--list` | list all the tests, but do not run them.|
| `--max-failures <N>` or `-x`| Stop after the first `N` test failures. Passing `-x` stops after the first failure.|
| `--no-deps` | Ignore the dependencies between projects and behave as if they were not specified. |
@ -101,6 +102,5 @@ Complete set of Playwright Test options is available in the [configuration file]
| `--tag <tag>` | Only run tests with a tag matching this tag expression. Learn more about [tagging](./test-annotations.md#tag-tests). |
| `--timeout <number>` | Maximum timeout in milliseconds for each test, defaults to 30 seconds. Learn more about [various timeouts](./test-timeouts.md).|
| `--trace <mode>` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure` |
| `--ignore-snapshots` | Whether to ignore [snapshots](./test-snapshots.md). Use this when snapshot expectations are known to be different, e.g. running tests on Linux against Windows screenshots. |
| `--update-snapshots` or `-u` | Whether to update [snapshots](./test-snapshots.md) with actual results instead of comparing them. Use this when snapshot expectations have changed.|
| `--workers <number>` or `-j <number>`| The maximum number of concurrent worker processes that run in [parallel](./test-parallel.md). |

View file

@ -129,7 +129,7 @@ You can use the `globalSetup` option in the [configuration file](./test-configur
Similarly, use `globalTeardown` to run something once after all the tests. Alternatively, let `globalSetup` return a function that will be used as a global teardown. You can pass data such as port number, authentication tokens, etc. from your global setup to your tests using environment variables.
:::note
Using `globalSetup` and `globalTeardown` will not produce traces or artifacts. If you want to produce traces and artifacts, use [project dependencies](#option-1-project-dependencies).
Using `globalSetup` and `globalTeardown` will not produce traces or artifacts, and options like `headless` or `testIdAttribute` specified in the config file are not applied. If you want to produce traces and artifacts and respect config options, use [project dependencies](#option-1-project-dependencies).
:::
```js title="playwright.config.ts"

View file

@ -30,7 +30,7 @@ Returns the list of all test cases in this suite and its descendants, as opposit
* since: v1.44
- type: <[Array]<[TestCase]|[Suite]>>
Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can discriminate between different entry types using [`property: TestCase.type`] and [`property: Suite.type`].
Test cases and suites defined directly in this suite. The elements are returned in their declaration order. You can differentiate between various entry types by using [`property: TestCase.type`] and [`property: Suite.type`].
## property: Suite.location
* since: v1.10

View file

@ -34,7 +34,7 @@ See also [`property: TestResult.status`] for the actual status.
* since: v1.25
- type: <[string]>
Unique test ID that is computed based on the test file name, test title and project name. Test ID can be used as a history ID.
A test ID that is computed based on the test file name, test title and project name. The ID is unique within Playwright session.
## property: TestCase.location
* since: v1.10
@ -112,4 +112,4 @@ Returns a list of titles from the root down to this test.
* since: v1.44
- returns: <[TestCaseType]<"test">>
Returns type of the test.
Returns "test". Useful for detecting test cases in [`method: Suite.entries`].

View file

@ -215,16 +215,24 @@ Blob reports contain all the details about the test run and can be used later to
npx playwright test --reporter=blob
```
By default, the report is written into the `blob-report` directory in the package.json directory or current working directory (if no package.json is found). The report file name is `report.zip` or `report-<shard_number>.zip` when [sharding](./test-sharding.md) is used. Both output directory and report file name can be overridden in the configuration file:
By default, the report is written into the `blob-report` directory in the package.json directory or current working directory (if no package.json is found). The report file name looks like `report-<hash>.zip` or `report-<hash>-<shard_number>.zip` when [sharding](./test-sharding.md) is used. The hash is an optional value computed from `--grep`, `--grepInverted`, `--project` and file filters passed as command line arguments. The hash guarantees that running Playwright with different command line options will produce different but stable between runs report names. The output file name can be overridden in the configuration file or pass as `'PLAYWRIGHT_BLOB_OUTPUT_FILE'` environment variable.
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['blob', { outputDir: 'my-report', fileName: `report-${os.platform()}.zip` }]],
reporter: [['blob', { outputFile: `./blob-report/report-${os.platform()}.zip` }]],
});
```
Blob report supports following configuration options and environment variables:
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_BLOB_OUTPUT_DIR` | `outputDir` | Directory to save the output. Existing content is deleted before writing the new report. | `blob-report`
| `PLAYWRIGHT_BLOB_OUTPUT_NAME` | `fileName` | Report file name. | `report-<project>-<hash>-<shard_number>.zip`
| `PLAYWRIGHT_BLOB_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `outputDir` and `fileName` will be ignored. | `undefined`
### JSON reporter
JSON reporter produces an object with all information about the test run.
@ -255,6 +263,14 @@ export default defineConfig({
});
```
JSON report supports following configuration options and environment variables:
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_JSON_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is specified. | `cwd` or config directory.
| `PLAYWRIGHT_JSON_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JSON report is printed to the stdout.
| `PLAYWRIGHT_JSON_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JSON_OUTPUT_DIR` and `PLAYWRIGHT_JSON_OUTPUT_NAME` will be ignored. | JSON report is printed to the stdout.
### JUnit reporter
JUnit reporter produces a JUnit-style xml report.
@ -285,6 +301,18 @@ export default defineConfig({
});
```
JUnit report supports following configuration options and environment variables:
| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_JUNIT_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is not specified. | `cwd` or config directory.
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JUnit report is printed to the stdout.
| `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout.
| | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is.
| | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included.
| `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string.
| `PLAYWRIGHT_JUNIT_SUITE_NAME` | | Value of the `name` attribute on the root `<testsuites/>` report entry. | Empty string.
### GitHub Actions annotations
You can use the built in `github` reporter to get automatic failure annotations when running in GitHub actions.
@ -355,6 +383,7 @@ npx playwright test --reporter="./myreporter/my-awesome-reporter.ts"
* [Argos Visual Testing](https://argos-ci.com/docs/playwright)
* [Currents](https://www.npmjs.com/package/@currents/playwright)
* [GitHub Actions Reporter](https://www.npmjs.com/package/@estruyf/github-actions-reporter)
* [GitHub Pull Request Comment](https://github.com/marketplace/actions/playwright-report-comment)
* [Monocart](https://github.com/cenfun/monocart-reporter)
* [ReportPortal](https://github.com/reportportal/agent-js-playwright)
* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test)

View file

@ -5,107 +5,18 @@ title: "Test Runners"
## Introduction
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience
it works best with the built-in .NET test runner, and using NUnit as the test framework. NUnit is
also what we use internally for [our tests](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.Tests).
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience it works best with the built-in .NET test runner, and using NUnit as the test framework. NUnit is also what we use internally for [our tests](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.Tests).
Playwright and Browser instances can be reused between tests for better performance. We
recommend running each test case in a new BrowserContext, this way browser state will be
isolated between the tests.
<!-- TOC -->
## NUnit
Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package.
### Creating an NUnit project
```bash
# Create a new project
dotnet new nunit -n PlaywrightTests
cd PlaywrightTests
# Add the required reference
dotnet add package Microsoft.Playwright.NUnit
dotnet build
# Install the required browsers and operating system dependencies
pwsh bin/Debug/netX/playwright.ps1 install --with-deps
```
Modify the UnitTest1.cs:
```csharp
using Microsoft.Playwright.NUnit;
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class MyTest : PageTest
{
[Test]
public async Task ShouldHaveTheCorrectSlogan()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page.Locator("text=enables reliable end-to-end testing for modern web apps")).ToBeVisibleAsync();
}
[Test]
public async Task ShouldHaveTheCorrectTitle()
{
await Page.GotoAsync("https://playwright.dev");
var title = Page.Locator(".navbar__inner .navbar__title");
await Expect(title).ToHaveTextAsync("Playwright");
}
}
```
Run your tests against Chromium
```bash
dotnet test
```
Run your tests against WebKit
```bash tab=bash-bash lang=csharp
BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
Run your tests with GUI
```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```batch tab=bash-batch lang=csharp
set HEADED=1
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
You can also choose specifically which tests to run, using the [filtering capabilities](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=nunit):
```bash
dotnet test --filter "Name~Slogan"
```
Check out the [installation guide](./intro.md) to get started.
### Running NUnit tests in Parallel
@ -114,6 +25,10 @@ Only `ParallelScope.Self` is supported.
For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores.
```bash
dotnet test -- NUnit.NumberOfTestWorkers=5
```
### Customizing [BrowserContext] options
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example:
@ -223,91 +138,7 @@ There are a few base classes available to you in `Microsoft.Playwright.NUnit` na
Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package.
### Creating an MSTest project
```bash
# Create a new project
dotnet new mstest -n PlaywrightTests
cd PlaywrightTests
# Add the required reference
dotnet add package Microsoft.Playwright.MSTest
dotnet build
# Install the required browsers and operating system dependencies
pwsh bin/Debug/netX/playwright.ps1 install --with-deps
```
Modify the UnitTest1.cs:
```csharp
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1: PageTest
{
[TestMethod]
public async Task ShouldHaveTheCorrectSlogan()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page.Locator("text=enables reliable end-to-end testing for modern web apps")).ToBeVisibleAsync();
}
[TestMethod]
public async Task ShouldHaveTheCorrectTitle()
{
await Page.GotoAsync("https://playwright.dev");
var title = Page.Locator(".navbar__inner .navbar__title");
await Expect(title).ToHaveTextAsync("Playwright");
}
}
```
Run your tests against Chromium
```bash
dotnet test
```
Run your tests against WebKit
```bash tab=bash-bash lang=csharp
BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
Run your tests with GUI
```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```batch tab=bash-batch lang=csharp
set HEADED=1
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
You can also choose specifically which tests to run, using the [filtering capabilities](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest):
```bash
dotnet test --filter "Name~Slogan"
```
Check out the [installation guide](./intro.md) to get started.
### Running MSTest tests in Parallel
@ -331,7 +162,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestMethod]
public async Task TestWithCustomContextOptions()

View file

@ -55,6 +55,7 @@ def test_my_app_is_working(fixture_name):
- `context`: New [browser context](./browser-contexts) for a test.
- `page`: New [browser page](./pages) for a test.
- `new_context`: Allows creating different [browser contexts](./browser-contexts) for a test. Useful for multi-user scenarios. Accepts the same parameters as [`method: Browser.newContext`].
**Session scope**: These fixtures are created when requested in a test function and destroyed when all tests end.
@ -211,30 +212,6 @@ def browser_context_args(browser_context_args, playwright):
Or via the CLI `--device="iPhone 11 Pro"`
### Persistent context
```py title="conftest.py"
import pytest
from playwright.sync_api import BrowserType
from typing import Dict
@pytest.fixture(scope="session")
def context(
browser_type: BrowserType,
browser_type_launch_args: Dict,
browser_context_args: Dict
):
context = browser_type.launch_persistent_context("./foobar", **{
**browser_type_launch_args,
**browser_context_args,
"locale": "de-DE",
})
yield context
context.close()
```
When using that all pages inside your test are created from the persistent context.
### Using with `unittest.TestCase`
See the following example for using it with `unittest.TestCase`. This has a limitation,

View file

@ -7,7 +7,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Introduction
UI Mode let's you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. Filter tests by **text** or **@tag** or by **passed**, **failed** and **skipped** tests as well as by [**projects**](./test-projects) as set in your `playwright.config` file. See a full trace of your tests and hover back and forward over each action to see what was happening during each step and pop out the DOM snapshot to a separate window for a better debugging experience.
UI Mode lets you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. Filter tests by **text** or **@tag** or by **passed**, **failed** and **skipped** tests as well as by [**projects**](./test-projects) as set in your `playwright.config` file. See a full trace of your tests and hover back and forward over each action to see what was happening during each step and pop out the DOM snapshot to a separate window for a better debugging experience.
<LiteYouTube
id="d0u6XhXknzU"

View file

@ -10,7 +10,6 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright
**You will learn**
- How to record a trace
- How to open the HTML report
- How to open the trace viewer
## Recording a trace
@ -124,8 +123,6 @@ public class Tests : PageTest
[TearDown]
public async Task TearDown()
{
// This will produce e.g.:
// bin/Debug/net8.0/playwright-traces/PlaywrightTests.Tests.Test1.zip
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
@ -152,17 +149,17 @@ using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTestsMSTest;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestInitialize]
public async Task TestInitialize()
{
await Context.Tracing.StartAsync(new()
{
Title = TestContext.TestName,
Title = TestContext.CurrentContext.Test.ClassName + "." + TestContext.
Screenshots = true,
Snapshots = true,
Sources = true
@ -172,20 +169,18 @@ public class UnitTest1 : PageTest
[TestCleanup]
public async Task TestCleanup()
{
// This will produce e.g.:
// bin/Debug/net8.0/playwright-traces/PlaywrightTests.UnitTest1.zip
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
Environment.CurrentDirectory,
"playwright-traces",
$"{TestContext.FullyQualifiedTestClassName}.zip"
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
)
});
}
[TestMethod]
public async Task TestYourOnlineShop()
public async Task GetStartedLink()
{
// ...
}
@ -195,11 +190,11 @@ public class UnitTest1 : PageTest
</TabItem>
</Tabs>
This will record the trace and place it into the `bin/Debug/net8.0/playwright-traces/` directory.
This will record a zip file for each test, e.g. `PlaywrightTests.ExampleTest.GetStartedLink.zip` and place it into the `bin/Debug/net8.0/playwright-traces/` directory.
## Opening the trace
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located. This should include the `test-results` directory followed by the test name and then `trace.zip`.
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your trace's zip file is located. Once opened you can click on each action or use the timeline to see the state of the page before and after each action. You can also inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
```bash java
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
@ -210,14 +205,19 @@ playwright show-trace trace.zip
```
```bash csharp
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
pwsh bin/Debug/net8.0/playwright.ps1 show-trace bin/Debug/net8.0/playwright-traces/PlaywrightTests.ExampleTest.GetStartedLink.zip
```
## Viewing the trace
View traces of your test by clicking through each action or hovering using the timeline and see the state of the page before and after the action. Inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
######
* langs: python, java
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274)
######
* langs: csharp
![playwright trace viewer dotnet](https://github.com/microsoft/playwright/assets/13063165/4372d661-5bfa-4e1f-be65-0d2fe165a75c)
To learn more check out our detailed guide on [Trace Viewer](/trace-viewer.md).
## What's next

View file

@ -5,9 +5,33 @@ title: "Writing tests"
## Introduction
Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides the [Expect](./test-assertions) function to write assertions.
Playwright tests are simple, they
Take a look at the example test below to see how to write a test using using [locators](/locators.md) and web first assertions.
- **perform actions**, and
- **assert the state** against expectations.
There is no need to wait for anything prior to performing an action: Playwright
automatically waits for the wide range of [actionability](./actionability.md)
checks to pass prior to performing each action.
There is also no need to deal with the race conditions when performing the checks -
Playwright assertions are designed in a way that they describe the expectations
that need to be eventually met.
That's it! These design choices allow Playwright users to forget about flaky
timeouts and racy checks in their tests altogether.
**You will learn**
- [How to write the first test](/writing-tests.md#first-test)
- [How to perform actions](/writing-tests.md#actions)
- [How to use assertions](/writing-tests.md#assertions)
- [How tests run in isolation](/writing-tests.md#test-isolation)
- [How to use test hooks](/writing-tests.md#using-test-hooks)
## First test
Take a look at the following example to see how to write a test.
<Tabs
groupId="test-runners"
@ -30,30 +54,28 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
public class ExampleTest : PageTest
{
[Test]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage()
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
[Test]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await getStarted.ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation.
await Expect(Page
.GetByRole(AriaRole.Heading, new() { Name = "Installation" }))
.ToBeVisibleAsync();
}
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
```
@ -70,55 +92,109 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestMethod]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage()
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
[TestMethod]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await getStarted.ClickAsync();
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro"));
}
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
```
</TabItem>
</Tabs>
### Assertions
## Actions
### Navigation
Most of the tests will start by navigating the page to a URL. After that, the test
will be able to interact with the page elements.
```csharp
await Page.GotoAsync("https://playwright.dev");
```
Playwright will wait for the page to reach the load state prior to moving forward.
Learn more about the [`method: Page.goto`] options.
### Interactions
Performing actions starts with locating the elements. Playwright uses [Locators API](./locators.md) for that. Locators represent a way to find element(s) on the page at any moment, learn more about the [different types](./locators.md) of locators available. Playwright will wait for the element to be [actionable](./actionability.md) prior to performing the action, so there is no need to wait for it to become available.
```csharp
// Create a locator.
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Click it.
await getStarted.ClickAsync();
```
In most cases, it'll be written in one line:
```csharp
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
```
### Basic actions
This is the list of the most popular Playwright actions. Note that there are many more, so make sure to check the [Locator API](./api/class-locator.md) section to
learn more about them.
| Action | Description |
| :- | :- |
| [`method: Locator.check`] | Check the input checkbox |
| [`method: Locator.click`] | Click the element |
| [`method: Locator.uncheck`] | Uncheck the input checkbox |
| [`method: Locator.hover`] | Hover mouse over the element |
| [`method: Locator.fill`] | Fill the form field, input text |
| [`method: Locator.focus`] | Focus the element |
| [`method: Locator.press`] | Press single key |
| [`method: Locator.setInputFiles`] | Pick files to upload |
| [`method: Locator.selectOption`] | Select option in the drop down |
## Assertions
Playwright provides an async function called [Expect](./test-assertions) to assert and wait until the expected condition is met.
```csharp
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
```
```
Here is the list of the most popular async assertions. Note that there are [many more](./test-assertions.md) to get familiar with:
| Assertion | Description |
| :- | :- |
| [`method: LocatorAssertions.toBeChecked`] | Checkbox is checked |
| [`method: LocatorAssertions.toBeEnabled`] | Control is enabled |
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
| [`method: LocatorAssertions.toContainText`] | Element contains text |
| [`method: LocatorAssertions.toHaveAttribute`] | Element has attribute |
| [`method: LocatorAssertions.toHaveCount`] | List of elements has given length |
| [`method: LocatorAssertions.toHaveText`] | Element matches text |
| [`method: LocatorAssertions.toHaveValue`] | Input element has value |
| [`method: PageAssertions.toHaveTitle`] | Page has title |
| [`method: PageAssertions.toHaveURL`] | Page has URL |
### Locators
[Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.ClickAsync` `.FillAsync` etc.
```csharp
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/installation");
await getStarted.ClickAsync();
```
### Test Isolation
## Test Isolation
The Playwright NUnit and MSTest test framework base classes will isolate each test from each other by providing a separate `Page` instance. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser.
@ -141,7 +217,7 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
public class ExampleTest : PageTest
{
[Test]
public async Task BasicTest()
@ -162,7 +238,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestMethod]
public async Task BasicTest()
@ -175,7 +251,7 @@ public class UnitTest1 : PageTest
</TabItem>
</Tabs>
### Using Test Hooks
## Using Test Hooks
You can use `SetUp`/`TearDown` in NUnit or `TestInitialize`/`TestCleanup` in MSTest to prepare and clean up your test environment:
@ -198,7 +274,7 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
public class ExampleTest : PageTest
{
[Test]
public async Task MainNavigation()
@ -226,7 +302,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1 : PageTest
public class ExampleTest : PageTest
{
[TestMethod]
public async Task MainNavigation()
@ -251,3 +327,5 @@ public class UnitTest1 : PageTest
- [Run single test, multiple tests, headed mode](./running-tests.md)
- [Generate tests with Codegen](./codegen.md)
- [See a trace of your tests](./trace-viewer-intro.md)
- [Run tests on CI](./ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

68
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.44.0-next",
"version": "1.45.0-next",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -8162,10 +8162,10 @@
}
},
"packages/playwright": {
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8179,11 +8179,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"engines": {
"node": ">=16"
@ -8191,11 +8191,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"engines": {
"node": ">=16"
@ -8203,22 +8203,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"engines": {
"node": ">=16"
}
},
"packages/playwright-chromium": {
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8228,7 +8228,7 @@
}
},
"packages/playwright-core": {
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -8239,11 +8239,11 @@
},
"packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.0-next",
"playwright-core": "1.44.0-next",
"playwright": "1.45.0-next",
"playwright-core": "1.45.0-next",
"vite": "^5.2.8"
},
"engines": {
@ -8252,10 +8252,10 @@
},
"packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -8267,10 +8267,10 @@
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -8282,10 +8282,10 @@
},
"packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"vite-plugin-solid": "^2.7.0"
},
"bin": {
@ -8300,10 +8300,10 @@
},
"packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
@ -8318,10 +8318,10 @@
},
"packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
@ -8333,10 +8333,10 @@
},
"packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next",
"@playwright/experimental-ct-core": "1.45.0-next",
"@vitejs/plugin-vue2": "^2.2.0"
},
"bin": {
@ -8385,11 +8385,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8400,10 +8400,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.44.0-next",
"version": "1.45.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.0-next"
"playwright": "1.45.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8413,11 +8413,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.44.0-next",
"version": "1.45.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0-next"
"playwright-core": "1.45.0-next"
},
"bin": {
"playwright": "cli.js"

View file

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

View file

@ -98,27 +98,7 @@ export class Filter {
}
matches(test: TestCaseSummary): boolean {
if (!(test as any).searchValues) {
let status = 'passed';
if (test.outcome === 'unexpected')
status = 'failed';
if (test.outcome === 'flaky')
status = 'flaky';
if (test.outcome === 'skipped')
status = 'skipped';
const searchValues: SearchValues = {
text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(),
status: status as any,
file: test.location.file,
line: String(test.location.line),
column: String(test.location.column),
labels: test.tags.map(tag => tag.toLowerCase()),
};
(test as any).searchValues = searchValues;
}
const searchValues = (test as any).searchValues as SearchValues;
const searchValues = cacheSearchValues(test);
if (this.project.length) {
const matches = !!this.project.find(p => searchValues.project.includes(p));
if (!matches)
@ -128,6 +108,9 @@ export class Filter {
const matches = !!this.status.find(s => searchValues.status.includes(s));
if (!matches)
return false;
} else {
if (searchValues.status === 'skipped')
return false;
}
if (this.text.length) {
for (const text of this.text) {
@ -159,3 +142,50 @@ type SearchValues = {
labels: string[];
};
const searchValuesSymbol = Symbol('searchValues');
function cacheSearchValues(test: TestCaseSummary): SearchValues {
const cached = (test as any)[searchValuesSymbol] as SearchValues | undefined;
if (cached)
return cached;
let status: SearchValues['status'] = 'passed';
if (test.outcome === 'unexpected')
status = 'failed';
if (test.outcome === 'flaky')
status = 'flaky';
if (test.outcome === 'skipped')
status = 'skipped';
const searchValues: SearchValues = {
text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(),
status,
file: test.location.file,
line: String(test.location.line),
column: String(test.location.column),
labels: test.tags.map(tag => tag.toLowerCase()),
};
(test as any)[searchValuesSymbol] = searchValues;
return searchValues;
}
export function filterWithToken(tokens: string[], token: string, append: boolean): string {
if (append) {
if (!tokens.includes(token))
return '#?q=' + [...tokens, token].join(' ').trim();
return '#?q=' + tokens.filter(t => t !== token).join(' ').trim();
}
// if metaKey or ctrlKey is not pressed, replace existing token with new token
let prefix: 's:' | 'p:' | '@';
if (token.startsWith('s:'))
prefix = 's:';
if (token.startsWith('p:'))
prefix = 'p:';
if (token.startsWith('@'))
prefix = '@';
const newTokens = tokens.filter(t => !t.startsWith(prefix));
newTokens.push(token);
return '#?q=' + newTokens.join(' ').trim();
}

View file

@ -28,7 +28,7 @@ test('should render counters', async ({ mount }) => {
skipped: 10,
ok: false,
}} filterText='' setFilterText={() => {}}></HeaderView>);
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100');
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('90');
await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17');

View file

@ -22,6 +22,7 @@ import './headerView.css';
import * as icons from './icons';
import { Link, navigate } from './links';
import { statusIcon } from './statusIcon';
import { filterWithToken } from './filter';
export const HeaderView: React.FC<React.PropsWithChildren<{
stats: Stats,
@ -64,20 +65,23 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
const StatsNavView: React.FC<{
stats: Stats
}> = ({ stats }) => {
const searchParams = new URLSearchParams(window.location.hash.slice(1));
const q = searchParams.get('q')?.toString() || '';
const tokens = q.split(' ');
return <nav>
<Link className='subnav-item' href='#?'>
All <span className='d-inline counter'>{stats.total}</span>
All <span className='d-inline counter'>{stats.total - stats.skipped}</span>
</Link>
<Link className='subnav-item' href='#?q=s:passed'>
<Link className='subnav-item' click={filterWithToken(tokens, 's:passed', false)} ctrlClick={filterWithToken(tokens, 's:passed', true)}>
Passed <span className='d-inline counter'>{stats.expected}</span>
</Link>
<Link className='subnav-item' href='#?q=s:failed'>
<Link className='subnav-item' click={filterWithToken(tokens, 's:failed', false)} ctrlClick={filterWithToken(tokens, 's:failed', true)}>
{!!stats.unexpected && statusIcon('unexpected')} Failed <span className='d-inline counter'>{stats.unexpected}</span>
</Link>
<Link className='subnav-item' href='#?q=s:flaky'>
<Link className='subnav-item' click={filterWithToken(tokens, 's:flaky', false)} ctrlClick={filterWithToken(tokens, 's:flaky', true)}>
{!!stats.flaky && statusIcon('flaky')} Flaky <span className='d-inline counter'>{stats.flaky}</span>
</Link>
<Link className='subnav-item' href='#?q=s:skipped'>
<Link className='subnav-item' click={filterWithToken(tokens, 's:skipped', false)} ctrlClick={filterWithToken(tokens, 's:skipped', true)}>
Skipped <span className='d-inline counter'>{stats.skipped}</span>
</Link>
</nav>;

View file

@ -41,12 +41,19 @@ export const Route: React.FunctionComponent<{
};
export const Link: React.FunctionComponent<{
href: string,
href?: string,
click?: string,
ctrlClick?: string,
className?: string,
title?: string,
children: any,
}> = ({ href, className, children, title }) => {
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)' }} className={`${className || ''}`} href={href} title={title}>{children}</a>;
}> = ({ href, click, ctrlClick, className, children, title }) => {
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)', cursor: 'pointer' }} href={href} className={`${className || ''}`} title={title} onClick={e => {
if (click) {
e.preventDefault();
navigate(e.metaKey || e.ctrlKey ? ctrlClick || click : click);
}
}}>{children}</a>;
};
export const ProjectLink: React.FunctionComponent<{

View file

@ -78,11 +78,21 @@ const TestCaseViewLoader: React.FC<{
const testId = searchParams.get('testId');
const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
const run = +(searchParams.get('run') || '0');
const testIdToFileIdMap = React.useMemo(() => {
const map = new Map<string, string>();
for (const file of report.json().files) {
for (const test of file.tests)
map.set(test.testId, file.fileId);
}
return map;
}, [report]);
React.useEffect(() => {
(async () => {
if (!testId || testId === test?.testId)
return;
const fileId = testId.split('-')[0];
const fileId = testIdToFileIdMap.get(testId);
if (!fileId)
return;
const file = await report.entry(`${fileId}.json`) as TestFile;
@ -93,7 +103,7 @@ const TestCaseViewLoader: React.FC<{
}
}
})();
}, [test, report, testId]);
}, [test, report, testId, testIdToFileIdMap]);
return <TestCaseView projectNames={report.json().projectNames} test={test} anchor={anchor} run={run}></TestCaseView>;
};

View file

@ -18,7 +18,7 @@ import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
import { msToString } from './uiUtils';
import { Chip } from './chip';
import type { Filter } from './filter';
import { filterWithToken, type Filter } from './filter';
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
@ -94,23 +94,9 @@ const LabelsClickView: React.FC<React.PropsWithChildren<{
const onClickHandle = (e: React.MouseEvent, label: string) => {
e.preventDefault();
const searchParams = new URLSearchParams(window.location.hash.slice(1));
let q = searchParams.get('q')?.toString() || '';
// If metaKey or ctrlKey is pressed, add tag to search query without replacing existing tags.
// If metaKey or ctrlKey is pressed and tag is already in search query, remove tag from search query.
if (e.metaKey || e.ctrlKey) {
if (!q.includes(label))
q = `${q} ${label}`.trim();
else
q = q.split(' ').filter(t => t !== label).join(' ').trim();
} else {
// if metaKey or ctrlKey is not pressed, replace existing tags with new tag
if (!q.includes('@'))
q = `${q} ${label}`.trim();
else
q = (q.split(' ').filter(t => !t.startsWith('@')).join(' ').trim() + ` ${label}`).trim();
}
navigate(q ? `#?q=${q}` : '#');
const q = searchParams.get('q')?.toString() || '';
const tokens = q.split(' ');
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
};
return labels.length > 0 ? (

View file

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

View file

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

View file

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

View file

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

View file

@ -3,37 +3,31 @@
"browsers": [
{
"name": "chromium",
"revision": "1112",
"revision": "1117",
"installByDefault": true,
"browserVersion": "124.0.6367.29"
"browserVersion": "125.0.6422.26"
},
{
"name": "chromium-tip-of-tree",
"revision": "1210",
"revision": "1215",
"installByDefault": false,
"browserVersion": "125.0.6408.0"
"browserVersion": "126.0.6439.0"
},
{
"name": "firefox",
"revision": "1447",
"revision": "1449",
"installByDefault": true,
"browserVersion": "124.0"
},
{
"name": "firefox-asan",
"revision": "1447",
"installByDefault": false,
"browserVersion": "124.0"
"browserVersion": "125.0.1"
},
{
"name": "firefox-beta",
"revision": "1447",
"revision": "1449",
"installByDefault": false,
"browserVersion": "125.0b3"
"browserVersion": "126.0b1"
},
{
"name": "webkit",
"revision": "1998",
"revision": "2005",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",

View file

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

View file

@ -432,6 +432,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._browser._contexts.delete(this);
this._browserType?._contexts?.delete(this);
this._disposeHarRouters();
this.tracing._resetStackCounter();
this.emit(Events.BrowserContext.Close, this);
}

View file

@ -19,7 +19,7 @@ import type * as channels from '@protocol/channels';
import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { debugLogger } from '../utils/debugLogger';
import type { ExpectZone } from '../utils/stackTrace';
import { captureRawStack, captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
import { isUnderTest } from '../utils';
import { zones } from '../utils/zones';
import type { ClientInstrumentation } from './clientInstrumentation';
@ -161,12 +161,11 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
const logger = this._logger;
const stack = captureRawStack();
const apiZone = zones.zoneData<ApiZone>('apiZone', stack);
const apiZone = zones.zoneData<ApiZone>('apiZone');
if (apiZone)
return await func(apiZone);
const stackTrace = captureLibraryStackTrace(stack);
const stackTrace = captureLibraryStackTrace();
let apiName: string | undefined = stackTrace.apiName;
const frames: channels.StackFrame[] = stackTrace.frames;
@ -175,7 +174,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
apiName = undefined;
// Enclosing zone could have provided the apiName and wallTime.
const expectZone = zones.zoneData<ExpectZone>('expectZone', stack);
const expectZone = zones.zoneData<ExpectZone>('expectZone');
const wallTime = expectZone ? expectZone.wallTime : Date.now();
if (!isInternal && expectZone)
apiName = expectZone.title;
@ -188,9 +187,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
try {
logApiCall(logger, `=> ${apiName} started`, isInternal);
const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, callCookie, wallTime };
const result = await zones.run<ApiZone, Promise<R>>('apiZone', apiZone, async () => {
return await func(apiZone);
});
const result = await zones.run('apiZone', apiZone, async () => await func(apiZone));
csi?.onApiCallEnd(callCookie);
logApiCall(logger, `<= ${apiName} succeeded`, isInternal);
return result;

View file

@ -44,7 +44,7 @@ import { Tracing } from './tracing';
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { createInstrumentation } from './clientInstrumentation';
import type { ClientInstrumentation } from './clientInstrumentation';
import { formatCallLog, rewriteErrorMessage } from '../utils';
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
class Root extends ChannelOwner<channels.RootChannel> {
constructor(connection: Connection) {
@ -136,7 +136,9 @@ export class Connection extends EventEmitter {
const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName };
if (this._tracingCount && frames && type !== 'LocalUtils')
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
this.onmessage({ ...message, metadata });
// We need to exit zones before calling into the server, otherwise
// when we receive events from the server, we would be in an API zone.
zones.exitZones(() => this.onmessage({ ...message, metadata }));
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method }));
}

View file

@ -102,6 +102,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
async dispose(): Promise<void> {
await this._instrumentation.onWillCloseRequestContext(this);
await this._channel.dispose();
this._tracing._resetStackCounter();
this._request?._contexts.delete(this);
}
@ -186,18 +187,24 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
formData = objectToArray(options.form);
} else if (options.multipart) {
multipartData = [];
// Convert file-like values to ServerFilePayload structs.
for (const [name, value] of Object.entries(options.multipart)) {
if (isFilePayload(value)) {
const payload = value as FilePayload;
if (!Buffer.isBuffer(payload.buffer))
throw new Error(`Unexpected buffer type of 'data.${name}'`);
multipartData.push({ name, file: filePayloadToJson(payload) });
} else if (value instanceof fs.ReadStream) {
multipartData.push({ name, file: await readStreamToJson(value as fs.ReadStream) });
} else {
multipartData.push({ name, value: String(value) });
if (globalThis.FormData && options.multipart instanceof FormData) {
const form = options.multipart;
for (const [name, value] of form.entries()) {
if (isString(value)) {
multipartData.push({ name, value });
} else {
const file: ServerFilePayload = {
name: value.name,
mimeType: value.type,
buffer: Buffer.from(await value.arrayBuffer()),
};
multipartData.push({ name, file });
}
}
} else {
// Convert file-like values to ServerFilePayload structs.
for (const [name, value] of Object.entries(options.multipart))
multipartData.push(await toFormField(name, value));
}
}
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
@ -234,6 +241,19 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
}
}
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
if (isFilePayload(value)) {
const payload = value as FilePayload;
if (!Buffer.isBuffer(payload.buffer))
throw new Error(`Unexpected buffer type of 'data.${name}'`);
return { name, file: filePayloadToJson(payload) };
} else if (value instanceof fs.ReadStream) {
return { name, file: await readStreamToJson(value as fs.ReadStream) };
} else {
return { name, value: String(value) };
}
}
function isJsonParsable(value: any) {
if (typeof value !== 'string')
return false;

View file

@ -80,6 +80,10 @@ export class Locator implements api.Locator {
});
}
_equals(locator: Locator) {
return this._frame === locator._frame && this._selector === locator._selector;
}
page() {
return this._frame.page();
}

View file

@ -96,7 +96,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
_closeWasCalled: boolean = false;
private _harRouters: HarRouter[] = [];
private _locatorHandlers = new Map<number, Function>();
private _locatorHandlers = new Map<number, { locator: Locator, handler: (locator: Locator) => any, times: number | undefined }>();
static from(page: channels.PageChannel): Page {
return (page as any)._object;
@ -362,19 +362,38 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response);
}
async addLocatorHandler(locator: Locator, handler: Function): Promise<void> {
async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, noWaitAfter?: boolean } = {}): Promise<void> {
if (locator._frame !== this._mainFrame)
throw new Error(`Locator must belong to the main frame of this page`);
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector });
this._locatorHandlers.set(uid, handler);
if (options.times === 0)
return;
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, noWaitAfter: options.noWaitAfter });
this._locatorHandlers.set(uid, { locator, handler, times: options.times });
}
private async _onLocatorHandlerTriggered(uid: number) {
let remove = false;
try {
const handler = this._locatorHandlers.get(uid);
await handler?.();
if (handler && handler.times !== 0) {
if (handler.times !== undefined)
handler.times--;
await handler.handler(handler.locator);
}
remove = handler?.times === 0;
} finally {
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid }), true).catch(() => {});
if (remove)
this._locatorHandlers.delete(uid);
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid, remove }), true).catch(() => {});
}
}
async removeLocatorHandler(locator: Locator): Promise<void> {
for (const [uid, data] of this._locatorHandlers) {
if (data.locator._equals(locator)) {
this._locatorHandlers.delete(uid);
await this._channel.unregisterLocatorHandler({ uid }).catch(() => {});
}
}
}

View file

@ -76,10 +76,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
}
private async _doStopChunk(filePath: string | undefined) {
if (this._isTracing) {
this._isTracing = false;
this._connection.setIsTracing(false);
}
this._resetStackCounter();
if (!filePath) {
// Not interested in artifacts.
@ -113,4 +110,11 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
}
_resetStackCounter() {
if (this._isTracing) {
this._isTracing = false;
this._connection.setIsTracing(false);
}
}
}

View file

@ -332,6 +332,7 @@ scheme.PlaywrightNewRequestParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
proxy: tOptional(tObject({
server: tString,
@ -545,6 +546,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -623,6 +625,7 @@ scheme.BrowserNewContextParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -684,6 +687,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),
@ -1046,14 +1050,20 @@ scheme.PageGoForwardResult = tObject({
});
scheme.PageRegisterLocatorHandlerParams = tObject({
selector: tString,
noWaitAfter: tOptional(tBoolean),
});
scheme.PageRegisterLocatorHandlerResult = tObject({
uid: tNumber,
});
scheme.PageResolveLocatorHandlerNoReplyParams = tObject({
uid: tNumber,
remove: tOptional(tBoolean),
});
scheme.PageResolveLocatorHandlerNoReplyResult = tOptional(tObject({}));
scheme.PageUnregisterLocatorHandlerParams = tObject({
uid: tNumber,
});
scheme.PageUnregisterLocatorHandlerResult = tOptional(tObject({}));
scheme.PageReloadParams = tObject({
timeout: tOptional(tNumber),
waitUntil: tOptional(tType('LifecycleEvent')),
@ -1336,7 +1346,7 @@ scheme.FrameClickParams = tObject({
strict: tOptional(tBoolean),
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
delay: tOptional(tNumber),
button: tOptional(tEnum(['left', 'right', 'middle'])),
@ -1366,7 +1376,7 @@ scheme.FrameDblclickParams = tObject({
strict: tOptional(tBoolean),
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
delay: tOptional(tNumber),
button: tOptional(tEnum(['left', 'right', 'middle'])),
@ -1444,7 +1454,7 @@ scheme.FrameHoverParams = tObject({
selector: tString,
strict: tOptional(tBoolean),
force: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
timeout: tOptional(tNumber),
trial: tOptional(tBoolean),
@ -1591,7 +1601,7 @@ scheme.FrameTapParams = tObject({
strict: tOptional(tBoolean),
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
timeout: tOptional(tNumber),
trial: tOptional(tBoolean),
@ -1786,7 +1796,7 @@ scheme.ElementHandleCheckResult = tOptional(tObject({}));
scheme.ElementHandleClickParams = tObject({
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
delay: tOptional(tNumber),
button: tOptional(tEnum(['left', 'right', 'middle'])),
@ -1802,7 +1812,7 @@ scheme.ElementHandleContentFrameResult = tObject({
scheme.ElementHandleDblclickParams = tObject({
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
delay: tOptional(tNumber),
button: tOptional(tEnum(['left', 'right', 'middle'])),
@ -1832,7 +1842,7 @@ scheme.ElementHandleGetAttributeResult = tObject({
});
scheme.ElementHandleHoverParams = tObject({
force: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
timeout: tOptional(tNumber),
trial: tOptional(tBoolean),
@ -1956,7 +1966,7 @@ scheme.ElementHandleSetInputFilesResult = tOptional(tObject({}));
scheme.ElementHandleTapParams = tObject({
force: tOptional(tBoolean),
noWaitAfter: tOptional(tBoolean),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'Meta', 'Shift']))),
modifiers: tOptional(tArray(tEnum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']))),
position: tOptional(tType('Point')),
timeout: tOptional(tNumber),
trial: tOptional(tBoolean),
@ -2468,6 +2478,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
username: tString,
password: tString,
origin: tOptional(tString),
sendImmediately: tOptional(tBoolean),
})),
deviceScaleFactor: tOptional(tNumber),
isMobile: tOptional(tBoolean),

View file

@ -152,8 +152,8 @@ export class Chromium extends BrowserType {
error.logs = [
`Chromium sandboxing failed!`,
`================================`,
`To workaround sandboxing issues, do either of the following:`,
` - (preferred): Configure environment to support sandboxing: https://playwright.dev/docs/troubleshooting`,
`To avoid the sandboxing issue, do either of the following:`,
` - (preferred): Configure your environment to support sandboxing`,
` - (alternative): Launch Chromium without sandbox using 'chromiumSandbox: false' option`,
`================================`,
``,

View file

@ -54,4 +54,6 @@ export const chromiumSwitches = [
'--export-tagged-pdf',
// https://chromium-review.googlesource.com/c/chromium/src/+/4853540
'--disable-search-engine-choice-screen',
// https://issues.chromium.org/41491762
'--unsafely-disable-devtools-self-xss-warnings',
];

View file

@ -459,7 +459,7 @@ export class CRBrowserContext extends BrowserContext {
for (const page of this.pages())
await (page._delegate as CRPage).updateExtraHTTPHeaders();
for (const sw of this.serviceWorkers())
await (sw as CRServiceWorker).updateExtraHTTPHeaders(false);
await (sw as CRServiceWorker).updateExtraHTTPHeaders();
}
async setUserAgent(userAgent: string | undefined): Promise<void> {
@ -474,7 +474,7 @@ export class CRBrowserContext extends BrowserContext {
for (const page of this.pages())
await (page._delegate as CRPage).updateOffline();
for (const sw of this.serviceWorkers())
await (sw as CRServiceWorker).updateOffline(false);
await (sw as CRServiceWorker).updateOffline();
}
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
@ -482,7 +482,7 @@ export class CRBrowserContext extends BrowserContext {
for (const page of this.pages())
await (page._delegate as CRPage).updateHttpCredentials();
for (const sw of this.serviceWorkers())
await (sw as CRServiceWorker).updateHttpCredentials(false);
await (sw as CRServiceWorker).updateHttpCredentials();
}
async doAddInitScript(source: string) {

View file

@ -16,18 +16,15 @@
import { Worker } from '../page';
import type { CRBrowserContext } from './crBrowser';
import type { CRSession } from './crConnection';
import type * as types from '../types';
import { CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager';
import * as network from '../network';
import { BrowserContext } from '../browserContext';
import { headersArrayToObject } from '../../utils';
export class CRServiceWorker extends Worker {
readonly _browserContext: CRBrowserContext;
readonly _networkManager?: CRNetworkManager;
private _session: CRSession;
private _extraHTTPHeaders: types.HeadersArray | null = null;
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
super(browserContext, url);
@ -40,11 +37,11 @@ export class CRServiceWorker extends Worker {
});
if (this._networkManager && this._isNetworkInspectionEnabled()) {
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
this.updateRequestInterception();
this.updateExtraHTTPHeaders(true);
this.updateHttpCredentials(true);
this.updateOffline(true);
this.updateExtraHTTPHeaders();
this.updateHttpCredentials();
this.updateOffline();
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
}
session.send('Runtime.enable', {}).catch(e => { });
@ -61,41 +58,28 @@ export class CRServiceWorker extends Worker {
super.didClose();
}
async updateOffline(initial: boolean): Promise<void> {
async updateOffline(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
return;
const offline = !!this._browserContext._options.offline;
if (!initial || offline)
await this._networkManager?.setOffline(offline);
await this._networkManager?.setOffline(!!this._browserContext._options.offline).catch(() => {});
}
async updateHttpCredentials(initial: boolean): Promise<void> {
async updateHttpCredentials(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
return;
const credentials = this._browserContext._options.httpCredentials || null;
if (!initial || credentials)
await this._networkManager?.authenticate(credentials);
await this._networkManager?.authenticate(this._browserContext._options.httpCredentials || null).catch(() => {});
}
async updateExtraHTTPHeaders(initial: boolean): Promise<void> {
async updateExtraHTTPHeaders(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
return;
const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders,
this._extraHTTPHeaders,
]);
if (!initial || headers.length)
await this._session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
await this._networkManager?.setExtraHTTPHeaders(this._browserContext._options.extraHTTPHeaders || []).catch(() => {});
}
updateRequestInterception(): Promise<void> {
if (!this._networkManager || !this._isNetworkInspectionEnabled())
return Promise.resolve();
return this._networkManager.setRequestInterception(this.needsRequestInterception()).catch(e => { });
async updateRequestInterception(): Promise<void> {
if (!this._isNetworkInspectionEnabled())
return;
await this._networkManager?.setRequestInterception(this.needsRequestInterception()).catch(() => {});
}
needsRequestInterception(): boolean {

View file

@ -525,6 +525,15 @@ percentage [0 - 100] for scroll driven animations
*/
animation: Animation;
}
/**
* Event for animation that has been updated.
*/
export type animationUpdatedPayload = {
/**
* Animation that was updated.
*/
animation: Animation;
}
/**
* Disables animation domain notifications.
@ -917,7 +926,7 @@ Should be updated alongside RequestIdTokenStatus in
third_party/blink/public/mojom/devtools/inspector_issue.mojom to include
all cases except for success.
*/
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"DisabledInSettings"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp";
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"DisabledInSettings"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByButtonMode";
export interface FederatedAuthUserInfoRequestIssueDetails {
federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason;
}
@ -3441,7 +3450,7 @@ front-end.
/**
* Pseudo element type.
*/
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
/**
* Shadow root type.
*/
@ -5973,11 +5982,31 @@ is turned-off.
/**
* If set, the posture of a foldable device. If not set the posture is set
to continuous.
Deprecated, use Emulation.setDevicePostureOverride.
*/
devicePosture?: DevicePosture;
}
export type setDeviceMetricsOverrideReturnValue = {
}
/**
* Start reporting the given posture value to the Device Posture API.
This override can also be set in setDeviceMetricsOverride().
*/
export type setDevicePostureOverrideParameters = {
posture: DevicePosture;
}
export type setDevicePostureOverrideReturnValue = {
}
/**
* Clears a device posture override set with either setDeviceMetricsOverride()
or setDevicePostureOverride() and starts using posture information from the
platform again.
Does nothing if no override is set.
*/
export type clearDevicePostureOverrideParameters = {
}
export type clearDevicePostureOverrideReturnValue = {
}
export type setScrollbarsHiddenParameters = {
/**
* Whether scrollbars should be always hidden.
@ -8351,6 +8380,10 @@ records.
* Specifies that the request was served from the prefetch cache.
*/
fromPrefetchCache?: boolean;
/**
* Specifies that the request was served from the prefetch cache.
*/
fromEarlyHints?: boolean;
/**
* Information about how Service Worker Static Router was used.
*/
@ -8614,6 +8647,10 @@ corresponding reason. A cookie could only have at most one exemption reason.
* The reason the cookie was exempted.
*/
exemptionReason: CookieExemptionReason;
/**
* The string representing this individual cookie as it would appear in the header.
*/
cookieLine: string;
/**
* The cookie object representing the cookie.
*/
@ -9537,6 +9574,21 @@ the response with the corresponding reason.
*/
exemptedCookies?: ExemptedSetCookieWithReason[];
}
/**
* Fired when 103 Early Hints headers is received in addition to the common response.
Not every responseReceived event will have an responseReceivedEarlyHints fired.
Only one responseReceivedEarlyHints may be fired for eached responseReceived event.
*/
export type responseReceivedEarlyHintsPayload = {
/**
* Request identifier. Used to match this information to another responseReceived event.
*/
requestId: RequestId;
/**
* Raw response headers as they were received over the wire.
*/
headers: Headers;
}
/**
* Fired exactly once for each Trust Token operation. Depending on
the type of the operation and whether the operation succeeded or
@ -11573,6 +11625,125 @@ Example URLs: http://www.google.com/file.html -> "google.com"
*/
eager?: boolean;
}
export interface FileFilter {
name?: string;
accepts?: string[];
}
export interface FileHandler {
action: string;
name: string;
icons?: ImageResource[];
/**
* Mimic a map, name is the key, accepts is the value.
*/
accepts?: FileFilter[];
/**
* Won't repeat the enums, using string for easy comparison. Same as the
other enums below.
*/
launchType: string;
}
/**
* The image definition used in both icon and screenshot.
*/
export interface ImageResource {
/**
* The src field in the definition, but changing to url in favor of
consistency.
*/
url: string;
sizes?: string;
type?: string;
}
export interface LaunchHandler {
clientMode: string;
}
export interface ProtocolHandler {
protocol: string;
url: string;
}
export interface RelatedApplication {
id?: string;
url: string;
}
export interface ScopeExtension {
/**
* Instead of using tuple, this field always returns the serialized string
for easy understanding and comparison.
*/
origin: string;
hasOriginWildcard: boolean;
}
export interface Screenshot {
image: ImageResource;
formFactor: string;
label?: string;
}
export interface ShareTarget {
action: string;
method: string;
enctype: string;
/**
* Embed the ShareTargetParams
*/
title?: string;
text?: string;
url?: string;
files?: FileFilter[];
}
export interface Shortcut {
name: string;
url: string;
}
export interface WebAppManifest {
backgroundColor?: string;
/**
* The extra description provided by the manifest.
*/
description?: string;
dir?: string;
display?: string;
/**
* The overrided display mode controlled by the user.
*/
displayOverrides?: string[];
/**
* The handlers to open files.
*/
fileHandlers?: FileHandler[];
icons?: ImageResource[];
id?: string;
lang?: string;
/**
* TODO(crbug.com/1231886): This field is non-standard and part of a Chrome
experiment. See:
https://github.com/WICG/web-app-launch/blob/main/launch_handler.md
*/
launchHandler?: LaunchHandler;
name?: string;
orientation?: string;
preferRelatedApplications?: boolean;
/**
* The handlers to open protocols.
*/
protocolHandlers?: ProtocolHandler[];
relatedApplications?: RelatedApplication[];
scope?: string;
/**
* Non-standard, see
https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-explainer.md
*/
scopeExtensions?: ScopeExtension[];
/**
* The screenshots used by chromium.
*/
screenshots?: Screenshot[];
shareTarget?: ShareTarget;
shortName?: string;
shortcuts?: Shortcut[];
startUrl?: string;
themeColor?: string;
}
/**
* Enum of possible auto-response for permission / prompt dialogs.
*/
@ -12158,7 +12329,15 @@ option, use with caution.
}
export type enableReturnValue = {
}
/**
* Gets the processed manifest for this current document.
This API always waits for the manifest to be loaded.
If manifestId is provided, and it does not match the manifest of the
current document, this API errors out.
If there isnt a loaded page, this API errors out immediately.
*/
export type getAppManifestParameters = {
manifestId?: string;
}
export type getAppManifestReturnValue = {
/**
@ -12171,9 +12350,10 @@ option, use with caution.
*/
data?: string;
/**
* Parsed manifest properties
* Parsed manifest properties. Deprecated, use manifest instead.
*/
parsed?: AppManifestParsedProperties;
manifest: WebAppManifest;
}
export type getInstallabilityErrorsParameters = {
}
@ -13778,7 +13958,7 @@ int
debugKey?: UnsignedInt64AsBase10;
triggerDataMatching: AttributionReportingTriggerDataMatching;
}
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity";
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxTriggerStateCardinality";
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
export interface AttributionReportingAggregatableValueDictEntry {
key: string;
@ -14400,6 +14580,18 @@ interestGroupAuctionNetworkRequestCreated.
}
export type setAttributionReportingTrackingReturnValue = {
}
/**
* Sends all pending Attribution Reports immediately, regardless of their
scheduled report time.
*/
export type sendPendingAttributionReportsParameters = {
}
export type sendPendingAttributionReportsReturnValue = {
/**
* The number of reports that were sent.
*/
numSent: number;
}
/**
* Returns the effective Related Website Sets in use by this profile for the browser
session. The effective Related Website Sets will not change during a browser session.
@ -16691,6 +16883,46 @@ a dialog even if one was recently dismissed by the user.
}
}
/**
* This domain allows interacting with the browser to control PWAs.
*/
export module PWA {
/**
* The following types are the replica of
https://crsrc.org/c/chrome/browser/web_applications/proto/web_app_os_integration_state.proto;drc=9910d3be894c8f142c977ba1023f30a656bc13fc;l=67
*/
export interface FileHandlerAccept {
/**
* New name of the mimetype according to
https://www.iana.org/assignments/media-types/media-types.xhtml
*/
mediaType: string;
fileExtensions: string[];
}
export interface FileHandler {
action: string;
accepts: FileHandlerAccept[];
displayName: string;
}
/**
* Returns the following OS state for the given manifest id.
*/
export type getOsAppStateParameters = {
/**
* The id from the webapp's manifest file, commonly it's the url of the
site installing the webapp. See
https://web.dev/learn/pwa/web-app-manifest.
*/
manifestId: string;
}
export type getOsAppStateReturnValue = {
badgeCount: number;
fileHandlers: FileHandler[];
}
}
/**
* This domain is deprecated - use Runtime or Log instead.
*/
@ -19329,6 +19561,7 @@ Error was thrown.
"Animation.animationCanceled": Animation.animationCanceledPayload;
"Animation.animationCreated": Animation.animationCreatedPayload;
"Animation.animationStarted": Animation.animationStartedPayload;
"Animation.animationUpdated": Animation.animationUpdatedPayload;
"Audits.issueAdded": Audits.issueAddedPayload;
"Autofill.addressFormFilled": Autofill.addressFormFilledPayload;
"BackgroundService.recordingStateChanged": BackgroundService.recordingStateChangedPayload;
@ -19392,6 +19625,7 @@ Error was thrown.
"Network.webTransportClosed": Network.webTransportClosedPayload;
"Network.requestWillBeSentExtraInfo": Network.requestWillBeSentExtraInfoPayload;
"Network.responseReceivedExtraInfo": Network.responseReceivedExtraInfoPayload;
"Network.responseReceivedEarlyHints": Network.responseReceivedEarlyHintsPayload;
"Network.trustTokenOperationDone": Network.trustTokenOperationDonePayload;
"Network.subresourceWebBundleMetadataReceived": Network.subresourceWebBundleMetadataReceivedPayload;
"Network.subresourceWebBundleMetadataError": Network.subresourceWebBundleMetadataErrorPayload;
@ -19694,6 +19928,8 @@ Error was thrown.
"Emulation.setCPUThrottlingRate": Emulation.setCPUThrottlingRateParameters;
"Emulation.setDefaultBackgroundColorOverride": Emulation.setDefaultBackgroundColorOverrideParameters;
"Emulation.setDeviceMetricsOverride": Emulation.setDeviceMetricsOverrideParameters;
"Emulation.setDevicePostureOverride": Emulation.setDevicePostureOverrideParameters;
"Emulation.clearDevicePostureOverride": Emulation.clearDevicePostureOverrideParameters;
"Emulation.setScrollbarsHidden": Emulation.setScrollbarsHiddenParameters;
"Emulation.setDocumentCookieDisabled": Emulation.setDocumentCookieDisabledParameters;
"Emulation.setEmitTouchEventsForMouse": Emulation.setEmitTouchEventsForMouseParameters;
@ -19950,6 +20186,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsParameters;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeParameters;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsParameters;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
@ -20018,6 +20255,7 @@ Error was thrown.
"FedCm.openUrl": FedCm.openUrlParameters;
"FedCm.dismissDialog": FedCm.dismissDialogParameters;
"FedCm.resetCooldown": FedCm.resetCooldownParameters;
"PWA.getOsAppState": PWA.getOsAppStateParameters;
"Console.clearMessages": Console.clearMessagesParameters;
"Console.disable": Console.disableParameters;
"Console.enable": Console.enableParameters;
@ -20278,6 +20516,8 @@ Error was thrown.
"Emulation.setCPUThrottlingRate": Emulation.setCPUThrottlingRateReturnValue;
"Emulation.setDefaultBackgroundColorOverride": Emulation.setDefaultBackgroundColorOverrideReturnValue;
"Emulation.setDeviceMetricsOverride": Emulation.setDeviceMetricsOverrideReturnValue;
"Emulation.setDevicePostureOverride": Emulation.setDevicePostureOverrideReturnValue;
"Emulation.clearDevicePostureOverride": Emulation.clearDevicePostureOverrideReturnValue;
"Emulation.setScrollbarsHidden": Emulation.setScrollbarsHiddenReturnValue;
"Emulation.setDocumentCookieDisabled": Emulation.setDocumentCookieDisabledReturnValue;
"Emulation.setEmitTouchEventsForMouse": Emulation.setEmitTouchEventsForMouseReturnValue;
@ -20534,6 +20774,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsReturnValue;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeReturnValue;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsReturnValue;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
@ -20602,6 +20843,7 @@ Error was thrown.
"FedCm.openUrl": FedCm.openUrlReturnValue;
"FedCm.dismissDialog": FedCm.dismissDialogReturnValue;
"FedCm.resetCooldown": FedCm.resetCooldownReturnValue;
"PWA.getOsAppState": PWA.getOsAppStateReturnValue;
"Console.clearMessages": Console.clearMessagesReturnValue;
"Console.disable": Console.disableReturnValue;
"Console.enable": Console.enableReturnValue;

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@ -978,7 +978,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -989,7 +989,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36 Edg/124.0.6367.29",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36 Edg/125.0.6422.26",
"screen": {
"width": 1792,
"height": 1120
@ -1472,7 +1472,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1",
"screen": {
"width": 1792,
"height": 1120
@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.29 Safari/537.36 Edg/124.0.6367.29",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36 Edg/125.0.6422.26",
"screen": {
"width": 1920,
"height": 1080
@ -1532,7 +1532,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1",
"screen": {
"width": 1920,
"height": 1080

View file

@ -138,12 +138,16 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}
async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise<channels.PageRegisterLocatorHandlerResult> {
const uid = this._page.registerLocatorHandler(params.selector);
const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter);
return { uid };
}
async resolveLocatorHandlerNoReply(params: channels.PageResolveLocatorHandlerNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.resolveLocatorHandler(params.uid);
this._page.resolveLocatorHandler(params.uid, params.remove);
}
async unregisterLocatorHandler(params: channels.PageUnregisterLocatorHandlerParams, metadata: CallMetadata): Promise<void> {
this._page.unregisterLocatorHandler(params.uid);
}
async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> {

Some files were not shown because too many files have changed in this diff Show more