Compare commits

..

21 Commits

Author SHA1 Message Date
Max Schmitt ceb756dad3
chore: mark v1.48.1 (#33136) 2024-10-16 11:31:18 +02:00
Lars Hanisch c3740d37af cherry-pick(#33133): (docker): correct Ubuntu Noble name in name template 2024-10-16 10:16:42 +02:00
Pavel Feldman 2ec0c86b93 cherry-pick(#33124): test: unflake ff debugger test 2024-10-15 16:23:58 -07:00
Pavel Feldman 8ef381fc5f cherry-pick(#33122): chore: fix ff test for codegen 2024-10-15 13:35:28 -07:00
Dmitry Gozman c72a2538bc
cherry-pick(#33110): fix(chromium): disable PlzDedicatedWorker again (#33113) 2024-10-15 04:56:49 -07:00
Dmitry Gozman 3d7ef3c062
cherry-pick(#33095): fix(routeWebSocket): make sure ws url without trailing slash is supported (#33112) 2024-10-15 04:56:29 -07:00
Dmitry Gozman 78c43bc5d3 cherry-pick(#33097): docs: improve docs for WebSocketRoute 2024-10-15 10:07:44 +01:00
Pavel Feldman 6dc9ec7fe9 cherry-pick(#33099): chore: fix codegen selector while debugging 2024-10-14 14:05:42 -07:00
Max Schmitt e5bbd5effe cherry-pick(#33096): chore: various v1.48.0 roll fixes for .NET 2024-10-14 16:33:00 +02:00
Pavel Feldman daff1a9025 cherry-pick(#33030): fix(ui): bring back the headed param 2024-10-09 17:47:50 -07:00
Pavel Feldman 8d524e24ce cherry-pick(#32996): chore: allow minimal height for trace attachments 2024-10-08 10:31:10 -07:00
Max Schmitt 0cdbb11068
chore: mark v1.48.0 (#33009) 2024-10-08 14:37:38 +02:00
Dmitry Gozman ca368d43fa
cherry-pick(#32991): fix(routeWebSocket): do not show in the trace (#33004) 2024-10-08 03:56:00 -07:00
Max Schmitt c329c5c1ec cherry-pick(#33005): chore(driver): roll driver to recent Node.js LTS version 2024-10-08 12:54:50 +02:00
Simon Knott 97aaa12be4
cherry-pick(#32956): fix(fetch): listener leaks on Socket
Closes https://github.com/microsoft/playwright/issues/32951

`node:http` reuses TCP Sockets under the hood. We weren't cleaning up
our listeners, leading to the `MaxListenersExceededWarning`.

This PR adds cleanup logic. It also raises the warning threshhold, so
that it doesn't trigger until there's 100 concurrent requests over the
same socket.
2024-10-08 09:18:24 +02:00
Max Schmitt 0c17732e91 cherry-pick(#32949): feat(chromium): roll to r1140 2024-10-04 11:34:50 +02:00
Playwright Service 530f04304e
cherry-pick(#32938): feat(firefox): roll to r1465
This PR cherry-picks the following commits:
- 616425a0fb
2024-10-03 09:15:18 +02:00
Max Schmitt 1054930edd cherry-pick(#32924): docs: fix Java/.NET types for docs rolling 2024-10-02 13:30:41 +02:00
Playwright Service e732f68eee cherry-pick(#32906): feat(chromium): roll to r1139
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-01 19:06:53 +02:00
Max Schmitt dfa0e8bf35 cherry-pick(#32905): chore: remove 'screenshot instead of snapshot' usages 2024-10-01 18:38:59 +02:00
Max Schmitt 7155356e3c cherry-pick(#32880): chore: unflake 'should record' 2024-09-30 20:33:37 +02:00
1250 changed files with 40708 additions and 75668 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"
]
}

21
.eslintignore Normal file
View File

@ -0,0 +1,21 @@
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
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"
},
};

136
.eslintrc.js Normal file
View File

@ -0,0 +1,136 @@
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "notice"],
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
},
extends: [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
settings: {
react: { version: "18" }
},
/**
* 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
}],
"jsx-quotes": [2, "prefer-single"],
"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"),
}],
// react
"react/react-in-jsx-scope": 0
}
};

View File

@ -23,5 +23,5 @@ body:
> [!IMPORTANT]
> This issue will be closed.
options:
- label: I understand that this issue will be closed
- label: I understand
required: true

View File

@ -1,6 +1,7 @@
name: Report regression
description: Functionality that used to work and does not any more
title: "[Regression]: "
body:
- type: markdown
attributes:

View File

@ -41,6 +41,9 @@ runs:
npm ci
echo "::endgroup::"
shell: bash
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- run: |
echo "::group::npm run build"
npm run build

View File

@ -22,3 +22,13 @@ runs:
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' }}
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' }}
uses: actions/upload-artifact@v4
with:
name: pull-request-${{ inputs.job_name }}
path: pull_request_number.txt

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

@ -33,8 +33,8 @@ jobs:
- name: Cherry-pick commits
id: cherry-pick
run: |
git config --global user.name microsoft-playwright-automation[bot]
git config --global user.email 203992400+microsoft-playwright-automation[bot]@users.noreply.github.com
git config --global user.name github-actions
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
for COMMIT_HASH in $(echo "${{ github.event.inputs.commit_hashes }}" | tr "," "\n"); do
git cherry-pick --no-commit "$COMMIT_HASH"
@ -59,15 +59,10 @@ jobs:
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
git checkout -b "$BRANCH_NAME"
git push origin $BRANCH_NAME
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
- name: Create Pull Request
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
const readableCommitHashesList = '${{ github.event.inputs.commit_hashes }}'.split(',').map(hash => `- ${hash}`).join('\n');
const response = await github.rest.pulls.create({

View File

@ -20,7 +20,8 @@ jobs:
node-version: 18
- run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- name: Download blob report artifact
@ -33,9 +34,7 @@ jobs:
run: |
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
HTML_REPORT_URL: 'https://mspwblobreport.z1.web.core.windows.net/run-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}-${{ github.sha }}/index.html'
NODE_OPTIONS: --max-old-space-size=4096
- name: Azure Login
uses: azure/login@v2
@ -51,3 +50,91 @@ jobs:
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html"
env:
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
- name: Read pull request number
uses: ./.github/actions/download-artifact
with:
namePrefix: 'pull-request'
path: '.'
- name: Comment on PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
let prNumber;
if (context.payload.workflow_run.event === 'pull_request') {
const prs = context.payload.workflow_run.pull_requests;
if (prs.length) {
prNumber = prs[0].number;
} else {
prNumber = parseInt(fs.readFileSync('pull_request_number.txt').toString());
console.log('Read pull request number from file: ' + prNumber);
}
} else {
core.error('Unsupported workflow trigger event: ' + context.payload.workflow_run.event);
return;
}
if (!prNumber) {
core.error('No pull request found for commit ' + context.sha + ' and workflow triggered by: ' + context.payload.workflow_run.event);
return;
}
{
// Mark previous comments as outdated by minimizing them.
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: prNumber,
});
for (const comment of comments) {
if (comment.user.login === 'github-actions[bot]' && /\[Test results\]\(https:\/\/.+?\) for "${{ github.event.workflow_run.name }}"/.test(comment.body)) {
await github.graphql(`
mutation {
minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) {
clientMutationId
}
}
`);
}
}
}
const reportDir = 'run-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}-${{ github.sha }}';
const reportUrl = `https://mspwblobreport.z1.web.core.windows.net/${reportDir}/index.html#?q=s%3Afailed%20s%3Aflaky`;
core.notice('Report url: ' + reportUrl);
const mergeWorkflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const reportMd = await fs.promises.readFile('report.md', 'utf8');
function formatComment(lines) {
let body = lines.join('\n');
if (body.length > 65535)
body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`;
return body;
}
const { data: response } = await github.rest.issues.createComment({
...context.repo,
issue_number: prNumber,
body: formatComment([
`### [Test results](${reportUrl}) for "${{ github.event.workflow_run.name }}"`,
reportMd,
'',
`Merge [workflow run](${mergeWorkflowUrl}).`
]),
});
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
@ -24,7 +24,8 @@ jobs:
node-version: 18
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps
- run: npx playwright install-deps
- run: npx playwright install
- run: npm run lint
- name: Verify clean tree
run: |
@ -34,11 +35,10 @@ jobs:
exit 1
fi
- name: Audit prod NPM dependencies
run: node utils/check_audit.js
continue-on-error: true
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
@ -50,12 +50,6 @@ jobs:
- uses: actions/setup-dotnet@v4
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-dashboard/lib/ghaMarkdownReporter')], ['html']]
reporter: [['markdown'], ['html']]
};

View File

@ -12,24 +12,14 @@ 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
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
repositories: |
playwright
playwright-python
playwright-java
playwright-dotnet
- name: Create GitHub issue
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
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({
@ -38,10 +28,6 @@ jobs:
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;
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
@ -64,7 +50,7 @@ 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,

View File

@ -28,6 +28,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- name: "@next: publish with commit timestamp (triggered manually)"
if: contains(github.ref, 'main') && github.event_name == 'workflow_dispatch'
run: |
@ -64,26 +65,20 @@ 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
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
repositories: trace.playwright.dev
- name: Deploy Canary
run: bash utils/build/deploy-trace-viewer.sh --canary
if: contains(github.ref, 'main')
env:
GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }}
GH_SERVICE_ACCOUNT_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
- name: Deploy BETA
run: bash utils/build/deploy-trace-viewer.sh --beta
if: contains(github.ref, 'release')
env:
GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }}
GH_SERVICE_ACCOUNT_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}

View File

@ -29,6 +29,7 @@ jobs:
platforms: arm64
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- name: Azure Login
uses: azure/login@v2
with:

View File

@ -24,6 +24,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- run: utils/build/build-playwright-driver.sh
- name: Azure Login
uses: azure/login@v2

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
@ -23,6 +23,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- run: utils/publish_all_packages.sh --release-candidate
if: ${{ github.event.release.prerelease }}
env:

View File

@ -7,20 +7,14 @@ 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
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
repositories: trace.playwright.dev
- name: Deploy Stable
run: bash utils/build/deploy-trace-viewer.sh --stable
env:
GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }}
GH_SERVICE_ACCOUNT_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}

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,35 +24,30 @@ 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 microsoft-playwright-automation[bot]
git config --global user.email 203992400+microsoft-playwright-automation[bot]@users.noreply.github.com
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
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
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
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
const response = await github.rest.pulls.create({
owner: 'microsoft',
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

@ -27,22 +27,17 @@ jobs:
echo "HAS_CHANGES=1" >> $GITHUB_OUTPUT
BRANCH_NAME="roll-driver-nodejs/$(date +%Y-%b-%d)"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
git config --global user.name microsoft-playwright-automation[bot]
git config --global user.email 203992400+microsoft-playwright-automation[bot]@users.noreply.github.com
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 "chore(driver): roll driver to recent Node.js LTS version"
git push origin $BRANCH_NAME
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
- name: Create Pull Request
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
script: |
await github.rest.pulls.create({
owner: 'microsoft',

View File

@ -2,24 +2,18 @@ name: tests BiDi
on:
workflow_dispatch:
inputs:
ref:
description: Playwright SHA / ref to test. Use 'refs/pull/PULL_REQUEST_ID/head' to test a PR. Defaults to the current branch.
required: false
default: ''
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
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_bidi:
@ -32,53 +26,21 @@ jobs:
strategy:
fail-fast: false
matrix:
channel: [bidi-chromium, moz-firefox]
channel: [bidi-chromium, bidi-firefox-nightly]
steps:
- uses: actions/checkout@v4
if: github.event_name != 'workflow_dispatch'
- uses: actions/checkout@v4
if: github.event_name == 'workflow_dispatch'
with:
ref: ${{ github.event.inputs.ref }}
- 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'
- if: matrix.channel == 'moz-firefox'
id: install_firefox
run: |
npx -y @puppeteer/browsers install firefox@nightly |\
awk 'END { $1=""; sub(/^ /,""); print "bidi_ffpath="$0 }' |\
tee -a $GITHUB_OUTPUT
- 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'
BIDI_FFPATH: ${{ steps.install_firefox.outputs.bidi_ffpath }}
- 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

@ -147,13 +147,6 @@ jobs:
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

View File

@ -23,7 +23,6 @@ env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
DEBUG_GIT_COMMIT_INFO: 1
jobs:
test_linux:
@ -64,7 +63,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
os: [ubuntu-20.04]
runs-on: ${{ matrix.os }}
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
@ -135,6 +134,8 @@ jobs:
with:
node-version: 18
- run: npm ci
env:
DEBUG: pw:install
- run: npm run build
- run: npx playwright install --with-deps
@ -164,7 +165,6 @@ jobs:
runs-on: ubuntu-latest
env:
PWTEST_BOT_NAME: "vscode-extension"
DEBUG_GIT_COMMIT_INFO: ""
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -184,7 +184,7 @@ jobs:
run: node -e "const p = require('./package.json'); delete p.devDependencies['@playwright/test']; fs.writeFileSync('./package.json', JSON.stringify(p, null, 2));"
working-directory: ./playwright-vscode
- name: Build extension
run: npm ci && npm run build
run: npm install && npm run build
working-directory: ./playwright-vscode
- name: Run extension tests
run: npm run test -- --workers=1
@ -215,13 +215,6 @@ jobs:
- uses: actions/checkout@v4
- 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
shell: bash
- uses: ./.github/actions/run-test
with:
command: npm run itest

View File

@ -31,7 +31,7 @@ jobs:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
os: [ubuntu-24.04]
os: [ubuntu-20.04, ubuntu-24.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -107,13 +107,6 @@ jobs:
- uses: actions/checkout@v4
- 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
shell: bash
- uses: ./.github/actions/run-test
with:
node-version: ${{ matrix.node_version }}
@ -130,11 +123,7 @@ jobs:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
os: [ubuntu-24.04, macos-14-xlarge, windows-latest]
include:
# We have different binaries per Ubuntu version for WebKit.
- browser: webkit
os: ubuntu-22.04
os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14-xlarge, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -154,7 +143,7 @@ jobs:
fail-fast: false
matrix:
mode: [driver, service]
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
@ -180,7 +169,7 @@ jobs:
- browser: webkit
- browser: chromium
channel: chromium-tip-of-tree
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
@ -195,96 +184,354 @@ jobs:
PWTEST_TRACE: 1
PWTEST_CHANNEL: ${{ matrix.channel }}
test_chromium_channels:
name: Test ${{ matrix.channel }} on ${{ matrix.runs-on }}
chrome_stable_linux:
name: "Chrome Stable (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
channel: [chrome, chrome-beta, msedge, msedge-beta, msedge-dev]
runs-on: [ubuntu-22.04, macos-latest, windows-latest]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: ${{ matrix.channel }}
browsers-to-install: chrome
command: npm run ctest
bot-name: ${{ matrix.channel }}-${{ matrix.runs-on }}
bot-name: "chrome-stable-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:
PWTEST_CHANNEL: ${{ matrix.channel }}
PWTEST_CHANNEL: chrome
chrome_stable_win:
name: "Chrome Stable (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chrome
command: npm run ctest
bot-name: "chrome-stable-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 }}
env:
PWTEST_CHANNEL: chrome
chrome_stable_mac:
name: "Chrome Stable (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chrome
command: npm run ctest
bot-name: "chrome-stable-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chrome
chromium_tot:
name: Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }}
name: Chromium tip-of-tree ${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-13, windows-latest]
headed: ['--headed', '']
exclude:
# Tested in tests_primary.yml already
- os: ubuntu-22.04
headed: ''
os: [ubuntu-20.04, macos-13, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium-tip-of-tree
command: npm run ctest -- ${{ matrix.headed }}
bot-name: "chromium-tip-of-tree-${{ matrix.os }}${{ matrix.headed }}"
command: npm run ctest
bot-name: "tip-of-tree-${{ 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:
PWTEST_CHANNEL: chromium-tip-of-tree
chromium_tot_headless_shell:
name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
chromium_tot_headed:
name: Chromium tip-of-tree headed ${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium-tip-of-tree-headless-shell
command: npm run ctest
bot-name: "chromium-tip-of-tree-headless-shell-${{ matrix.os }}"
browsers-to-install: chromium-tip-of-tree
command: npm run ctest -- --headed
bot-name: "tip-of-tree-headed-${{ 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:
PWTEST_CHANNEL: chromium-tip-of-tree-headless-shell
PWTEST_CHANNEL: chromium-tip-of-tree
firefox_beta:
name: Firefox Beta ${{ matrix.os }}
firefox_beta_linux:
name: "Firefox Beta (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest, macos-latest]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: firefox-beta chromium
command: npm run ftest
bot-name: "firefox-beta-${{ matrix.os }}"
bot-name: "firefox-beta-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:
PWTEST_CHANNEL: firefox-beta
firefox_beta_win:
name: "Firefox Beta (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: firefox-beta chromium
command: npm run ftest -- --workers=1
bot-name: "firefox-beta-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 }}
env:
PWTEST_CHANNEL: firefox-beta
firefox_beta_mac:
name: "Firefox Beta (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: firefox-beta chromium
command: npm run ftest
bot-name: "firefox-beta-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: firefox-beta
edge_stable_mac:
name: "Edge Stable (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge
command: npm run ctest
bot-name: "edge-stable-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: msedge
edge_stable_win:
name: "Edge Stable (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge
command: npm run ctest
bot-name: "edge-stable-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 }}
env:
PWTEST_CHANNEL: msedge
edge_stable_linux:
name: "Edge Stable (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge
command: npm run ctest
bot-name: "edge-stable-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:
PWTEST_CHANNEL: msedge
edge_beta_mac:
name: "Edge Beta (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-beta
command: npm run ctest
bot-name: "edge-beta-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: msedge-beta
edge_beta_win:
name: "Edge Beta (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-beta
command: npm run ctest
bot-name: "edge-beta-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 }}
env:
PWTEST_CHANNEL: msedge-beta
edge_beta_linux:
name: "Edge Beta (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-beta
command: npm run ctest
bot-name: "edge-beta-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:
PWTEST_CHANNEL: msedge-beta
edge_dev_mac:
name: "Edge Dev (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-dev
command: npm run ctest
bot-name: "edge-dev-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: msedge-dev
edge_dev_win:
name: "Edge Dev (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-dev
command: npm run ctest
bot-name: "edge-dev-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 }}
env:
PWTEST_CHANNEL: msedge-dev
edge_dev_linux:
name: "Edge Dev (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: msedge-dev
command: npm run ctest
bot-name: "edge-dev-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:
PWTEST_CHANNEL: msedge-dev
chrome_beta_linux:
name: "Chrome Beta (Linux)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chrome-beta
command: npm run ctest
bot-name: "chrome-beta-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:
PWTEST_CHANNEL: chrome-beta
chrome_beta_win:
name: "Chrome Beta (Win)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chrome-beta
command: npm run ctest
bot-name: "chrome-beta-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 }}
env:
PWTEST_CHANNEL: chrome-beta
chrome_beta_mac:
name: "Chrome Beta (Mac)"
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chrome-beta
command: npm run ctest
bot-name: "chrome-beta-mac"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chrome-beta
build-playwright-driver:
name: "build-playwright-driver"
runs-on: ubuntu-24.04
@ -295,27 +542,22 @@ jobs:
node-version: 18
- run: npm ci
- run: npm run build
- run: npx playwright install-deps
- run: utils/build/build-playwright-driver.sh
test_channel_chromium:
name: Test channel=chromium
test_linux_chromium_headless_new:
name: Linux Chromium Headless New
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
runs-on: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.runs-on }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
# TODO: this should pass --no-shell.
# However, codegen tests do not inherit the channel and try to launch headless shell.
browsers-to-install: chromium
command: npm run ctest
bot-name: "channel-chromium-${{ matrix.runs-on }}"
bot-name: "headless-new"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chromium
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1

70
.github/workflows/tests_service.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: "tests service"
on:
workflow_dispatch:
env:
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test:
name: "Service"
strategy:
fail-fast: false
matrix:
service-os: [linux, windows]
browser: [chromium, firefox, webkit]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
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
env:
PWTEST_MODE: service2
PWTEST_TRACE: 1
PWTEST_BOT_NAME: "${{ matrix.browser }}-${{ matrix.service-os }}-service"
PLAYWRIGHT_SERVICE_ACCESS_KEY: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_KEY }}
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
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
with:
name: all-blob-reports
path: blob-report
retention-days: 2
merge_reports:
name: "Merge reports"
needs: [test]
if: ${{ !cancelled() }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- name: Download blob report artifact
uses: actions/download-artifact@v4
with:
name: all-blob-reports
path: all-blob-reports
- run: npx playwright merge-reports --reporter markdown,html ./all-blob-reports
- name: Upload HTML report to Azure
run: |
REPORT_DIR='run-service-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}'
azcopy cp --recursive "./playwright-report/*" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR"
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html#?q=s:failed"
env:
AZCOPY_AUTO_LOGIN_TYPE: SPN
AZCOPY_SPA_APPLICATION_ID: '${{ secrets.AZCOPY_SPA_APPLICATION_ID }}'
AZCOPY_SPA_CLIENT_SECRET: '${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}'
AZCOPY_TENANT_ID: '${{ secrets.AZCOPY_TENANT_ID }}'

View File

@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
os: [ubuntu-22.04, ubuntu-24.04]
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

View File

@ -9,14 +9,8 @@ on:
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
repositories: playwright-browsers
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
@ -24,4 +18,4 @@ jobs:
--data "{\"event_type\": \"playwright_tests\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \
https://api.github.com/repos/microsoft/playwright-browsers/dispatches
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}

1
.gitignore vendored
View File

@ -35,4 +35,3 @@ test-results
.cache/
.eslintcache
playwright.env
/firefox/

View File

@ -1,92 +1,92 @@
# 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](https://github.com/microsoft/playwright/issues?q=is%3Aissue%20state%3Aopen%20label%3Aopen-to-a-pull-request)
for your first contribution to the project.
## How to Contribute
If you are passionate 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
```
2. Install dependencies
```bash
npm ci
```
3. Build Playwright
```bash
npm run build
```
4. Run tests
This will run a test on line `23` in `page-fill.spec.ts`:
```bash
npm run ctest -- page-fill:23
```
See [here](#running--writing-tests) for more information about running and writing tests.
### Code reviews
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.
### Code Style
- 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.
To run code linter, use:
```bash
git clone https://github.com/microsoft/playwright
cd playwright
npm run eslint
```
Install dependencies and run the build in watch mode.
```bash
npm ci
npm run watch
npx playwright install
```
### API guidelines
**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode**
When authoring new API methods, consider the following:
```
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
```
- 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.
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).
### Commit Messages
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).
Coding style is fully defined in [eslint.config.mjs](https://github.com/microsoft/playwright/blob/main/eslint.config.mjs). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:
```bash
npm run lint
```
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.
### Write documentation
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.
Various other files are generated from the API specification. If you are running `npm run watch`, these will be re-generated automatically.
Larger changes will require updates to the documentation guides as well. This will be made clear during the code review.
## Add a test
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.
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:
- Library tests cover APIs not related to the test runner.
```bash
# fast path runs all tests in Chromium
npm run ctest
# 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
@ -97,59 +97,131 @@ 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): ..` 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
2. *namespace* is put in parentheses after label and is optional. Must be lowercase.
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.
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.
Make sure to keep your PR (diff) small and readable. If necessary, split your contribution into multiple PRs.
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
**No new dependencies**
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.
**Custom browser build**
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
npm run doc
```
You will also find `DEBUG=pw:browser` useful for debugging custom-builds.
To build the documentation site locally and test how your changes will look in practice:
**Building documentation site**
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
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.
### Adding New Dependencies
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.
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 tests in Chromium
```bash
npm run ctest # also `ftest` for firefox and `wtest` for WebKit
npm run ctest -- page-fill:23 # runs line 23 of page-fill.spec.ts
```
- To run tests in WebKit / Firefox, use `wtest` or `ftest`.
- 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:
```bash
CRPATH=<path-to-executable> npm run ctest
```
- When should a test be marked with `skip` or `fixme`?
- **`skip(condition)`**: This test *should ***never*** work* for `condition`
where `condition` is usually something like: `test.skip(browserName === 'chromium', 'This does not work because of ...')`.
- **`fixme(condition)`**: This test *should ***eventually*** work* for `condition`
where `condition` is usually something like: `test.fixme(browserName === 'chromium', 'We are waiting for version x')`.
## 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-136.0.7103.25-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-137.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.4-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-130.0.6723.31-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-131.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.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->136.0.7103.25<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->137.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->130.0.6723.31<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->131.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)

17
babel.config.json Normal file
View File

@ -0,0 +1,17 @@
{
"assumptions": {
"setPublicClassFields": true
},
"plugins": [
["@babel/plugin-transform-typescript", { "allowDeclareFields": true } ],
"@babel/plugin-transform-export-namespace-from",
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-logical-assignment-operators",
"@babel/plugin-transform-nullish-coalescing-operator",
"@babel/plugin-transform-optional-chaining",
"@babel/plugin-transform-modules-commonjs"
],
"ignore": [
"**/*.d.ts"
]
}

View File

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

View File

@ -145,13 +145,10 @@ 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 = [];
@ -204,13 +201,11 @@ class NetworkRequest {
this._interceptedChannel.synthesizeHeader(header.name, header.value);
if (header.name.toLowerCase() === 'set-cookie') {
Services.cookies.QueryInterface(Ci.nsICookieService);
for (const cookieString of header.value.split('\n'))
Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, cookieString, this.httpChannel);
Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel);
}
}
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
if (base64body)
synthesized.setByteStringData(atob(base64body));
synthesized.data = base64body ? atob(base64body) : '';
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
this._interceptedChannel.finishSynthesizedResponse();
this._interceptedChannel = undefined;
@ -235,13 +230,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)
@ -771,20 +773,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)
@ -872,7 +860,7 @@ function setPostData(httpChannel, postData, headers) {
return;
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
const body = atob(postData);
synthesized.setByteStringData(body);
synthesized.setData(body, body.length);
const overriddenHeader = (lowerCaseName) => {
if (headers) {
@ -904,7 +892,7 @@ function convertString(s, source, dest) {
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
is.setByteStringData(s);
is.setData(s, s.length);
const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
Ci.nsIStreamLoader
);

View File

@ -22,8 +22,6 @@ const ALL_PERMISSIONS = [
];
let globalTabAndWindowActivationChain = Promise.resolve();
// This is a workaround for https://github.com/microsoft/playwright/issues/34586
let globalNewPageChain = Promise.resolve();
class DownloadInterceptor {
constructor(registry) {
@ -310,59 +308,55 @@ class TargetRegistry {
}
async newPage({browserContextId}) {
const result = globalNewPageChain.then(async () => {
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
// window.arguments[1]: unused (bug 871161)
// [2]: referrerInfo (nsIReferrerInfo)
// [3]: postData (nsIInputStream)
// [4]: allowThirdPartyFixup (bool)
// [5]: userContextId (int)
// [6]: originPrincipal (nsIPrincipal)
// [7]: originStoragePrincipal (nsIPrincipal)
// [8]: triggeringPrincipal (nsIPrincipal)
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlSupports.data = 'about:blank';
args.appendElement(urlSupports); // 0
args.appendElement(undefined); // 1
args.appendElement(undefined); // 2
args.appendElement(undefined); // 3
args.appendElement(undefined); // 4
const userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
userContextIdSupports.data = browserContext.userContextId;
args.appendElement(userContextIdSupports); // 5
args.appendElement(undefined); // 6
args.appendElement(undefined); // 7
args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
// window.arguments[1]: unused (bug 871161)
// [2]: referrerInfo (nsIReferrerInfo)
// [3]: postData (nsIInputStream)
// [4]: allowThirdPartyFixup (bool)
// [5]: userContextId (int)
// [6]: originPrincipal (nsIPrincipal)
// [7]: originStoragePrincipal (nsIPrincipal)
// [8]: triggeringPrincipal (nsIPrincipal)
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlSupports.data = 'about:blank';
args.appendElement(urlSupports); // 0
args.appendElement(undefined); // 1
args.appendElement(undefined); // 2
args.appendElement(undefined); // 3
args.appendElement(undefined); // 4
const userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
userContextIdSupports.data = browserContext.userContextId;
args.appendElement(userContextIdSupports); // 5
args.appendElement(undefined); // 6
args.appendElement(undefined); // 7
args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8
const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
await waitForWindowReady(window);
if (window.gBrowser.browsers.length !== 1)
throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
const browser = window.gBrowser.browsers[0];
let target = this._browserToTarget.get(browser);
while (!target) {
await helper.awaitEvent(this, TargetRegistry.Events.TargetCreated);
target = this._browserToTarget.get(browser);
}
browser.focus();
if (browserContext.crossProcessCookie.settings.timezoneId) {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
return target.id();
});
globalNewPageChain = result.catch(error => { /* swallow errors to keep chain running */ });
return result;
const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
await waitForWindowReady(window);
if (window.gBrowser.browsers.length !== 1)
throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
const browser = window.gBrowser.browsers[0];
let target = this._browserToTarget.get(browser);
while (!target) {
await helper.awaitEvent(this, TargetRegistry.Events.TargetCreated);
target = this._browserToTarget.get(browser);
}
browser.focus();
if (browserContext.crossProcessCookie.settings.timezoneId) {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
return target.id();
}
targets() {
@ -390,7 +384,6 @@ class PageTarget {
this._linkedBrowser = tab.linkedBrowser;
this._browserContext = browserContext;
this._viewportSize = undefined;
this._zoom = 1;
this._initialDPPX = this._linkedBrowser.browsingContext.overrideDPPX;
this._url = 'about:blank';
this._openerId = opener ? opener.id() : undefined;
@ -400,7 +393,7 @@ class PageTarget {
this._videoRecordingInfo = undefined;
this._screencastRecordingInfo = undefined;
this._dialogs = new Map();
this.forcedColors = 'none';
this.forcedColors = 'no-override';
this.disableCache = false;
this.mediumOverride = '';
this.crossProcessCookie = {
@ -503,11 +496,9 @@ class PageTarget {
this.updateUserAgent(browsingContext);
this.updatePlatform(browsingContext);
this.updateDPPXOverride(browsingContext);
this.updateZoom(browsingContext);
this.updateEmulatedMedia(browsingContext);
this.updateColorSchemeOverride(browsingContext);
this.updateReducedMotionOverride(browsingContext);
this.updateContrastOverride(browsingContext);
this.updateForcedColorsOverride(browsingContext);
this.updateForceOffline(browsingContext);
this.updateCacheDisabled(browsingContext);
@ -543,16 +534,7 @@ class PageTarget {
}
updateDPPXOverride(browsingContext = undefined) {
browsingContext ||= this._linkedBrowser.browsingContext;
const dppx = this._zoom * (this._browserContext.deviceScaleFactor || this._initialDPPX);
browsingContext.overrideDPPX = dppx;
}
async updateZoom(browsingContext = undefined) {
browsingContext ||= this._linkedBrowser.browsingContext;
// Update dpr first, and then UI zoom.
this.updateDPPXOverride(browsingContext);
browsingContext.fullZoom = this._zoom;
(browsingContext || this._linkedBrowser.browsingContext).overrideDPPX = this._browserContext.deviceScaleFactor || this._initialDPPX;
}
_updateModalDialogs() {
@ -602,7 +584,7 @@ class PageTarget {
const toolbarTop = stackRect.y;
this._window.resizeBy(width - this._window.innerWidth, height + toolbarTop - this._window.innerHeight);
await this._channel.connect('').send('awaitViewportDimensions', { width: width / this._zoom, height: height / this._zoom });
await this._channel.connect('').send('awaitViewportDimensions', { width, height });
} else {
this._linkedBrowser.style.removeProperty('width');
this._linkedBrowser.style.removeProperty('height');
@ -614,8 +596,8 @@ class PageTarget {
const actualSize = this._linkedBrowser.getBoundingClientRect();
await this._channel.connect('').send('awaitViewportDimensions', {
width: actualSize.width / this._zoom,
height: actualSize.height / this._zoom,
width: actualSize.width,
height: actualSize.height,
});
}
}
@ -647,23 +629,13 @@ class PageTarget {
(browsingContext || this._linkedBrowser.browsingContext).prefersReducedMotionOverride = this.reducedMotion || this._browserContext.reducedMotion || 'none';
}
setContrast(contrast) {
this.contrast = fromProtocolContrast(contrast);
this.updateContrastOverride();
}
updateContrastOverride(browsingContext = undefined) {
(browsingContext || this._linkedBrowser.browsingContext).prefersContrastOverride = this.contrast || this._browserContext.contrast || 'none';
}
setForcedColors(forcedColors) {
this.forcedColors = fromProtocolForcedColors(forcedColors);
this.updateForcedColorsOverride();
}
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) {
@ -677,14 +649,6 @@ class PageTarget {
await this.updateViewportSize();
}
async setZoom(zoom) {
// This is default range from the ZoomManager.
if (zoom < 0.3 || zoom > 5)
throw new Error('Invalid zoom value, must be between 0.3 and 5');
this._zoom = zoom;
await this.updateZoom();
}
close(runBeforeUnload = false) {
this._gBrowser.removeTab(this._tab, {
skipPermitUnload: !runBeforeUnload,
@ -891,19 +855,11 @@ function fromProtocolReducedMotion(reducedMotion) {
throw new Error('Unknown reduced motion: ' + reducedMotion);
}
function fromProtocolContrast(contrast) {
if (contrast === 'more' || contrast === 'less' || contrast === 'custom' || contrast === 'no-preference')
return contrast;
if (contrast === null)
return undefined;
throw new Error('Unknown contrast: ' + contrast);
}
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);
}
@ -937,9 +893,8 @@ class BrowserContext {
this.forceOffline = false;
this.disableCache = false;
this.colorScheme = 'none';
this.forcedColors = 'none';
this.forcedColors = 'no-override';
this.reducedMotion = 'none';
this.contrast = 'none';
this.videoRecordingOptions = undefined;
this.crossProcessCookie = {
initScripts: [],
@ -966,12 +921,6 @@ class BrowserContext {
page.updateReducedMotionOverride();
}
setContrast(contrast) {
this.contrast = fromProtocolContrast(contrast);
for (const page of this.pages)
page.updateContrastOverride();
}
setForcedColors(forcedColors) {
this.forcedColors = fromProtocolForcedColors(forcedColors);
for (const page of this.pages)

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

@ -120,8 +120,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);
}
}),
@ -527,14 +526,8 @@ class PageAgent {
});
}
_getCurrentDragSession() {
const frame = this._frameTree.mainFrame();
const domWindow = frame?.domWindow();
return domWindow ? dragService.getCurrentSession(domWindow) : undefined;
}
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 +551,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

@ -45,6 +45,10 @@ function initialize(browsingContext, docShell) {
docShell.languageOverride = locale;
},
scrollbarsHidden: (hidden) => {
data.frameTree.setScrollbarsHidden(hidden);
},
javaScriptDisabled: (javaScriptDisabled) => {
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
},

View File

@ -219,10 +219,6 @@ class BrowserHandler {
await this._targetRegistry.browserContextForId(browserContextId).setForcedColors(nullToUndefined(forcedColors));
}
async ['Browser.setContrast']({browserContextId, contrast}) {
await this._targetRegistry.browserContextForId(browserContextId).setContrast(nullToUndefined(contrast));
}
async ['Browser.setVideoRecordingOptions']({browserContextId, options}) {
await this._targetRegistry.browserContextForId(browserContextId).setVideoRecordingOptions(options);
}
@ -259,6 +255,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

@ -240,10 +240,6 @@ class PageHandler {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
async ['Page.setZoom']({zoom}) {
await this._pageTarget.setZoom(zoom);
}
async ['Runtime.evaluate'](options) {
return await this._contentPage.send('evaluate', options);
}
@ -302,11 +298,10 @@ class PageHandler {
return await this._contentPage.send('setFileInputFiles', options);
}
async ['Page.setEmulatedMedia']({colorScheme, type, reducedMotion, forcedColors, contrast}) {
async ['Page.setEmulatedMedia']({colorScheme, type, reducedMotion, forcedColors}) {
this._pageTarget.setColorScheme(colorScheme || null);
this._pageTarget.setReducedMotion(reducedMotion || null);
this._pageTarget.setForcedColors(forcedColors || null);
this._pageTarget.setContrast(contrast || null);
this._pageTarget.setEmulatedMedia(type);
}

View File

@ -394,6 +394,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),
@ -463,12 +469,6 @@ const Browser = {
forcedColors: t.Nullable(t.Enum(['active', 'none'])),
},
},
'setContrast': {
params: {
browserContextId: t.Optional(t.String),
contrast: t.Nullable(t.Enum(['less', 'more', 'custom', 'no-preference'])),
},
},
'setVideoRecordingOptions': {
params: {
browserContextId: t.Optional(t.String),
@ -800,11 +800,6 @@ const Page = {
viewportSize: t.Nullable(pageTypes.Size),
},
},
'setZoom': {
params: {
zoom: t.Number,
},
},
'bringToFront': {
params: {
},
@ -815,7 +810,6 @@ const Page = {
colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
reducedMotion: t.Optional(t.Enum(['reduce', 'no-preference'])),
forcedColors: t.Optional(t.Enum(['active', 'none'])),
contrast: t.Optional(t.Enum(['less', 'more', 'custom', 'no-preference'])),
},
},
'setCacheDisabled': {

View File

@ -23,8 +23,8 @@ XPCOM_MANIFESTS += [
LOCAL_INCLUDES += [
'/dom/media/systemservices',
'/media/libyuv/libyuv/include',
'/third_party/abseil-cpp',
'/third_party/libwebrtc',
'/third_party/libwebrtc/third_party/abseil-cpp',
]
LOCAL_INCLUDES += [

View File

@ -343,17 +343,10 @@ nsresult nsScreencastService::StartVideoRecording(nsIScreencastServiceClient* aC
return NS_ERROR_FAILURE;
gfx::IntMargin margin;
// Screen bounds is the widget location on screen.
auto screenBounds = widget->GetScreenBounds().ToUnknownRect();
// Client bounds is the content location, in terms of parent widget.
// To use it, we need to translate it to screen coordinates first.
auto bounds = widget->GetScreenBounds().ToUnknownRect();
auto clientBounds = widget->GetClientBounds().ToUnknownRect();
for (auto parent = widget->GetParent(); parent != nullptr; parent = parent->GetParent()) {
auto pb = parent->GetClientBounds().ToUnknownRect();
clientBounds.MoveBy(pb.X(), pb.Y());
}
// Crop the image to exclude frame (if any).
margin = screenBounds - clientBounds;
margin = bounds - clientBounds;
// Crop the image to exclude controls.
margin.top += offsetTop;

File diff suppressed because it is too large Load Diff

View File

@ -8,10 +8,6 @@ pref("dom.input_events.security.minTimeElapsedInMS", 0);
pref("dom.iframe_lazy_loading.enabled", false);
// This setting is experimental and is only enabled on early betas.
// Disable it unconditionally since it breaks proxy tests.
pref("dom.security.https_first", false);
pref("datareporting.policy.dataSubmissionEnabled", false);
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
@ -92,10 +88,6 @@ pref("geo.provider.testing", true);
// THESE ARE NICHE PROPERTIES THAT ARE NICE TO HAVE
// =================================================================
// We never want to have interactive screen capture picker enabled in FF build.
pref("media.getdisplaymedia.screencapturekit.enabled", false);
pref("media.getdisplaymedia.screencapturekit.picker.enabled", false);
// Enable software-backed webgl. See https://phabricator.services.mozilla.com/D164016
pref("webgl.forbid-software", false);
@ -108,11 +100,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.

View File

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

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

@ -1 +1 @@
1007
1006

View File

@ -1,5 +1,3 @@
SET /p BUILD_NUMBER=<BUILD_NUMBER
SET CL=/DBUILD_NUMBER=%BUILD_NUMBER%
call "%VS_PATH%\VC\Auxiliary\Build\vcvars64.bat"
devenv %~dp0\PrintDeps.sln /build "Release|x64"
%DEVENV% %~dp0\PrintDeps.sln /build "Release|x64"

View File

@ -70,24 +70,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());
}
```
@ -107,7 +105,7 @@ AxeResults accessibilityScanResults = new AxeBuilder(page)
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
```
You can find a complete listing of the rule tags axe-core supports in [the "Axe-core Tags" section of the axe API documentation](https://www.deque.com/axe/core-documentation/api-documentation/#axecore-tags).
You can find a complete listing of the rule tags axe-core supports in [the "Axe-core Tags" section of the axe API documentation](https://www.deque.com/axe/core-documentation/api-documentation/#axe-core-tags).
## Handling known issues
@ -160,40 +158,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 +208,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 +229,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

@ -113,7 +113,7 @@ test('should not have any automatically detectable WCAG A or AA violations', asy
});
```
You can find a complete listing of the rule tags axe-core supports in [the "Axe-core Tags" section of the axe API documentation](https://www.deque.com/axe/core-documentation/api-documentation/#axecore-tags).
You can find a complete listing of the rule tags axe-core supports in [the "Axe-core Tags" section of the axe API documentation](https://www.deque.com/axe/core-documentation/api-documentation/#axe-core-tags).
## Handling known issues
@ -244,7 +244,7 @@ type AxeFixture = {
// This new "test" can be used in multiple test files, and each of them will get
// a consistently configured AxeBuilder instance.
export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use) => {
makeAxeBuilder: async ({ page }, use, testInfo) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
@ -255,7 +255,8 @@ export const test = base.extend<AxeFixture>({
export { expect } from '@playwright/test';
```
```js tab=js-js title="axe-test.js"
```js tab=js-js
// axe-test.js
const base = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
@ -264,7 +265,7 @@ const AxeBuilder = require('@axe-core/playwright').default;
// This new "test" can be used in multiple test files, and each of them will get
// a consistently configured AxeBuilder instance.
exports.test = base.test.extend({
makeAxeBuilder: async ({ page }, use) => {
makeAxeBuilder: async ({ page }, use, testInfo) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');

View File

@ -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

@ -194,7 +194,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 +223,6 @@ public class TestGitHubAPI {
disposeAPIRequestContext();
closePlaywright();
}
}
```
### Complete test example
@ -383,20 +381,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 +402,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

@ -258,7 +258,7 @@ test('last created issue should be on the server', async ({ page }) => {
await page.getByRole('textbox', { name: 'Title' }).fill('Bug report 1');
await page.getByRole('textbox', { name: 'Comment body' }).fill('Bug description');
await page.getByText('Submit new issue').click();
const issueId = new URL(page.url()).pathname.split('/').pop();
const issueId = page.url().substr(page.url().lastIndexOf('/'));
const newIssue = await apiContext.get(
`https://api.github.com/repos/${USER}/${REPO}/issues/${issueId}`

View File

@ -114,7 +114,7 @@ def test_should_create_feature_request(api_request_context: APIRequestContext) -
### Setup and teardown
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use a [session fixture](https://docs.pytest.org/en/stable/how-to/fixtures.html#fixture-scopes) for that. The part before `yield` is the before all and after is the after all.
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use a [session fixture](https://docs.pytest.org/en/stable/fixture.html#fixture-scopes) for that. The part before `yield` is the before all and after is the after all.
```python
# ...

View File

@ -22,7 +22,7 @@ assistive technologies themselves. By default, Playwright tries to approximate t
* deprecated: This method is deprecated. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe.
- returns: <[null]|[Object]>
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage).
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro).
- `name` <[string]> A human readable name for the node.
- `value` <[string]|[float]> The current value of the node, if applicable.
- `description` <[string]> An additional human readable description of the node, if applicable.

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.

View File

@ -21,13 +21,6 @@ Creates new instances of [APIRequestContext].
### 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
@ -37,13 +30,6 @@ for all status codes.
### option: APIRequest.newContext.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%%
* since: v1.16
### option: APIRequest.newContext.maxRedirects
* since: v1.52
- `maxRedirects` <[int]>
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request individually.
### option: APIRequest.newContext.timeout
* since: v1.16
- `timeout` <[float]>

View File

@ -890,9 +890,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

@ -13,8 +13,7 @@ async def run(playwright: Playwright):
assert response.ok
assert response.status == 200
assert response.headers["content-type"] == "application/json; charset=utf-8"
json_data = await response.json()
assert json_data["name"] == "foobar"
assert response.json()["name"] == "foobar"
assert await response.body() == '{"status": "ok"}'

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

@ -18,15 +18,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 +96,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.
@ -202,7 +202,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();
@ -331,7 +331,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

@ -655,7 +655,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());
@ -813,9 +813,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,14 +962,9 @@ 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:
A permission or an array of permissions to grant. Permissions can be one of the following values:
* `'accelerometer'`
* `'accessibility-events'`
* `'ambient-light-sensor'`
* `'background-sync'`
* `'camera'`
@ -1203,7 +1197,9 @@ Enabling routing disables http cache.
* since: v1.8
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If [`option: Browser.newContext.baseURL`] is set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
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,
it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
### param: BrowserContext.route.handler
* since: v1.8
@ -1410,7 +1406,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
@ -1510,7 +1506,7 @@ Whether to emulate network being offline for the browser context.
- `name` <[string]>
- `value` <[string]>
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
@ -1520,13 +1516,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](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot.
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this.
## property: BrowserContext.tracing
* since: v1.12
- type: <[Tracing]>

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
@ -324,13 +316,11 @@ this context will automatically close the browser.
* since: v1.8
- `userDataDir` <[path]>
Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty string to create a temporary directory.
More details for
Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for
[Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and
[Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`.
Note that browsers do not allow launching multiple instances with the same User Data Directory.
[Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
Note that Chromium's user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty string to
use a temporary directory instead.
### option: BrowserType.launchPersistentContext.-inline- = %%-shared-browser-options-list-v1.8-%%
* since: v1.8

View File

@ -161,41 +161,6 @@ 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
@ -228,8 +193,6 @@ Resumes timers. Once this method is called, time resumes flowing, timers are fir
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
@ -286,7 +249,7 @@ 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.
Sets current system time but does not trigger any timers.
**Usage**

View File

@ -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

@ -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")
@ -28,7 +28,7 @@ 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")
@ -100,7 +100,7 @@ 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")

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
@ -242,7 +257,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();
```
@ -150,80 +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.emitGeneric
* since: v1.53
- `emitGeneric` <[boolean]>
Generate `generic` aria nodes for elements w/o roles (similar to Chrome DevTools).
### option: Locator.ariaSnapshot.ref
* since: v1.52
- `ref` <[boolean]>
Generate symbolic reference for each element. One can use `aria-ref=<ref>` locator immediately after capturing the
snapshot to perform actions on the element.
### 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
@ -646,11 +572,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);
@ -658,11 +586,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})
```
@ -877,35 +807,29 @@ If [`param: expression`] throws or rejects, this method throws.
**Usage**
Passing argument to [`param: expression`]:
```js
const result = await page.getByTestId('myId').evaluate((element, [x, y]) => {
return element.textContent + ' ' + x * y;
}, [7, 8]);
console.log(result); // prints "myId text 56"
const tweets = page.locator('.tweet .retweets');
expect(await tweets.evaluate(node => node.innerText)).toBe('10 retweets');
```
```java
Object result = page.getByTestId("myId").evaluate("(element, [x, y]) => {\n" +
" return element.textContent + ' ' + x * y;\n" +
"}", Arrays.asList(7, 8));
System.out.println(result); // prints "myId text 56"
Locator tweets = page.locator(".tweet .retweets");
assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
```
```python async
result = await page.get_by_testid("myId").evaluate("(element, [x, y]) => element.textContent + ' ' + x * y", [7, 8])
print(result) # prints "myId text 56"
tweets = page.locator(".tweet .retweets")
assert await tweets.evaluate("node => node.innerText") == "10 retweets"
```
```python sync
result = page.get_by_testid("myId").evaluate("(element, [x, y]) => element.textContent + ' ' + x * y", [7, 8])
print(result) # prints "myId text 56"
tweets = page.locator(".tweet .retweets")
assert tweets.evaluate("node => node.innerText") == "10 retweets"
```
```csharp
var result = await page.GetByTestId("myId").EvaluateAsync<string>("(element, [x, y]) => element.textContent + ' ' + x * y)", new[] { 7, 8 });
Console.WriteLine(result); // prints "myId text 56"
var tweets = page.Locator(".tweet .retweets");
Assert.AreEqual("10 retweets", await tweets.EvaluateAsync("node => node.innerText"));
```
### param: Locator.evaluate.expression = %%-evaluate-expression-%%
@ -920,19 +844,11 @@ Console.WriteLine(result); // prints "myId text 56"
Optional argument to pass to [`param: expression`].
### option: Locator.evaluate.timeout
### option: Locator.evaluate.timeout = %%-input-timeout-%%
* since: v1.14
* langs: python, java, csharp
- `timeout` <[float]>
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
### option: Locator.evaluate.timeout
### option: Locator.evaluate.timeout = %%-input-timeout-js-%%
* since: v1.14
* langs: js
- `timeout` <[float]>
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `0` - no timeout.
## async method: Locator.evaluateAll
* since: v1.14
@ -1017,19 +933,11 @@ See [`method: Page.evaluateHandle`] for more details.
Optional argument to pass to [`param: expression`].
### option: Locator.evaluateHandle.timeout
### option: Locator.evaluateHandle.timeout = %%-input-timeout-%%
* since: v1.14
* langs: python, java, csharp
- `timeout` <[float]>
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
### option: Locator.evaluateHandle.timeout
### option: Locator.evaluateHandle.timeout = %%-input-timeout-js-%%
* since: v1.14
* langs: js
- `timeout` <[float]>
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `0` - no timeout.
## async method: Locator.fill
* since: v1.14
@ -1150,9 +1058,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]>
@ -1517,7 +1422,7 @@ 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]
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.
@ -1751,21 +1656,16 @@ var banana = await page.GetByRole(AriaRole.Listitem).Nth(2);
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.
Note that when both locators match something, the resulting locator will have multiple matches and violate [locator strictness](../locators.md#strictness) guidelines.
**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();
@ -1774,7 +1674,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();
@ -1783,7 +1683,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()
@ -1792,7 +1692,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()
@ -1801,7 +1701,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();
@ -2098,9 +1998,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>
```
@ -2395,7 +2295,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**

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");
}
@ -197,21 +197,6 @@ The opposite of [`method: LocatorAssertions.toBeVisible`].
### option: LocatorAssertions.NotToBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.NotToContainClass
* since: v1.52
* langs: python
The opposite of [`method: LocatorAssertions.toContainClass`].
### param: LocatorAssertions.NotToContainClass.expected
* since: v1.52
- `expected` <[string]|[Array]<[string]>>
Expected class or RegExp or a list of those.
### option: LocatorAssertions.NotToContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.52
## async method: LocatorAssertions.NotToContainText
* since: v1.20
* langs: python
@ -255,24 +240,6 @@ Expected accessible description.
### 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
@ -475,23 +442,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
@ -574,16 +524,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
@ -761,7 +701,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
@ -1033,107 +973,6 @@ await Expect(
### option: LocatorAssertions.toBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18
## async method: LocatorAssertions.toContainClass
* since: v1.52
* langs:
- alias-java: containsClass
Ensures the [Locator] points to an element with given CSS classes. All classes from the asserted value, separated by spaces, must be present in the [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order.
**Usage**
```html
<div class='middle selected row' id='component'></div>
```
```js
const locator = page.locator('#component');
await expect(locator).toContainClass('middle selected row');
await expect(locator).toContainClass('selected');
await expect(locator).toContainClass('row middle');
```
```java
assertThat(page.locator("#component")).containsClass("middle selected row");
assertThat(page.locator("#component")).containsClass("selected");
assertThat(page.locator("#component")).containsClass("row middle");
```
```python async
from playwright.async_api import expect
locator = page.locator("#component")
await expect(locator).to_contain_class("middle selected row")
await expect(locator).to_contain_class("selected")
await expect(locator).to_contain_class("row middle")
```
```python sync
from playwright.sync_api import expect
locator = page.locator("#component")
expect(locator).to_contain_class("middle selected row")
expect(locator).to_contain_class("selected")
expect(locator).to_contain_class("row middle")
```
```csharp
var locator = Page.Locator("#component");
await Expect(locator).ToContainClassAsync("middle selected row");
await Expect(locator).ToContainClassAsync("selected");
await Expect(locator).ToContainClassAsync("row middle");
```
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class lists. Each element's class attribute is matched against the corresponding class in the array:
```html
<div class='list'></div>
<div class='component inactive'></div>
<div class='component active'></div>
<div class='component inactive'></div>
</div>
```
```js
const locator = page.locator('list > .component');
await expect(locator).toContainClass(['inactive', 'active', 'inactive']);
```
```java
assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
```
```python async
from playwright.async_api import expect
locator = page.locator("list > .component")
await expect(locator).to_contain_class(["inactive", "active", "inactive"])
```
```python sync
from playwright.sync_api import expect
locator = page.locator("list > .component")
await expect(locator).to_contain_class(["inactive", "active", "inactive"])
```
```csharp
var locator = Page.Locator("list > .component");
await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "inactive"});
```
### param: LocatorAssertions.toContainClass.expected
* since: v1.52
- `expected` <[string]|[Array]<[string]>>
A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
### option: LocatorAssertions.toContainClass.timeout = %%-js-assertions-timeout-%%
* since: v1.52
### option: LocatorAssertions.toContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.52
## async method: LocatorAssertions.toContainText
* since: v1.20
* langs:
@ -1342,7 +1181,7 @@ 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");
await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk");
```
### param: LocatorAssertions.toHaveAccessibleDescription.description
@ -1361,56 +1200,6 @@ Expected accessible description.
* 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:
@ -1442,7 +1231,7 @@ expect(locator).to_have_accessible_name("Save to disk")
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).ToHaveAccessibleNameAsync("Save to disk");
await Expect(locator).toHaveAccessibleNameAsync("Save to disk");
```
### param: LocatorAssertions.toHaveAccessibleName.name
@ -1547,48 +1336,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 use [`method: LocatorAssertions.toContainClass`].
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("middle selected row");
assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
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("middle selected row")
await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
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("middle selected row")
expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
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("middle selected row");
await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
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');
@ -2258,7 +2048,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") });
```
@ -2313,91 +2103,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 `.aria.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.aria.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

@ -1041,9 +1041,9 @@ await page.dragAndDrop('#source', '#target', {
```
```java
page.dragAndDrop("#source", "#target");
page.dragAndDrop("#source", '#target');
// or specify exact positions relative to the top-left corners of the elements:
page.dragAndDrop("#source", "#target", new Page.DragAndDropOptions()
page.dragAndDrop("#source", '#target', new Page.DragAndDropOptions()
.setSourcePosition(34, 7).setTargetPosition(10, 20));
```
@ -1217,6 +1217,8 @@ await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches);
// → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches);
// → false
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches);
// → false
```
```java
@ -1225,6 +1227,8 @@ page.evaluate("() => matchMedia('(prefers-color-scheme: dark)').matches");
// → true
page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches");
// → false
page.evaluate("() => matchMedia('(prefers-color-scheme: no-preference)').matches");
// → false
```
```python async
@ -1233,6 +1237,8 @@ await page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches")
# → True
await page.evaluate("matchMedia('(prefers-color-scheme: light)').matches")
# → False
await page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches")
# → False
```
```python sync
@ -1241,6 +1247,7 @@ page.evaluate("matchMedia('(prefers-color-scheme: dark)').matches")
# → True
page.evaluate("matchMedia('(prefers-color-scheme: light)').matches")
# → False
page.evaluate("matchMedia('(prefers-color-scheme: no-preference)').matches")
```
```csharp
@ -1249,6 +1256,8 @@ await page.EvaluateAsync("matchMedia('(prefers-color-scheme: dark)').matches");
// → true
await page.EvaluateAsync("matchMedia('(prefers-color-scheme: light)').matches");
// → false
await page.EvaluateAsync("matchMedia('(prefers-color-scheme: no-preference)').matches");
// → false
```
### option: Page.emulateMedia.media
@ -1272,16 +1281,16 @@ Passing `'Null'` disables CSS media emulation.
* langs: js, java
- `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">>
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. Passing
`null` disables color scheme emulation. `'no-preference'` is deprecated.
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing
`null` disables color scheme emulation.
### option: Page.emulateMedia.colorScheme
* since: v1.9
* langs: csharp, python
- `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">>
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. Passing
`'Null'` disables color scheme emulation. `'no-preference'` is deprecated.
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing
`'Null'` disables color scheme emulation.
### option: Page.emulateMedia.reducedMotion
* since: v1.12
@ -1309,18 +1318,6 @@ Emulates `'forced-colors'` media feature, supported values are `'active'` and `'
* langs: csharp, python
- `forcedColors` <[ForcedColors]<"active"|"none"|"null">>
### option: Page.emulateMedia.contrast
* since: v1.51
* langs: js, java
- `contrast` <null|[Contrast]<"no-preference"|"more">>
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. Passing `null` disables contrast emulation.
### option: Page.emulateMedia.contrast
* since: v1.51
* langs: csharp, python
- `contrast` <[Contrast]<"no-preference"|"more"|"null">>
## async method: Page.evalOnSelector
* since: v1.9
* discouraged: This method does not wait for the element to pass actionability
@ -1719,7 +1716,7 @@ public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType webkit = playwright.webkit();
Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
Browser browser = webkit.launch({ headless: false });
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.exposeBinding("pageURL", (source, args) -> source.page().url());
@ -1889,27 +1886,26 @@ public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
BrowserType webkit = playwright.webkit();
Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
Browser browser = webkit.launch({ headless: false });
Page page = browser.newPage();
page.exposeFunction("sha256", args -> {
String text = (String) args[0];
MessageDigest crypto;
try {
String text = (String) args[0];
MessageDigest crypto = MessageDigest.getInstance("SHA-256");
byte[] token = crypto.digest(text.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(token);
crypto = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
return null;
}
byte[] token = crypto.digest(text.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(token);
});
page.setContent(
"<script>\n" +
page.setContent("<script>\n" +
" async function onClick() {\n" +
" document.querySelector('div').textContent = await window.sha256('PLAYWRIGHT');\n" +
" }\n" +
"</script>\n" +
"<button onclick=\"onClick()\">Click me</button>\n" +
"<div></div>"
);
"<div></div>\n");
page.click("button");
}
}
@ -2110,7 +2106,7 @@ const frame = page.frame({ url: /.*domain.*/ });
```
```java
Frame frame = page.frameByUrl(Pattern.compile(".*domain.*"));
Frame frame = page.frameByUrl(Pattern.compile(".*domain.*");
```
```py
@ -2774,6 +2770,10 @@ This method requires Playwright to be started in a headed mode, with a falsy [`o
Returns the PDF buffer.
:::note
Generating a pdf is currently only supported in Chromium headless.
:::
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
[`method: Page.emulateMedia`] before calling `page.pdf()`:
@ -3161,12 +3161,12 @@ await page.getByRole('button', { name: 'Start here' }).click();
```java
// Setup the handler.
page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () -> {
page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () => {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("No thanks")).click();
});
// Write the test as usual.
page.navigate("https://example.com");
page.goto("https://example.com");
page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
```
@ -3218,12 +3218,12 @@ await page.getByRole('button', { name: 'Start here' }).click();
```java
// Setup the handler.
page.addLocatorHandler(page.getByText("Confirm your security details"), () -> {
page.addLocatorHandler(page.getByText("Confirm your security details")), () => {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Remind me later")).click();
});
// Write the test as usual.
page.navigate("https://example.com");
page.goto("https://example.com");
page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
```
@ -3275,12 +3275,12 @@ await page.getByRole('button', { name: 'Start here' }).click();
```java
// Setup the handler.
page.addLocatorHandler(page.locator("body"), () -> {
page.addLocatorHandler(page.locator("body")), () => {
page.evaluate("window.removeObstructionsForTestIfNeeded()");
}, new Page.AddLocatorHandlerOptions().setNoWaitAfter(true));
}, new Page.AddLocatorHandlerOptions.setNoWaitAfter(true));
// Write the test as usual.
page.navigate("https://example.com");
page.goto("https://example.com");
page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
```
@ -3326,7 +3326,7 @@ await page.addLocatorHandler(page.getByLabel('Close'), async locator => {
```
```java
page.addLocatorHandler(page.getByLabel("Close"), locator -> {
page.addLocatorHandler(page.getByLabel("Close"), locator => {
locator.click();
}, new Page.AddLocatorHandlerOptions().setTimes(1));
```
@ -3607,7 +3607,9 @@ Enabling routing disables http cache.
* since: v1.8
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If [`option: Browser.newContext.baseURL`] is set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
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,
it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
### param: Page.route.handler
* since: v1.8
@ -3697,8 +3699,8 @@ await page.routeWebSocket('/ws', ws => {
```java
page.routeWebSocket("/ws", ws -> {
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.onMessage(message -> {
if ("request".equals(message))
ws.send("response");
});
});
@ -3728,8 +3730,8 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(frame => {
if (frame.Text == "request")
ws.OnMessage(message => {
if (message == "request")
ws.Send("response");
});
});
@ -3980,7 +3982,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: Page.setExtraHTTPHeaders
* since: v1.8

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"));
}
@ -296,18 +296,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
@ -334,17 +323,8 @@ expect(page).to_have_url(re.compile(".*checkout"))
await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
```
### param: PageAssertions.toHaveURL.url
* since: v1.18
* langs: js
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
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.
### param: PageAssertions.toHaveURL.urlOrRegExp
* since: v1.18
* langs: csharp, python, java
- `urlOrRegExp` <[string]|[RegExp]>
Expected URL string or RegExp.
@ -353,7 +333,7 @@ Expected URL string or RegExp.
* 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.
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### 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");
}

View File

@ -102,14 +102,10 @@ await page.RouteAsync("**/*", async route =>
**Details**
The [`option: headers`] option applies to both the routed request and any redirects it initiates. However, [`option: url`], [`option: method`], and [`option: postData`] only apply to the original request and are not carried over to redirected requests.
Note that any overrides such as [`option: url`] or [`option: headers`] only apply to the request being routed. If this request results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header through redirects, use the combination of [`method: Route.fetch`] and [`method: Route.fulfill`] instead.
[`method: Route.continue`] will immediately send the request to the network, other matching handlers won't be invoked. Use [`method: Route.fallback`] If you want next matching handler in the chain to be invoked.
:::warning
The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
:::
### option: Route.continue.url
* since: v1.8
- `url` <[string]>

View File

@ -4,8 +4,6 @@
The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true.
This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch events, see the [emulating legacy touch events](../touch-events.md) page.
## async method: Touchscreen.tap
* since: v1.8

View File

@ -281,80 +281,6 @@ given name prefix inside the [`option: BrowserType.launch.tracesDir`] directory
To specify the final trace zip file name, you need to pass `path` option to
[`method: Tracing.stopChunk`] instead.
## async method: Tracing.group
* since: v1.49
:::caution
Use `test.step` instead when available.
:::
Creates a new group within the trace, assigning any subsequent API calls to this group, until [`method: Tracing.groupEnd`] is called. Groups can be nested and will be visible in the trace viewer.
**Usage**
```js
// use test.step instead
await test.step('Log in', async () => {
// ...
});
```
```java
// All actions between group and groupEnd
// will be shown in the trace viewer as a group.
page.context().tracing().group("Open Playwright.dev > API");
page.navigate("https://playwright.dev/");
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
page.context().tracing().groupEnd();
```
```python sync
# All actions between group and group_end
# will be shown in the trace viewer as a group.
page.context.tracing.group("Open Playwright.dev > API")
page.goto("https://playwright.dev/")
page.get_by_role("link", name="API").click()
page.context.tracing.group_end()
```
```python async
# All actions between group and group_end
# will be shown in the trace viewer as a group.
await page.context.tracing.group("Open Playwright.dev > API")
await page.goto("https://playwright.dev/")
await page.get_by_role("link", name="API").click()
await page.context.tracing.group_end()
```
```csharp
// All actions between GroupAsync and GroupEndAsync
// will be shown in the trace viewer as a group.
await Page.Context.Tracing.GroupAsync("Open Playwright.dev > API");
await Page.GotoAsync("https://playwright.dev/");
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
await Page.Context.Tracing.GroupEndAsync();
```
### param: Tracing.group.name
* since: v1.49
- `name` <[string]>
Group name shown in the trace viewer.
### option: Tracing.group.location
* since: v1.49
- `location` ?<[Object]>
- `file` <[string]>
- `line` ?<[int]>
- `column` ?<[int]>
Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the [`method: Tracing.group`] call.
## async method: Tracing.groupEnd
* since: v1.49
Closes the last group created by [`method: Tracing.group`].
## async method: Tracing.stop
* since: v1.12

View File

@ -1,9 +1,7 @@
# class: WebSocket
* since: v1.8
The [WebSocket] class represents WebSocket connections within a page. It provides the ability to inspect and manipulate the data being transmitted and received.
If you want to intercept or modify WebSocket frames, consider using [WebSocketRoute].
The [WebSocket] class represents websocket connections in the page.
## event: WebSocket.close
* since: v1.8

View File

@ -18,8 +18,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java
page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.onMessage(message -> {
if ("request".equals(message))
ws.send("response");
});
});
@ -47,8 +47,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(frame => {
if (frame.Text == "request")
ws.OnMessage(message => {
if (message == "request")
ws.Send("response");
});
});
@ -70,8 +70,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java
page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
ws.onMessage(message -> {
JsonObject json = new JsonParser().parse(message).getAsJsonObject();
if ("question".equals(json.get("request").getAsString())) {
Map<String, String> result = new HashMap();
result.put("response", "answer");
@ -105,8 +105,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(frame => {
using var jsonDoc = JsonDocument.Parse(frame.Text);
ws.OnMessage(message => {
using var jsonDoc = JsonDocument.Parse(message);
JsonElement root = jsonDoc.RootElement;
if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question")
{
@ -140,11 +140,11 @@ await page.routeWebSocket('/ws', ws => {
```java
page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.onMessage(message -> {
if ("request".equals(message))
server.send("request2");
else
server.send(frame.text());
server.send(message);
});
});
```
@ -180,11 +180,11 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer();
ws.OnMessage(frame => {
if (frame.Text == "request")
ws.OnMessage(message => {
if (message == "request")
server.Send("request2");
else
server.Send(frame.Text);
server.Send(message);
});
});
```
@ -215,13 +215,13 @@ await page.routeWebSocket('/ws', ws => {
```java
page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(frame -> {
if (!"blocked-from-the-page".equals(frame.text()))
server.send(frame.text());
ws.onMessage(message -> {
if (!"blocked-from-the-page".equals(message))
server.send(message);
});
server.onMessage(frame -> {
if (!"blocked-from-the-server".equals(frame.text()))
ws.send(frame.text());
server.onMessage(message -> {
if (!"blocked-from-the-server".equals(message))
ws.send(message);
});
});
```
@ -263,13 +263,13 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer();
ws.OnMessage(frame => {
if (frame.Text != "blocked-from-the-page")
server.Send(frame.Text);
ws.OnMessage(message => {
if (message != "blocked-from-the-page")
server.Send(message);
});
server.OnMessage(frame => {
if (frame.Text != "blocked-from-the-server")
ws.Send(frame.Text);
server.OnMessage(message => {
if (message != "blocked-from-the-server")
ws.Send(message);
});
});
```

View File

@ -259,9 +259,9 @@ Specify environment variables that will be visible to the browser. Defaults to `
- `httpOnly` <[boolean]>
- `secure` <[boolean]>
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> sameSite flag
- `origins` <[Array]<[Object]>>
- `origins` <[Array]<[Object]>> localStorage to set for context
- `origin` <[string]>
- `localStorage` <[Array]<[Object]>> localStorage to set for context
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
@ -639,14 +639,14 @@ If no origin is specified, the username and password are sent to any servers upo
* langs: js, java
- `colorScheme` <null|[ColorScheme]<"light"|"dark"|"no-preference">>
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. See
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
[`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'light'`.
## context-option-colorscheme-csharp-python
* langs: csharp, python
- `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">>
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. See
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
[`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
## context-option-reducedMotion
@ -673,18 +673,6 @@ Emulates `'forced-colors'` media feature, supported values are `'active'`, `'non
Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`.
## context-option-contrast
* langs: js, java
- `contrast` <null|[Contrast]<"no-preference"|"more">>
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See [`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
## context-option-contrast-csharp-python
* langs: csharp, python
- `contrast` <[Contrast]<"no-preference"|"more"|"null">>
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'no-preference'`.
## context-option-logger
* langs: js
- `logger` <[Logger]>
@ -985,8 +973,6 @@ between the same pixel in compared images, between zero (strict) and one (lax),
- %%-context-option-reducedMotion-csharp-python-%%
- %%-context-option-forcedColors-%%
- %%-context-option-forcedColors-csharp-python-%%
- %%-context-option-contrast-%%
- %%-context-option-contrast-csharp-python-%%
- %%-context-option-logger-%%
- %%-context-option-videospath-%%
- %%-context-option-videosize-%%
@ -1015,11 +1001,7 @@ Additional arguments to pass to the browser instance. The list of Chromium flags
## browser-option-channel
- `channel` <[string]>
Browser distribution channel.
Use "chromium" to [opt in to new headless mode](../browsers.md#chromium-new-headless-mode).
Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
## browser-option-chromiumsandbox
- `chromiumSandbox` <[boolean]>
@ -1061,7 +1043,7 @@ Close the browser process on SIGHUP. Defaults to `true`.
Whether to run browser in headless mode. More details for
[Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and
[Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the
[Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the
[`option: BrowserType.launch.devtools`] option is `true`.
## js-python-browser-option-firefoxuserprefs
@ -1154,11 +1136,6 @@ Note that outer and inner locators must belong to the same frame. Inner locator
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element. When passed a [string], matching is case-insensitive and searches for a substring.
## locator-option-visible
- `visible` <[boolean]>
Only matches visible or invisible elements.
## locator-options-list-v1.14
- %%-locator-option-has-text-%%
- %%-locator-option-has-%%
@ -1209,7 +1186,6 @@ Specify screenshot type, defaults to `png`.
Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with
a pink box `#FF00FF` (customized by [`option: maskColor`]) that completely covers its bounding box.
The mask is also applied to invisible elements, see [Matching only visible elements](../locators.md#matching-only-visible-elements) to disable that.
## screenshot-option-mask-color
* since: v1.35
@ -1512,19 +1488,19 @@ page.get_by_text(re.compile("^hello$", re.IGNORECASE))
```java
// Matches <span>
page.getByText("world");
page.getByText("world")
// Matches first <div>
page.getByText("Hello world");
page.getByText("Hello world")
// Matches second <div>
page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
// Matches both <div>s
page.getByText(Pattern.compile("Hello"));
page.getByText(Pattern.compile("Hello"))
// Matches second <div>
page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
```
```csharp
@ -1778,9 +1754,7 @@ await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
- `type` ?<[string]>
* langs: js
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`], [`method: LocatorAssertions.toMatchAriaSnapshot#2`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
You can configure templates for each assertion separately in [`property: TestConfig.expect`].
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
**Usage**
@ -1789,19 +1763,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Single template for all assertions
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
// Assertion-specific templates
expect: {
toHaveScreenshot: {
pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
},
toMatchAriaSnapshot: {
pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
},
},
});
```
@ -1832,22 +1794,22 @@ test.describe('suite', () => {
The list of supported tokens:
* `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be an auto-generated snapshot name.
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* Value: `foo/bar/baz`
* `{ext}` - Snapshot extension (with the leading dot).
* `{ext}` - snapshot extension (with dots)
* Value: `.png`
* `{platform}` - The value of `process.platform`.
* `{projectName}` - Project's file-system-sanitized name, if any.
* Value: `''` (empty string).
* `{snapshotDir}` - Project's [`property: TestProject.snapshotDir`].
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{testDir}` - Project's [`property: TestProject.testDir`].
* Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with config)
* `{testDir}` - Project's [`property: TestConfig.testDir`].
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* Value: `page`
* `{testFileName}` - Test file name with extension.
* Value: `page-click.spec.ts`
* `{testFilePath}` - Relative path from `testDir` to **test file**.
* `{testFilePath}` - Relative path from `testDir` to **test file**
* Value: `page/page-click.spec.ts`
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* Value: `suite-test-should-work`

View File

@ -1,574 +0,0 @@
---
id: aria-snapshots
title: "Snapshot testing"
---
import LiteYouTube from '@site/src/components/LiteYouTube';
## Overview
With Playwright's Snapshot testing you can assert the accessibility tree of a page against a predefined snapshot template.
```js
await page.goto('https://playwright.dev/');
await expect(page.getByRole('banner')).toMatchAriaSnapshot(`
- banner:
- heading /Playwright enables reliable end-to-end/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- link /[\\d]+k\\+ stargazers on GitHub/
`);
```
```python sync
page.goto('https://playwright.dev/')
expect(page.query_selector('banner')).to_match_aria_snapshot("""
- banner:
- heading /Playwright enables reliable end-to-end/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- link /[\\d]+k\\+ stargazers on GitHub/
""")
```
```python async
await page.goto('https://playwright.dev/')
await expect(page.query_selector('banner')).to_match_aria_snapshot("""
- banner:
- heading /Playwright enables reliable end-to-end/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- link /[\\d]+k\\+ stargazers on GitHub/
""")
```
```java
page.navigate("https://playwright.dev/");
assertThat(page.locator("banner")).matchesAriaSnapshot("""
- banner:
- heading /Playwright enables reliable end-to-end/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- link /[\\d]+k\\+ stargazers on GitHub/
""");
```
```csharp
await page.GotoAsync("https://playwright.dev/");
await Expect(page.Locator("banner")).ToMatchAriaSnapshotAsync(@"
- banner:
- heading ""Playwright enables reliable end-to-end testing for modern web apps."" [level=1]
- link ""Get started""
- link ""Star microsoft/playwright on GitHub""
- link /[\\d]+k\\+ stargazers on GitHub/
");
```
<LiteYouTube
id="P4R6hnsE0UY"
title="Getting started with ARIA Snapshots"
/>
## Assertion testing vs Snapshot testing
Snapshot testing and assertion testing serve different purposes in test automation:
### Assertion testing
Assertion testing is a targeted approach where you assert specific values or conditions about elements or components. For instance, with Playwright, [`method: LocatorAssertions.toHaveText`]
verifies that an element contains the expected text, and [`method: LocatorAssertions.toHaveValue`]
confirms that an input field has the expected value.
Assertion tests are specific and generally check the current state of an element or property
against an expected, predefined state.
They work well for predictable, single-value checks but are limited in scope when testing the
broader structure or variations.
**Advantages**
- **Clarity**: The intent of the test is explicit and easy to understand.
- **Specificity**: Tests focus on particular aspects of functionality, making them more robust
against unrelated changes.
- **Debugging**: Failures provide targeted feedback, pointing directly to the problematic aspect.
**Disadvantages**
- **Verbose for complex outputs**: Writing assertions for complex data structures or large outputs
can be cumbersome and error-prone.
- **Maintenance overhead**: As code evolves, manually updating assertions can be time-consuming.
### Snapshot testing
Snapshot testing captures a “snapshot” or representation of the entire
state of an element, component, or data at a given moment, which is then saved for future
comparisons. When re-running tests, the current state is compared to the snapshot, and if there
are differences, the test fails. This approach is especially useful for complex or dynamic
structures, where manually asserting each detail would be too time-consuming. Snapshot testing
is broader and more holistic than assertion testing, allowing you to track more complex changes over time.
**Advantages**
- **Simplifies complex outputs**: For example, testing a UI component's rendered output can be tedious with traditional assertions. Snapshots capture the entire output for easy comparison.
- **Quick Feedback loop**: Developers can easily spot unintended changes in the output.
- **Encourages consistency**: Helps maintain consistent output as code evolves.
**Disadvantages**
- **Over-Reliance**: It can be tempting to accept changes to snapshots without fully understanding
them, potentially hiding bugs.
- **Granularity**: Large snapshots may be hard to interpret when differences arise, especially
if minor changes affect large portions of the output.
- **Suitability**: Not ideal for highly dynamic content where outputs change frequently or
unpredictably.
### When to use
- **Snapshot testing** is ideal for:
- UI testing of whole pages and components.
- Broad structural checks for complex UI components.
- Regression testing for outputs that rarely change structure.
- **Assertion testing** is ideal for:
- Core logic validation.
- Computed value testing.
- Fine-grained tests requiring precise conditions.
By combining snapshot testing for broad, structural checks and assertion testing for specific functionality, you can achieve a well-rounded testing strategy.
## Aria snapshots
In Playwright, aria snapshots provide a YAML representation of the accessibility tree of a page.
These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined
expectations.
The YAML format describes the hierarchical structure of accessible elements on the page, detailing **roles**, **attributes**, **values**, and **text content**.
The structure follows a tree-like syntax, where each node represents an accessible element, and indentation indicates
nested elements.
Each accessible element in the tree is represented as a YAML node:
```yaml
- role "name" [attribute=value]
```
- **role**: Specifies the ARIA or HTML role of the element (e.g., `heading`, `list`, `listitem`, `button`).
- **"name"**: Accessible name of the element. Quoted strings indicate exact values, `/patterns/` are used for regular expression.
- **[attribute=value]**: Attributes and values, in square brackets, represent specific ARIA attributes, such
as `checked`, `disabled`, `expanded`, `level`, `pressed`, or `selected`.
These values are derived from ARIA attributes or calculated based on HTML semantics. To inspect the accessibility tree
structure of a page, use the [Chrome DevTools Accessibility Tab](https://developer.chrome.com/docs/devtools/accessibility/reference#tab).
## Snapshot matching
The [`method: LocatorAssertions.toMatchAriaSnapshot`] assertion method in Playwright compares the accessible
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
testing requirements.
For the following DOM:
```html
<h1>title</h1>
```
You can match it using the following snapshot template:
```js
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "title"
`);
```
```python sync
expect(page.locator("body")).to_match_aria_snapshot("""
- heading "title"
""")
```
```python async
await expect(page.locator("body")).to_match_aria_snapshot("""
- heading "title"
""")
```
```java
assertThat(page.locator("body")).matchesAriaSnapshot("""
- heading "title"
""");
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
- heading ""title""
");
```
When matching, the snapshot template is compared to the current accessibility tree of the page:
* If the tree structure matches the template, the test passes; otherwise, it fails, indicating a mismatch between
expected and actual accessibility states.
* The comparison is case-sensitive and collapses whitespace, so indentation and line breaks are ignored.
* The comparison is order-sensitive, meaning the order of elements in the snapshot template must match the order in the
page's accessibility tree.
### Partial matching
You can perform partial matches on nodes by omitting attributes or accessible names, enabling verification of specific
parts of the accessibility tree without requiring exact matches. This flexibility is helpful for dynamic or irrelevant
attributes.
```html
<button>Submit</button>
```
*aria snapshot*
```yaml
- button
```
In this example, the button role is matched, but the accessible name ("Submit") is not specified, allowing the test to
pass regardless of the button's label.
<hr/>
For elements with ARIA attributes like `checked` or `disabled`, omitting these attributes allows partial matching,
focusing solely on role and hierarchy.
```html
<input type="checkbox" checked>
```
*aria snapshot for partial match*
```yaml
- checkbox
```
In this partial match, the `checked` attribute is ignored, so the test will pass regardless of the checkbox state.
<hr/>
Similarly, you can partially match children in lists or groups by omitting specific list items or nested elements.
```html
<ul>
<li>Feature A</li>
<li>Feature B</li>
<li>Feature C</li>
</ul>
```
*aria snapshot for partial match*
```yaml
- list
- listitem: Feature B
```
Partial matches let you create flexible snapshot tests that verify essential page structure without enforcing
specific content or attributes.
### Strict matching
By default, a template containing the subset of children will be matched:
```html
<ul>
<li>Feature A</li>
<li>Feature B</li>
<li>Feature C</li>
</ul>
```
*aria snapshot for partial match*
```yaml
- list
- listitem: Feature B
```
The `/children` property can be used to control how child elements are matched:
- `contain` (default): Matches if all specified children are present in order
- `equal`: Matches if the children exactly match the specified list in order
- `deep-equal`: Matches if the children exactly match the specified list in order, including nested children
```html
<ul>
<li>Feature A</li>
<li>Feature B</li>
<li>Feature C</li>
</ul>
```
*aria snapshot will fail due to Feature C not being in the template*
```yaml
- list
- /children: equal
- listitem: Feature A
- listitem: Feature B
```
### Matching with regular expressions
Regular expressions allow flexible matching for elements with dynamic or variable text. Accessible names and text can
support regex patterns.
```html
<h1>Issues 12</h1>
```
*aria snapshot with regular expression*
```yaml
- heading /Issues \d+/
```
## Generating snapshots
Creating aria snapshots in Playwright helps ensure and maintain your application's structure.
You can generate snapshots in various ways depending on your testing setup and workflow.
### Generating snapshots with the Playwright code generator
If you're using Playwright's [Code Generator](./codegen.md), generating aria snapshots is streamlined with its
interactive interface:
- **"Assert snapshot" Action**: In the code generator, you can use the "Assert snapshot" action to automatically create
a snapshot assertion for the selected elements. This is a quick way to capture the aria snapshot as part of your
recorded test flow.
- **"Aria snapshot" Tab**: The "Aria snapshot" tab within the code generator interface visually represents the
aria snapshot for a selected locator, letting you explore, inspect, and verify element roles, attributes, and
accessible names to aid snapshot creation and review.
### Updating snapshots with `@playwright/test` and the `--update-snapshots` flag
* langs: js
When using the Playwright test runner (`@playwright/test`), you can automatically update snapshots with the `--update-snapshots` flag, `-u` for short.
Running tests with the `--update-snapshots` flag will update snapshots that did not match. Matching snapshots will not be updated.
```bash
npx playwright test --update-snapshots
```
Updating snapshots is useful when application structure changes require new snapshots as a baseline. Note that Playwright will wait for the maximum expect timeout specified in the test runner configuration to ensure the page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout while generating snapshots.
#### Empty template for snapshot generation
Passing an empty string as the template in an assertion generates a snapshot on-the-fly:
```js
await expect(locator).toMatchAriaSnapshot('');
```
Note that Playwright will wait for the maximum expect timeout specified in the test runner configuration to ensure the
page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout
while generating snapshots.
#### Snapshot patch files
When updating snapshots, Playwright creates patch files that capture differences. These patch files can be reviewed,
applied, and committed to source control, allowing teams to track structural changes over time and ensure updates are
consistent with application requirements.
The way source code is updated can be changed using the `--update-source-method` flag. There are several options available:
- **"patch"** (default): Generates a unified diff file that can be applied to the source code using `git apply`.
- **"3way"**: Generates merge conflict markers in your source code, allowing you to choose whether to accept changes.
- **"overwrite"**: Overwrites the source code with the new snapshot values.
```bash
npx playwright test --update-snapshots --update-source-method=3way
```
#### Snapshots as separate files
To store your snapshots in a separate file, use the `toMatchAriaSnapshot` method with the `name` option, specifying a `.aria.yml` file extension.
```js
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.aria.yml' });
```
By default, snapshots from a test file `example.spec.ts` are placed in the `example.spec.ts-snapshots` directory. As snapshots should be the same across browsers, only one snapshot is saved even if testing with multiple browsers. Should you wish, you can customize the [snapshot path template](./api/class-testconfig#test-config-snapshot-path-template) using the following configuration:
```js
export default defineConfig({
expect: {
toMatchAriaSnapshot: {
pathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
},
},
});
```
### Using the `Locator.ariaSnapshot` method
The [`method: Locator.ariaSnapshot`] method allows you to programmatically create a YAML representation of accessible
elements within a locator's scope, especially helpful for generating snapshots dynamically during test execution.
**Example**:
```js
const snapshot = await page.locator('body').ariaSnapshot();
console.log(snapshot);
```
```python sync
snapshot = page.locator("body").aria_snapshot()
print(snapshot)
```
```python async
snapshot = await page.locator("body").aria_snapshot()
print(snapshot)
```
```java
String snapshot = page.locator("body").ariaSnapshot();
System.out.println(snapshot);
```
```csharp
var snapshot = await page.Locator("body").AriaSnapshotAsync();
Console.WriteLine(snapshot);
```
This command outputs the aria snapshot within the specified locator's scope in YAML format, which you can validate
or store as needed.
## Accessibility tree examples
### Headings with level attributes
Headings can include a `level` attribute indicating their heading level.
```html
<h1>Title</h1>
<h2>Subtitle</h2>
```
*aria snapshot*
```yaml
- heading "Title" [level=1]
- heading "Subtitle" [level=2]
```
### Text nodes
Standalone or descriptive text elements appear as text nodes.
```html
<div>Sample accessible name</div>
```
*aria snapshot*
```yaml
- text: Sample accessible name
```
### Inline multiline text
Multiline text, such as paragraphs, is normalized in the aria snapshot.
```html
<p>Line 1<br>Line 2</p>
```
*aria snapshot*
```yaml
- paragraph: Line 1 Line 2
```
### Links
Links display their text or composed content from pseudo-elements.
```html
<a href="#more-info">Read more about Accessibility</a>
```
*aria snapshot*
```yaml
- link "Read more about Accessibility"
```
### Text boxes
Input elements of type `text` show their `value` attribute content.
```html
<input type="text" value="Enter your name">
```
*aria snapshot*
```yaml
- textbox: Enter your name
```
### Lists with items
Ordered and unordered lists include their list items.
```html
<ul aria-label="Main Features">
<li>Feature 1</li>
<li>Feature 2</li>
</ul>
```
*aria snapshot*
```yaml
- list "Main Features":
- listitem: Feature 1
- listitem: Feature 2
```
### Grouped elements
Groups capture nested elements, such as `<details>` elements with summary content.
```html
<details>
<summary>Summary</summary>
<p>Detail content here</p>
</details>
```
*aria snapshot*
```yaml
- group: Summary
```
### Attributes and states
Commonly used ARIA attributes, like `checked`, `disabled`, `expanded`, `level`, `pressed`, and `selected`, represent
control states.
#### Checkbox with `checked` attribute
```html
<input type="checkbox" checked>
```
*aria snapshot*
```yaml
- checkbox [checked]
```
#### Button with `pressed` attribute
```html
<button aria-pressed="true">Toggle</button>
```
*aria snapshot*
```yaml
- button "Toggle" [pressed=true]
```

View File

@ -13,11 +13,6 @@ Regardless of the authentication strategy you choose, you are likely to store au
We recommend to create `playwright/.auth` directory and add it to your `.gitignore`. Your authentication routine will produce authenticated browser state and save it to a file in this `playwright/.auth` directory. Later on, tests will reuse this state and start already authenticated.
:::danger
The browser state file may contain sensitive cookies and headers that could be used to impersonate you or your test account.
We strongly discourage checking them into private or public repositories.
:::
```bash tab=bash-bash
mkdir -p playwright/.auth
echo $'\nplaywright/.auth' >> .gitignore
@ -237,7 +232,7 @@ await page.goto('https://github.com/login')
# Interact with login form
await page.get_by_label("Username or email address").fill("username")
await page.get_by_label("Password").fill("password")
await page.get_by_role("button", name="Sign in").click()
await page.page.get_by_role("button", name="Sign in").click()
# Continue with the test
```
@ -271,9 +266,9 @@ existing authentication state instead.
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
in only once and then skip the log in step for all of the tests.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) or in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
Cookies, local storage and IndexedDB state can be used across different browsers. They depend on your application's authentication model which may require some combination of cookies, local storage or IndexedDB.
Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
The following code snippet retrieves state from an authenticated context and creates a new context with that state.
@ -588,7 +583,7 @@ test('admin and user', async ({ adminPage, userPage }) => {
### Session storage
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
```js
// Get session storage and store as env variable

View File

@ -90,7 +90,7 @@ await page
#### Prefer user-facing attributes to XPath or CSS selectors
Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change, thus breaking your test.
Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change breaking your test.
```js
@ -112,41 +112,10 @@ Playwright has a [test generator](./codegen.md) that can generate tests and pick
To pick a locator run the `codegen` command followed by the URL that you would like to pick a locator from.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright codegen playwright.dev
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright codegen playwright.dev
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright codegen playwright.dev
```
</TabItem>
</Tabs>
This will open a new browser window as well as the Playwright inspector. To pick a locator first click on the 'Record' button to stop the recording. By default when you run the `codegen` command it will start a new recording. Once you stop the recording the 'Pick Locator' button will be available to click.
You can then hover over any element on your page in the browser window and see the locator highlighted below your cursor. Clicking on an element will add the locator into the Playwright inspector. You can either copy the locator and paste into your test file or continue to explore the locator by editing it in the Playwright Inspector, for example by modifying the text, and seeing the results in the browser window.
@ -201,41 +170,10 @@ You can live debug your test by clicking or editing the locators in your test in
You can also debug your tests with the Playwright inspector by running your tests with the `--debug` flag.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright test --debug
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --debug
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --debug
```
</TabItem>
</Tabs>
You can then step through your test, view actionability logs and edit the locator live and see it highlighted in the browser window. This will show you which locators match, how many of them there are.
<img width="1350" alt="debugging with the playwright inspector" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212276296-4f5b18e7-2bd7-4766-9aa5-783517bd4aa2.png" />
@ -244,40 +182,9 @@ You can then step through your test, view actionability logs and edit the locato
To debug a specific test add the name of the test file and the line number of the test followed by the `--debug` flag.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright test example.spec.ts:9 --debug
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test example.spec.ts:9 --debug
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test example.spec.ts:9 --debug
```
</TabItem>
</Tabs>
#### Debugging on CI
For CI failures, use the Playwright [trace viewer](./trace-viewer.md) instead of videos and screenshots. The trace viewer gives you a full trace of your tests as a local Progressive Web App (PWA) that can easily be shared. With the trace viewer you can view the timeline, inspect DOM snapshots for each action using dev tools, view network requests and more.
@ -286,77 +193,14 @@ For CI failures, use the Playwright [trace viewer](./trace-viewer.md) instead of
Traces are configured in the Playwright config file and are set to run on CI on the first retry of a failed test. We don't recommend setting this to `on` so that traces are run on every test as it's very performance heavy. However you can run a trace locally when developing with the `--trace` flag.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright test --trace on
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --trace on
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --trace on
```
</TabItem>
</Tabs>
Once you run this command your traces will be recorded for each test and can be viewed directly from the HTML report.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright show-report
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright show-report
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright show-report
```
</TabItem>
</Tabs>
````
<img width="1516" alt="Playwrights HTML report" loading="lazy" src="https://user-images.githubusercontent.com/13063165/212279022-d929d4c0-2271-486a-a75f-166ac231d25f.png" />
@ -402,101 +246,23 @@ export default defineConfig({
By keeping your Playwright version up to date you will be able to test your app on the latest browser versions and catch failures before the latest browser version is released to the public.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npm install -D @playwright/test@latest
```
</TabItem>
<TabItem value="yarn">
```bash
yarn add --dev @playwright/test@latest
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm install --save-dev @playwright/test@latest
```
</TabItem>
</Tabs>
Check the [release notes](./release-notes.md) to see what the latest version is and what changes have been released.
You can see what version of Playwright you have by running the following command.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright --version
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright --version
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright --version
```
</TabItem>
</Tabs>
### Run tests on CI
Setup CI/CD and run your tests frequently. The more often you run your tests the better. Ideally you should run your tests on each commit and pull request. Playwright comes with a [GitHub actions workflow](/ci-intro.md) so that tests will run on CI for you with no setup required. Playwright can also be setup on the [CI environment](/ci.md) of your choice.
Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI. Consider setting up [Sharding](./test-sharding.md) to make CI faster.
#### Optimize browser downloads on CI
Only install the browsers that you actually need, especially on CI. For example, if you're only testing with Chromium, install just Chromium.
```bash title=".github/workflows/playwright.yml"
# Instead of installing all browsers
npx playwright install --with-deps
# Install only Chromium
npx playwright install chromium --with-deps
```
This saves both download time and disk space on your CI machines.
### Lint your tests
We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature.
@ -516,41 +282,10 @@ test('runs in parallel 2', async ({ page }) => { /* ... */ });
Playwright can [shard](./test-parallel.md#shard-tests-between-multiple-machines) a test suite, so that it can be executed on multiple machines.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'yarn', value: 'yarn'},
{label: 'pnpm', value: 'pnpm'}
]
}>
<TabItem value="npm">
```bash
npx playwright test --shard=1/3
```
</TabItem>
<TabItem value="yarn">
```bash
yarn playwright test --shard=1/3
```
</TabItem>
<TabItem value="pnpm">
```bash
pnpm exec playwright test --shard=1/3
```
</TabItem>
</Tabs>
## Productivity tips
### Use Soft assertions

View File

@ -338,123 +338,16 @@ dotnet test --settings:webkit.runsettings
For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later.
### Chromium: headless shell
Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode.
If you are only running tests in headless shell (i.e. the `channel` option is **not** specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation.
```bash js
# only running tests headlessly
npx playwright install --with-deps --only-shell
```
```bash java
# only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --only-shell"
```
```bash python
# only running tests headlessly
playwright install --with-deps --only-shell
```
```bash csharp
# only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell
```
### Chromium: new headless mode
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
```js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
},
],
});
```
```java
import com.microsoft.playwright.*;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
Page page = browser.newPage();
// ...
}
}
}
```
```bash python
pytest test_login.py --browser-channel chromium
```
```xml csharp
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
<LaunchOptions>
<Channel>chromium</Channel>
</LaunchOptions>
</Playwright>
</RunSettings>
```
```bash csharp
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
```
With the new headless mode, you can skip downloading the headless shell during browser installation by using the `--no-shell` option:
```bash js
# only running tests headlessly
npx playwright install --with-deps --no-shell
```
```bash java
# only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --no-shell"
```
```bash python
# only running tests headlessly
playwright install --with-deps --no-shell
```
```bash csharp
# only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --no-shell
```
### Google Chrome & Microsoft Edge
While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers.
Available channels are `chrome`, `msedge`, `chrome-beta`, `msedge-beta`, `chrome-dev`, `msedge-dev`, `chrome-canary`, `msedge-canary`.
Available channels are `chrome`, `msedge`, `chrome-beta`, `msedge-beta` or `msedge-dev`.
:::warning
Certain Enterprise Browser Policies may impact Playwright's ability to launch and control Google Chrome and Microsoft Edge. Running in an environment with browser policies is outside of the Playwright project's scope.
:::
:::warning
Google Chrome and Microsoft Edge have switched to a [new headless mode](https://developer.chrome.com/docs/chromium/headless) implementation that is closer to a regular headed mode. This differs from [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) that is used in Playwright by default when running headless, so expect different behavior in some cases. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
:::
```js
import { defineConfig, devices } from '@playwright/test';
@ -508,23 +401,6 @@ pytest test_login.py --browser-channel msedge
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=msedge
```
######
* langs: python
Alternatively when using the library directly, you can specify the browser [`option: BrowserType.launch.channel`] when launching the browser:
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Channel can be "chrome", "msedge", "chrome-beta", "msedge-beta" or "msedge-dev".
browser = p.chromium.launch(channel="msedge")
page = browser.new_page()
page.goto("https://playwright.dev")
print(page.title())
browser.close()
```
#### Installing Google Chrome & Microsoft Edge
If Google Chrome or Microsoft Edge is not available on your machine, you can install

View File

@ -18,6 +18,9 @@ Using a canary release in production might seem risky, but in practice, it's not
A canary release passes all automated tests and is used to test e.g. the HTML report, Trace Viewer, or Playwright Inspector with end-to-end tests.
:::
```txt
npm install -D @playwright/test@next
```
## Next npm Dist Tag
@ -31,7 +34,7 @@ You can see on [npm](https://www.npmjs.com/package/@playwright/test?activeTab=ve
## Using a Canary Release
```bash
```txt
npm install -D @playwright/test@next
```

View File

@ -9,9 +9,7 @@ title: "Chrome extensions"
Extensions only work in Chrome / Chromium launched with a persistent context. Use custom browser args at your own risk, as some of them may break Playwright functionality.
:::
The snippet below retrieves the [background page](https://developer.chrome.com/extensions/background_pages) of a [Manifest v2](https://developer.chrome.com/docs/extensions/mv2/) extension whose source is located in `./my-extension`.
Note the use of the `chromium` channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode.
The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of a [Manifest v2](https://developer.chrome.com/docs/extensions/mv2/) extension whose source is located in `./my-extension`:
```js
const { chromium } = require('playwright');
@ -20,7 +18,7 @@ const { chromium } = require('playwright');
const pathToExtension = require('path').join(__dirname, 'my-extension');
const userDataDir = '/tmp/test-user-data-dir';
const browserContext = await chromium.launchPersistentContext(userDataDir, {
channel: 'chromium',
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`
@ -46,7 +44,7 @@ user_data_dir = "/tmp/test-user-data-dir"
async def run(playwright: Playwright):
context = await playwright.chromium.launch_persistent_context(
user_data_dir,
channel="chromium",
headless=False,
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
@ -80,7 +78,7 @@ user_data_dir = "/tmp/test-user-data-dir"
def run(playwright: Playwright):
context = playwright.chromium.launch_persistent_context(
user_data_dir,
channel="chromium",
headless=False,
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
@ -103,8 +101,6 @@ with sync_playwright() as playwright:
To have the extension loaded when running tests you can use a test fixture to set the context. You can also dynamically retrieve the extension id and use it to load and test the popup page for example.
Note the use of the `chromium` channel that allows to run extensions in headless mode. Alternatively, you can launch the browser in headed mode.
First, add fixtures that will load the extension:
```js title="fixtures.ts"
@ -118,7 +114,7 @@ export const test = base.extend<{
context: async ({ }, use) => {
const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', {
channel: 'chromium',
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
@ -159,7 +155,7 @@ def context(playwright: Playwright) -> Generator[BrowserContext, None, None]:
path_to_extension = Path(__file__).parent.joinpath("my-extension")
context = playwright.chromium.launch_persistent_context(
"",
channel="chromium",
headless=False,
args=[
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
@ -215,3 +211,39 @@ def test_popup_page(page: Page, extension_id: str) -> None:
page.goto(f"chrome-extension://{extension_id}/popup.html")
expect(page.locator("body")).to_have_text("my-extension popup")
```
## Headless mode
:::danger
`headless=new` mode is not officially supported by Playwright and might result in unexpected behavior.
:::
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using the following code:
```js title="fixtures.ts"
// ...
const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--headless=new`,
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
// ...
```
```python title="conftest.py"
path_to_extension = Path(__file__).parent.joinpath("my-extension")
context = playwright.chromium.launch_persistent_context(
"",
headless=False,
args=[
"--headless=new",
f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}",
],
)
```

View File

@ -21,7 +21,7 @@ Playwright tests can be run on any CI provider. This guide covers one way of run
## Introduction
* langs: python, java, csharp
Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
#### You will learn
* langs: python, java, csharp
@ -69,11 +69,11 @@ jobs:
The workflow performs these steps:
1. Clone your repository
1. Install Node.js
1. Install NPM Dependencies
1. Install Playwright Browsers
1. Run Playwright tests
1. Upload HTML report to the GitHub UI
2. Install Node.js
3. Install NPM Dependencies
4. Install Playwright Browsers
5. Run Playwright tests
6. Upload HTML report to the GitHub UI
To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
@ -169,10 +169,10 @@ To learn more about this, see ["Understanding GitHub Actions"](https://docs.gith
Looking at the list of steps in `jobs.test.steps`, you can see that the workflow performs these steps:
1. Clone your repository
1. Install language dependencies
1. Install project dependencies and build
1. Install Playwright Browsers
1. Run tests
2. Install language dependencies
3. Install project dependencies and build
4. Install Playwright Browsers
5. Run tests
## Create a Repo and Push to GitHub
@ -291,7 +291,7 @@ Downloading the HTML report as a zip file is not very convenient. However, we ca
- `AZCOPY_SPA_CLIENT_SECRET`
- `AZCOPY_TENANT_ID`
For a detailed guide on how to authorize a service principal using a client secret, refer to [this Microsoft documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-authorize-azure-active-directory#authorize-a-service-principal-by-using-a-client-secret).
For a detailed guide on how to authorize a service principal using a client secret, refer to [this Microsoft documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-authorize-azure-active-directory#authorize-a-service-principal-by-using-a-client-secret-1).
1. Add a step that uploads the HTML report to Azure Storage.
```yaml title=".github/workflows/playwright.yml"
@ -312,7 +312,7 @@ Downloading the HTML report as a zip file is not very convenient. However, we ca
The contents of the `$web` storage container can be accessed from a browser by using the [public URL](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website-how-to?tabs=azure-portal#portal-find-url) of the website.
:::note
This step will not work for pull requests created from a forked repository because such workflow [doesn't have access to the secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#using-secrets-in-a-workflow).
This step will not work for pull requests created from a forked repository because such workflow [doesn't have access to the secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow).
:::

View File

@ -208,7 +208,7 @@ jobs:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v%%VERSION%%-noble
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
options: --user 1001
steps:
- uses: actions/checkout@v4
@ -233,7 +233,7 @@ jobs:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-noble
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
options: --user 1001
steps:
- uses: actions/checkout@v4
@ -262,7 +262,7 @@ jobs:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-noble
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
options: --user 1001
steps:
- uses: actions/checkout@v4
@ -288,7 +288,7 @@ jobs:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-noble
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
options: --user 1001
steps:
- uses: actions/checkout@v4
@ -415,7 +415,7 @@ Large test suites can take very long to execute. By executing a preliminary test
This will give you a faster feedback loop and slightly lower CI consumption while working on Pull Requests.
To detect test files affected by your changeset, `--only-changed` analyses your suites' dependency graph. This is a heuristic and might miss tests, so it's important that you always run the full test suite after the preliminary test run.
```yml js title=".github/workflows/playwright.yml" {24-26}
```yml js title=".github/workflows/playwright.yml" {20-23}
name: Playwright Tests
on:
push:
@ -454,11 +454,19 @@ jobs:
### Docker
We have a [pre-built Docker image](./docker.md) which can either be used directly or as a reference to update your existing Docker definitions. Make sure to follow the [Recommended Docker Configuration](./docker.md#recommended-docker-configuration) to ensure the best performance.
We have a [pre-built Docker image](./docker.md) which can either be used directly, or as a reference to update your existing Docker definitions.
Suggested configuration
1. Using `--ipc=host` is also recommended when using Chromium. Without it Chromium can run out of memory
and crash. Learn more about this option in [Docker docs](https://docs.docker.com/engine/reference/run/#ipc-settings---ipc).
1. Seeing other weird errors when launching Chromium? Try running your container
with `docker run --cap-add=SYS_ADMIN` when developing locally.
1. Using `--init` Docker flag or [dumb-init](https://github.com/Yelp/dumb-init) is recommended to avoid special
treatment for processes with PID=1. This is a common reason for zombie processes.
### Azure Pipelines
For Windows or macOS agents, no additional configuration is required, just install Playwright and run your tests.
For Windows or macOS agents, no additional configuration required, just install Playwright and run your tests.
For Linux agents, you can use [our Docker container](./docker.md) with Azure
Pipelines support [running containerized
@ -758,28 +766,28 @@ Running Playwright on CircleCI is very similar to running on GitHub Actions. In
```yml js
executors:
pw-noble-development:
pw-jammy-development:
docker:
- image: mcr.microsoft.com/playwright:v%%VERSION%%-noble
```
```yml python
executors:
pw-noble-development:
pw-jammy-development:
docker:
- image: mcr.microsoft.com/playwright/python:v%%VERSION%%-noble
```
```yml java
executors:
pw-noble-development:
pw-jammy-development:
docker:
- image: mcr.microsoft.com/playwright/java:v%%VERSION%%-noble
```
```yml csharp
executors:
pw-noble-development:
pw-jammy-development:
docker:
- image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-noble
```
@ -793,10 +801,10 @@ Sharding in CircleCI is indexed with 0 which means that you will need to overrid
```yml
playwright-job-name:
executor: pw-noble-development
executor: pw-jammy-development
parallelism: 4
steps:
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npx playwright test --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npx playwright test -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
```
### Jenkins
@ -989,7 +997,7 @@ type: docker
steps:
- name: test
image: mcr.microsoft.com/playwright:v%%VERSION%%-noble
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
commands:
- npx playwright test
```

View File

@ -34,10 +34,6 @@ The recommended approach is to use `setFixedTime` to set the time to a specific
- `Event.timeStamp`
:::
:::warning
If you call `install` at any point in your test, the call _MUST_ occur before any other clock related calls (see note above for list). Calling these methods out of order will result in undefined behavior. For example, you cannot call `setInterval`, followed by `install`, then `clearInterval`, as `install` overrides the native definition of the clock functions.
:::
## Test with predefined time
Often you only need to fake `Date.now` while keeping the timers going.
@ -168,11 +164,11 @@ await Page.GotoAsync("http://localhost:3333");
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
// Assert the page state.
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM");
// Close the laptop lid again and open it at 10:30am.
await Page.Clock.FastForwardAsync("30:00");
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM");
```
## Test inactivity monitoring

View File

@ -21,7 +21,7 @@ Install the VS Code extension and generate tests directly from VS Code. The exte
### Record a New Test
To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window.
To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window.
<img width="1385" alt="record new in vs code" src="https://user-images.githubusercontent.com/13063165/220961665-615d0ab8-3f0b-439c-ad0b-0424d9aa154b.png" />
@ -58,16 +58,16 @@ In the test file in VS Code you will see your new generated actions added to you
### Generating locators
You can generate locators with the test generator.
- Click on the **Pick locator** button form the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element.
- Click the element you require and it will now show up in the **Pick locator** box in VS Code.
You can generate locators with the test generator.
- Click on the **Pick locator** button form the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element.
- Click the element you require and it will now show up in the **Pick locator** box in VS Code.
- Press <kbd>Enter</kbd> on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel.
<img width="1641" alt="Pick locators in VS code" src="https://user-images.githubusercontent.com/13063165/220958368-95b03620-3c9b-40a8-be74-01c96ba03cad.png" />
## Generate tests with the Playwright Inspector
When running the `codegen` command two windows will be opened, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests and then copy them into your editor.
When running the `codegen` command two windows will be opened, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests and then copy them into your editor.
### Running Codegen
@ -128,10 +128,10 @@ When you have finished interacting with the page, press the **record** button to
Use the **clear** button to clear the code to start recording again. Once finished, close the Playwright inspector window or stop the terminal command.
### Generating locators
You can generate [locators](/locators.md) with the test generator.
You can generate [locators](/locators.md) with the test generator.
* Press the `'Record'` button to stop the recording and the `'Pick Locator'` button will appear.
* Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element.
* Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element.
* To choose a locator, click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button.
* You can then edit the locator in this field to fine tune it or use the copy button to copy it and paste it into your code.
@ -164,19 +164,19 @@ You can use the test generator to generate tests using emulation so as to genera
Playwright opens a browser window with its viewport set to a specific width and height and is not responsive as tests need to be run under the same conditions. Use the `--viewport` option to generate tests with a different viewport size.
```bash js
npx playwright codegen --viewport-size="800,600" playwright.dev
npx playwright codegen --viewport-size=800,600 playwright.dev
```
```bash java
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size='800,600' playwright.dev"
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size=800,600 playwright.dev"
```
```bash python
playwright codegen --viewport-size="800,600" playwright.dev
playwright codegen --viewport-size=800,600 playwright.dev
```
```bash csharp
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size="800,600" playwright.dev
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size=800,600 playwright.dev
```
######
* langs: js
@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio
### Preserve authenticated state
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
```bash js
npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json
@ -367,7 +367,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen github.com/microsoft/playwright --sav
#### Login
After performing authentication and closing the browser, `auth.json` will contain the storage state which you can then reuse in your tests.
After performing authentication and closing the browser, `auth.json` will contain the storage state which you can then reuse in your tests.
<img width="1394" alt="login to GitHub screen" src="https://user-images.githubusercontent.com/13063165/220561688-04b2b984-4ba6-4446-8b0a-8058876e2a02.png" />
@ -375,7 +375,7 @@ Make sure you only use the `auth.json` locally as it contains sensitive informat
#### Load authenticated state
Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state.
Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state.
```bash js
npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright

View File

@ -44,10 +44,6 @@ A browser window will open and the test will run and pause at where the breakpoi
<img width="1269" alt="running test in debug mode" src="https://user-images.githubusercontent.com/13063165/212740233-3f278825-13e7-4a88-a118-dd4478d43a16.png" />
### Debug Tests Using Chrome DevTools
Instead of using `Debug Test`, choose `Run Test` in VS Code. With `Show Browser` enabled, the browser session is reused, letting you open Chrome DevTools for continuous debugging of your tests and the web application.
### Debug in different Browsers
By default, debugging is done using the Chromium profile. You can debug your tests on different browsers by right clicking on the debug icon in the testing sidebar and clicking on the 'Select Default Profile' option from the dropdown.
@ -229,6 +225,7 @@ Playwright [Trace Viewer](/trace-viewer.md) is a GUI tool that lets you explore
<video width="100%" height="100%" controls muted>
<source src="https://user-images.githubusercontent.com/13063165/219132713-17b9d75b-71e3-42c4-a43f-3f9e2e15f834.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
To learn more about how to record traces and use the Trace Viewer, check out the [Trace Viewer](/trace-viewer.md) guide.

View File

@ -5,7 +5,7 @@ title: "Docker"
## Introduction
[Dockerfile.noble] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately.
[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately.
## Usage
@ -94,102 +94,15 @@ docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_prof
}
```
### Recommended Docker Configuration
:::note
Using `--ipc=host` is recommended when using Chrome ([Docker docs](https://docs.docker.com/engine/reference/run/#ipc-settings---ipc)). Chrome can run out of memory without this flag.
:::
When running Playwright in Docker, the following configuration is recommended:
1. **Using [`--init`](https://docs.docker.com/reference/cli/docker/container/run/#init)** Docker flag is recommended to avoid special treatment for processes with PID=1. This is a common reason for zombie processes.
1. **Using `--ipc=host`** is recommended when using Chromium. Without it, Chromium can run out of memory and crash. Learn more about this option in [Docker docs](https://docs.docker.com/reference/cli/docker/container/run/#ipc).
1. **If seeing weird errors when launching Chromium**, try running your container with `docker run --cap-add=SYS_ADMIN` when developing locally.
### Using on CI
See our [Continuous Integration guides](./ci.md) for sample configs.
### Remote Connection
You can run Playwright Server in Docker while keeping your tests running on the host system or another machine. This is useful for running tests on unsupported Linux distributions or remote execution scenarios.
#### Running the Playwright Server
Start the Playwright Server in Docker:
```bash
docker run -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
```
#### Connecting to the Server
* langs: js
There are two ways to connect to the remote Playwright server:
1. Using environment variable with `@playwright/test`:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/ npx playwright test
```
2. Using the [`method: BrowserType.connect`] API for other applications:
```js
const browser = await playwright['chromium'].connect('ws://127.0.0.1:3000/');
```
#### Connecting to the Server
* langs: python, csharp, java
```python sync
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect("ws://127.0.0.1:3000/")
```
```python async
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect("ws://127.0.0.1:3000/")
```
```csharp
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.ConnectAsync("ws://127.0.0.1:3000/");
```
```java
package org.example;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect("ws://127.0.0.1:3000/");
}
}
}
```
#### Network Configuration
If you need to access local servers from within the Docker container:
```bash
docker run --add-host=hostmachine:host-gateway -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
```
This makes `hostmachine` point to the host's localhost. Your tests should use `hostmachine` instead of `localhost` when accessing local servers.
:::note
When running tests remotely, ensure the Playwright version in your tests matches the version running in the Docker container.
:::
## Image tags
See [all available image tags].
@ -198,6 +111,7 @@ We currently publish images with the following tags:
- `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
- `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
- `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish).
- `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
:::note
It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables.
@ -208,6 +122,7 @@ It is recommended to always pin your Docker image to a specific version if possi
We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions:
- **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble`
- **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy`
- **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal`
#### Alpine
@ -219,7 +134,7 @@ Browser builds for Firefox and WebKit are built for the [glibc](https://en.wikip
You can use the [.NET install script](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script) in order to install different SDK versions:
```bash
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --install-dir /usr/share/dotnet --channel 9.0
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --install-dir /usr/share/dotnet --channel 6.0
```
## Build your own image

View File

@ -188,7 +188,7 @@ page.setViewportSize(1600, 1200);
// Emulate high-DPI
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(2560, 1440)
.setDeviceScaleFactor(2));
.setDeviceScaleFactor(2);
```
```python async
@ -289,17 +289,17 @@ await using var context = await browser.NewContextAsync(new()
## Locale & Timezone
Emulate the browser Locale and Timezone which can be set globally for all tests in the config and then overridden for particular tests.
Emulate the user Locale and Timezone which can be set globally for all tests in the config and then overridden for particular tests.
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Emulates the browser locale.
// Emulates the user locale.
locale: 'en-GB',
// Emulates the browser timezone.
// Emulates the user timezone.
timezoneId: 'Europe/Paris',
},
});
@ -355,13 +355,6 @@ await using var context = await browser.NewContextAsync(new()
```
<img width="1394" alt="Bing in german lang and timezone" src="https://user-images.githubusercontent.com/13063165/220416571-ccc96ab1-44bb-4579-8430-64502fc24a15.png" />
######
* langs: js
Note that this only affects the browser timezone and locale, not the test runner timezone.
To set the test runner timezone, you can use the [`TZ` environment variable](https://nodejs.org/api/cli.html#tz).
## Permissions
Allow app to show system notifications.
@ -385,7 +378,7 @@ const context = await browser.newContext({
```java
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setPermissions(Arrays.asList("notifications")));
.setPermissions(Arrays.asList("notifications"));
```
```python async
@ -565,7 +558,7 @@ await context.SetGeolocationAsync(new Geolocation() { Longitude = 48.858455, Lat
**Note** you can only change geolocation for all pages in the context.
## Color Scheme and Media
Emulate the users `"colorScheme"`. Supported values are 'light' and 'dark'. You can also emulate the media type with [`method: Page.emulateMedia`].
Emulate the users `"colorScheme"`. Supported values are 'light', 'dark', 'no-preference'. You can also emulate the media type with [`method: Page.emulateMedia`].
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

View File

@ -18,7 +18,7 @@ Get started by installing Playwright and generating a test to see it in action.
## Installation
Playwright has a VS Code extension which is available when testing with Node.js. Install [it from the VS Code marketplace](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) or from the extensions tab in VS Code.
Install the [VS Code extension from the marketplace](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) or from the extensions tab in VS Code.
![VS Code extension for Playwright](https://github.com/microsoft/playwright/assets/13063165/cab54568-3168-4b3f-bf3d-854976594903)
@ -202,7 +202,7 @@ To run the **setup** test only once, deselect it from the projects section in th
## Global Setup
**Global setup** runs when you execute your first test. It runs only once and is useful for setting up a database or starting a server. You can manually run **global setup** by clicking the `Run global setup` option from the **Setup** section in the Playwright sidebar. **Global teardown** does not run by default; you need to manually initiate it by clicking the `Run global teardown` option.
**Global setup** tests are run when you execute your first test. This runs only once and is useful for setting up a database or starting a server. You can manually run a **global setup** test by clicking the `Run global setup` option from the **Setup** section in the Playwright sidebar. You can also run **global teardown** tests by clicking the `Run global teardown` option.
Global setup will re-run when you debug tests as this ensures an isolated environment and dedicated setup for the test.

View File

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

View File

@ -7,7 +7,7 @@ title: "Installation"
Playwright was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with native mobile emulation.
You can choose to use MSTest, NUnit, or xUnit [base classes](./test-runners.md) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
You can choose to use [MSTest base classes](./test-runners.md#mstest) or [NUnit base classes](./test-runners.md#nunit) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
1. Start by creating a new project with `dotnet new`. This will create the `PlaywrightTests` directory which includes a `UnitTest1.cs` file:
@ -17,7 +17,6 @@ You can choose to use MSTest, NUnit, or xUnit [base classes](./test-runners.md)
values={[
{label: 'MSTest', value: 'mstest'},
{label: 'NUnit', value: 'nunit'},
{label: 'xUnit', value: 'xunit'},
]
}>
<TabItem value="nunit">
@ -35,14 +34,6 @@ dotnet new mstest -n PlaywrightTests
cd PlaywrightTests
```
</TabItem>
<TabItem value="xunit">
```bash
dotnet new xunit -n PlaywrightTests
cd PlaywrightTests
```
</TabItem>
</Tabs>
@ -54,7 +45,6 @@ cd PlaywrightTests
values={[
{label: 'MSTest', value: 'mstest'},
{label: 'NUnit', value: 'nunit'},
{label: 'xUnit', value: 'xunit'},
]
}>
<TabItem value="nunit">
@ -70,13 +60,6 @@ dotnet add package Microsoft.Playwright.NUnit
dotnet add package Microsoft.Playwright.MSTest
```
</TabItem>
<TabItem value="xunit">
```bash
dotnet add package Microsoft.Playwright.Xunit
```
</TabItem>
</Tabs>
@ -104,7 +87,6 @@ Edit the `UnitTest1.cs` file with the code below to create an example end-to-end
values={[
{label: 'MSTest', value: 'mstest'},
{label: 'NUnit', value: 'nunit'},
{label: 'xUnit', value: 'xunit'},
]
}>
<TabItem value="nunit">
@ -182,41 +164,6 @@ public class ExampleTest : PageTest
```
</TabItem>
<TabItem value="xunit">
```csharp title="UnitTest1.cs"
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
namespace PlaywrightTests;
public class UnitTest1: PageTest
{
[Fact]
public async Task HasTitle()
{
await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
[Fact]
public async Task GetStartedLink()
{
await Page.GotoAsync("https://playwright.dev");
// Click the get started link.
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation.
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
}
}
```
</TabItem>
</Tabs>
## Running the Example Tests
@ -233,8 +180,8 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a
- Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8.
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 14 Ventura, or later.
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
- macOS 13 Ventura, or macOS 14 Sonoma.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next
@ -243,4 +190,4 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a
- [Generate tests with Codegen](./codegen-intro.md)
- [See a trace of your tests](./trace-viewer-intro.md)
- [Run tests on CI](./ci-intro.md)
- [Learn more about the MSTest, NUnit, and xUnit base classes](./test-runners.md)
- [Learn more about the MSTest and NUnit base classes](./test-runners.md)

View File

@ -32,7 +32,7 @@ public class App {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("https://playwright.dev");
page.navigate("http://playwright.dev");
System.out.println(page.title());
}
}
@ -130,8 +130,8 @@ By default browsers launched with Playwright run headless, meaning no browser UI
- Java 8 or higher.
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 14 Ventura, or later.
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
- macOS 13 Ventura, or macOS 14 Sonoma.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next

View File

@ -20,7 +20,6 @@ Playwright Test was created specifically to accommodate the needs of end-to-end
Get started by installing Playwright using npm, yarn or pnpm. Alternatively you can also get started and run your tests using the [VS Code Extension](./getting-started-vscode.md).
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -81,10 +80,9 @@ The `tests` folder contains a basic example test to help you get started with te
## Running the Example Test
By default tests will be run on all 3 browsers, Chromium, Firefox and WebKit using several workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -118,7 +116,7 @@ pnpm exec playwright test
</Tabs>
![tests running in command line](https://github.com/microsoft/playwright/assets/13063165/981c1b2b-dc7e-4b85-b241-272b44da6628)
![tests running in command line](https://github.com/microsoft/playwright/assets/13063165/981c1b2b-dc7e-4b85-b241-272b44da6628)
See our doc on [Running Tests](./running-tests.md) to learn more about running tests in headed mode, running multiple tests, running specific tests etc.
## HTML Test Reports
@ -126,7 +124,6 @@ See our doc on [Running Tests](./running-tests.md) to learn more about running t
After your test completes, an [HTML Reporter](./test-reporters.md#html-reporter) will be generated, which shows you a full report of your tests allowing you to filter the report by browsers, passed tests, failed tests, skipped tests and flaky tests. You can click on each test and explore the test's errors as well as each step of the test. By default, the HTML report is opened automatically if some of the tests failed.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -167,7 +164,6 @@ pnpm exec playwright show-report
Run your tests with [UI Mode](./test-ui-mode.md) for a better developer experience with time travel debugging, watch mode and more.
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -211,7 +207,6 @@ Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about
To update Playwright to the latest version run the following command:
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -255,7 +250,6 @@ pnpm exec playwright install --with-deps
You can always check which version of Playwright you have by running the following command:
<Tabs
groupId="js-package-manager"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
@ -292,10 +286,10 @@ pnpm exec playwright --version
## System requirements
- Latest version of Node.js 18, 20 or 22.
- Node.js 18+
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 14 Ventura, or later.
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
- macOS 13 Ventura, or macOS 14 Sonoma.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next

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