Compare commits

..

19 commits

Author SHA1 Message Date
Dmitry Gozman dd8a1193fc cherry-pick(#29909): docs: mark addLocatorHandler as experimental 2024-03-12 12:03:02 -07:00
Yury Semikhatsky 87a6f9cb77
cherry-pick(#29885): docs: junit integration release notes (#29886) 2024-03-11 16:41:25 -07:00
Playwright Service e164ac7697
cherry-pick(#29406): docs(java): update JUnit examples with new fixtures (#29881)
This PR cherry-picks the following commits:

- 8f4c2f714d
2024-03-11 11:29:49 -07:00
Playwright Service 1a80286a4d
cherry-pick(#29825): docs: add release-notes for language bindings (#29830)
This PR cherry-picks the following commits:

- 502d21e96a
- b3edb2a130

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-06 10:51:20 +01:00
Dmitry Gozman f822d650b2 cherry-pick(#29821): feat(ui mode): text filter should filter by explicit tags
Fixes #29815.
2024-03-05 11:02:25 -08:00
Dmitry Gozman 9749f75426
chore: mark 1.42.1 (#29774) 2024-03-01 14:42:55 -08:00
Dmitry Gozman d4ac444f7d cherry-pick(#29770): docs: improve addLocatorHandler docs 2024-03-01 11:20:22 -08:00
Dmitry Gozman a7025956c3 cherry-pick(#29765): Revert "chore(role): cache element list by role (#29130)"
This reverts commit 1ce3ca25a2.

Added a regression test.

Fixes #29760.
2024-03-01 09:40:44 -08:00
Pavel Feldman b67050638b cherry-pick(#29766): fix(tsload): fix tsconfig inheritance resolution 2024-03-01 09:37:31 -08:00
Pavel Feldman 3a4381303c cherry-pick(#29744): fix(ct): stop-gap for shared file import 2024-02-29 19:15:15 -08:00
Pavel Feldman d23fb005d4 cherry-pick(#29754): chore: remove pw- binaries from ct 2024-02-29 16:50:58 -08:00
Pavel Feldman 56e50a7304 cherry-pick(#29748): fix(ct): fix the non-react cli entry points 2024-02-29 12:49:23 -08:00
Pavel Feldman 3f6b6419c5 cherry-pick(#29738): fix(HEAD): revert GET->HEAD migration, net-effect was negative 2024-02-29 09:03:46 -08:00
Playwright Service 20db86da3e
cherry-pick(#29706): fit(ct): remove unused type import (#29709)
This PR cherry-picks the following commits:

- 772345c83c

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-29 10:28:45 +01:00
Pavel Feldman 84780b1b75 cherry-pick(#29715): fix: point to the right cli program export 2024-02-28 14:07:49 -08:00
Playwright Service e7f0635c17
cherry-pick(#29692): docs: better addLocatorHandler example in release notes (#29697)
This PR cherry-picks the following commits:

- 321e9d72c3
2024-02-27 11:05:24 -08:00
Playwright Service 8709a3a24b
cherry-pick(#29687): chore: fix docs roll for functions without args follow-up (#29688)
This PR cherry-picks the following commits:

- 03902d8e85
- 1a43adc506

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-27 16:52:29 +01:00
Yury Semikhatsky aa9f6fb718
cherry-pick(#29669): chore: strengthen linting (#29674) 2024-02-26 17:43:55 -08:00
Yury Semikhatsky f5899c1556
chore: set version to 1.42.0 (#29671) 2024-02-26 17:40:03 -08:00
1741 changed files with 45193 additions and 151165 deletions

View file

@ -0,0 +1,11 @@
{
"name": "Playwright",
"image": "mcr.microsoft.com/playwright:next",
"postCreateCommand": "npm install && npm run build && apt-get update && apt-get install -y software-properties-common && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" && apt-get install -y docker-ce-cli",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"runArgs": [
"-v", "/var/run/docker.sock:/var/run/docker.sock"
]
}

23
.eslintignore Normal file
View file

@ -0,0 +1,23 @@
test/assets/modernizr.js
/tests/third_party/
/packages/*/lib/
*.js
/packages/playwright-core/src/generated/*
/packages/playwright-core/src/third_party/
/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/
**/*.d.ts
output/
test-results/
tests/components/
tests/installation/fixture-scripts/
examples/
DEPS
.cache/
utils/

View file

@ -0,0 +1,15 @@
module.exports = {
extends: "./.eslintrc.js",
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
project: "./tsconfig.json",
},
rules: {
"@typescript-eslint/no-base-to-string": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
},
parserOptions: {
project: "./tsconfig.json"
},
};

127
.eslintrc.js Normal file
View file

@ -0,0 +1,127 @@
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "notice"],
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
},
extends: [
"plugin:react-hooks/recommended"
],
/**
* ESLint rules
*
* All available rules: http://eslint.org/docs/rules/
*
* Rules take the following form:
* "rule-name", [severity, { opts }]
* Severity: 2 == error, 1 == warning, 0 == off.
*/
rules: {
"@typescript-eslint/no-unused-vars": [2, {args: "none"}],
"@typescript-eslint/consistent-type-imports": [2, {disallowTypeAnnotations: false}],
/**
* Enforced rules
*/
// syntax preferences
"object-curly-spacing": ["error", "always"],
"quotes": [2, "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"no-extra-semi": 2,
"@typescript-eslint/semi": [2],
"comma-style": [2, "last"],
"wrap-iife": [2, "inside"],
"spaced-comment": [2, "always", {
"markers": ["*"]
}],
"eqeqeq": [2],
"accessor-pairs": [2, {
"getWithoutSet": false,
"setWithoutGet": false
}],
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
"curly": [2, "multi-or-nest", "consistent"],
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"prefer-const": 2,
"quote-props": [2, "consistent"],
"nonblock-statement-body-position": [2, "below"],
// anti-patterns
"no-var": 2,
"no-with": 2,
"no-multi-str": 2,
"no-caller": 2,
"no-implied-eval": 2,
"no-labels": 2,
"no-new-object": 2,
"no-octal-escape": 2,
"no-self-compare": 2,
"no-shadow-restricted-names": 2,
"no-cond-assign": 2,
"no-debugger": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-unreachable": 2,
"no-unsafe-negation": 2,
"radix": 2,
"valid-typeof": 2,
"no-implicit-globals": [2],
"no-unused-expressions": [2, { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true}],
"no-proto": 2,
// es2015 features
"require-yield": 2,
"template-curly-spacing": [2, "never"],
// spacing details
"space-infix-ops": 2,
"space-in-parens": [2, "never"],
"array-bracket-spacing": [2, "never"],
"comma-spacing": [2, { "before": false, "after": true }],
"keyword-spacing": [2, "always"],
"space-before-function-paren": [2, {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"no-whitespace-before-property": 2,
"keyword-spacing": [2, {
"overrides": {
"if": {"after": true},
"else": {"after": true},
"for": {"after": true},
"while": {"after": true},
"do": {"after": true},
"switch": {"after": true},
"return": {"after": true}
}
}],
"arrow-spacing": [2, {
"after": true,
"before": true
}],
"@typescript-eslint/func-call-spacing": 2,
"@typescript-eslint/type-annotation-spacing": 2,
// file whitespace
"no-multiple-empty-lines": [2, {"max": 2}],
"no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 2,
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
"indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
"key-spacing": [2, {
"beforeColon": false
}],
// copyright
"notice/notice": [2, {
"mustMatch": "Copyright",
"templateFile": require("path").join(__dirname, "utils", "copyright.js"),
}],
}
};

View file

@ -24,7 +24,6 @@ body:
## Make a minimal reproduction
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
Please make sure you only include the code and the dependencies absolutely necessary for your repro. Due to the security considerations, we can only run the code we trust. Major web frameworks are Ok to use, but smaller convenience libraries are not.
- type: markdown
attributes:
value: |

View file

@ -4,10 +4,12 @@ inputs:
namePrefix:
description: 'Name prefix of the artifacts to download'
required: true
type: string
default: 'blob-report'
path:
description: 'Directory with downloaded artifacts'
required: true
type: string
default: '.'
runs:
using: "composite"
@ -34,7 +36,7 @@ runs:
artifact_id: artifact.id,
archive_format: 'zip'
});
console.log(`Downloaded ${artifact.name}.zip (${result.data.byteLength} bytes)`);
console.log('downloaded artifact', result);
fs.writeFileSync(`${{ inputs.path }}/artifacts/${artifact.name}.zip`, Buffer.from(result.data));
}
- name: Unzip artifacts

View file

@ -0,0 +1,29 @@
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
type: string
output_dir:
description: 'Output directory where downloaded blobs will be stored'
required: true
type: string
default: 'blob-report'
connection_string:
description: 'Azure connection string'
required: true
type: string
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

View file

@ -1,25 +0,0 @@
name: Enable Microphone Access (macOS)
description: 'Allow microphone access to all apps on macOS'
runs:
using: composite
steps:
# https://github.com/actions/runner-images/issues/9330
- name: Allow microphone access to all apps
shell: bash
run: |
if [[ "$(uname)" != "Darwin" ]]; then
echo "Not macOS, exiting"
exit 0
fi
echo "Allowing microphone access to all apps"
version=$(sw_vers -productVersion | cut -d. -f1)
if [[ "$version" == "14" || "$version" == "15" ]]; then
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);"
elif [[ "$version" == "12" || "$version" == "13" ]]; then
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR REPLACE INTO access VALUES('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159);"
else
echo "Skipping unsupported macOS version $version"
exit 0
fi
echo "Successfully allowed microphone access"

View file

@ -1,93 +0,0 @@
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 }}
- uses: ./.github/actions/enable-microphone-access
- run: |
echo "::group::npm ci"
npm ci
echo "::endgroup::"
shell: bash
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- run: |
echo "::group::npm run build"
npm run build
echo "::endgroup::"
shell: bash
- run: |
echo "::group::npx playwright install --with-deps"
npx playwright install --with-deps ${{ inputs.browsers-to-install }}
echo "::endgroup::"
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: |
echo "::group::./utils/upload_flakiness_dashboard.sh"
./utils/upload_flakiness_dashboard.sh ./test-results/report.json
echo "::endgroup::"
if: ${{ !cancelled() }}
shell: bash
- name: Upload blob report
# We only merge reports for PRs as per .github/workflows/create_test_report.yml.
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ inputs.bot-name }}

View file

@ -4,30 +4,29 @@ inputs:
report_dir:
description: 'Directory containing blob report'
required: true
type: string
default: 'test-results/blob-report'
job_name:
description: 'Unique job name'
required: true
type: string
default: ''
runs:
using: "composite"
steps:
- name: Integrity check
shell: bash
run: find "${{ inputs.report_dir }}" -name "*.zip" -exec unzip -t {} \;
- name: Upload blob report to GitHub
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
if: always() && github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ inputs.job_name }}
path: ${{ inputs.report_dir }}/**
retention-days: 7
- name: Write triggering pull request number in a file
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
if: always() && github.event_name == 'pull_request'
shell: bash
run: echo '${{ github.event.number }}' > pull_request_number.txt;
- name: Upload artifact with the pull request number
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
if: always() && github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: pull-request-${{ inputs.job_name }}

View file

@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"

View file

@ -0,0 +1,7 @@
{
"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

@ -0,0 +1,7 @@
{
"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

@ -0,0 +1,7 @@
{
"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

@ -0,0 +1,7 @@
{
"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

@ -0,0 +1,7 @@
{
"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

@ -0,0 +1,7 @@
{
"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

@ -12,9 +12,6 @@ on:
description: Comma-separated list of commit hashes to cherry-pick
required: true
permissions:
contents: write
jobs:
roll:
runs-on: ubuntu-22.04
@ -60,7 +57,7 @@ jobs:
git checkout -b "$BRANCH_NAME"
git push origin $BRANCH_NAME
- name: Create Pull Request
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |

View file

@ -1,7 +1,7 @@
name: Publish Test Results
on:
workflow_run:
workflows: ["tests 1", "tests 2", "tests others"]
workflows: ["tests 1", "tests 2"]
types:
- completed
jobs:
@ -9,8 +9,6 @@ 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:
@ -18,11 +16,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
- run: npm run build
- name: Download blob report artifact
@ -35,14 +33,7 @@ jobs:
run: |
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
env:
NODE_OPTIONS: --max-old-space-size=8192
- 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 }}
NODE_OPTIONS: --max-old-space-size=4096
- name: Upload HTML report to Azure
run: |
@ -50,7 +41,10 @@ jobs:
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: AZCLI
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 }}'
- name: Read pull request number
uses: ./.github/actions/download-artifact
@ -59,7 +53,7 @@ jobs:
path: '.'
- name: Comment on PR
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@ -121,3 +115,21 @@ jobs:
]),
});
core.info('Posted comment: ' + response.html_url);
const check = await github.rest.checks.create({
...context.repo,
name: 'Merge report (${{ github.event.workflow_run.name }})',
head_sha: '${{ github.event.workflow_run.head_sha }}',
status: 'completed',
conclusion: 'success',
details_url: reportUrl,
output: {
title: 'Test results for "${{ github.event.workflow_run.name }}"',
summary: [
reportMd,
'',
'---',
`Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).`
].join('\n'),
}
});

View file

@ -16,7 +16,7 @@ env:
jobs:
doc-and-lint:
name: "docs & lint"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -35,27 +35,21 @@ jobs:
exit 1
fi
- name: Audit prod NPM dependencies
run: node utils/check_audit.js
run: npm audit --omit dev
lint-snippets:
name: "Lint snippets"
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-python@v5
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '21'
- run: npm ci
- run: pip install -r utils/doclint/linting-code-snippets/python/requirements.txt
- run: mvn package
working-directory: utils/doclint/linting-code-snippets/java
- run: node utils/doclint/linting-code-snippets/cli.js

View file

@ -1,4 +1,4 @@
export default {
testDir: '../../tests',
reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']]
reporter: [['markdown'], ['html']]
};

View file

@ -12,31 +12,25 @@ on:
jobs:
check:
name: Check
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
steps:
- uses: actions/checkout@v4
- name: Create GitHub issue
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
const currentPlaywrightVersion = require('./package.json').version.match(/\d+\.\d+/)[0];
const { data } = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
});
const commitHeader = data.message.split('\n')[0];
const prMatch = commitHeader.match(/#(\d+)/);
const formattedCommit = prMatch
? `https://github.com/microsoft/playwright/pull/${prMatch[1]}`
: `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${context.sha} (${commitHeader})`;
const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion;
const title = '[Ports]: Backport client side changes';
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
const { data: issuesData } = await github.rest.search.issuesAndPullRequests({
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}" author:playwrightmachine`
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}"`
})
let issueNumber = null;
let issueBody = '';
@ -54,11 +48,11 @@ jobs:
issueBody = issueCreateData.body;
}
const newBody = issueBody.trimEnd() + `
- [ ] ${formattedCommit}`;
- [ ] https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${context.sha} (${commitHeader})`;
const data = await github.rest.issues.update({
owner: context.repo.owner,
repo: repo,
issue_number: issueNumber,
body: newBody
})
}
}

View file

@ -14,12 +14,11 @@ env:
jobs:
publish-canary:
name: "publish canary NPM"
runs-on: ubuntu-24.04
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)
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -50,22 +49,18 @@ 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
publish-trace-viewer:
name: "publish Trace Viewer to trace.playwright.dev"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
steps:
- uses: actions/checkout@v4

View file

@ -2,6 +2,12 @@ name: "publish release - Docker"
on:
workflow_dispatch:
inputs:
is_release:
required: true
type: boolean
description: "Is this a release image?"
release:
types: [published]
@ -12,17 +18,18 @@ 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:
@ -30,12 +37,7 @@ 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
if: (github.event_name != 'workflow_dispatch' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release != 'true')

View file

@ -10,12 +10,8 @@ env:
jobs:
publish-driver-release:
name: "publish playwright driver to CDN"
runs-on: ubuntu-24.04
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
@ -26,12 +22,8 @@ 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

@ -10,7 +10,7 @@ env:
jobs:
publish-npm-release:
name: "publish to NPM"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
permissions:
contents: read
@ -25,10 +25,10 @@ jobs:
- run: npm run build
- run: npx playwright install-deps
- run: utils/publish_all_packages.sh --release-candidate
if: ${{ github.event.release.prerelease }}
if: "github.event.release.prerelease"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: utils/publish_all_packages.sh --release
if: ${{ !github.event.release.prerelease }}
if: "!github.event.release.prerelease"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -7,7 +7,7 @@ on:
jobs:
publish-trace-viewer:
name: "publish Trace Viewer to trace.playwright.dev"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright'
steps:
- uses: actions/checkout@v4

View file

@ -3,28 +3,16 @@ name: Roll Browser into Playwright
on:
repository_dispatch:
types: [roll_into_pw]
workflow_dispatch:
inputs:
browser:
description: 'Browser name, e.g. chromium'
required: true
type: string
revision:
description: 'Browser revision without v prefix, e.g. 1234'
required: true
type: string
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
BROWSER: ${{ github.event.client_payload.browser || github.event.inputs.browser }}
REVISION: ${{ github.event.client_payload.revision || github.event.inputs.revision }}
permissions:
contents: write
jobs:
roll:
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -36,21 +24,21 @@ jobs:
run: npx playwright install-deps
- name: Roll to new revision
run: |
./utils/roll_browser.js $BROWSER $REVISION
./utils/roll_browser.js ${{ github.event.client_payload.browser }} ${{ github.event.client_payload.revision }}
npm run build
- name: Prepare branch
id: prepare-branch
run: |
BRANCH_NAME="roll-into-pw-${BROWSER}/${REVISION}"
BRANCH_NAME="roll-into-pw-${{ github.event.client_payload.browser }}/${{ github.event.client_payload.revision }}"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
git config --global user.name github-actions
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
git checkout -b "$BRANCH_NAME"
git add .
git commit -m "feat(${BROWSER}): roll to r${REVISION}"
git push origin $BRANCH_NAME --force
git commit -m "feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}"
git push origin $BRANCH_NAME
- name: Create Pull Request
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
@ -59,7 +47,7 @@ jobs:
repo: 'playwright',
head: 'microsoft:${{ steps.prepare-branch.outputs.BRANCH_NAME }}',
base: 'main',
title: 'feat(${{ env.BROWSER }}): roll to r${{ env.REVISION }}',
title: 'feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}',
});
await github.rest.issues.addLabels({
owner: 'microsoft',

View file

@ -9,8 +9,6 @@ jobs:
name: Trigger Roll
runs-on: ubuntu-22.04
if: github.repository == 'microsoft/playwright'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -35,7 +33,7 @@ jobs:
git push origin $BRANCH_NAME
- name: Create Pull Request
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |

View file

@ -1,71 +0,0 @@
name: tests BiDi
on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- .github/workflows/tests_bidi.yml
- packages/playwright-core/src/server/bidi/**
- tests/bidi/**
schedule:
# Run every day at midnight
- cron: '0 0 * * *'
env:
FORCE_COLOR: 1
jobs:
test_bidi:
name: BiDi
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-24.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
strategy:
fail-fast: false
matrix:
channel: [bidi-chromium, bidi-firefox-nightly]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- run: npm run build
- run: npx playwright install --with-deps chromium
if: matrix.channel == 'bidi-chromium'
- run: npx -y @puppeteer/browsers install firefox@nightly
if: matrix.channel == 'bidi-firefox-nightly'
- name: Run tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
env:
PWTEST_USE_BIDI_EXPECTATIONS: '1'
- name: Upload csv report to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: csv-report-${{ matrix.channel }}
path: test-results/report.csv
retention-days: 7
- name: Azure Login
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
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 report.csv to Azure
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
run: |
REPORT_DIR='bidi-reports'
azcopy cp "./test-results/report.csv" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR/${{ matrix.channel }}.csv"
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/${{ matrix.channel }}.csv"
env:
AZCOPY_AUTO_LOGIN_TYPE: AZCLI

View file

@ -27,9 +27,9 @@ jobs:
node-version: [18]
include:
- os: ubuntu-latest
node-version: 20
node-version: 16
- os: ubuntu-latest
node-version: 22
node-version: 20
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

48
.github/workflows/tests_electron.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: "electron"
on:
push:
branches:
- main
- release-*
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
types: [ labeled ]
branches:
- main
- release-*
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 }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run etest
if: matrix.os == 'ubuntu-latest'
- run: npm run etest
if: matrix.os != 'ubuntu-latest'
- run: node tests/config/checkCoverage.js electron
if: always() && matrix.os == 'ubuntu-latest'
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
shell: bash

View file

@ -1,166 +0,0 @@
name: tests others
on:
push:
branches:
- main
- release-*
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
types: [ labeled ]
branches:
- main
- release-*
env:
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_stress:
name: Stress - ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps
- run: npm run stest contexts -- --project=chromium
if: ${{ !cancelled() }}
- run: npm run stest browsers -- --project=chromium
if: ${{ !cancelled() }}
- run: npm run stest frames -- --project=chromium
if: ${{ !cancelled() }}
- run: npm run stest contexts -- --project=webkit
if: ${{ !cancelled() }}
- run: npm run stest browsers -- --project=webkit
if: ${{ !cancelled() }}
- run: npm run stest frames -- --project=webkit
if: ${{ !cancelled() }}
- run: npm run stest contexts -- --project=firefox
if: ${{ !cancelled() }}
- run: npm run stest browsers -- --project=firefox
if: ${{ !cancelled() }}
- run: npm run stest frames -- --project=firefox
if: ${{ !cancelled() }}
- run: npm run stest heap -- --project=chromium
if: ${{ !cancelled() }}
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-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet build
working-directory: tests/webview2/webview2-app/
- name: Update to Evergreen WebView2 Runtime
shell: pwsh
run: |
# See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- uses: ./.github/actions/run-test
with:
node-version: 20
browsers-to-install: chromium
command: npm run webview2test
bot-name: "webview2-chromium-windows"
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_clock_frozen_time_linux:
name: time library - ${{ matrix.clock }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
strategy:
fail-fast: false
matrix:
clock: [frozen, realtime]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
node-version: 20
browsers-to-install: chromium
command: npm run test -- --project=chromium-*
bot-name: "${{ matrix.clock }}-time-library-chromium-linux"
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:
PW_CLOCK: ${{ matrix.clock }}
test_clock_frozen_time_test_runner:
name: time test runner - ${{ matrix.clock }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
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
strategy:
fail-fast: false
matrix:
clock: [frozen, realtime]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
node-version: 20
command: npm run ttest
bot-name: "${{ matrix.clock }}-time-runner-chromium-linux"
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:
PW_CLOCK: ${{ matrix.clock }}
test_electron:
name: Electron - ${{ 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
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
if: ${{ runner.os == 'Linux' }}
run: |
if grep -q "Ubuntu 24" /etc/os-release; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
shell: bash
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium
command: npm run etest
bot-name: "electron-${{ matrix.os }}"
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:
ELECTRON_SKIP_BINARY_DOWNLOAD:

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:
@ -36,54 +36,73 @@ jobs:
node-version: [18]
include:
- os: ubuntu-22.04
node-version: 20
node-version: 16
browser: chromium
- os: ubuntu-22.04
node-version: 22
node-version: 20
browser: chromium
runs-on: ${{ 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
env:
PWTEST_BOT_NAME: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
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 }}
- 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: always()
shell: bash
- name: Upload blob report
if: always()
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
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 }}
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
env:
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
- uses: actions/setup-node@v4
with:
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 }}
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
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: always()
shell: bash
- name: Upload blob report
if: always()
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:
@ -93,37 +112,47 @@ jobs:
shardTotal: [2]
include:
- os: ubuntu-latest
node-version: 20
node-version: 16
shardIndex: 1
shardTotal: 2
- os: ubuntu-latest
node-version: 20
node-version: 16
shardIndex: 2
shardTotal: 2
- os: ubuntu-latest
node-version: 22
node-version: 20
shardIndex: 1
shardTotal: 2
- os: ubuntu-latest
node-version: 22
node-version: 20
shardIndex: 2
shardTotal: 2
runs-on: ${{ 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
env:
PWTEST_BOT_NAME: "${{ matrix.os }}-node${{ matrix.node-version }}-${{ matrix.shardIndex }}"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
- uses: actions/setup-node@v4
with:
node-version: ${{matrix.node-version}}
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 }}
- run: npm ci
env:
PWTEST_CHANNEL: firefox-beta
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: always()
shell: bash
- name: Upload blob report
if: always()
uses: ./.github/actions/upload-blob-report
with:
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}
test_web_components:
name: Web Components
@ -143,18 +172,18 @@ jobs:
env:
PWTEST_BOT_NAME: "web-components-html-reporter"
- name: Upload blob report
if: ${{ !cancelled() }}
if: always()
uses: ./.github/actions/upload-blob-report
with:
report_dir: packages/html-reporter/blob-report
job_name: "web-components-html-reporter"
- run: npm run test-web
if: ${{ !cancelled() }}
if: always()
env:
PWTEST_BOT_NAME: "web-components-web"
- name: Upload blob report
if: ${{ !cancelled() }}
if: always()
uses: ./.github/actions/upload-blob-report
with:
report_dir: packages/web/blob-report
@ -190,7 +219,7 @@ jobs:
run: npm run test -- --workers=1
working-directory: ./playwright-vscode
- name: Upload blob report
if: ${{ !cancelled() }}
if: always()
uses: ./.github/actions/upload-blob-report
with:
report_dir: playwright-vscode/blob-report
@ -198,7 +227,6 @@ jobs:
test_package_installations:
name: "Installation Test ${{ matrix.os }}"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
@ -208,25 +236,30 @@ jobs:
- windows-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 30
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
env:
PWTEST_BOT_NAME: "package-installations-${{ matrix.os }}"
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
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
if: ${{ runner.os == 'Linux' }}
run: |
if grep -q "Ubuntu 24" /etc/os-release; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- 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: always()
shell: bash
- uses: ./.github/actions/run-test
- name: Upload blob report
if: always()
uses: ./.github/actions/upload-blob-report
with:
command: npm run itest
bot-name: "package-installations-${{ matrix.os }}"
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 }}
report_dir: blob-report
job_name: ${{ env.PWTEST_BOT_NAME }}

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@ jobs:
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --workers=10 --retries=0
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }} --workers=10 --retries=0
env:
PWTEST_MODE: service2
PWTEST_TRACE: 1
@ -33,8 +33,8 @@ jobs:
PLAYWRIGHT_SERVICE_OS: ${{ matrix.service-os }}
PLAYWRIGHT_SERVICE_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}
- name: Upload blob report to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
if: always()
uses: actions/upload-artifact@v3
with:
name: all-blob-reports
path: blob-report
@ -43,7 +43,7 @@ jobs:
merge_reports:
name: "Merge reports"
needs: [test]
if: ${{ !cancelled() }}
if: always()
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
@ -53,7 +53,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- name: Download blob report artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: all-blob-reports
path: all-blob-reports

58
.github/workflows/tests_stress.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: "stress"
on:
push:
branches:
- main
- release-*
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
types: [ labeled ]
branches:
- main
- release-*
env:
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_components:
name: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
- 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: always()
- run: npm run stest browsers -- --project=chromium
if: always()
- run: npm run stest frames -- --project=chromium
if: always()
- run: npm run stest contexts -- --project=webkit
if: always()
- run: npm run stest browsers -- --project=webkit
if: always()
- run: npm run stest frames -- --project=webkit
if: always()
- run: npm run stest contexts -- --project=firefox
if: always()
- run: npm run stest browsers -- --project=firefox
if: always()
- run: npm run stest frames -- --project=firefox
if: always()
- run: npm run stest heap -- --project=chromium
if: always()

View file

@ -9,30 +9,32 @@ 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
- uses: ./.github/actions/run-test
- uses: actions/setup-node@v4
with:
browsers-to-install: ${{ matrix.browser }} chromium
command: npm run test -- --project=${{ matrix.browser }}-*
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
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 }}
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 ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
env:
PWTEST_VIDEO: 1
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
shell: bash

44
.github/workflows/tests_webview2.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: "WebView2 Tests"
on:
push:
branches:
- main
- release-*
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
types: [ labeled ]
branches:
- main
- release-*
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
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: dotnet build
working-directory: tests/webview2/webview2-app/
- run: npm run webview2test
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
shell: bash

View file

@ -0,0 +1,30 @@
name: "Trigger: Chromium with Symbols Builds"
on:
workflow_dispatch:
release:
types: [published]
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
- name: Get Chromium revision
id: chromium-version
run: |
REVISION=$(node -e "console.log(require('./packages/playwright-core/browsers.json').browsers.find(b => b.name === 'chromium-with-symbols').revision)")
echo "REVISION=$REVISION" >> $GITHUB_OUTPUT
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GH_TOKEN}" \
--data "{\"event_type\": \"build_chromium_with_symbols\", \"client_payload\": {\"revision\": \"${CHROMIUM_REVISION}\"}}" \
https://api.github.com/repos/microsoft/playwright-browsers/dispatches
env:
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
CHROMIUM_REVISION: ${{ steps.chromium-version.outputs.REVISION }}

View file

@ -9,7 +9,7 @@ on:
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- run: |
curl -X POST \

5
.gitignore vendored
View file

@ -7,7 +7,6 @@ node_modules/
*.swp
*.pyc
.vscode
.mono
.idea
yarn.lock
/packages/playwright-core/src/generated
@ -33,6 +32,4 @@ test-results
/tests/installation/output/
/tests/installation/.registry.json
.cache/
.eslintcache
playwright.env
/firefox/
.eslintcache

View file

@ -1,87 +1,88 @@
# Contributing
## Choose an issue
- [How to Contribute](#how-to-contribute)
* [Getting Code](#getting-code)
* [Code reviews](#code-reviews)
* [Code Style](#code-style)
* [API guidelines](#api-guidelines)
* [Commit Messages](#commit-messages)
* [Writing Documentation](#writing-documentation)
* [Adding New Dependencies](#adding-new-dependencies)
* [Running & Writing Tests](#running--writing-tests)
* [Public API Coverage](#public-api-coverage)
- [Contributor License Agreement](#contributor-license-agreement)
* [Code of Conduct](#code-of-conduct)
Playwright **requires an issue** for every contribution, except for minor documentation updates. We strongly recommend to pick an issue labeled `open-to-a-pull-request` for your first contribution to the project.
## How to Contribute
If you are passioned about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will facilitate the discussion and you might get some early feedback from project maintainers before spending your time on creating a pull request.
We strongly recommend that you open an issue before beginning any code modifications. This is particularly important if the changes involve complex logic or if the existing code isn't immediately clear. By doing so, we can discuss and agree upon the best approach to address a bug or implement a feature, ensuring that our efforts are aligned.
## Make a change
### Getting Code
Make sure you're running Node.js 20 to verify and upgrade NPM do:
Make sure you're running Node.js 20 or later.
```bash
node --version
npm --version
npm i -g npm@latest
```
Clone the repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first.
1. Clone this repository
```bash
git clone https://github.com/microsoft/playwright
cd playwright
```
Install dependencies and run the build in watch mode.
2. Install dependencies
```bash
npm ci
npm run watch
npx playwright install
```
**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode**
3. Build Playwright
```
PW_HMR=1 npm run watch
PW_HMR=1 npx playwright show-trace
PW_HMR=1 npm run ctest -- --ui
PW_HMR=1 npx playwright codegen
PW_HMR=1 npx playwright show-report
```bash
npm run build
```
Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright).
4. Run all Playwright tests locally. For more information about tests, read [Running & Writing Tests](#running--writing-tests).
Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src).
```bash
npm test
```
Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:
```bash
npm run lint
```
### Code reviews
Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
### Write documentation
### Code Style
Every part of the public API should be documented in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src), in the same change that adds/changes the API. We use markdown files with custom structure to specify the API. Take a look around for an example.
- Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js)
- Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
Various other files are generated from the API specification. If you are running `npm run watch`, these will be re-generated automatically.
To run code linter, use:
Larger changes will require updates to the documentation guides as well. This will be made clear during the code review.
```bash
npm run eslint
```
## Add a test
### API guidelines
Playwright requires a test for almost any new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that.
When authoring new API methods, consider the following:
There are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. The two most important that you need to run locally are:
- Expose as little information as needed. When in doubt, dont expose new information.
- Methods are used in favor of getters/setters.
- The only exception is namespaces, e.g. `page.keyboard` and `page.coverage`
- All string literals must be lowercase. This includes event names and option values.
- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** common.
- Library tests cover APIs not related to the test runner.
```bash
# fast path runs all tests in Chromium
npm run ctest
### Commit Messages
# slow path runs all tests in three browsers
npm run test
```
- Test runner tests.
```bash
npm run ttest
```
Since Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests).
Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.
## Write a commit message
Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:
Commit messages should follow the Semantic Commit Messages format:
```
label(namespace): title
@ -92,57 +93,144 @@ footer
```
1. *label* is one of the following:
- `fix` - bug fixes
- `feat` - new features
- `docs` - documentation-only changes
- `test` - test-only changes
- `devops` - changes to the CI or build
- `fix` - playwright bug fixes.
- `feat` - playwright features.
- `docs` - changes to docs, e.g. `docs(api.md): ..` to change documentation.
- `test` - changes to playwright tests infrastructure.
- `devops` - build-related work, e.g. CI related patches and general changes to the browser build infrastructure
- `chore` - everything that doesn't fall under previous categories
1. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
1. *title* is a brief summary of changes.
1. *description* is **optional**, new-line separated from title and is in present tense.
1. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
2. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
3. *title* is a brief summary of changes.
4. *description* is **optional**, new-line separated from title and is in present tense.
5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
Example:
```
feat(trace viewer): network panel filtering
fix(firefox): make sure session cookies work
This patch adds a filtering toolbar to the network panel.
<link to a screenshot>
This patch fixes session cookies in the firefox browser.
Fixes #123, references #234.
Fixes #123, fixes #234
```
## Send a pull request
### Writing Documentation
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
All API classes, methods, and events should have a description in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). There's a [documentation linter](https://github.com/microsoft/playwright/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase.
After a successful code review, one of the maintainers will merge your pull request. Congratulations!
To run the documentation linter, use:
## More details
```bash
npm run doc
```
**No new dependencies**
To build the documentation site locally and test how your changes will look in practice:
There is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies.
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes
**Custom browser build**
### Adding New Dependencies
For all dependencies (both installation and development):
- **Do not add** a dependency if the desired functionality is easily implementable.
- If adding a dependency, it should be well-maintained and trustworthy.
A barrier for introducing new installation dependencies is especially high:
- **Do not add** installation dependency unless it's critical to project success.
### Running & Writing Tests
- Every feature should be accompanied by a test.
- Every public api event/method should be accompanied by a test.
- Tests should be *hermetic*. Tests should not depend on external services.
- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests.
Playwright tests are located in [`tests`](https://github.com/microsoft/playwright/blob/main/tests) and use `@playwright/test` test runner.
These are integration tests, making sure public API methods and events work as expected.
- To run all tests:
```bash
npx playwright install
npm run test
```
Be sure to run `npm run build` or let `npm run watch` run before you re-run the
tests after making your changes to check them.
- To run all tests in Chromium
```bash
npm run ctest # also `ftest` for firefox and `wtest` for WebKit
```
- To run the Playwright test runner tests
```bash
npm run ttest
npm run ttest -- --grep "specific test"
```
- To run a specific test, substitute `it` with `it.only`, or use the `--grep 'My test'` CLI parameter:
```js
...
// Using "it.only" to run a specific test
it.only('should work', async ({server, page}) => {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok).toBe(true);
});
// or
playwright test --config=xxx --grep 'should work'
```
- To disable a specific test, substitute `it` with `it.skip`:
```js
...
// Using "it.skip" to skip a specific test
it.skip('should work', async ({server, page}) => {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok).toBe(true);
});
```
- To run tests in non-headless (headed) mode:
```bash
npm run ctest -- --headed
```
- To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
```bash
CRPATH=<path-to-executable> npm run ctest
```
You will also find `DEBUG=pw:browser` useful for debugging custom builds.
- To run tests in slow-mode:
**Building documentation site**
```bash
SLOW_MO=500 npm run wtest -- --headed
```
The [playwright.dev](https://playwright.dev/) documentation site lives in a separate repository, and documentation from [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src) is frequently rolled there.
- When should a test be marked with `skip` or `fail`?
Most of the time this should not concern you. However, if you are doing something unusual in the docs, you can build locally and test how your changes will look in practice:
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo.
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress.
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes.
- **`skip(condition)`**: This test *should ***never*** work* for `condition`
where `condition` is usually a certain browser like `FFOX` (for Firefox),
`WEBKIT` (for WebKit), and `CHROMIUM` (for Chromium).
For example, the [alt-click downloads test](https://github.com/microsoft/playwright/blob/471ccc72d3f0847caa36f629b394a028c7750d93/test/download.spec.js#L86) is marked
with `skip(FFOX)` since an alt-click in Firefox will not produce a download
even if a person was driving the browser.
- **`fail(condition)`**: This test *should ***eventually*** work* for `condition`
where `condition` is usually a certain browser like `FFOX` (for Firefox),
`WEBKIT` (for WebKit), and `CHROMIUM` (for Chromium).
For example, the [alt-click downloads test](https://github.com/microsoft/playwright/blob/471ccc72d3f0847caa36f629b394a028c7750d93/test/download.spec.js#L86) is marked
with `fail(CHROMIUM || WEBKIT)` since Playwright performing these actions
currently diverges from what a user would experience driving a Chromium or
WebKit.
## Contributor License Agreement

View file

@ -1,35 +0,0 @@
# How to File a Bug Report That Actually Gets Resolved
Make sure youre on the latest Playwright release before filing. Check existing GitHub issues to avoid duplicates.
## Use the Template
Follow the **Bug Report** template. It guides you step-by-step:
- Fill it out thoroughly.
- Clearly list the steps needed to reproduce the bug.
- Provide what you expected to see versus what happened in reality.
- Include system info from `npx envinfo --preset playwright`.
## Keep Your Repro Minimal
We can't parse your entire code base. Reduce it down to the absolute essentials:
- Start a fresh project (`npm init playwright@latest new-project`).
- Add only the code/DOM needed to show the problem.
- Only use major frameworks if necessary (React, Angular, static HTTP server, etc.).
- Avoid adding extra libraries unless absolutely necessary. Note that we won't install any suspect dependencies.
## Why This Matters
- Most issues that lack a repro turn out to be misconfigurations or usage errors.
- We can't fix problems if we cant reproduce them ourselves.
- We cant debug entire private projects or handle sensitive credentials.
- Each confirmed bug will have a test in our repo, so your repro must be as clean as possible.
## More Help
- [Stack Overflows Minimal Reproducible Example Guide](https://stackoverflow.com/help/minimal-reproducible-example)
- [Playwright Debugging Tools](https://playwright.dev/docs/debug)
## Bottom Line
A well-isolated bug speeds up verification and resolution. Minimal, public repro or its unlikely we can assist.

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-134.0.6998.35-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-135.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-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-123.0.6312.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-123.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 -->
## [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 -->134.0.6998.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->123.0.6312.4<!-- 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 -->123.0<!-- 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.
@ -46,6 +46,7 @@ npx playwright install
You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers).
* [Getting started](https://playwright.dev/docs/intro)
* [Installation configuration](https://playwright.dev/docs/installation)
* [API reference](https://playwright.dev/docs/api/class-playwright)
## Capabilities
@ -162,7 +163,7 @@ test('Intercept network requests', async ({ page }) => {
## Resources
* [Documentation](https://playwright.dev)
* [Documentation](https://playwright.dev/docs/intro)
* [API reference](https://playwright.dev/docs/api/class-playwright/)
* [Contribution guide](CONTRIBUTING.md)
* [Changelog](https://github.com/microsoft/playwright/releases)

View file

@ -1,20 +1,20 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
@ -28,7 +28,7 @@ Please include the requested information listed below (as much as you can provid
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
## Preferred Languages
@ -36,6 +36,6 @@ We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

View file

@ -1,17 +0,0 @@
# Support
## How to file issues and get help
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
For help and questions about using this project, please see the [docs site for Playwright][docs].
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
## Microsoft Support Policy
Support for Playwright is limited to the resources listed above.
[gh-issues]: https://github.com/microsoft/playwright/issues/
[docs]: https://playwright.dev/
[discord-server]: https://aka.ms/playwright/discord

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/mozilla/gecko-dev"
BASE_BRANCH="release"
BASE_REVISION="5cfa81898f6eef8fb1abe463e5253cea5bc17f3f"
BASE_REVISION="a32b8662993085139ac91212a297123b632fc1c0"

View file

@ -145,13 +145,9 @@ class NetworkRequest {
}
this._expectingInterception = false;
this._expectingResumedRequest = undefined; // { method, headers, postData }
this._overriddenHeadersForRedirect = redirectedFrom?._overriddenHeadersForRedirect;
this._sentOnResponse = false;
this._fulfilled = false;
if (this._overriddenHeadersForRedirect)
overrideRequestHeaders(httpChannel, this._overriddenHeadersForRedirect);
else if (this._pageNetwork)
if (this._pageNetwork)
appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
this._responseBodyChunks = [];
@ -198,7 +194,6 @@ class NetworkRequest {
// Public interception API.
fulfill(status, statusText, headers, base64body) {
this._fulfilled = true;
this._interceptedChannel.synthesizeStatus(status, statusText);
for (const header of headers) {
this._interceptedChannel.synthesizeHeader(header.name, header.value);
@ -233,13 +228,20 @@ class NetworkRequest {
if (!this._expectingResumedRequest)
return;
const { method, headers, postData } = this._expectingResumedRequest;
this._overriddenHeadersForRedirect = headers;
this._expectingResumedRequest = undefined;
if (headers)
overrideRequestHeaders(this.httpChannel, headers);
else if (this._pageNetwork)
if (headers) {
for (const header of requestHeaders(this.httpChannel)) {
// We cannot remove the "host" header.
if (header.name.toLowerCase() === 'host')
continue;
this.httpChannel.setRequestHeader(header.name, '', false /* merge */);
}
for (const header of headers)
this.httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
} else if (this._pageNetwork) {
appendExtraHTTPHeaders(this.httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
}
if (method)
this.httpChannel.requestMethod = method;
if (postData !== undefined)
@ -551,11 +553,7 @@ class NetworkRequest {
_sendOnRequestFinished() {
const pageNetwork = this._pageNetwork;
// Undefined |responseEndTime| means there has been no response yet.
// This happens when request interception API is used to redirect
// the request to a different URL.
// In this case, we should not emit "requestFinished" event.
if (pageNetwork && this.httpChannel.responseEndTime !== undefined) {
if (pageNetwork) {
let protocolVersion = undefined;
try {
protocolVersion = this.httpChannel.protocolVersion;
@ -598,8 +596,6 @@ class NetworkObserver {
proxyFilter.onProxyFilterResult(defaultProxyInfo);
return;
}
if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy))
Services.obs.notifyObservers(null, "net:clear-active-logins");
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
proxy.type,
proxy.host,
@ -769,20 +765,6 @@ function requestHeaders(httpChannel) {
return headers;
}
function clearRequestHeaders(httpChannel) {
for (const header of requestHeaders(httpChannel)) {
// We cannot remove the "host" header.
if (header.name.toLowerCase() === 'host')
continue;
httpChannel.setRequestHeader(header.name, '', false /* merge */);
}
}
function overrideRequestHeaders(httpChannel, headers) {
clearRequestHeaders(httpChannel);
appendExtraHTTPHeaders(httpChannel, headers);
}
function causeTypeToString(causeType) {
for (let key in Ci.nsIContentPolicy) {
if (Ci.nsIContentPolicy[key] === causeType)
@ -815,8 +797,7 @@ class ResponseStorage {
return;
}
let encodings = [];
// Note: fulfilled request comes with decoded body right away.
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) {
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) {
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
}

View file

@ -21,8 +21,6 @@ const ALL_PERMISSIONS = [
'desktop-notification',
];
let globalTabAndWindowActivationChain = Promise.resolve();
class DownloadInterceptor {
constructor(registry) {
this._registry = registry
@ -116,7 +114,6 @@ class TargetRegistry {
this._browserToTarget = new Map();
this._browserIdToTarget = new Map();
this._proxiesWithClashingAuthCacheKeys = new Set();
this._browserProxy = null;
// Cleanup containers from previous runs (if any)
@ -235,50 +232,12 @@ class TargetRegistry {
onOpenWindow(win);
}
// Firefox uses nsHttpAuthCache to cache authentication to the proxy.
// If we're provided with a single proxy with a multiple different authentications, then
// we should clear the nsHttpAuthCache on every request.
shouldBustHTTPAuthCacheForProxy(proxy) {
return this._proxiesWithClashingAuthCacheKeys.has(proxy);
}
_updateProxiesWithSameAuthCacheAndDifferentCredentials() {
const proxyIdToCredentials = new Map();
const allProxies = [...this._browserContextIdToBrowserContext.values()].map(bc => bc._proxy).filter(Boolean);
if (this._browserProxy)
allProxies.push(this._browserProxy);
const proxyAuthCacheKeyAndProxy = allProxies.map(proxy => [
JSON.stringify({
type: proxy.type,
host: proxy.host,
port: proxy.port,
}),
proxy,
]);
this._proxiesWithClashingAuthCacheKeys.clear();
proxyAuthCacheKeyAndProxy.sort(([cacheKey1], [cacheKey2]) => cacheKey1 < cacheKey2 ? -1 : 1);
for (let i = 0; i < proxyAuthCacheKeyAndProxy.length - 1; ++i) {
const [cacheKey1, proxy1] = proxyAuthCacheKeyAndProxy[i];
const [cacheKey2, proxy2] = proxyAuthCacheKeyAndProxy[i + 1];
if (cacheKey1 !== cacheKey2)
continue;
if (proxy1.username === proxy2.username && proxy1.password === proxy2.password)
continue;
// `proxy1` and `proxy2` have the same caching key, but serve different credentials.
// We have to bust HTTP Auth Cache everytime there's a request that will use either of the proxies.
this._proxiesWithClashingAuthCacheKeys.add(proxy1);
this._proxiesWithClashingAuthCacheKeys.add(proxy2);
}
}
async cancelDownload(options) {
this._downloadInterceptor.cancelDownload(options.uuid);
}
setBrowserProxy(proxy) {
this._browserProxy = proxy;
this._updateProxiesWithSameAuthCacheAndDifferentCredentials();
}
getProxyInfo(channel) {
@ -393,8 +352,7 @@ class PageTarget {
this._videoRecordingInfo = undefined;
this._screencastRecordingInfo = undefined;
this._dialogs = new Map();
this.forcedColors = 'none';
this.disableCache = false;
this.forcedColors = 'no-override';
this.mediumOverride = '';
this.crossProcessCookie = {
initScripts: [],
@ -407,7 +365,7 @@ class PageTarget {
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
};
this._eventListeners = [
helper.addObserver(this._updateModalDialogs.bind(this), 'common-dialog-loaded'),
helper.addObserver(this._updateModalDialogs.bind(this), 'tabmodal-dialog-loaded'),
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
helper.addEventListener(this._linkedBrowser, 'WillChangeBrowserRemoteness', event => this._willChangeBrowserRemoteness()),
@ -426,7 +384,7 @@ class PageTarget {
const tabBrowser = ownerWindow.gBrowser;
// Serialize all tab-switching commands per tabbed browser
// to disallow concurrent tab switching.
const result = globalTabAndWindowActivationChain.then(async () => {
const result = (tabBrowser.__serializedChain ?? Promise.resolve()).then(async () => {
this._window.focus();
if (tabBrowser.selectedTab !== this._tab) {
const promise = helper.awaitEvent(ownerWindow, 'TabSwitchDone');
@ -441,7 +399,7 @@ class PageTarget {
notificationsPopup?.style.removeProperty('pointer-events');
}
});
globalTabAndWindowActivationChain = result.catch(error => { /* swallow errors to keep chain running */ });
tabBrowser.__serializedChain = result.catch(error => { /* swallow errors to keep chain running */ });
return result;
}
@ -501,26 +459,12 @@ class PageTarget {
this.updateReducedMotionOverride(browsingContext);
this.updateForcedColorsOverride(browsingContext);
this.updateForceOffline(browsingContext);
this.updateCacheDisabled(browsingContext);
}
updateForceOffline(browsingContext = undefined) {
(browsingContext || this._linkedBrowser.browsingContext).forceOffline = this._browserContext.forceOffline;
}
setCacheDisabled(disabled) {
this.disableCache = disabled;
this.updateCacheDisabled();
}
updateCacheDisabled(browsingContext = this._linkedBrowser.browsingContext) {
const enableFlags = Ci.nsIRequest.LOAD_NORMAL;
const disableFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING;
browsingContext.defaultLoadFlags = (this._browserContext.disableCache || this.disableCache) ? disableFlags : enableFlags;
}
updateTouchOverride(browsingContext = undefined) {
(browsingContext || this._linkedBrowser.browsingContext).touchEventsOverride = this._browserContext.touchOverride ? 'enabled' : 'none';
}
@ -538,7 +482,7 @@ class PageTarget {
}
_updateModalDialogs() {
const prompts = new Set(this._linkedBrowser.tabDialogBox.getContentDialogManager().dialogs.map(dialog => dialog.frameContentWindow.Dialog));
const prompts = new Set(this._linkedBrowser.tabModalPromptBox ? this._linkedBrowser.tabModalPromptBox.listPrompts() : []);
for (const dialog of this._dialogs.values()) {
if (!prompts.has(dialog.prompt())) {
this._dialogs.delete(dialog.id());
@ -635,8 +579,7 @@ class PageTarget {
}
updateForcedColorsOverride(browsingContext = undefined) {
const isActive = this.forcedColors === 'active' || this._browserContext.forcedColors === 'active';
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = isActive ? 'active' : 'none';
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = (this.forcedColors !== 'no-override' ? this.forcedColors : this._browserContext.forcedColors) || 'no-override';
}
async setInterceptFileChooserDialog(enabled) {
@ -859,8 +802,8 @@ function fromProtocolReducedMotion(reducedMotion) {
function fromProtocolForcedColors(forcedColors) {
if (forcedColors === 'active' || forcedColors === 'none')
return forcedColors;
if (!forcedColors)
return 'none';
if (forcedColors === null)
return undefined;
throw new Error('Unknown forced colors: ' + forcedColors);
}
@ -892,9 +835,8 @@ class BrowserContext {
this.defaultPlatform = null;
this.touchOverride = false;
this.forceOffline = false;
this.disableCache = false;
this.colorScheme = 'none';
this.forcedColors = 'none';
this.forcedColors = 'no-override';
this.reducedMotion = 'none';
this.videoRecordingOptions = undefined;
this.crossProcessCookie = {
@ -946,14 +888,12 @@ class BrowserContext {
}
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
}
setProxy(proxy) {
// Clear AuthCache.
Services.obs.notifyObservers(null, "net:clear-active-logins");
this._proxy = proxy;
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
}
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
@ -996,12 +936,6 @@ class BrowserContext {
page.updateForceOffline();
}
setCacheDisabled(disabled) {
this.disableCache = disabled;
for (const page of this.pages)
page.updateCacheDisabled();
}
async setDefaultViewport(viewport) {
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;

View file

@ -105,10 +105,7 @@ class Juggler {
};
// Force create hidden window here, otherwise its creation later closes the web socket!
// Since https://phabricator.services.mozilla.com/D219834, hiddenDOMWindow is only available on MacOS.
if (Services.appShell.hasHiddenWindow) {
Services.appShell.hiddenDOMWindow;
}
Services.appShell.hiddenDOMWindow;
let pipeStopped = false;
let browserHandler;

View file

@ -46,6 +46,8 @@ class FrameTree {
Ci.nsISupportsWeakReference,
]);
this._addedScrollbarsStylesheetSymbol = Symbol('_addedScrollbarsStylesheetSymbol');
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
this._wdmListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
@ -128,12 +130,24 @@ class FrameTree {
}
_onDOMWindowCreated(window) {
if (!window[this._addedScrollbarsStylesheetSymbol] && this.scrollbarsHidden) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
const sheet = styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
window.windowUtils.addSheet(sheet, styleSheetService.AGENT_SHEET);
window[this._addedScrollbarsStylesheetSymbol] = true;
}
const frame = this.frameForDocShell(window.docShell);
if (!frame)
return;
frame._onGlobalObjectCleared();
}
setScrollbarsHidden(hidden) {
this.scrollbarsHidden = hidden;
}
setJavaScriptDisabled(javaScriptDisabled) {
this._javaScriptDisabled = javaScriptDisabled;
for (const frame of this.frames())

View file

@ -8,8 +8,6 @@ const helper = new Helper();
let sameProcessInstanceNumber = 0;
const topBrowingContextToAgents = new Map();
class JugglerFrameChild extends JSWindowActorChild {
constructor() {
super();
@ -18,66 +16,46 @@ class JugglerFrameChild extends JSWindowActorChild {
}
handleEvent(aEvent) {
const agents = this._agents();
if (!agents)
return;
if (aEvent.type === 'DOMWillOpenModalDialog') {
agents.channel.pause();
if (this._agents && aEvent.type === 'DOMWillOpenModalDialog') {
this._agents.channel.pause();
return;
}
if (aEvent.type === 'DOMModalDialogClosed') {
agents.channel.resumeSoon();
if (this._agents && aEvent.type === 'DOMModalDialogClosed') {
this._agents.channel.resumeSoon();
return;
}
if (aEvent.target === this.document) {
agents.pageAgent.onWindowEvent(aEvent);
agents.frameTree.onWindowEvent(aEvent);
}
}
_agents() {
return topBrowingContextToAgents.get(this.browsingContext.top);
if (this._agents && aEvent.target === this.document)
this._agents.pageAgent.onWindowEvent(aEvent);
if (this._agents && aEvent.target === this.document)
this._agents.frameTree.onWindowEvent(aEvent);
}
actorCreated() {
this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => {
this._agents()?.pageAgent.onWindowEvent(event);
this._agents?.pageAgent.onWindowEvent(event);
}));
if (this.document.documentURI.startsWith('moz-extension://'))
return;
this._agents = initialize(this.browsingContext, this.docShell, this);
}
// Child frame events will be forwarded to related top-level agents.
if (this.browsingContext.parent)
return;
_dispose() {
helper.removeListeners(this._eventListeners);
// We do not cleanup since agents are shared for all frames in the process.
let agents = topBrowingContextToAgents.get(this.browsingContext);
if (!agents) {
agents = initialize(this.browsingContext, this.docShell);
topBrowingContextToAgents.set(this.browsingContext, agents);
}
agents.channel.bindToActor(this);
agents.actor = this;
// TODO: restore the cleanup.
// Reset transport so that all messages will be pending and will not throw any errors.
// this._channel.resetTransport();
// this._agents.pageAgent.dispose();
// this._agents.frameTree.dispose();
// this._agents = undefined;
}
didDestroy() {
helper.removeListeners(this._eventListeners);
if (this.browsingContext.parent)
return;
const agents = topBrowingContextToAgents.get(this.browsingContext);
// The agents are already re-bound to a new actor.
if (agents?.actor !== this)
return;
topBrowingContextToAgents.delete(this.browsingContext);
agents.channel.resetTransport();
agents.pageAgent.dispose();
agents.frameTree.dispose();
this._dispose();
}
receiveMessage() { }

View file

@ -62,6 +62,7 @@ class PageAgent {
const docShell = frameTree.mainFrame().docShell();
this._docShell = docShell;
this._initialDPPX = docShell.contentViewer.overrideDPPX;
// Dispatch frameAttached events for all initial frames
for (const frame of this._frameTree.frames()) {
@ -120,8 +121,7 @@ class PageAgent {
// After the dragStart event is dispatched and handled by Web,
// it might or might not create a new drag session, depending on its preventing default.
setTimeout(() => {
const session = this._getCurrentDragSession();
this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!session });
this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!dragService.getCurrentSession() });
}, 0);
}
}),
@ -153,6 +153,7 @@ class PageAgent {
getFullAXTree: this._getFullAXTree.bind(this),
insertText: this._insertText.bind(this),
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
setCacheDisabled: this._setCacheDisabled.bind(this),
setFileInputFiles: this._setFileInputFiles.bind(this),
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
@ -162,6 +163,15 @@ class PageAgent {
];
}
_setCacheDisabled({cacheDisabled}) {
const enable = Ci.nsIRequest.LOAD_NORMAL;
const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING;
const docShell = this._frameTree.mainFrame().docShell();
docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
}
_emitAllEvents(frame) {
this._browserPage.emit('pageEventFired', {
frameId: frame.id(),
@ -371,19 +381,8 @@ class PageAgent {
const unsafeObject = frame.unsafeObject(objectId);
if (!unsafeObject)
throw new Error('Object is not input!');
let nsFiles;
if (unsafeObject.webkitdirectory) {
nsFiles = await new Directory(files[0]).getFiles(true);
} else {
nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
}
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}) {
@ -515,26 +514,75 @@ class PageAgent {
false /* aIgnoreRootScrollFrame */,
true /* aFlushLayout */);
await this._dispatchTouchEvent({
const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({
type: 'touchstart',
modifiers,
touchPoints: [{x, y}]
});
await this._dispatchTouchEvent({
const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({
type: 'touchend',
modifiers,
touchPoints: [{x, y}]
});
}
if (startPrevented || endPrevented)
return;
_getCurrentDragSession() {
const frame = this._frameTree.mainFrame();
const domWindow = frame?.domWindow();
return domWindow ? dragService.getCurrentSession(domWindow) : undefined;
const winUtils = frame.domWindow().windowUtils;
winUtils.jugglerSendMouseEvent(
'mousemove',
x,
y,
0 /*button*/,
0 /*clickCount*/,
modifiers,
false /*aIgnoreRootScrollFrame*/,
0.0 /*pressure*/,
5 /*inputSource*/,
true /*isDOMEventSynthesized*/,
false /*isWidgetEventSynthesized*/,
0 /*buttons*/,
winUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
true /*disablePointerEvent*/
);
winUtils.jugglerSendMouseEvent(
'mousedown',
x,
y,
0 /*button*/,
1 /*clickCount*/,
modifiers,
false /*aIgnoreRootScrollFrame*/,
0.0 /*pressure*/,
5 /*inputSource*/,
true /*isDOMEventSynthesized*/,
false /*isWidgetEventSynthesized*/,
1 /*buttons*/,
winUtils.DEFAULT_MOUSE_POINTER_ID /*pointerIdentifier*/,
true /*disablePointerEvent*/,
);
winUtils.jugglerSendMouseEvent(
'mouseup',
x,
y,
0 /*button*/,
1 /*clickCount*/,
modifiers,
false /*aIgnoreRootScrollFrame*/,
0.0 /*pressure*/,
5 /*inputSource*/,
true /*isDOMEventSynthesized*/,
false /*isWidgetEventSynthesized*/,
0 /*buttons*/,
winUtils.DEFAULT_MOUSE_POINTER_ID /*pointerIdentifier*/,
true /*disablePointerEvent*/,
);
}
async _dispatchDragEvent({type, x, y, modifiers}) {
const session = this._getCurrentDragSession();
const session = dragService.getCurrentSession();
const dropEffect = session.dataTransfer.dropEffect;
if ((type === 'drop' && dropEffect !== 'none') || type === 'dragover') {
@ -558,8 +606,9 @@ class PageAgent {
return;
}
if (type === 'dragend') {
const session = this._getCurrentDragSession();
session?.endDragSession(true);
const session = dragService.getCurrentSession();
if (session)
dragService.endDragSession(true);
return;
}
}

View file

@ -7,10 +7,24 @@ const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTr
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
const browsingContextToAgents = new Map();
const helper = new Helper();
function initialize(browsingContext, docShell) {
const data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
function initialize(browsingContext, docShell, actor) {
if (browsingContext.parent) {
// For child frames, return agents from the main frame.
return browsingContextToAgents.get(browsingContext.top);
}
let data = browsingContextToAgents.get(browsingContext);
if (data) {
// Rebind from one main frame actor to another one.
data.channel.bindToActor(actor);
return data;
}
data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
browsingContextToAgents.set(browsingContext, data);
const applySetting = {
geolocation: (geolocation) => {
@ -45,6 +59,10 @@ function initialize(browsingContext, docShell) {
docShell.languageOverride = locale;
},
scrollbarsHidden: (hidden) => {
data.frameTree.setScrollbarsHidden(hidden);
},
javaScriptDisabled: (javaScriptDisabled) => {
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
},
@ -66,6 +84,7 @@ function initialize(browsingContext, docShell) {
data.frameTree.addBinding(worldName, name, script);
data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]);
data.channel = new SimpleChannel('', 'process-' + Services.appinfo.processID);
data.channel.bindToActor(actor);
data.pageAgent = new PageAgent(data.channel, data.frameTree);
docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog;

View file

@ -186,10 +186,6 @@ class BrowserHandler {
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
}
['Browser.setCacheDisabled']({browserContextId, cacheDisabled}) {
this._targetRegistry.browserContextForId(browserContextId).setCacheDisabled(cacheDisabled);
}
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
}
@ -255,6 +251,10 @@ class BrowserHandler {
await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
}
async ['Browser.setScrollbarsHidden']({browserContextId, hidden}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
}
async ['Browser.setInitScripts']({browserContextId, scripts}) {
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
}

View file

@ -256,13 +256,6 @@ class PageHandler {
return await this._contentPage.send('disposeObject', options);
}
async ['Heap.collectGarbage']() {
Services.obs.notifyObservers(null, "child-gc-request");
Cu.forceGC();
Services.obs.notifyObservers(null, "child-cc-request");
Cu.forceCC();
}
async ['Network.getResponseBody']({requestId}) {
return this._pageNetwork.getResponseBody(requestId);
}
@ -309,8 +302,8 @@ class PageHandler {
await this._pageTarget.activateAndRun(() => {});
}
async ['Page.setCacheDisabled']({cacheDisabled}) {
return await this._pageTarget.setCacheDisabled(cacheDisabled);
async ['Page.setCacheDisabled'](options) {
return await this._contentPage.send('setCacheDisabled', options);
}
async ['Page.addBinding']({ worldName, name, script }) {

View file

@ -322,12 +322,6 @@ const Browser = {
enabled: t.Boolean,
},
},
'setCacheDisabled': {
params: {
browserContextId: t.Optional(t.String),
cacheDisabled: t.Boolean,
},
},
'setGeolocationOverride': {
params: {
browserContextId: t.Optional(t.String),
@ -394,6 +388,12 @@ const Browser = {
viewport: t.Nullable(pageTypes.Viewport),
}
},
'setScrollbarsHidden': {
params: {
browserContextId: t.Optional(t.String),
hidden: t.Boolean,
}
},
'setInitScripts': {
params: {
browserContextId: t.Optional(t.String),
@ -481,17 +481,6 @@ const Browser = {
},
};
const Heap = {
targets: ['page'],
types: {},
events: {},
methods: {
'collectGarbage': {
params: {},
},
},
};
const Network = {
targets: ['page'],
types: networkTypes,
@ -1007,7 +996,7 @@ const Accessibility = {
}
this.protocol = {
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
domains: {Browser, Page, Runtime, Network, Accessibility},
};
this.checkScheme = checkScheme;
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];

View file

@ -129,7 +129,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
capability.height = 960;
capability.maxFPS = ScreencastEncoder::fps;
capability.videoType = webrtc::VideoType::kI420;
int error = mCaptureModule->StartCaptureCounted(capability);
int error = mCaptureModule->StartCapture(capability);
if (error) {
fprintf(stderr, "StartCapture error %d\n", error);
return false;
@ -152,7 +152,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
mCaptureModule->DeRegisterCaptureDataCallback(this);
else
mCaptureModule->DeRegisterRawFrameCallback(this);
mCaptureModule->StopCaptureCounted();
mCaptureModule->StopCapture();
if (mEncoder) {
mEncoder->finish([this, protect = RefPtr{this}] {
NS_DispatchToMainThread(NS_NewRunnableFunction(

File diff suppressed because it is too large Load diff

View file

@ -15,9 +15,6 @@ pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Force pdfs into downloads.
pref("pdfjs.disabled", true);
// This preference breaks our authentication flow.
pref("network.auth.use_redirect_for_retries", false);
// Disable cross-process iframes, but not cross-process navigations.
pref("fission.webContentIsolationStrategy", 0);
@ -47,9 +44,6 @@ pref("permissions.isolateBy.userContext", true);
// |Page.setFileInputFiles| protocol method.
pref("dom.file.createInChild", true);
// Allow uploading directorys in content process.
pref("dom.filesystem.pathcheck.disabled", true);
// Do not warn when closing all open tabs
pref("browser.tabs.warnOnClose", false);
@ -100,11 +94,6 @@ pref("extensions.formautofill.addresses.supported", "off");
// firefox behavior with other browser defaults.
pref("security.enterprise_roots.enabled", true);
// There's a security features warning that might be shown on certain Linux distributions & configurations:
// https://support.mozilla.org/en-US/kb/install-firefox-linux#w_security-features-warning
// This notification should never be shown in automation scenarios.
pref("security.sandbox.warn_unprivileged_namespaces", false);
// Avoid stalling on shutdown, after "xpcom-will-shutdown" phase.
// This at least happens when shutting down soon after launching.
// See AppShutdown.cpp for more details on shutdown phases.
@ -120,20 +109,9 @@ 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", "");
// Prevent Remote Settings (firefox.settings.services.mozilla.com) to issue non local connections.
// This setting breaks settings loading.
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);
@ -330,6 +308,3 @@ pref("devtools.toolbox.host", "window");
// Disable auto translations
pref("browser.translations.enable", false);
// Disable spell check
pref("layout.spellcheckDefault", 0);

View file

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

View file

@ -33,7 +33,6 @@
#import <WebKit/WKUserContentControllerPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebpagePreferencesPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/WebNSURLExtras.h>
#import <WebKit/WebKit.h>
@ -98,7 +97,7 @@ const NSActivityOptions ActivityOptions =
for (NSString *argument in subArray) {
if (![argument hasPrefix:@"--"])
_initialURL = [argument copy];
_initialURL = argument;
if ([argument hasPrefix:@"--user-data-dir="]) {
NSRange range = NSMakeRange(16, [argument length] - 16);
_userDataDir = [[argument substringWithRange:range] copy];
@ -231,7 +230,7 @@ const NSActivityOptions ActivityOptions =
configuration = [[WKWebViewConfiguration alloc] init];
configuration.websiteDataStore = [self persistentDataStore];
configuration._controlledByAutomation = true;
configuration.preferences.elementFullscreenEnabled = YES;
configuration.preferences._fullScreenEnabled = YES;
configuration.preferences._developerExtrasEnabled = YES;
configuration.preferences._mediaDevicesEnabled = YES;
configuration.preferences._mockCaptureDevicesEnabled = YES;
@ -241,8 +240,6 @@ const NSActivityOptions ActivityOptions =
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
configuration.preferences._domTimersThrottlingEnabled = NO;
// Do not auto play audio and video with sound.
configuration.defaultWebpagePreferences._autoplayPolicy = _WKWebsiteAutoplayPolicyAllowWithoutSound;
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
processConfiguration.forceOverlayScrollbars = YES;
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];

File diff suppressed because it is too large Load diff

View file

@ -39,4 +39,5 @@ fi
# create a TMP directory to copy all necessary files
cd ./x64/Release
7z a "$ZIP_PATH" ./PrintDeps.exe
zip $ZIP_PATH ./PrintDeps.exe

View file

@ -14,6 +14,8 @@ A few examples of problems this can catch include:
The following examples rely on the [`com.deque.html.axe-core/playwright`](https://mvnrepository.com/artifact/com.deque.html.axe-core/playwright) Maven package which adds support for running the [axe accessibility testing engine](https://www.deque.com/axe/) as part of your Playwright tests.
<!-- TOC -->
## Disclaimer
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
@ -70,24 +72,22 @@ For example, you can use [`AxeBuilder.include()`](https://github.com/dequelabs/a
`AxeBuilder.analyze()` will scan the page *in its current state* when you call it. To scan parts of a page that are revealed based on UI interactions, use [Locators](./locators.md) to interact with the page before invoking `analyze()`:
```java
public class HomepageTests {
@Test
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
page.navigate("https://your-site.com/");
@Test
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
page.navigate("https://your-site.com/");
page.locator("button[aria-label=\"Navigation Menu\"]").click();
page.locator("button[aria-label=\"Navigation Menu\"]").click();
// It is important to waitFor() the page to be in the desired
// state *before* running analyze(). Otherwise, axe might not
// find all the elements your test expects it to scan.
page.locator("#navigation-menu-flyout").waitFor();
// It is important to waitFor() the page to be in the desired
// state *before* running analyze(). Otherwise, axe might not
// find all the elements your test expects it to scan.
page.locator("#navigation-menu-flyout").waitFor();
AxeResults accessibilityScanResults = new AxeBuilder(page)
.include(Arrays.asList("#navigation-menu-flyout"))
.analyze();
AxeResults accessibilityScanResults = new AxeBuilder(page)
.include(Arrays.asList("#navigation-menu-flyout"))
.analyze();
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
}
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
}
```
@ -135,7 +135,7 @@ If the element in question is used repeatedly in many pages, consider [using a t
### Disabling individual scan rules
If your application contains many different preexisting violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-maven-html/blob/develop/playwright/README.md#axebuilderdisablerulesliststring-rules) to temporarily disable individual rules until you're able to fix the issues.
If your application contains many different pre-existing violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-maven-html/blob/develop/playwright/README.md#axebuilderdisablerulesliststring-rules) to temporarily disable individual rules until you're able to fix the issues.
You can find the rule IDs to pass to `disableRules()` in the `id` property of the violations you want to suppress. A [complete list of axe's rules](https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md) can be found in `axe-core`'s documentation.
@ -160,40 +160,38 @@ This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost o
Here is an example of using fingerprints based on only rule IDs and "target" selectors pointing to each violation:
```java
public class HomepageTests {
@Test
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
page.navigate("https://your-site.com/");
@Test
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
page.navigate("https://your-site.com/");
AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();
AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();
List<ViolationFingerprint> violationFingerprints = fingerprintsFromScanResults(accessibilityScanResults);
List<ViolationFingerprint> violationFingerprints = fingerprintsFromScanResults(accessibilityScanResults);
assertEquals(Arrays.asList(
new ViolationFingerprint("aria-roles", "[span[role=\"invalid\"]]"),
new ViolationFingerprint("color-contrast", "[li:nth-child(2) > span]"),
new ViolationFingerprint("label", "[input]")
), violationFingerprints);
}
assertEquals(Arrays.asList(
new ViolationFingerprint("aria-roles", "[span[role=\"invalid\"]]"),
new ViolationFingerprint("color-contrast", "[li:nth-child(2) > span]"),
new ViolationFingerprint("label", "[input]")
), violationFingerprints);
}
// You can make your "fingerprint" as specific as you like. This one considers a violation to be
// "the same" if it corresponds the same Axe rule on the same element.
//
// Using a record type makes it easy to compare fingerprints with assertEquals
public record ViolationFingerprint(String ruleId, String target) { }
// You can make your "fingerprint" as specific as you like. This one considers a violation to be
// "the same" if it corresponds the same Axe rule on the same element.
//
// Using a record type makes it easy to compare fingerprints with assertEquals
public record ViolationFingerprint(String ruleId, String target) { }
public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
return results.getViolations().stream()
// Each violation refers to one rule and multiple "nodes" which violate it
.flatMap(violation -> violation.getNodes().stream()
.map(node -> new ViolationFingerprint(
violation.getId(),
// Each node contains a "target", which is a CSS selector that uniquely identifies it
// If the page involves iframes or shadow DOMs, it may be a chain of CSS selectors
node.getTarget().toString()
)))
.collect(Collectors.toList());
}
public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
return results.getViolations().stream()
// Each violation refers to one rule and multiple "nodes" which violate it
.flatMap(violation -> violation.getNodes().stream()
.map(node -> new ViolationFingerprint(
violation.getId(),
// Each node contains a "target", which is a CSS selector that uniquely identifies it
// If the page involves iframes or shadow DOMs, it may be a chain of CSS selectors
node.getTarget().toString()
)))
.collect(Collectors.toList());
}
```
@ -212,11 +210,11 @@ This example fixture creates an `AxeBuilder` object which is pre-configured with
```java
class AxeTestFixtures extends TestFixtures {
AxeBuilder makeAxeBuilder() {
return new AxeBuilder(page)
.withTags(new String[]{"wcag2a", "wcag2aa", "wcag21a", "wcag21aa"})
.exclude("#commonly-reused-element-with-known-issue");
}
AxeBuilder makeAxeBuilder() {
return new AxeBuilder(page)
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
}
}
```
@ -233,7 +231,7 @@ public class HomepageTests extends AxeTestFixtures {
AxeResults accessibilityScanResults = makeAxeBuilder()
// Automatically uses the shared AxeBuilder configuration,
// but supports additional test-specific configuration too
.include("#specific-element-under-test")
.include('#specific-element-under-test')
.analyze();
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());

View file

@ -14,7 +14,7 @@ A few examples of problems this can catch include:
The following examples rely on the [`@axe-core/playwright`](https://npmjs.org/@axe-core/playwright) package which adds support for running the [axe accessibility testing engine](https://www.deque.com/axe/) as part of your Playwright tests.
:::note[Disclaimer]
:::note Disclaimer
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
For manual assessments, we recommend [Accessibility Insights for Web](https://accessibilityinsights.io/docs/web/overview/?referrer=playwright-accessibility-testing-js), a free and open source dev tool that walks you through assessing a website for [WCAG 2.1 AA](https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&levels=aaa) coverage.
@ -147,7 +147,7 @@ If the element in question is used repeatedly in many pages, consider [using a t
### Disabling individual scan rules
If your application contains many different preexisting violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderdisablerulesrules-stringarray) to temporarily disable individual rules until you're able to fix the issues.
If your application contains many different pre-existing violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderdisablerulesrules-stringarray) to temporarily disable individual rules until you're able to fix the issues.
You can find the rule IDs to pass to `disableRules()` in the `id` property of the violations you want to suppress. A [complete list of axe's rules](https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md) can be found in `axe-core`'s documentation.
@ -167,7 +167,7 @@ test('should not have any accessibility violations outside of rules with known i
### Using snapshots to allow specific known issues
If you would like to allow for a more granular set of known issues, you can use [Snapshots](./test-snapshots.md) to verify that a set of preexisting violations has not changed. This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost of slightly more complexity and fragility.
If you would like to allow for a more granular set of known issues, you can use [Snapshots](./test-snapshots.md) to verify that a set of pre-existing violations has not changed. This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost of slightly more complexity and fragility.
Do not use a snapshot of the entire `accessibilityScanResults.violations` array. It contains implementation details of the elements in question, such as a snippet of their rendered HTML; if you include these in your snapshots, it will make your tests prone to breaking every time one of the components in question changes for an unrelated reason:

View file

@ -9,7 +9,7 @@ Playwright performs a range of actionability checks on the elements before makin
behave as expected. It auto-waits for all the relevant checks to pass and only then performs the requested action. If the required checks do not pass within the given `timeout`, action fails with the `TimeoutError`.
For example, for [`method: Locator.click`], Playwright will ensure that:
- locator resolves to exactly one element
- locator resolves to an exactly one element
- element is [Visible]
- element is [Stable], as in not animating or completed animation
- element [Receives Events], as in not obscured by other elements
@ -93,20 +93,11 @@ Element is considered stable when it has maintained the same bounding box for at
## Enabled
Element is considered enabled when it is **not disabled**.
Element is **disabled** when:
- it is a `<button>`, `<select>`, `<input>`, `<textarea>`, `<option>` or `<optgroup>` with a `[disabled]` attribute;
- it is a `<button>`, `<select>`, `<input>`, `<textarea>`, `<option>` or `<optgroup>` that is a part of a `<fieldset>` with a `[disabled]` attribute;
- it is a descendant of an element with `[aria-disabled=true]` attribute.
Element is considered enabled unless it is a `<button>`, `<select>`, `<input>` or `<textarea>` with a `disabled` property.
## Editable
Element is considered editable when it is [enabled] and is **not readonly**.
Element is **readonly** when:
- it is a `<select>`, `<input>` or `<textarea>` with a `[readonly]` attribute;
- it has an `[aria-readonly=true]` attribute and an aria role that [supports it](https://w3c.github.io/aria/#aria-readonly).
Element is considered editable when it is [enabled] and does not have `readonly` property set.
## Receives Events

View file

@ -16,7 +16,9 @@ A few examples where it may come in handy:
All of that could be achieved via [APIRequestContext] methods.
The following examples rely on the [`Microsoft.Playwright.MSTest`](./test-runners.md) package which creates a Playwright and Page instance for each test.
The following examples rely on the [`Microsoft.Playwright.NUnit`](./test-runners.md) package which creates a Playwright and Page instance for each test.
<!-- TOC -->
## Writing API Test
@ -32,19 +34,22 @@ The following example demonstrates how to use Playwright to test issues creation
GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the `baseURL` to simplify the tests.
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
private IAPIRequestContext Request = null!;
private IAPIRequestContext Request = null;
[TestInitialize]
[SetUp]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
@ -66,7 +71,7 @@ public class TestGitHubAPI : PlaywrightTest
});
}
[TestCleanup]
[TearDown]
public async Task TearDownAPITesting()
{
await Request.DisposeAsync();
@ -78,34 +83,36 @@ public class TestGitHubAPI : PlaywrightTest
Now that we initialized request object we can add a few tests that will create new issues in the repository.
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.Playwright.NUnit;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
[TestFixture]
public class TestGitHubAPI : PlaywrightTest
{
static string REPO = "test";
static string REPO = "test-repo-2";
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
private IAPIRequestContext Request = null!;
private IAPIRequestContext Request = null;
[TestMethod]
[Test]
public async Task ShouldCreateBugReport()
{
var data = new Dictionary<string, string>
{
{ "title", "[Bug] report 1" },
{ "body", "Bug description" }
};
var data = new Dictionary<string, string>();
data.Add("title", "[Bug] report 1");
data.Add("body", "Bug description");
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();
Assert.True(newIssue.Ok);
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
Assert.True(issues.Ok);
var issuesJsonResponse = await issues.JsonAsync();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
@ -118,24 +125,23 @@ public class TestGitHubAPI : PlaywrightTest
}
}
}
Assert.IsNotNull(issue);
Assert.NotNull(issue);
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
}
[TestMethod]
[Test]
public async Task ShouldCreateFeatureRequests()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var data = new Dictionary<string, string>();
data.Add("title", "[Feature] request 1");
data.Add("body", "Feature description");
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();
Assert.True(newIssue.Ok);
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
Assert.True(issues.Ok);
var issuesJsonResponse = await issues.JsonAsync();
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
@ -148,7 +154,7 @@ public class TestGitHubAPI : PlaywrightTest
}
}
}
Assert.IsNotNull(issue);
Assert.NotNull(issue);
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
}
@ -161,47 +167,41 @@ public class TestGitHubAPI : PlaywrightTest
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that.
```csharp
using System.Text.Json;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
// ...
[TestInitialize]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
await CreateTestRepository();
}
// ...
private async Task CreateTestRepository()
{
var resp = await Request.PostAsync("/user/repos", new()
{
DataObject = new Dictionary<string, string>()
{
["name"] = REPO,
},
});
await Expect(resp).ToBeOKAsync();
}
[SetUp]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
await CreateTestRepository();
}
[TestCleanup]
public async Task TearDownAPITesting()
{
await DeleteTestRepository();
await Request.DisposeAsync();
}
private async Task CreateTestRepository()
{
var resp = await Request.PostAsync("/user/repos", new()
{
DataObject = new Dictionary<string, string>()
{
["name"] = REPO,
},
});
Assert.True(resp.Ok);
}
private async Task DeleteTestRepository()
{
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
await Expect(resp).ToBeOKAsync();
}
[TearDown]
public async Task TearDownAPITesting()
{
await DeleteTestRepository();
await Request.DisposeAsync();
}
private async Task DeleteTestRepository()
{
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
Assert.True(resp.Ok);
}
}
```
@ -210,34 +210,36 @@ public class TestGitHubAPI : PlaywrightTest
Here is the complete example of an API test:
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.Playwright.NUnit;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
[TestFixture]
public class TestGitHubAPI : PlaywrightTest
{
static string REPO = "test-repo-2";
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
private IAPIRequestContext Request = null!;
private IAPIRequestContext Request = null;
[TestMethod]
[Test]
public async Task ShouldCreateBugReport()
{
var data = new Dictionary<string, string>
{
{ "title", "[Bug] report 1" },
{ "body", "Bug description" }
};
var data = new Dictionary<string, string>();
data.Add("title", "[Bug] report 1");
data.Add("body", "Bug description");
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();
Assert.True(newIssue.Ok);
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
Assert.True(issues.Ok);
var issuesJsonResponse = await issues.JsonAsync();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
@ -250,24 +252,23 @@ public class TestGitHubAPI : PlaywrightTest
}
}
}
Assert.IsNotNull(issue);
Assert.NotNull(issue);
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
}
[TestMethod]
[Test]
public async Task ShouldCreateFeatureRequests()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var data = new Dictionary<string, string>();
data.Add("title", "[Feature] request 1");
data.Add("body", "Feature description");
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();
Assert.True(newIssue.Ok);
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
Assert.True(issues.Ok);
var issuesJsonResponse = await issues.JsonAsync();
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
@ -280,11 +281,11 @@ public class TestGitHubAPI : PlaywrightTest
}
}
}
Assert.IsNotNull(issue);
Assert.NotNull(issue);
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
}
[TestInitialize]
[SetUp]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
@ -293,16 +294,14 @@ public class TestGitHubAPI : PlaywrightTest
private async Task CreateAPIRequestContext()
{
var headers = new Dictionary<string, string>
{
// We set this header per GitHub guidelines.
{ "Accept", "application/vnd.github.v3+json" },
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
{ "Authorization", "token " + API_TOKEN }
};
var headers = new Dictionary<string, string>();
// We set this header per GitHub guidelines.
headers.Add("Accept", "application/vnd.github.v3+json");
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
headers.Add("Authorization", "token " + API_TOKEN);
Request = await Playwright.APIRequest.NewContextAsync(new()
Request = await this.Playwright.APIRequest.NewContextAsync(new()
{
// All requests we send go to this API endpoint.
BaseURL = "https://api.github.com",
@ -319,10 +318,10 @@ public class TestGitHubAPI : PlaywrightTest
["name"] = REPO,
},
});
await Expect(resp).ToBeOKAsync();
Assert.True(resp.Ok);
}
[TestCleanup]
[TearDown]
public async Task TearDownAPITesting()
{
await DeleteTestRepository();
@ -332,7 +331,7 @@ public class TestGitHubAPI : PlaywrightTest
private async Task DeleteTestRepository()
{
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
await Expect(resp).ToBeOKAsync();
Assert.True(resp.Ok);
}
}
```
@ -345,23 +344,21 @@ project to check that it appears at the top of the list. The check is performed
```csharp
class TestGitHubAPI : PageTest
{
[TestMethod]
public async Task LastCreatedIssueShouldBeFirstInTheList()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();
[Test]
public async Task LastCreatedIssueShouldBeFirstInTheList()
{
var data = new Dictionary<string, string>();
data.Add("title", "[Feature] request 1");
data.Add("body", "Feature description");
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
Assert.True(newIssue.Ok);
// When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start
// a browser, context, and page manually or inherit from 'PageTest' which will launch it for you.
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First;
await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1");
}
// When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start
// a browser, context, and page manually or inherit from 'PageTest' which will launch it for you.
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First;
await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1");
}
}
```
@ -371,23 +368,22 @@ The following test creates a new issue via user interface in the browser and the
it was created:
```csharp
// Make sure to extend from PageTest if you want to use the Page class.
class GitHubTests : PageTest
{
[TestMethod]
public async Task LastCreatedIssueShouldBeOnTheServer()
{
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
await Page.Locator("text=New Issue").ClickAsync();
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description");
await Page.Locator("text=Submit new issue").ClickAsync();
var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));
[Test]
public async Task LastCreatedIssueShouldBeOnTheServer()
{
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
await Page.Locator("text=New Issue").ClickAsync();
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description");
await Page.Locator("text=Submit new issue").ClickAsync();
String issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));
var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
await Expect(newIssue).ToBeOKAsync();
StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1");
}
var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
Assert.True(newIssue.Ok);
StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1");
}
}
```

View file

@ -16,6 +16,8 @@ A few examples where it may come in handy:
All of that could be achieved via [APIRequestContext] methods.
<!-- TOC -->
## Writing API Test
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
@ -194,7 +196,6 @@ public class TestGitHubAPI {
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `@BeforeAll` and `@AfterAll` hooks for that.
```java
public class TestGitHubAPI {
// ...
void createTestRepository() {
@ -224,7 +225,6 @@ public class TestGitHubAPI {
disposeAPIRequestContext();
closePlaywright();
}
}
```
### Complete test example
@ -383,20 +383,18 @@ The following test creates a new issue via API and then navigates to the list of
project to check that it appears at the top of the list. The check is performed using [LocatorAssertions].
```java
public class TestGitHubAPI {
@Test
void lastCreatedIssueShouldBeFirstInTheList() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Feature] request 1");
data.put("body", "Feature description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());
@Test
void lastCreatedIssueShouldBeFirstInTheList() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Feature] request 1");
data.put("body", "Feature description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
assertThat(firstIssue).hasText("[Feature] request 1");
}
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
assertThat(firstIssue).hasText("[Feature] request 1");
}
```
@ -406,20 +404,18 @@ The following test creates a new issue via user interface in the browser and the
it was created:
```java
public class TestGitHubAPI {
@Test
void lastCreatedIssueShouldBeOnTheServer() {
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
page.locator("text=New Issue").click();
page.locator("[aria-label='Title']").fill("Bug report 1");
page.locator("[aria-label='Comment body']").fill("Bug description");
page.locator("text=Submit new issue").click();
String issueId = page.url().substring(page.url().lastIndexOf('/'));
@Test
void lastCreatedIssueShouldBeOnTheServer() {
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
page.locator("text=New Issue").click();
page.locator("[aria-label='Title']").fill("Bug report 1");
page.locator("[aria-label='Comment body']").fill("Bug description");
page.locator("text=Submit new issue").click();
String issueId = page.url().substring(page.url().lastIndexOf('/'));
APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
assertThat(newIssue).isOK();
assertTrue(newIssue.text().contains("Bug report 1"));
}
APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
assertThat(newIssue).isOK();
assertTrue(newIssue.text().contains("Bug report 1"));
}
```

View file

@ -16,6 +16,8 @@ A few examples where it may come in handy:
All of that could be achieved via [APIRequestContext] methods.
<!-- TOC3 -->
## Writing API Test
[APIRequestContext] can send all kinds of HTTP(S) requests over network.

View file

@ -18,6 +18,8 @@ All of that could be achieved via [APIRequestContext] methods.
The following examples rely on the [`pytest-playwright`](./test-runners.md) package which add Playwright fixtures to the Pytest test-runner.
<!-- TOC -->
## Writing API Test
[APIRequestContext] can send all kinds of HTTP(S) requests over network.

View file

@ -202,12 +202,6 @@ Prevents automatic playwright driver installation on attach. Assumes that the dr
Optional device serial number to launch the browser on. If not specified, it will
throw if multiple devices are connected.
### option: Android.launchServer.host
* since: v1.45
- `host` <[string]>
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
### option: Android.launchServer.port
* since: v1.28
- `port` <[int]>

View file

@ -136,7 +136,7 @@ Launches Chrome browser on the device, and returns its persistent context.
### option: AndroidDevice.launchBrowser.pkg
* since: v1.9
- `pkg` <[string]>
- `command` <[string]>
Optional package name to launch instead of default Chrome for Android.
@ -177,9 +177,7 @@ Launches a process in the shell on the device and returns a socket to communicat
### param: AndroidDevice.open.command
* since: v1.9
- `command` <[string]>
Shell command to execute.
- `command` <[string]> Shell command to execute.
## async method: AndroidDevice.pinchClose
* since: v1.9
@ -447,7 +445,7 @@ Either a predicate that receives an event or an options object. Optional.
* since: v1.9
- returns: <[AndroidWebView]>
This method waits until [AndroidWebView] matching the [`param: selector`] is opened and returns it. If there is already an open [AndroidWebView] matching the [`param: selector`], returns immediately.
This method waits until [AndroidWebView] matching the [`option: selector`] is opened and returns it. If there is already an open [AndroidWebView] matching the [`option: selector`], returns immediately.
### param: AndroidDevice.webView.selector
* since: v1.9

View file

@ -12,22 +12,12 @@ see [APIRequestContext].
Creates new instances of [APIRequestContext].
### option: APIRequest.newContext.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: APIRequest.newContext.useragent = %%-context-option-useragent-%%
* since: v1.16
### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
* since: v1.16
### option: APIRequest.newContext.failOnStatusCode
* since: v1.51
- `failOnStatusCode` <[boolean]>
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
for all status codes.
### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%%
* since: v1.16
@ -71,7 +61,6 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
Populates context with given storage state. This option can be used to initialize context with logged-in information
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the

View file

@ -138,40 +138,28 @@ context cookies from the response. The method will automatically follow redirect
### param: APIRequestContext.delete.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.delete.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.delete.params = %%-java-fetch-params-%%
### param: APIRequestContext.delete.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.delete.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.delete.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.delete.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.delete.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.delete.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.17
### option: APIRequestContext.delete.form = %%-js-fetch-option-form-%%
* since: v1.17
### option: APIRequestContext.delete.form = %%-python-fetch-option-form-%%
### option: APIRequestContext.delete.form = %%-js-python-fetch-option-form-%%
* since: v1.17
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-js-fetch-option-multipart-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.delete.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.17
### option: APIRequestContext.delete.multipart = %%-csharp-fetch-option-multipart-%%
@ -189,31 +177,20 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.delete.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.delete.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.dispose
* since: v1.16
All responses returned by [`method: APIRequestContext.get`] and similar methods are stored in the memory, so that you can later call [`method: APIResponse.body`].This method discards all its resources, calling any method on disposed [APIRequestContext] will throw an exception.
### option: APIRequestContext.dispose.reason
* since: v1.45
- `reason` <[string]>
The reason to be reported to the operations interrupted by the context disposal.
## async method: APIRequestContext.fetch
* since: v1.16
- 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.
context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly to the request.
**Usage**
JSON objects can be passed directly to the request:
```js
await request.fetch('https://example.com/api/createBook', {
method: 'post',
@ -247,17 +224,28 @@ 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 upload them as form fields with `multipart/form-data` encoding, by specifiying the `multipart` parameter:
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:
```js
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
// 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);')
}
}
});
```
@ -271,14 +259,15 @@ 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/uploadScript",
APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
RequestOptions.create().setMethod("post").setMultipart(
FormData.create().set("fileField", filePayload)));
```
```python
api_request_context.fetch(
"https://example.com/api/uploadScript", method="post",
"https://example.com/api/uploadScrip'",
method="post",
multipart={
"fileField": {
"name": "f.js",
@ -300,28 +289,21 @@ multipart.Set("fileField", file);
await Request.FetchAsync("https://example.com/api/uploadScript", new() { Method = "post", Multipart = multipart });
```
### param: APIRequestContext.fetch.urlOrRequest
* since: v1.16
- `urlOrRequest` <[string]|[Request]>
Target URL or Request to get all parameters from.
### option: APIRequestContext.fetch.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.fetch.params = %%-java-fetch-params-%%
### param: APIRequestContext.fetch.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.fetch.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.fetch.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.fetch.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.fetch.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.fetch.method
* since: v1.16
* langs: js, python, csharp
@ -336,19 +318,13 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16
### option: APIRequestContext.fetch.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.fetch.form = %%-python-fetch-option-form-%%
### option: APIRequestContext.fetch.form = %%-js-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.fetch.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.fetch.multipart = %%-csharp-fetch-option-multipart-%%
@ -366,9 +342,6 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
### option: APIRequestContext.fetch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.fetch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.get
* since: v1.16
- returns: <[APIResponse]>
@ -382,24 +355,12 @@ context cookies from the response. The method will automatically follow redirect
Request parameters can be configured with `params` option, they will be serialized into the URL search parameters:
```js
// Passing params as object
await request.get('https://example.com/api/getText', {
params: {
'isbn': '1234',
'page': 23,
}
});
// Passing params as URLSearchParams
const searchParams = new URLSearchParams();
searchParams.set('isbn', '1234');
searchParams.append('page', 23);
searchParams.append('page', 24);
await request.get('https://example.com/api/getText', { params: searchParams });
// Passing params as string
const queryString = 'isbn=1234&page=23&page=24';
await request.get('https://example.com/api/getText', { params: queryString });
```
```java
@ -428,40 +389,28 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### param: APIRequestContext.get.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.get.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.get.params = %%-java-fetch-params-%%
### param: APIRequestContext.get.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.get.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.get.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.get.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.get.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.get.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.26
### option: APIRequestContext.get.form = %%-js-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.get.form = %%-python-fetch-option-form-%%
### option: APIRequestContext.get.form = %%-js-python-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-js-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.get.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.get.multipart = %%-csharp-fetch-option-multipart-%%
@ -479,9 +428,6 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### option: APIRequestContext.get.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.get.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.head
* since: v1.16
- returns: <[APIResponse]>
@ -493,40 +439,28 @@ context cookies from the response. The method will automatically follow redirect
### param: APIRequestContext.head.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.head.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.head.params = %%-java-fetch-params-%%
### param: APIRequestContext.head.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.head.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.head.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.head.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.head.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.head.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.26
### option: APIRequestContext.head.form = %%-python-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.head.form = %%-js-fetch-option-form-%%
### option: APIRequestContext.head.form = %%-js-python-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-js-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.head.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.26
### option: APIRequestContext.head.multipart = %%-csharp-fetch-option-multipart-%%
@ -544,9 +478,6 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.head.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.head.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.patch
* since: v1.16
- returns: <[APIResponse]>
@ -558,40 +489,28 @@ context cookies from the response. The method will automatically follow redirect
### param: APIRequestContext.patch.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.patch.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.patch.params = %%-java-fetch-params-%%
### param: APIRequestContext.patch.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.patch.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.patch.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.patch.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.patch.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.patch.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16
### option: APIRequestContext.patch.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.patch.form = %%-python-fetch-option-form-%%
### option: APIRequestContext.patch.form = %%-js-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.patch.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.patch.multipart = %%-csharp-fetch-option-multipart-%%
@ -609,9 +528,6 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.patch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.patch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.post
* since: v1.16
- returns: <[APIResponse]>
@ -650,7 +566,7 @@ api_request_context.post("https://example.com/api/createBook", data=data)
```csharp
var data = new Dictionary<string, object>() {
{ "firstName", "John" },
{ "firstNam", "John" },
{ "lastName", "Doe" }
};
await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data });
@ -688,17 +604,26 @@ 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. Use [FormData] to construct request body and pass it to the request as `multipart` parameter:
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:
```js
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
// 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);')
}
}
});
```
@ -710,16 +635,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 filePayload1 = new FilePayload("f1.js", "text/javascript",
FilePayload filePayload = new FilePayload("f.js", "text/javascript",
"console.log(2022);".getBytes(StandardCharsets.UTF_8));
APIResponse response = request.post("https://example.com/api/uploadScript",
APIResponse response = request.post("https://example.com/api/uploadTeamList",
RequestOptions.create().setMultipart(
FormData.create().set("fileField", filePayload)));
```
```python
api_request_context.post(
"https://example.com/api/uploadScript'",
"https://example.com/api/uploadScrip'",
multipart={
"fileField": {
"name": "f.js",
@ -744,40 +669,28 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### param: APIRequestContext.post.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.post.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.post.params = %%-java-fetch-params-%%
### param: APIRequestContext.post.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.post.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.post.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.post.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.post.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.post.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16
### option: APIRequestContext.post.form = %%-js-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.post.form = %%-python-fetch-option-form-%%
### option: APIRequestContext.post.form = %%-js-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.post.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.post.multipart = %%-csharp-fetch-option-multipart-%%
@ -795,9 +708,6 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### option: APIRequestContext.post.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.post.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.put
* since: v1.16
- returns: <[APIResponse]>
@ -809,40 +719,28 @@ context cookies from the response. The method will automatically follow redirect
### param: APIRequestContext.put.url = %%-fetch-param-url-%%
* since: v1.16
### option: APIRequestContext.put.params = %%-js-fetch-option-params-%%
* since: v1.16
### param: APIRequestContext.put.params = %%-java-fetch-params-%%
### param: APIRequestContext.put.params = %%-java-csharp-fetch-params-%%
* since: v1.18
### option: APIRequestContext.put.params = %%-python-fetch-option-params-%%
### option: APIRequestContext.put.params = %%-js-python-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.put.params = %%-csharp-fetch-option-params-%%
* since: v1.16
### option: APIRequestContext.put.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.put.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16
### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%%
* since: v1.16
### option: APIRequestContext.put.form = %%-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.put.form = %%-js-fetch-option-form-%%
### option: APIRequestContext.put.form = %%-js-python-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-js-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-python-fetch-option-multipart-%%
### option: APIRequestContext.put.multipart = %%-js-python-fetch-option-multipart-%%
* since: v1.16
### option: APIRequestContext.put.multipart = %%-csharp-fetch-option-multipart-%%
@ -860,9 +758,6 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.put.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
* since: v1.26
### option: APIRequestContext.put.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
* since: v1.46
## async method: APIRequestContext.storageState
* since: v1.16
- returns: <[Object]>
@ -880,7 +775,6 @@ context cookies from the response. The method will automatically follow redirect
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `indexedDB` <[Array]<[unknown]>>
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
@ -891,9 +785,3 @@ Returns storage state for this request context, contains current cookies and loc
### option: APIRequestContext.storageState.path = %%-storagestate-option-path-%%
* since: v1.16
### option: APIRequestContext.storageState.indexedDB
* since: v1.51
- `indexedDB` ?<boolean>
Set to `true` to include IndexedDB in the storage state snapshot.

View file

@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
- `name` <[string]> Name of the header.
- `value` <[string]> Value of the header.
An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## async method: APIResponse.json

View file

@ -14,15 +14,15 @@ test('navigates to login', async ({ page }) => {
```
```java
// ...
...
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TestPage {
// ...
...
@Test
void navigatesToLoginPage() {
// ...
APIResponse response = page.request().get("https://playwright.dev");
...
APIResponse response = page.request().get('https://playwright.dev');
assertThat(response).isOK();
}
}

View file

@ -1,5 +1,6 @@
# class: Browser
* since: v1.8
* extends: [EventEmitter]
A Browser is created via [`method: BrowserType.launch`]. An example of using a [Browser] to create a [Page]:
@ -18,15 +19,15 @@ const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
import com.microsoft.playwright.*;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType firefox = playwright.firefox();
Browser browser = firefox.launch();
Page page = browser.newPage();
page.navigate("https://example.com");
browser.close();
}
}
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType firefox = playwright.firefox()
Browser browser = firefox.launch();
Page page = browser.newPage();
page.navigate('https://example.com');
browser.close();
}
}
}
```
@ -96,7 +97,7 @@ In case this browser is connected to, clears all created contexts belonging to t
browser server.
:::note
This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call [`method: BrowserContext.close`] on any [BrowserContext] instances you explicitly created earlier using [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
This is similar to force quitting the browser. Therefore, you should call [`method: BrowserContext.close`] on any [BrowserContext]'s you explicitly created earlier with [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
:::
The [Browser] object itself is considered to be disposed and cannot be used anymore.
@ -132,16 +133,16 @@ System.out.println(browser.contexts().size()); // prints "1"
```python async
browser = await pw.webkit.launch()
print(len(browser.contexts)) # prints `0`
print(len(browser.contexts())) # prints `0`
context = await browser.new_context()
print(len(browser.contexts)) # prints `1`
print(len(browser.contexts())) # prints `1`
```
```python sync
browser = pw.webkit.launch()
print(len(browser.contexts)) # prints `0`
print(len(browser.contexts())) # prints `0`
context = browser.new_context()
print(len(browser.contexts)) # prints `1`
print(len(browser.contexts())) # prints `1`
```
```csharp
@ -202,7 +203,7 @@ Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
BrowserContext context = browser.newContext();
// Create a new page in a pristine context.
Page page = context.newPage();
page.navigate("https://example.com");
page.navigate('https://example.com');
// Graceful close up everything
context.close();
@ -255,9 +256,6 @@ await browser.CloseAsync();
### option: Browser.newContext.proxy = %%-context-option-proxy-%%
* since: v1.8
### option: Browser.newContext.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.8
@ -283,9 +281,6 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
### option: Browser.newPage.proxy = %%-context-option-proxy-%%
* since: v1.8
### option: Browser.newPage.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.8
@ -295,20 +290,6 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
### option: Browser.newPage.storageStatePath = %%-csharp-java-context-option-storage-state-path-%%
* since: v1.9
## async method: Browser.removeAllListeners
* since: v1.47
* langs: js
Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: Browser.removeAllListeners.type
* since: v1.47
- `type` ?<[string]>
### option: Browser.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
* since: v1.47
## async method: Browser.startTracing
* since: v1.11
* langs: java, js, python
@ -331,7 +312,7 @@ await browser.stopTracing();
```java
browser.startTracing(page, new Browser.StartTracingOptions()
.setPath(Paths.get("trace.json")));
page.navigate("https://www.google.com");
page.goto('https://www.google.com');
browser.stopTracing();
```

View file

@ -1,12 +1,13 @@
# class: BrowserContext
* since: v1.8
* extends: [EventEmitter]
BrowserContexts provide a way to operate multiple independent browser sessions.
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
context.
Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser
Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser
contexts don't write any browsing data to disk.
```js
@ -63,6 +64,7 @@ await context.CloseAsync();
## event: BrowserContext.backgroundPage
* since: v1.11
* langs: js, python
- argument: <[Page]>
:::note
@ -71,12 +73,6 @@ Only works with Chromium browser's persistent context.
Emitted when new background page is created in the context.
```java
context.onBackgroundPage(backgroundPage -> {
System.out.println(backgroundPage.url());
});
```
```js
const backgroundPage = await context.waitForEvent('backgroundpage');
```
@ -89,20 +85,6 @@ background_page = await context.wait_for_event("backgroundpage")
background_page = context.wait_for_event("backgroundpage")
```
```csharp
context.BackgroundPage += (_, backgroundPage) =>
{
Console.WriteLine(backgroundPage.Url);
};
```
## property: BrowserContext.clock
* since: v1.45
- type: <[Clock]>
Playwright has ability to mock clock and passage of time.
## event: BrowserContext.close
* since: v1.8
- argument: <[BrowserContext]>
@ -204,10 +186,7 @@ context.on("dialog", lambda dialog: dialog.accept())
```
```csharp
Context.Dialog += async (_, dialog) =>
{
await dialog.AcceptAsync();
};
context.Dialog += (_, dialog) => dialog.AcceptAsync();
```
:::note
@ -223,7 +202,7 @@ also fire for popup pages. See also [`event: Page.popup`] to receive events abou
The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a
popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is
done and its response has started loading in the popup. If you would like to route/listen to this network request, use [`method: BrowserContext.route`] and [`event: BrowserContext.request`] respectively instead of similar methods on the [Page].
done and its response has started loading in the popup.
```js
const newPagePromise = context.waitForEvent('page');
@ -356,14 +335,18 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 });
- `cookies` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `url` ?<[string]> Either url or domain / path are required. Optional.
- `domain` ?<[string]> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or domain / path are required. Optional.
- `path` ?<[string]> Either url or domain / path are required Optional.
- `url` ?<[string]> either url or domain / path are required. Optional.
- `domain` ?<[string]> either url or domain / path are required Optional.
- `path` ?<[string]> either url or domain / path are required Optional.
- `expires` ?<[float]> Unix time in seconds. Optional.
- `httpOnly` ?<[boolean]> Optional.
- `secure` ?<[boolean]> Optional.
- `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional.
Adds cookies to the browser context.
For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com".
## async method: BrowserContext.addInitScript
* since: v1.8
@ -407,7 +390,7 @@ browser_context.add_init_script(path="preload.js")
```
```csharp
await Context.AddInitScriptAsync(scriptPath: "preload.js");
await context.AddInitScriptAsync(scriptPath: "preload.js");
```
:::note
@ -455,6 +438,7 @@ Script to be evaluated in all pages in the browser context. Optional.
## method: BrowserContext.backgroundPages
* since: v1.11
* langs: js, python
- returns: <[Array]<[Page]>>
:::note
@ -472,71 +456,7 @@ Returns the browser instance of the context. If it was launched as a persistent
## async method: BrowserContext.clearCookies
* since: v1.8
Removes cookies from context. Accepts optional filter.
**Usage**
```js
await context.clearCookies();
await context.clearCookies({ name: 'session-id' });
await context.clearCookies({ domain: 'my-origin.com' });
await context.clearCookies({ domain: /.*my-origin\.com/ });
await context.clearCookies({ path: '/api/v1' });
await context.clearCookies({ name: 'session-id', domain: 'my-origin.com' });
```
```java
context.clearCookies();
context.clearCookies(new BrowserContext.ClearCookiesOptions().setName("session-id"));
context.clearCookies(new BrowserContext.ClearCookiesOptions().setDomain("my-origin.com"));
context.clearCookies(new BrowserContext.ClearCookiesOptions().setPath("/api/v1"));
context.clearCookies(new BrowserContext.ClearCookiesOptions()
.setName("session-id")
.setDomain("my-origin.com"));
```
```python async
await context.clear_cookies()
await context.clear_cookies(name="session-id")
await context.clear_cookies(domain="my-origin.com")
await context.clear_cookies(path="/api/v1")
await context.clear_cookies(name="session-id", domain="my-origin.com")
```
```python sync
context.clear_cookies()
context.clear_cookies(name="session-id")
context.clear_cookies(domain="my-origin.com")
context.clear_cookies(path="/api/v1")
context.clear_cookies(name="session-id", domain="my-origin.com")
```
```csharp
await context.ClearCookiesAsync();
await context.ClearCookiesAsync(new() { Name = "session-id" });
await context.ClearCookiesAsync(new() { Domain = "my-origin.com" });
await context.ClearCookiesAsync(new() { Path = "/api/v1" });
await context.ClearCookiesAsync(new() { Name = "session-id", Domain = "my-origin.com" });
```
### option: BrowserContext.clearCookies.name
* since: v1.43
- `name` <[string]|[RegExp]>
Only removes cookies with the given name.
### option: BrowserContext.clearCookies.domain
* since: v1.43
- `domain` <[string]|[RegExp]>
Only removes cookies with the given domain.
### option: BrowserContext.clearCookies.path
* since: v1.43
- `path` <[string]|[RegExp]>
Only removes cookies with the given path.
Clears context cookies.
## async method: BrowserContext.clearPermissions
* since: v1.8
@ -655,7 +575,7 @@ import com.microsoft.playwright.*;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType webkit = playwright.webkit();
BrowserType webkit = playwright.webkit()
Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
BrowserContext context = browser.newContext();
context.exposeBinding("pageURL", (source, args) -> source.page().url());
@ -743,6 +663,83 @@ await page.SetContentAsync("<script>\n" +
await page.GetByRole(AriaRole.Button).ClickAsync();
```
An example of passing an element handle:
```js
await context.exposeBinding('clicked', async (source, element) => {
console.log(await element.textContent());
}, { handle: true });
await page.setContent(`
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
`);
```
```java
context.exposeBinding("clicked", (source, args) -> {
ElementHandle element = (ElementHandle) args[0];
System.out.println(element.textContent());
return null;
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
page.setContent("" +
"<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
```
```python async
async def print(source, element):
print(await element.text_content())
await context.expose_binding("clicked", print, handle=true)
await page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```python sync
def print(source, element):
print(element.text_content())
context.expose_binding("clicked", print, handle=true)
page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```csharp
var result = new TaskCompletionSource<string>();
var page = await Context.NewPageAsync();
await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
{
return result.TrySetResult(await t.AsElement().TextContentAsync());
});
await page.SetContentAsync("<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
await page.ClickAsync("div");
// Note: it makes sense to await the result here, because otherwise, the context
// gets closed and the binding function will throw an exception.
Assert.AreEqual("Click me", await result.Task);
```
### param: BrowserContext.exposeBinding.name
* since: v1.8
- `name` <[string]>
@ -757,7 +754,6 @@ Callback function that will be called in the Playwright's context.
### option: BrowserContext.exposeBinding.handle
* since: v1.8
* deprecated: This option will be removed in the future.
- `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
@ -813,9 +809,8 @@ import java.util.Base64;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType webkit = playwright.webkit();
BrowserType webkit = playwright.webkit()
Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
BrowserContext context = browser.newContext();
context.exposeFunction("sha256", args -> {
String text = (String) args[0];
MessageDigest crypto;
@ -963,28 +958,22 @@ specified.
* since: v1.8
- `permissions` <[Array]<[string]>>
A list of permissions to grant.
:::danger
Supported permissions differ between browsers, and even between different versions of the same browser. Any permission may stop working after an update.
:::
Here are some permissions that may be supported by some browsers:
* `'accelerometer'`
* `'ambient-light-sensor'`
* `'background-sync'`
* `'camera'`
* `'clipboard-read'`
* `'clipboard-write'`
A permission or an array of permissions to grant. Permissions can be one of the following values:
* `'geolocation'`
* `'midi'`
* `'midi-sysex'` (system-exclusive midi)
* `'notifications'`
* `'camera'`
* `'microphone'`
* `'background-sync'`
* `'ambient-light-sensor'`
* `'accelerometer'`
* `'gyroscope'`
* `'magnetometer'`
* `'microphone'`
* `'midi-sysex'` (system-exclusive midi)
* `'midi'`
* `'notifications'`
* `'accessibility-events'`
* `'clipboard-read'`
* `'clipboard-write'`
* `'payment-handler'`
* `'storage-access'`
### option: BrowserContext.grantPermissions.origin
* since: v1.8
@ -1021,20 +1010,6 @@ Creates a new page in the browser context.
Returns all open pages in the context.
## async method: BrowserContext.removeAllListeners
* since: v1.47
* langs: js
Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: BrowserContext.removeAllListeners.type
* since: v1.47
- `type` ?<[string]>
### option: BrowserContext.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
* since: v1.47
## property: BrowserContext.request
* since: v1.16
* langs:
@ -1184,7 +1159,7 @@ context.route("/api/**", handle_route)
await page.RouteAsync("/api/**", async r =>
{
if (r.Request.PostData.Contains("my-string"))
await r.FulfillAsync(new() { Body = "mocked-data" });
await r.FulfillAsync(body: "mocked-data");
else
await r.ContinueAsync();
});
@ -1204,7 +1179,7 @@ Enabling routing disables http cache.
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
When a [`option: Browser.newContext.baseURL`] via the context options was provided and the passed URL is a path,
When a [`option: baseURL`] via the context options was provided and the passed URL is a path,
it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
### param: BrowserContext.route.handler
@ -1272,99 +1247,6 @@ When set to `minimal`, only record information necessary for routing from HAR. T
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
## async method: BrowserContext.routeWebSocket
* since: v1.48
This method allows to modify websocket connections that are made by any page in the browser context.
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before creating any pages.
**Usage**
Below is an example of a simple handler that blocks some websocket messages.
See [WebSocketRoute] for more details and examples.
```js
await context.routeWebSocket('/ws', async ws => {
ws.routeSend(message => {
if (message === 'to-be-blocked')
return;
ws.send(message);
});
await ws.connect();
});
```
```java
context.routeWebSocket("/ws", ws -> {
ws.routeSend(message -> {
if ("to-be-blocked".equals(message))
return;
ws.send(message);
});
ws.connect();
});
```
```python async
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
async def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
await ws.connect()
await context.route_web_socket("/ws", handler)
```
```python sync
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
ws.connect()
context.route_web_socket("/ws", handler)
```
```csharp
await context.RouteWebSocketAsync("/ws", async ws => {
ws.RouteSend(message => {
if (message == "to-be-blocked")
return;
ws.Send(message);
});
await ws.ConnectAsync();
});
```
### param: BrowserContext.routeWebSocket.url
* since: v1.48
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: Browser.newContext.baseURL`] context option.
### param: BrowserContext.routeWebSocket.handler
* since: v1.48
* langs: js, python
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
Handler function to route the WebSocket.
### param: BrowserContext.routeWebSocket.handler
* since: v1.48
* langs: csharp, java
- `handler` <[function]\([WebSocketRoute]\)>
Handler function to route the WebSocket.
## method: BrowserContext.serviceWorkers
* since: v1.11
* langs: js, python
@ -1412,7 +1294,7 @@ This setting will change the default maximum time for all the methods accepting
* since: v1.8
- `timeout` <[float]>
Maximum time in milliseconds. Pass `0` to disable timeout.
Maximum time in milliseconds
## async method: BrowserContext.setExtraHTTPHeaders
* since: v1.8
@ -1511,9 +1393,8 @@ Whether to emulate network being offline for the browser context.
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `indexedDB` <[Array]<[unknown]>>
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
Returns storage state for this browser context, contains current cookies and local storage snapshot.
## async method: BrowserContext.storageState
* since: v1.8
@ -1523,17 +1404,6 @@ Returns storage state for this browser context, contains current cookies, local
### option: BrowserContext.storageState.path = %%-storagestate-option-path-%%
* since: v1.8
### option: BrowserContext.storageState.indexedDB
* since: v1.51
- `indexedDB` ?<boolean>
Set to `true` to include IndexedDB in the storage state snapshot.
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this.
:::note
IndexedDBs with typed arrays are currently not supported.
:::
## property: BrowserContext.tracing
* since: v1.12
- type: <[Tracing]>

View file

@ -31,5 +31,3 @@ Browser websocket url.
Browser websocket endpoint which can be used as an argument to [`method: BrowserType.connect`] to establish connection
to the browser.
Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway, even if the actual listening address is an unspecified address.

View file

@ -89,17 +89,13 @@ class BrowserTypeExamples
* since: v1.8
- returns: <[Browser]>
This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
:::note
The major and minor version of the Playwright instance that connects needs to match the version of Playwright that launches the browser (1.2.3 → is compatible with 1.2.x).
:::
This method attaches Playwright to an existing browser instance. When connecting to another browser launched via `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is compatible with 1.2.x).
### param: BrowserType.connect.wsEndpoint
* since: v1.10
- `wsEndpoint` <[string]>
A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`.
A browser websocket endpoint to connect to.
### option: BrowserType.connect.headers
* since: v1.11
@ -156,10 +152,6 @@ The default browser context is accessible via [`method: Browser.contexts`].
Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
:::
:::note
This connection is significantly lower fidelity than the Playwright protocol connection via [`method: BrowserType.connect`]. If you are experiencing issues or attempting to use advanced functionality, you probably want to use [`method: BrowserType.connect`].
:::
**Usage**
```js
@ -351,9 +343,6 @@ use a temporary directory instead.
### option: BrowserType.launchPersistentContext.firefoxUserPrefs2 = %%-csharp-java-browser-option-firefoxuserprefs-%%
* since: v1.40
### option: BrowserType.launchPersistentContext.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
## async method: BrowserType.launchServer
* since: v1.8
* langs: js
@ -391,12 +380,6 @@ const { chromium } = require('playwright'); // Or 'webkit' or 'firefox'.
### option: BrowserType.launchServer.logger = %%-browser-option-logger-%%
* since: v1.8
### option: BrowserType.launchServer.host
* since: v1.45
- `host` <[string]>
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
### option: BrowserType.launchServer.port
* since: v1.8
- `port` <[int]>

View file

@ -1,5 +1,6 @@
# class: CDPSession
* since: v1.8
* extends: [EventEmitter]
The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
* protocol methods can be called with `session.send` method.
@ -29,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
})
```
@ -40,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

@ -1,342 +0,0 @@
# class: Clock
* since: v1.45
Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn more about [clock emulation](../clock.md).
Note that clock is installed for the entire [BrowserContext], so the time
in all the pages and iframes is controlled by the same clock.
## async method: Clock.fastForward
* since: v1.45
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
reopening it later, after given time.
**Usage**
```js
await page.clock.fastForward(1000);
await page.clock.fastForward('30:00');
```
```python async
await page.clock.fast_forward(1000)
await page.clock.fast_forward("30:00")
```
```python sync
page.clock.fast_forward(1000)
page.clock.fast_forward("30:00")
```
```java
page.clock().fastForward(1000);
page.clock().fastForward("30:00");
```
```csharp
await page.Clock.FastForwardAsync(1000);
await page.Clock.FastForwardAsync("30:00");
```
### param: Clock.fastForward.ticks
* since: v1.45
- `ticks` <[long]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
## async method: Clock.install
* since: v1.45
Install fake implementations for the following time-related functions:
* `Date`
* `setTimeout`
* `clearTimeout`
* `setInterval`
* `clearInterval`
* `requestAnimationFrame`
* `cancelAnimationFrame`
* `requestIdleCallback`
* `cancelIdleCallback`
* `performance`
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information.
### option: Clock.install.time
* langs: js, java
* since: v1.45
- `time` <[long]|[string]|[Date]>
Time to initialize with, current system time by default.
### option: Clock.install.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to initialize with, current system time by default.
### option: Clock.install.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to initialize with, current system time by default.
## async method: Clock.runFor
* since: v1.45
Advance the clock, firing all the time-related callbacks.
**Usage**
```js
await page.clock.runFor(1000);
await page.clock.runFor('30:00');
```
```python async
await page.clock.run_for(1000);
await page.clock.run_for("30:00")
```
```python sync
page.clock.run_for(1000);
page.clock.run_for("30:00")
```
```java
page.clock().runFor(1000);
page.clock().runFor("30:00");
```
```csharp
await page.Clock.RunForAsync(1000);
await page.Clock.RunForAsync("30:00");
```
### param: Clock.runFor.ticks
* since: v1.45
- `ticks` <[long]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
## async method: Clock.pauseAt
* since: v1.45
Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers
are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.pauseAt`] or [`method: Clock.resume`] is called.
Only fires due timers at most once.
This is equivalent to user closing the laptop lid for a while and reopening it at the specified time and
pausing.
**Usage**
```js
await page.clock.pauseAt(new Date('2020-02-02'));
await page.clock.pauseAt('2020-02-02');
```
```python async
await page.clock.pause_at(datetime.datetime(2020, 2, 2))
await page.clock.pause_at("2020-02-02")
```
```python sync
page.clock.pause_at(datetime.datetime(2020, 2, 2))
page.clock.pause_at("2020-02-02")
```
```java
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
page.clock().pauseAt(format.parse("2020-02-02"));
page.clock().pauseAt("2020-02-02");
```
```csharp
await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
await page.Clock.PauseAtAsync("2020-02-02");
```
For best results, install the clock before navigating the page and set it to a time slightly before the intended test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the page has fully loaded, you can safely use [`method: Clock.pauseAt`] to pause the clock.
```js
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
await page.goto('http://localhost:3333');
await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
```
```python async
# Initialize clock with some time before the test time and let the page load
# naturally. `Date.now` will progress as the timers fire.
await page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
await page.goto("http://localhost:3333")
await page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
```
```python sync
# Initialize clock with some time before the test time and let the page load
# naturally. `Date.now` will progress as the timers fire.
page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
page.goto("http://localhost:3333")
page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
```
```java
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
page.navigate("http://localhost:3333");
page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
```
### param: Clock.pauseAt.time
* langs: js, java
* since: v1.45
- `time` <[long]|[string]|[Date]>
Time to pause at.
### param: Clock.pauseAt.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to pause at.
### param: Clock.pauseAt.time
* langs: csharp
* since: v1.45
- `time` <[Date]|[string]>
Time to pause at.
## async method: Clock.resume
* since: v1.45
Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
## async method: Clock.setFixedTime
* since: v1.45
Makes `Date.now` and `new Date()` return fixed fake time at all times,
keeps all the timers running.
Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios, use [`method: Clock.install`] instead. Read docs on [clock emulation](../clock.md) to learn more.
**Usage**
```js
await page.clock.setFixedTime(Date.now());
await page.clock.setFixedTime(new Date('2020-02-02'));
await page.clock.setFixedTime('2020-02-02');
```
```python async
await page.clock.set_fixed_time(datetime.datetime.now())
await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
await page.clock.set_fixed_time("2020-02-02")
```
```python sync
page.clock.set_fixed_time(datetime.datetime.now())
page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
page.clock.set_fixed_time("2020-02-02")
```
```java
page.clock().setFixedTime(new Date());
page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02");
```
```csharp
await page.Clock.SetFixedTimeAsync(DateTime.Now);
await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
await page.Clock.SetFixedTimeAsync("2020-02-02");
```
### param: Clock.setFixedTime.time
* langs: js, java
* since: v1.45
- `time` <[long]|[string]|[Date]>
Time to be set in milliseconds.
### param: Clock.setFixedTime.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to be set.
### param: Clock.setFixedTime.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to be set.
## async method: Clock.setSystemTime
* since: v1.45
Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example switching from summer to winter time, or changing time zones.
**Usage**
```js
await page.clock.setSystemTime(Date.now());
await page.clock.setSystemTime(new Date('2020-02-02'));
await page.clock.setSystemTime('2020-02-02');
```
```python async
await page.clock.set_system_time(datetime.datetime.now())
await page.clock.set_system_time(datetime.datetime(2020, 2, 2))
await page.clock.set_system_time("2020-02-02")
```
```python sync
page.clock.set_system_time(datetime.datetime.now())
page.clock.set_system_time(datetime.datetime(2020, 2, 2))
page.clock.set_system_time("2020-02-02")
```
```java
page.clock().setSystemTime(new Date());
page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setSystemTime("2020-02-02");
```
```csharp
await page.Clock.SetSystemTimeAsync(DateTime.Now);
await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
await page.Clock.SetSystemTimeAsync("2020-02-02");
```
### param: Clock.setSystemTime.time
* langs: js, java
* since: v1.45
- `time` <[long]|[string]|[Date]>
Time to be set in milliseconds.
### param: Clock.setSystemTime.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to be set.
### param: Clock.setSystemTime.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to be set.

View file

@ -2,7 +2,7 @@
* since: v1.8
[ConsoleMessage] objects are dispatched by page via the [`event: Page.console`] event.
For each console message logged in the page there will be corresponding event in the Playwright
For each console messages logged in the page there will be corresponding event in the Playwright
context.
```js
@ -44,8 +44,8 @@ ConsoleMessage msg = page.waitForConsoleMessage(() -> {
});
// Deconstruct console.log arguments
msg.args().get(0).jsonValue(); // hello
msg.args().get(1).jsonValue(); // 42
msg.args().get(0).jsonValue() // hello
msg.args().get(1).jsonValue() // 42
```
```python async

View file

@ -4,7 +4,7 @@
ElementHandle represents an in-page DOM element. ElementHandles can be created with the [`method: Page.querySelector`] method.
:::warning[Discouraged]
:::caution Discouraged
The use of ElementHandle is discouraged, use [Locator] objects and web-first assertions instead.
:::
@ -164,6 +164,7 @@ This method checks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked. If not, this method throws.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -177,7 +178,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.check.force = %%-input-force-%%
* since: v1.8
### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.check.timeout = %%-input-timeout-%%
@ -250,6 +251,8 @@ This method double clicks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that
if the first click of the `dblclick()` triggers a navigation event, this method will throw.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -275,7 +278,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.dblclick.force = %%-input-force-%%
* since: v1.8
### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.dblclick.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.dblclick.timeout = %%-input-timeout-%%
@ -534,7 +537,7 @@ Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
### option: ElementHandle.fill.force = %%-input-force-%%
* since: v1.13
### option: ElementHandle.fill.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.fill.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.fill.timeout = %%-input-timeout-%%
@ -570,6 +573,7 @@ This method hovers over the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to hover over the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -594,7 +598,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.hover.trial = %%-input-trial-%%
* since: v1.11
### option: ElementHandle.hover.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.hover.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.28
## async method: ElementHandle.innerHTML
@ -688,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`, `ControlOrMeta`.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -785,8 +789,6 @@ completely visible as defined by
Throws when `elementHandle` does not point to an element
[connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
See [scrolling](../input.md#scrolling) for alternative ways to scroll.
### option: ElementHandle.scrollIntoViewIfNeeded.timeout = %%-input-timeout-%%
* since: v1.8
@ -866,7 +868,7 @@ await handle.SelectOptionAsync(new[] {
### option: ElementHandle.selectOption.force = %%-input-force-%%
* since: v1.13
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%
@ -916,6 +918,7 @@ This method checks or unchecks an element by performing the following steps:
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked or unchecked. If not, this method throws.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -927,7 +930,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.setChecked.force = %%-input-force-%%
* since: v1.15
### option: ElementHandle.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.setChecked.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.15
### option: ElementHandle.setChecked.position = %%-input-position-%%
@ -948,7 +951,6 @@ When all steps combined have not finished during the specified [`option: timeout
Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they
are resolved relative to the current working directory. For empty array, clears the selected files.
For inputs with a `[webkitdirectory]` attribute, only a single directory path is supported.
This method expects [ElementHandle] to point to an
[input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). However, if the element is inside the `<label>` element that has an associated [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), targets the control instead.
@ -956,7 +958,7 @@ This method expects [ElementHandle] to point to an
### param: ElementHandle.setInputFiles.files = %%-input-files-%%
* since: v1.8
### option: ElementHandle.setInputFiles.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.setInputFiles.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.setInputFiles.timeout = %%-input-timeout-%%
@ -973,6 +975,7 @@ This method taps the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.touchscreen`] to tap the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -992,7 +995,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.tap.force = %%-input-force-%%
* since: v1.8
### option: ElementHandle.tap.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.tap.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.tap.timeout = %%-input-timeout-%%
@ -1033,7 +1036,7 @@ A text to type into a focused element.
Time to wait between key presses in milliseconds. Defaults to 0.
### option: ElementHandle.type.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.type.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.type.timeout = %%-input-timeout-%%
@ -1052,6 +1055,7 @@ This method checks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now unchecked. If not, this method throws.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -1065,7 +1069,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: ElementHandle.uncheck.force = %%-input-force-%%
* since: v1.8
### option: ElementHandle.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: ElementHandle.uncheck.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: ElementHandle.uncheck.timeout = %%-input-timeout-%%

View file

@ -6,6 +6,9 @@
Error is raised whenever certain operations are terminated abnormally, e.g.
browser closes while [`method: Page.evaluate`] is running. All Playwright exceptions
inherit from this class.
- [error.message](./class-error.md#errormessage)
- [error.name](./class-error.md#errorname)
- [error.stack](./class-error.md#errorstack)
## property: Error.message
* since: v1.11

View file

@ -65,7 +65,7 @@ they are resolved relative to the current working directory. For empty array, cl
### param: FileChooser.setFiles.files = %%-input-files-%%
* since: v1.8
### option: FileChooser.setFiles.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: FileChooser.setFiles.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: FileChooser.setFiles.timeout = %%-input-timeout-%%

View file

@ -6,7 +6,7 @@ The [FormData] is used create form data that is sent via [APIRequestContext].
```java
import com.microsoft.playwright.options.FormData;
// ...
...
FormData form = FormData.create()
.set("firstName", "John")
.set("lastName", "Doe")
@ -14,77 +14,6 @@ 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
@ -100,14 +29,14 @@ Sets a field on the form. File values can be passed either as `Path` or as `File
```java
import com.microsoft.playwright.options.FormData;
// ...
...
FormData form = FormData.create()
// Only name and value are set.
.set("firstName", "John")
// 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));
```
@ -123,7 +52,6 @@ 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

@ -154,7 +154,7 @@ Raw JavaScript content to be injected into frame.
* since: v1.8
- `type` <[string]>
Script type. Use 'module' in order to load a JavaScript ES6 module. See
Script type. Use 'module' in order to load a Javascript ES6 module. See
[script](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) for more details.
## async method: Frame.addStyleTag
@ -198,6 +198,7 @@ This method checks an element matching [`param: selector`] by performing the fol
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked. If not, this method throws.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -209,7 +210,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.check.force = %%-input-force-%%
* since: v1.8
### option: Frame.check.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.check.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.check.position = %%-input-position-%%
@ -280,7 +281,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.click.timeout = %%-input-timeout-js-%%
* since: v1.8
### option: Frame.click.trial = %%-input-trial-with-modifiers-%%
### option: Frame.click.trial = %%-input-trial-%%
* since: v1.11
## async method: Frame.content
@ -302,6 +303,7 @@ This method double clicks an element matching [`param: selector`] by performing
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that
if the first click of the `dblclick()` triggers a navigation event, this method will throw.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -326,7 +328,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.dblclick.modifiers = %%-input-modifiers-%%
* since: v1.8
### option: Frame.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.dblclick.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.dblclick.position = %%-input-position-%%
@ -341,7 +343,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.dblclick.timeout = %%-input-timeout-js-%%
* since: v1.8
### option: Frame.dblclick.trial = %%-input-trial-with-modifiers-%%
### option: Frame.dblclick.trial = %%-input-trial-%%
* since: v1.11
## async method: Frame.dispatchEvent
@ -461,7 +463,7 @@ Optional event-specific initialization properties.
### option: Frame.dragAndDrop.force = %%-input-force-%%
* since: v1.13
### option: Frame.dragAndDrop.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.dragAndDrop.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.13
### option: Frame.dragAndDrop.strict = %%-input-strict-%%
@ -854,7 +856,7 @@ Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
### option: Frame.fill.force = %%-input-force-%%
* since: v1.13
### option: Frame.fill.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.fill.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.fill.strict = %%-input-strict-%%
@ -1028,8 +1030,7 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Frame.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### param: Frame.getByRole.role = %%-locator-get-by-role-role-%%
### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -1128,6 +1129,7 @@ This method hovers over an element matching [`param: selector`] by performing th
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to hover over the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
[TimeoutError]. Passing zero timeout disables this.
@ -1153,10 +1155,10 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.hover.timeout = %%-input-timeout-js-%%
* since: v1.8
### option: Frame.hover.trial = %%-input-trial-with-modifiers-%%
### option: Frame.hover.trial = %%-input-trial-%%
* since: v1.11
### option: Frame.hover.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.hover.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.28
## async method: Frame.innerHTML
@ -1304,7 +1306,7 @@ Returns whether the element is [enabled](../actionability.md#enabled).
* discouraged: Use locator-based [`method: Locator.isHidden`] instead. Read more about [locators](../locators.md).
- returns: <[boolean]>
Returns whether the element is hidden, the opposite of [visible](../actionability.md#visible). [`param: selector`] that does not match any elements is considered hidden.
Returns whether the element is hidden, the opposite of [visible](../actionability.md#visible). [`option: selector`] that does not match any elements is considered hidden.
### param: Frame.isHidden.selector = %%-input-selector-%%
* since: v1.8
@ -1322,7 +1324,7 @@ Returns whether the element is hidden, the opposite of [visible](../actionabilit
* discouraged: Use locator-based [`method: Locator.isVisible`] instead. Read more about [locators](../locators.md).
- returns: <[boolean]>
Returns whether the element is [visible](../actionability.md#visible). [`param: selector`] that does not match any elements is considered not visible.
Returns whether the element is [visible](../actionability.md#visible). [`option: selector`] that does not match any elements is considered not visible.
### param: Frame.isVisible.selector = %%-input-selector-%%
* since: v1.8
@ -1391,8 +1393,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`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -1543,7 +1544,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
### option: Frame.selectOption.force = %%-input-force-%%
* since: v1.13
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.selectOption.strict = %%-input-strict-%%
@ -1580,6 +1581,7 @@ This method checks or unchecks an element matching [`param: selector`] by perfor
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked or unchecked. If not, this method throws.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -1594,7 +1596,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.setChecked.force = %%-input-force-%%
* since: v1.15
### option: Frame.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.setChecked.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.15
### option: Frame.setChecked.position = %%-input-position-%%
@ -1648,7 +1650,7 @@ This method expects [`param: selector`] to point to an
### param: Frame.setInputFiles.files = %%-input-files-%%
* since: v1.8
### option: Frame.setInputFiles.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.setInputFiles.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.setInputFiles.strict = %%-input-strict-%%
@ -1671,6 +1673,7 @@ This method taps an element matching [`param: selector`] by performing the follo
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.touchscreen`] to tap the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
[TimeoutError]. Passing zero timeout disables this.
@ -1688,7 +1691,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.tap.modifiers = %%-input-modifiers-%%
* since: v1.8
### option: Frame.tap.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.tap.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.tap.position = %%-input-position-%%
@ -1703,7 +1706,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.tap.timeout = %%-input-timeout-js-%%
* since: v1.8
### option: Frame.tap.trial = %%-input-trial-with-modifiers-%%
### option: Frame.tap.trial = %%-input-trial-%%
* since: v1.11
## async method: Frame.textContent
@ -1757,7 +1760,7 @@ A text to type into a focused element.
Time to wait between key presses in milliseconds. Defaults to 0.
### option: Frame.type.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.type.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.type.strict = %%-input-strict-%%
@ -1782,6 +1785,7 @@ This method checks an element matching [`param: selector`] by performing the fol
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now unchecked. If not, this method throws.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -1793,7 +1797,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.uncheck.force = %%-input-force-%%
* since: v1.8
### option: Frame.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Frame.uncheck.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.8
### option: Frame.uncheck.position = %%-input-position-%%
@ -1966,10 +1970,6 @@ 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

@ -1,30 +1,30 @@
# class: FrameLocator
* since: v1.17
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Locator.contentFrame`], [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
```js
const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
const locator = page.frameLocator('#my-frame').getByText('Submit');
await locator.click();
```
```java
Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
Locator locator = page.frameLocator("#my-frame").getByText("Submit");
locator.click();
```
```python async
locator = page.locator("#my-frame").content_frame.get_by_text("Submit")
locator = page.frame_locator("#my-frame").get_by_text("Submit")
await locator.click()
```
```python sync
locator = page.locator("my-frame").content_frame.get_by_text("Submit")
locator = page.frame_locator("my-frame").get_by_text("Submit")
locator.click()
```
```csharp
var locator = page.Locator("#my-frame").ContentFrame.GetByText("Submit");
var locator = page.FrameLocator("#my-frame").GetByText("Submit");
await locator.ClickAsync();
```
@ -34,55 +34,69 @@ Frame locators are strict. This means that all operations on frame locators will
```js
// Throws if there are several frames in DOM:
await page.locator('.result-frame').contentFrame().getByRole('button').click();
await page.frameLocator('.result-frame').getByRole('button').click();
// Works because we explicitly tell locator to pick the first frame:
await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
await page.frameLocator('.result-frame').first().getByRole('button').click();
```
```python async
# Throws if there are several frames in DOM:
await page.locator('.result-frame').content_frame.get_by_role('button').click()
await page.frame_locator('.result-frame').get_by_role('button').click()
# Works because we explicitly tell locator to pick the first frame:
await page.locator('.result-frame').first.content_frame.get_by_role('button').click()
await page.frame_locator('.result-frame').first.get_by_role('button').click()
```
```python sync
# Throws if there are several frames in DOM:
page.locator('.result-frame').content_frame.get_by_role('button').click()
page.frame_locator('.result-frame').get_by_role('button').click()
# Works because we explicitly tell locator to pick the first frame:
page.locator('.result-frame').first.content_frame.get_by_role('button').click()
page.frame_locator('.result-frame').first.get_by_role('button').click()
```
```java
// Throws if there are several frames in DOM:
page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click();
// Works because we explicitly tell locator to pick the first frame:
page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click();
```
```csharp
// Throws if there are several frames in DOM:
await page.Locator(".result-frame").ContentFrame.GetByRole(AriaRole.Button).ClickAsync();
await page.FrameLocator(".result-frame").GetByRole(AriaRole.Button).ClickAsync();
// Works because we explicitly tell locator to pick the first frame:
await page.Locator(".result-frame").First.ContentFrame.getByRole(AriaRole.Button).ClickAsync();
await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickAsync();
```
**Converting Locator to FrameLocator**
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.contentFrame`].
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector:
**Converting FrameLocator to Locator**
```js
const frameLocator = locator.frameLocator(':scope');
```
If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.owner`].
```java
Locator frameLocator = locator.frameLocator(':scope');
```
```python async
frameLocator = locator.frame_locator(":scope")
```
```python sync
frameLocator = locator.frame_locator(":scope")
```
```csharp
var frameLocator = locator.FrameLocator(":scope");
```
## method: FrameLocator.first
* deprecated: Use [`method: Locator.first`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>
@ -134,8 +148,7 @@ in that iframe.
%%-template-locator-get-by-role-%%
### param: FrameLocator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%%
### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -172,7 +185,6 @@ in that iframe.
### option: FrameLocator.getByTitle.exact = %%-locator-get-by-text-exact-%%
## method: FrameLocator.last
* deprecated: Use [`method: Locator.last`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>
@ -197,7 +209,6 @@ Returns locator to the last matching frame.
* since: v1.33
## method: FrameLocator.nth
* deprecated: Use [`method: Locator.nth`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>
@ -206,50 +217,3 @@ Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects th
### param: FrameLocator.nth.index
* since: v1.17
- `index` <[int]>
## method: FrameLocator.owner
* since: v1.43
- returns: <[Locator]>
Returns a [Locator] object pointing to the same `iframe` as this frame locator.
Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element.
For a reverse operation, use [`method: Locator.contentFrame`].
**Usage**
```js
const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
// ...
const locator = frameLocator.owner();
await expect(locator).toBeVisible();
```
```java
FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
// ...
Locator locator = frameLocator.owner();
assertThat(locator).isVisible();
```
```python async
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ...
locator = frame_locator.owner
await expect(locator).to_be_visible()
```
```python sync
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ...
locator = frame_locator.owner
expect(locator).to_be_visible()
```
```csharp
var frameLocator = Page.Locator("iframe[name=\"embedded\"]").ContentFrame;
// ...
var locator = frameLocator.Owner;
await Expect(locator).ToBeVisibleAsync();
```

View file

@ -104,23 +104,38 @@ await page.Keyboard.PressAsync("Shift+A");
An example to trigger select-all with the keyboard
```js
await page.keyboard.press('ControlOrMeta+A');
// on Windows and Linux
await page.keyboard.press('Control+A');
// on macOS
await page.keyboard.press('Meta+A');
```
```java
page.keyboard().press("ControlOrMeta+A");
// on Windows and Linux
page.keyboard().press("Control+A");
// on macOS
page.keyboard().press("Meta+A");
```
```python async
await page.keyboard.press("ControlOrMeta+A")
# on windows and linux
await page.keyboard.press("Control+A")
# on mac_os
await page.keyboard.press("Meta+A")
```
```python sync
page.keyboard.press("ControlOrMeta+A")
# on windows and linux
page.keyboard.press("Control+A")
# on mac_os
page.keyboard.press("Meta+A")
```
```csharp
await page.Keyboard.PressAsync("ControlOrMeta+A");
// on Windows and Linux
await page.Keyboard.PressAsync("Control+A");
// on macOS
await page.Keyboard.PressAsync("Meta+A");
```
## async method: Keyboard.down
@ -136,8 +151,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`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -213,8 +227,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`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -242,7 +255,7 @@ await browser.close();
Page page = browser.newPage();
page.navigate("https://keycode.info");
page.keyboard().press("A");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")));
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
page.keyboard().press("ArrowLeft");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
page.keyboard().press("Shift+O");

View file

@ -38,7 +38,7 @@ for li in page.get_by_role('listitem').all():
```
```java
for (Locator li : page.getByRole("listitem").all())
for (Locator li : page.getByRole('listitem').all())
li.click();
```
@ -53,8 +53,8 @@ foreach (var li in await page.GetByRole("listitem").AllAsync())
Returns an array of `node.innerText` values for all matching nodes.
:::warning[Asserting text]
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: LocatorAssertions.toHaveText.useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::caution Asserting text
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
**Usage**
@ -85,7 +85,7 @@ var texts = await page.GetByRole(AriaRole.Link).AllInnerTextsAsync();
Returns an array of `node.textContent` values for all matching nodes.
:::warning[Asserting text]
:::caution Asserting text
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -150,67 +150,6 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
Additional locator to match.
## async method: Locator.ariaSnapshot
* since: v1.49
- returns: <[string]>
Captures the aria snapshot of the given element.
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
**Usage**
```js
await page.getByRole('link').ariaSnapshot();
```
```java
page.getByRole(AriaRole.LINK).ariaSnapshot();
```
```python async
await page.get_by_role("link").aria_snapshot()
```
```python sync
page.get_by_role("link").aria_snapshot()
```
```csharp
await page.GetByRole(AriaRole.Link).AriaSnapshotAsync();
```
**Details**
This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the element and its children.
The snapshot can be used to assert the state of the element in the test, or to compare it to state in the future.
The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
* The keys of the objects are the roles and optional accessible names of the elements.
* The values are either text content or an array of child elements.
* Generic static text can be represented with the `text` key.
Below is the HTML markup and the respective ARIA snapshot:
```html
<ul aria-label="Links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<ul>
```
```yml
- list "Links":
- listitem:
- link "Home"
- listitem:
- link "About"
```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
* since: v1.49
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49
## async method: Locator.blur
* since: v1.28
@ -292,6 +231,7 @@ Performs the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked. If not, this method throws.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -327,7 +267,7 @@ await page.GetByRole(AriaRole.Checkbox).CheckAsync();
### option: Locator.check.force = %%-input-force-%%
* since: v1.14
### option: Locator.check.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.check.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.check.timeout = %%-input-timeout-%%
@ -377,7 +317,7 @@ await page.GetByRole(AriaRole.Textbox).ClearAsync();
### option: Locator.clear.force = %%-input-force-%%
* since: v1.28
### option: Locator.clear.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.clear.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.28
### option: Locator.clear.timeout = %%-input-timeout-%%
@ -494,7 +434,7 @@ await page.Locator("canvas").ClickAsync(new() {
### option: Locator.click.timeout = %%-input-timeout-js-%%
* since: v1.14
### option: Locator.click.trial = %%-input-trial-with-modifiers-%%
### option: Locator.click.trial = %%-input-trial-%%
* since: v1.14
## async method: Locator.count
@ -503,7 +443,7 @@ await page.Locator("canvas").ClickAsync(new() {
Returns the number of elements matching the locator.
:::warning[Asserting count]
:::caution Asserting count
If you need to assert the number of elements on the page, prefer [`method: LocatorAssertions.toHaveCount`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -543,6 +483,8 @@ This method double clicks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set. Note that
if the first click of the `dblclick()` triggers a navigation event, this method will throw.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -568,7 +510,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.dblclick.force = %%-input-force-%%
* since: v1.14
### option: Locator.dblclick.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.dblclick.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.dblclick.timeout = %%-input-timeout-%%
@ -577,7 +519,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.dblclick.timeout = %%-input-timeout-js-%%
* since: v1.14
### option: Locator.dblclick.trial = %%-input-trial-with-modifiers-%%
### option: Locator.dblclick.trial = %%-input-trial-%%
* since: v1.14
## async method: Locator.dispatchEvent
@ -633,11 +575,13 @@ properties:
You can also specify [JSHandle] as the property value if you want live objects to be passed into the event:
```js
// Note you can only create DataTransfer in Chromium and Firefox
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await locator.dispatchEvent('dragstart', { dataTransfer });
```
```java
// Note you can only create DataTransfer in Chromium and Firefox
JSHandle dataTransfer = page.evaluateHandle("() => new DataTransfer()");
Map<String, Object> arg = new HashMap<>();
arg.put("dataTransfer", dataTransfer);
@ -645,11 +589,13 @@ locator.dispatchEvent("dragstart", arg);
```
```python async
# note you can only create data_transfer in chromium and firefox
data_transfer = await page.evaluate_handle("new DataTransfer()")
await locator.dispatch_event("#source", "dragstart", {"dataTransfer": data_transfer})
```
```python sync
# note you can only create data_transfer in chromium and firefox
data_transfer = page.evaluate_handle("new DataTransfer()")
locator.dispatch_event("#source", "dragstart", {"dataTransfer": data_transfer})
```
@ -763,7 +709,7 @@ Locator of the element to drag to.
### option: Locator.dragTo.force = %%-input-force-%%
* since: v1.18
### option: Locator.dragTo.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.dragTo.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.18
### option: Locator.dragTo.timeout = %%-input-timeout-%%
@ -801,53 +747,6 @@ Resolves given locator to the first matching DOM element. If there are no matchi
Resolves given locator to all matching DOM elements. If there are no matching elements, returns an empty list.
## method: Locator.contentFrame
* since: v1.43
- returns: <[FrameLocator]>
Returns a [FrameLocator] object pointing to the same `iframe` as this locator.
Useful when you have a [Locator] object obtained somewhere, and later on would like to interact with the content inside the frame.
For a reverse operation, use [`method: FrameLocator.owner`].
**Usage**
```js
const locator = page.locator('iframe[name="embedded"]');
// ...
const frameLocator = locator.contentFrame();
await frameLocator.getByRole('button').click();
```
```java
Locator locator = page.locator("iframe[name=\"embedded\"]");
// ...
FrameLocator frameLocator = locator.contentFrame();
frameLocator.getByRole(AriaRole.BUTTON).click();
```
```python async
locator = page.locator("iframe[name=\"embedded\"]")
# ...
frame_locator = locator.content_frame
await frame_locator.get_by_role("button").click()
```
```python sync
locator = page.locator("iframe[name=\"embedded\"]")
# ...
frame_locator = locator.content_frame
frame_locator.get_by_role("button").click()
```
```csharp
var locator = Page.Locator("iframe[name=\"embedded\"]");
// ...
var frameLocator = locator.ContentFrame;
await frameLocator.GetByRole(AriaRole.Button).ClickAsync();
```
## async method: Locator.evaluate
* since: v1.14
- returns: <[Serializable]>
@ -864,6 +763,31 @@ If [`param: expression`] throws or rejects, this method throws.
**Usage**
```js
const tweets = page.locator('.tweet .retweets');
expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets');
```
```java
Locator tweets = page.locator(".tweet .retweets");
assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
```
```python async
tweets = page.locator(".tweet .retweets")
assert await tweets.evaluate("node => node.innerText") == "10 retweets"
```
```python sync
tweets = page.locator(".tweet .retweets")
assert tweets.evaluate("node => node.innerText") == "10 retweets"
```
```csharp
var tweets = page.Locator(".tweet .retweets");
Assert.AreEqual("10 retweets", await tweets.EvaluateAsync("node => node.innerText"));
```
### param: Locator.evaluate.expression = %%-evaluate-expression-%%
* since: v1.14
@ -1015,7 +939,7 @@ Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
### option: Locator.fill.force = %%-input-force-%%
* since: v1.14
### option: Locator.fill.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.fill.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.fill.timeout = %%-input-timeout-%%
@ -1090,9 +1014,6 @@ await rowLocator
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
* since: v1.33
### option: Locator.filter.visible = %%-locator-option-visible-%%
* since: v1.51
## method: Locator.first
* since: v1.14
- returns: <[Locator]>
@ -1153,7 +1074,7 @@ await locator.ClickAsync();
Returns the matching element's attribute value.
:::warning[Asserting attributes]
:::caution Asserting attributes
If you need to assert an element's attribute, prefer [`method: LocatorAssertions.toHaveAttribute`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1205,8 +1126,7 @@ Attribute name to get the value for.
%%-template-locator-get-by-role-%%
### param: Locator.getByRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.27
### param: Locator.getByRole.role = %%-locator-get-by-role-role-%%
### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
* since: v1.27
@ -1280,6 +1200,7 @@ This method hovers over the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to hover over the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -1301,10 +1222,10 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.hover.timeout = %%-input-timeout-js-%%
* since: v1.14
### option: Locator.hover.trial = %%-input-trial-with-modifiers-%%
### option: Locator.hover.trial = %%-input-trial-%%
* since: v1.14
### option: Locator.hover.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.hover.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.28
## async method: Locator.innerHTML
@ -1325,8 +1246,8 @@ Returns the [`element.innerHTML`](https://developer.mozilla.org/en-US/docs/Web/A
Returns the [`element.innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText).
:::warning[Asserting text]
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: LocatorAssertions.toHaveText.useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::caution Asserting text
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
### option: Locator.innerText.timeout = %%-input-timeout-%%
@ -1341,7 +1262,7 @@ If you need to assert text on the page, prefer [`method: LocatorAssertions.toHav
Returns the value for the matching `<input>` or `<textarea>` or `<select>` element.
:::warning[Asserting value]
:::caution Asserting value
If you need to assert input value, prefer [`method: LocatorAssertions.toHaveValue`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1383,7 +1304,7 @@ Throws elements that are not an input, textarea or a select. However, if the ele
Returns whether the element is checked. Throws if the element is not a checkbox or radio input.
:::warning[Asserting checked state]
:::caution Asserting checked state
If you need to assert that checkbox is checked, prefer [`method: LocatorAssertions.toBeChecked`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1421,7 +1342,7 @@ var isChecked = await page.GetByRole(AriaRole.Checkbox).IsCheckedAsync();
Returns whether the element is disabled, the opposite of [enabled](../actionability.md#enabled).
:::warning[Asserting disabled state]
:::caution Asserting disabled state
If you need to assert that an element is disabled, prefer [`method: LocatorAssertions.toBeDisabled`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1457,9 +1378,9 @@ Boolean disabled = await page.GetByRole(AriaRole.Button).IsDisabledAsync();
* since: v1.14
- returns: <[boolean]>
Returns whether the element is [editable](../actionability.md#editable). If the target element is not an `<input>`, `<textarea>`, `<select>`, `[contenteditable]` and does not have a role allowing `[aria-readonly]`, this method throws an error.
Returns whether the element is [editable](../actionability.md#editable).
:::warning[Asserting editable state]
:::caution Asserting editable state
If you need to assert that an element is editable, prefer [`method: LocatorAssertions.toBeEditable`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1497,7 +1418,7 @@ Boolean editable = await page.GetByRole(AriaRole.Textbox).IsEditableAsync();
Returns whether the element is [enabled](../actionability.md#enabled).
:::warning[Asserting enabled state]
:::caution Asserting enabled state
If you need to assert that an element is enabled, prefer [`method: LocatorAssertions.toBeEnabled`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1535,7 +1456,7 @@ Boolean enabled = await page.GetByRole(AriaRole.Button).IsEnabledAsync();
Returns whether the element is hidden, the opposite of [visible](../actionability.md#visible).
:::warning[Asserting visibility]
:::caution Asserting visibility
If you need to assert that element is hidden, prefer [`method: LocatorAssertions.toBeHidden`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1572,7 +1493,7 @@ Boolean hidden = await page.GetByRole(AriaRole.Button).IsHiddenAsync();
Returns whether the element is [visible](../actionability.md#visible).
:::warning[Asserting visibility]
:::caution Asserting visibility
If you need to assert that element is visible, prefer [`method: LocatorAssertions.toBeVisible`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -1689,23 +1610,16 @@ var banana = await page.GetByRole(AriaRole.Listitem).Nth(2);
- alias-python: or_
- returns: <[Locator]>
Creates a locator matching all elements that match one or both of the two locators.
Note that when both locators match something, the resulting locator will have multiple matches, potentially causing a [locator strictness](../locators.md#strictness) violation.
Creates a locator that matches either of the two locators.
**Usage**
Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly.
:::note
If both "New email" button and security dialog appear on screen, the "or" locator will match both of them,
possibly throwing the ["strict mode violation" error](../locators.md#strictness). In this case, you can use [`method: Locator.first`] to only match one of them.
:::
```js
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
await expect(newEmail.or(dialog)).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
@ -1714,7 +1628,7 @@ await newEmail.click();
```java
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
Locator dialog = page.getByText("Confirm security settings");
assertThat(newEmail.or(dialog).first()).isVisible();
assertThat(newEmail.or(dialog)).isVisible();
if (dialog.isVisible())
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
newEmail.click();
@ -1723,7 +1637,7 @@ newEmail.click();
```python async
new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings")
await expect(new_email.or_(dialog).first).to_be_visible()
await expect(new_email.or_(dialog)).to_be_visible()
if (await dialog.is_visible()):
await page.get_by_role("button", name="Dismiss").click()
await new_email.click()
@ -1732,7 +1646,7 @@ await new_email.click()
```python sync
new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings")
expect(new_email.or_(dialog).first).to_be_visible()
expect(new_email.or_(dialog)).to_be_visible()
if (dialog.is_visible()):
page.get_by_role("button", name="Dismiss").click()
new_email.click()
@ -1741,7 +1655,7 @@ new_email.click()
```csharp
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
await Expect(newEmail.Or(dialog)).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
await newEmail.ClickAsync();
@ -1799,8 +1713,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`, `ControlOrMeta`.
`ControlOrMeta` resolves to `Control` on Windows and Linux and to `Meta` on macOS.
Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`.
Holding down `Shift` will type the text that corresponds to the [`param: key`] in the upper case.
@ -1914,7 +1827,7 @@ String of characters to sequentially press into a focused element.
Time to wait between key presses in milliseconds. Defaults to 0.
### option: Locator.pressSequentially.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.pressSequentially.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.38
### option: Locator.pressSequentially.timeout = %%-input-timeout-%%
@ -2010,8 +1923,6 @@ This method waits for [actionability](../actionability.md) checks, then tries to
completely visible as defined by
[IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s `ratio`.
See [scrolling](../input.md#scrolling) for alternative ways to scroll.
### option: Locator.scrollIntoViewIfNeeded.timeout = %%-input-timeout-%%
* since: v1.14
@ -2038,9 +1949,9 @@ Triggers a `change` and `input` event once all the provided options have been se
```html
<select multiple>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
</select>
```
@ -2097,7 +2008,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" });
### option: Locator.selectOption.force = %%-input-force-%%
* since: v1.14
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.selectOption.timeout = %%-input-timeout-%%
@ -2171,6 +2082,7 @@ This method checks or unchecks an element by performing the following steps:
set. If the element is detached during the checks, the whole action is retried.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now checked or unchecked. If not, this method throws.
When all steps combined have not finished during the specified [`option: timeout`], this method throws a
@ -2182,7 +2094,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.setChecked.force = %%-input-force-%%
* since: v1.15
### option: Locator.setChecked.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.setChecked.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.15
### option: Locator.setChecked.position = %%-input-position-%%
@ -2201,7 +2113,6 @@ When all steps combined have not finished during the specified [`option: timeout
* since: v1.14
Upload file or multiple files into `<input type=file>`.
For inputs with a `[webkitdirectory]` attribute, only a single directory path is supported.
**Usage**
@ -2215,9 +2126,6 @@ await page.getByLabel('Upload files').setInputFiles([
path.join(__dirname, 'file2.txt'),
]);
// Select a directory
await page.getByLabel('Upload directory').setInputFiles(path.join(__dirname, 'mydir'));
// Remove all the selected files
await page.getByLabel('Upload file').setInputFiles([]);
@ -2236,9 +2144,6 @@ page.getByLabel("Upload file").setInputFiles(Paths.get("myfile.pdf"));
// Select multiple files
page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
// Select a directory
page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
// Remove all the selected files
page.getByLabel("Upload file").setInputFiles(new Path[0]);
@ -2254,9 +2159,6 @@ await page.get_by_label("Upload file").set_input_files('myfile.pdf')
# Select multiple files
await page.get_by_label("Upload files").set_input_files(['file1.txt', 'file2.txt'])
# Select a directory
await page.get_by_label("Upload directory").set_input_files('mydir')
# Remove all the selected files
await page.get_by_label("Upload file").set_input_files([])
@ -2275,9 +2177,6 @@ page.get_by_label("Upload file").set_input_files('myfile.pdf')
# Select multiple files
page.get_by_label("Upload files").set_input_files(['file1.txt', 'file2.txt'])
# Select a directory
page.get_by_label("Upload directory").set_input_files('mydir')
# Remove all the selected files
page.get_by_label("Upload file").set_input_files([])
@ -2296,9 +2195,6 @@ await page.GetByLabel("Upload file").SetInputFilesAsync("myfile.pdf");
// Select multiple files
await page.GetByLabel("Upload files").SetInputFilesAsync(new[] { "file1.txt", "file12.txt" });
// Select a directory
await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
// Remove all the selected files
await page.GetByLabel("Upload file").SetInputFilesAsync(new[] {});
@ -2323,7 +2219,7 @@ This method expects [Locator] to point to an
### param: Locator.setInputFiles.files = %%-input-files-%%
* since: v1.14
### option: Locator.setInputFiles.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.setInputFiles.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.setInputFiles.timeout = %%-input-timeout-%%
@ -2335,7 +2231,7 @@ This method expects [Locator] to point to an
## async method: Locator.tap
* since: v1.14
Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
Perform a tap gesture on the element matching the locator.
**Details**
@ -2343,6 +2239,7 @@ This method taps the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.touchscreen`] to tap the center of the element, or the specified [`option: position`].
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -2362,7 +2259,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.tap.force = %%-input-force-%%
* since: v1.14
### option: Locator.tap.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.tap.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.tap.timeout = %%-input-timeout-%%
@ -2371,7 +2268,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.tap.timeout = %%-input-timeout-js-%%
* since: v1.14
### option: Locator.tap.trial = %%-input-trial-with-modifiers-%%
### option: Locator.tap.trial = %%-input-trial-%%
* since: v1.14
## async method: Locator.textContent
@ -2380,7 +2277,7 @@ When all steps combined have not finished during the specified [`option: timeout
Returns the [`node.textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent).
:::warning[Asserting text]
:::caution Asserting text
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
:::
@ -2412,7 +2309,7 @@ A text to type into a focused element.
Time to wait between key presses in milliseconds. Defaults to 0.
### option: Locator.type.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.type.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.type.timeout = %%-input-timeout-%%
@ -2456,6 +2353,7 @@ This method unchecks the element by performing the following steps:
1. Wait for [actionability](../actionability.md) checks on the element, unless [`option: force`] option is set.
1. Scroll the element into view if needed.
1. Use [`property: Page.mouse`] to click in the center of the element.
1. Wait for initiated navigations to either succeed or fail, unless [`option: noWaitAfter`] option is set.
1. Ensure that the element is now unchecked. If not, this method throws.
If the element is detached from the DOM at any moment during the action, this method throws.
@ -2469,7 +2367,7 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.force = %%-input-force-%%
* since: v1.14
### option: Locator.uncheck.noWaitAfter = %%-input-no-wait-after-removed-%%
### option: Locator.uncheck.noWaitAfter = %%-input-no-wait-after-%%
* since: v1.14
### option: Locator.uncheck.timeout = %%-input-timeout-%%

View file

@ -14,14 +14,14 @@ test('status becomes submitted', async ({ page }) => {
```
```java
// ...
...
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TestLocator {
// ...
...
@Test
void statusBecomesSubmitted() {
// ...
...
page.getByRole(AriaRole.BUTTON).click();
assertThat(page.locator(".status")).hasText("Submitted");
}
@ -47,19 +47,21 @@ def test_status_becomes_submitted(page: Page) -> None:
```
```csharp
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
[TestFixture]
public class ExampleTests : PageTest
{
[TestMethod]
[Test]
public async Task StatusBecomesSubmitted()
{
// ...
await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync();
// ..
await Page.GetByRole(AriaRole.Button).ClickAsync();
await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted");
}
}
@ -209,8 +211,11 @@ The opposite of [`method: LocatorAssertions.toContainText`].
Expected substring or RegExp or a list of those.
### option: LocatorAssertions.NotToContainText.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.NotToContainText.ignoreCase
* 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
@ -221,63 +226,6 @@ 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
- `description` <[string]|[RegExp]>
Expected accessible description.
### 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.NotToHaveAccessibleErrorMessage
* since: v1.50
* langs: python
The opposite of [`method: LocatorAssertions.toHaveAccessibleErrorMessage`].
### param: LocatorAssertions.NotToHaveAccessibleErrorMessage.errorMessage
* since: v1.50
- `errorMessage` <[string]|[RegExp]>
Expected accessible error message.
### option: LocatorAssertions.NotToHaveAccessibleErrorMessage.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.50
### option: LocatorAssertions.NotToHaveAccessibleErrorMessage.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50
## 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
@ -296,8 +244,11 @@ Attribute name.
Expected attribute value.
### option: LocatorAssertions.NotToHaveAttribute.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.NotToHaveAttribute.ignoreCase
* 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
@ -389,23 +340,6 @@ 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.role = %%-get-by-role-to-have-role-role-%%
* since: v1.44
### option: LocatorAssertions.NotToHaveRole.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.NotToHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44
## async method: LocatorAssertions.NotToHaveText
* since: v1.20
* langs: python
@ -418,8 +352,11 @@ The opposite of [`method: LocatorAssertions.toHaveText`].
Expected string or RegExp or a list of those.
### option: LocatorAssertions.NotToHaveText.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.NotToHaveText.ignoreCase
* 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
@ -460,23 +397,6 @@ Expected options currently selected.
### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.23
## async method: LocatorAssertions.NotToMatchAriaSnapshot
* since: v1.49
* langs: python
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
* since: v1.49
- `expected` <string>
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49
## async method: LocatorAssertions.toBeAttached
* since: v1.33
@ -559,16 +479,6 @@ await Expect(locator).ToBeCheckedAsync();
* since: v1.18
- `checked` <[boolean]>
Provides state to assert for. Asserts for input to be checked by default.
This option can't be used when [`option: LocatorAssertions.toBeChecked.indeterminate`] is set to true.
### option: LocatorAssertions.toBeChecked.indeterminate
* since: v1.50
- `indeterminate` <[boolean]>
Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons.
This option can't be true when [`option: LocatorAssertions.toBeChecked.checked`] is provided.
### option: LocatorAssertions.toBeChecked.timeout = %%-js-assertions-timeout-%%
* since: v1.18
@ -746,7 +656,7 @@ expect(locator).to_be_enabled()
```csharp
var locator = Page.Locator("button.submit");
await Expect(locator).ToBeEnabledAsync();
await Expect(locator).toBeEnabledAsync();
```
### option: LocatorAssertions.toBeEnabled.enabled
@ -953,10 +863,10 @@ await expect(
assertThat(page.getByText("Welcome")).isVisible();
// At least one item in the list is visible.
assertThat(page.getByTestId("todo-item").first()).isVisible();
asserThat(page.getByTestId("todo-item").first()).isVisible();
// At least one of the two elements is visible, possibly both.
assertThat(
asserThat(
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
.or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")))
.first()
@ -1179,8 +1089,11 @@ Expected substring or RegExp or a list of those.
Expected substring or RegExp or a list of those.
### option: LocatorAssertions.toContainText.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.toContainText.ignoreCase
* 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
@ -1194,157 +1107,6 @@ 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.toHaveAccessibleErrorMessage
* since: v1.50
* langs:
- alias-java: hasAccessibleErrorMessage
Ensures the [Locator] points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
**Usage**
```js
const locator = page.getByTestId('username-input');
await expect(locator).toHaveAccessibleErrorMessage('Username is required.');
```
```java
Locator locator = page.getByTestId("username-input");
assertThat(locator).hasAccessibleErrorMessage("Username is required.");
```
```python async
locator = page.get_by_test_id("username-input")
await expect(locator).to_have_accessible_error_message("Username is required.")
```
```python sync
locator = page.get_by_test_id("username-input")
expect(locator).to_have_accessible_error_message("Username is required.")
```
```csharp
var locator = Page.GetByTestId("username-input");
await Expect(locator).ToHaveAccessibleErrorMessageAsync("Username is required.");
```
### param: LocatorAssertions.toHaveAccessibleErrorMessage.errorMessage
* since: v1.50
- `errorMessage` <[string]|[RegExp]>
Expected accessible error message.
### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-js-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toHaveAccessibleErrorMessage.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toHaveAccessibleErrorMessage.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.50
## 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:
@ -1400,8 +1162,11 @@ Expected attribute value.
### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
### option: LocatorAssertions.toHaveAttribute.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.toHaveAttribute.ignoreCase
* 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
@ -1431,48 +1196,49 @@ Attribute name.
* langs:
- alias-java: hasClass
Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression:
Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match
or using a relaxed regular expression.
**Usage**
```html
<div class='middle selected row' id='component'></div>
<div class='selected row' id='component'></div>
```
```js
const locator = page.locator('#component');
await expect(locator).toHaveClass('middle selected row');
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
await expect(locator).toHaveClass(/selected/);
await expect(locator).toHaveClass('selected row');
```
```java
assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
assertThat(page.locator("#component")).hasClass("middle selected row");
assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
assertThat(page.locator("#component")).hasClass("selected row");
```
```python async
from playwright.async_api import expect
locator = page.locator("#component")
await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
await expect(locator).to_have_class("middle selected row")
await expect(locator).to_have_class(re.compile(r"selected"))
await expect(locator).to_have_class("selected row")
```
```python sync
from playwright.sync_api import expect
locator = page.locator("#component")
expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
expect(locator).to_have_class("middle selected row")
expect(locator).to_have_class(re.compile(r"selected"))
expect(locator).to_have_class("selected row")
```
```csharp
var locator = Page.Locator("#component");
await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
await Expect(locator).ToHaveClassAsync("middle selected row");
await Expect(locator).ToHaveClassAsync(new Regex("selected"));
await Expect(locator).ToHaveClassAsync("selected row");
```
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array:
Note that if array is passed as an expected value, entire lists of elements can be asserted:
```js
const locator = page.locator('list > .component');
@ -1738,53 +1504,6 @@ 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
@ -2050,8 +1769,11 @@ Expected string or RegExp or a list of those.
Expected string or RegExp or a list of those.
### option: LocatorAssertions.toHaveText.ignoreCase = %%-assertions-ignore-case-%%
### option: LocatorAssertions.toHaveText.ignoreCase
* 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
@ -2142,7 +1864,7 @@ await expect(locator).toHaveValues([/R/, /G/]);
```
```java
page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
page.locator("id=favorite-colors").selectOption(["R", "G"]);
assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
```
@ -2197,91 +1919,3 @@ Expected options currently selected.
### option: LocatorAssertions.toHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.23
## async method: LocatorAssertions.toMatchAriaSnapshot
* since: v1.49
* langs:
- alias-java: matchesAriaSnapshot
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
**Usage**
```js
await page.goto('https://demo.playwright.dev/todomvc/');
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "todos"
- textbox "What needs to be done?"
`);
```
```python async
await page.goto("https://demo.playwright.dev/todomvc/")
await expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos"
- textbox "What needs to be done?"
''')
```
```python sync
page.goto("https://demo.playwright.dev/todomvc/")
expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos"
- textbox "What needs to be done?"
''')
```
```csharp
await page.GotoAsync("https://demo.playwright.dev/todomvc/");
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
- heading ""todos""
- textbox ""What needs to be done?""
");
```
```java
page.navigate("https://demo.playwright.dev/todomvc/");
assertThat(page.locator("body")).matchesAriaSnapshot("""
- heading "todos"
- textbox "What needs to be done?"
""");
```
### param: LocatorAssertions.toMatchAriaSnapshot.expected
* since: v1.49
- `expected` <string>
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49
## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.50
* langs: js
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
Snapshot is stored in a separate `.snapshot.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
**Usage**
```js
await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.snapshot.yml' });
```
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
* since: v1.50
* langs: js
- `name` <[string]>
Name of the snapshot to store in the snapshot folder corresponding to this test.
Generates sequential names if not specified.
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50

View file

@ -10,7 +10,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
(async () => {
const browser = await chromium.launch({
logger: {
isEnabled: (name, severity) => name === 'api',
isEnabled: (name, severity) => name === 'browser',
log: (name, severity, message, args) => console.log(`${name} ${message}`)
}
});

View file

@ -68,14 +68,10 @@ Shortcut for [`method: Mouse.move`], [`method: Mouse.down`], [`method: Mouse.up`
* since: v1.8
- `x` <[float]>
X coordinate relative to the main frame's viewport in CSS pixels.
### param: Mouse.click.y
* since: v1.8
- `y` <[float]>
Y coordinate relative to the main frame's viewport in CSS pixels.
### option: Mouse.click.button = %%-input-button-%%
* since: v1.8
@ -97,14 +93,10 @@ Shortcut for [`method: Mouse.move`], [`method: Mouse.down`], [`method: Mouse.up`
* since: v1.8
- `x` <[float]>
X coordinate relative to the main frame's viewport in CSS pixels.
### param: Mouse.dblclick.y
* since: v1.8
- `y` <[float]>
Y coordinate relative to the main frame's viewport in CSS pixels.
### option: Mouse.dblclick.button = %%-input-button-%%
* since: v1.8
@ -131,14 +123,10 @@ Dispatches a `mousemove` event.
* since: v1.8
- `x` <[float]>
X coordinate relative to the main frame's viewport in CSS pixels.
### param: Mouse.move.y
* since: v1.8
- `y` <[float]>
Y coordinate relative to the main frame's viewport in CSS pixels.
### option: Mouse.move.steps
* since: v1.8
- `steps` <[int]>
@ -159,7 +147,7 @@ Dispatches a `mouseup` event.
## async method: Mouse.wheel
* since: v1.15
Dispatches a `wheel` event. This method is usually used to manually scroll the page. See [scrolling](../input.md#scrolling) for alternative ways to scroll.
Dispatches a `wheel` event.
:::note
Wheel events may cause scrolling if they are not handled, and this method does not

File diff suppressed because it is too large Load diff

View file

@ -14,14 +14,14 @@ test('navigates to login', async ({ page }) => {
```
```java
// ...
...
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TestPage {
// ...
...
@Test
void navigatesToLoginPage() {
// ...
...
page.getByText("Sign in").click();
assertThat(page).hasURL(Pattern.compile(".*/login"));
}
@ -50,19 +50,21 @@ def test_navigates_to_login_page(page: Page) -> None:
```csharp
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
[TestFixture]
public class ExampleTests : PageTest
{
[TestMethod]
public async Task NavigateToLoginPage()
[Test]
public async Task NavigatetoLoginPage()
{
await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex(".*/login"));
// ..
await Page.GetByText("Sing in").ClickAsync();
await Expect(Page.Locator("div#foobar")).ToHaveURL(new Regex(".*/login"));
}
}
```
@ -83,7 +85,7 @@ assertThat(page).not().hasURL("error");
```
```csharp
await Expect(Page).Not.ToHaveURLAsync("error");
await Expect(Page).Not.ToHaveURL("error");
```
## async method: PageAssertions.NotToHaveTitle
@ -114,12 +116,6 @@ The opposite of [`method: PageAssertions.toHaveURL`].
Expected URL string or RegExp.
### option: PageAssertions.NotToHaveURL.ignoreCase
* since: v1.44
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: PageAssertions.NotToHaveURL.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
@ -271,7 +267,7 @@ expect(page).to_have_title(re.compile(r".*checkout"))
```
```csharp
await Expect(Page).ToHaveTitleAsync("Playwright");
await Expect(Page).ToHaveTitle("Playwright");
```
### param: PageAssertions.toHaveTitle.titleOrRegExp
@ -296,18 +292,7 @@ Ensures the page is navigated to the given URL.
**Usage**
```js
// Check for the page URL to be 'https://playwright.dev/docs/intro' (including query string)
await expect(page).toHaveURL('https://playwright.dev/docs/intro');
// Check for the page URL to contain 'doc', followed by an optional 's', followed by '/'
await expect(page).toHaveURL(/docs?\//);
// Check for the predicate to be satisfied
// For example: verify query strings
await expect(page).toHaveURL(url => {
const params = url.searchParams;
return params.has('search') && params.has('options') && params.get('id') === '5';
});
await expect(page).toHaveURL(/.*checkout/);
```
```java
@ -331,21 +316,14 @@ expect(page).to_have_url(re.compile(".*checkout"))
```
```csharp
await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
await Expect(Page).ToHaveURL(new Regex(".*checkout"));
```
### param: PageAssertions.toHaveURL.url
### param: PageAssertions.toHaveURL.urlOrRegExp
* since: v1.18
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
- `urlOrRegExp` <[string]|[RegExp]>
Expected URL string, RegExp, or predicate receiving [URL] to match.
When [`option: Browser.newContext.baseURL`] is provided via the context options and the `url` argument is a string, the two values are merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor and used for the comparison against the current browser URL.
### option: PageAssertions.toHaveURL.ignoreCase
* since: v1.44
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression parameter if specified. A provided predicate ignores this flag.
Expected URL string or RegExp.
### option: PageAssertions.toHaveURL.timeout = %%-js-assertions-timeout-%%
* since: v1.18

View file

@ -35,13 +35,14 @@ def test_status_becomes_submitted(page: Page) -> None:
```
```java
...
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TestExample {
// ...
...
@Test
void statusBecomesSubmitted() {
// ...
...
page.locator("#submit-button").click();
assertThat(page.locator(".status")).hasText("Submitted");
}
@ -49,18 +50,19 @@ public class TestExample {
```
```csharp
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
namespace PlaywrightTests;
[TestClass]
[TestFixture]
public class ExampleTests : PageTest
{
[TestMethod]
[Test]
public async Task StatusBecomesSubmitted()
{
await Page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();
await Page.Locator("#submit-button").ClickAsync();
await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted");
}
}

View file

@ -124,7 +124,7 @@ Headers with multiple entries, such as `Set-Cookie`, appear in the array multipl
* since: v1.15
- returns: <[null]|[string]>
Returns the value of the header matching the name. The name is case-insensitive.
Returns the value of the header matching the name. The name is case insensitive.
### param: Request.headerValue.name
* since: v1.15

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