Compare commits
6 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ee3aaa0f5 | ||
|
|
0594783ed6 | ||
|
|
a115439b47 | ||
|
|
3049d99bc8 | ||
|
|
ae31f58b43 | ||
|
|
5313514995 |
11
.devcontainer/devcontainer.json
Normal file
11
.devcontainer/devcontainer.json
Normal 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"
|
||||
]
|
||||
}
|
||||
20
.eslintignore
Normal file
20
.eslintignore
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
test/assets/modernizr.js
|
||||
/packages/*/lib/
|
||||
*.js
|
||||
/packages/playwright-core/src/generated/*
|
||||
/packages/playwright-core/src/third_party/
|
||||
/packages/playwright-core/types/*
|
||||
/index.d.ts
|
||||
utils/generate_types/overrides.d.ts
|
||||
utils/generate_types/test/test.ts
|
||||
node_modules/
|
||||
browser_patches/*/checkout/
|
||||
browser_patches/chromium/output/
|
||||
**/*.d.ts
|
||||
output/
|
||||
test-results/
|
||||
tests/components/
|
||||
tests/installation/fixture-scripts/
|
||||
examples/
|
||||
DEPS
|
||||
.cache/
|
||||
126
.eslintrc.js
Normal file
126
.eslintrc.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "notice"],
|
||||
parserOptions: {
|
||||
ecmaVersion: 9,
|
||||
sourceType: "module",
|
||||
},
|
||||
extends: [
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
|
||||
/**
|
||||
* ESLint rules
|
||||
*
|
||||
* All available rules: http://eslint.org/docs/rules/
|
||||
*
|
||||
* Rules take the following form:
|
||||
* "rule-name", [severity, { opts }]
|
||||
* Severity: 2 == error, 1 == warning, 0 == off.
|
||||
*/
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [2, {args: "none"}],
|
||||
"@typescript-eslint/consistent-type-imports": [2, {disallowTypeAnnotations: false}],
|
||||
/**
|
||||
* Enforced rules
|
||||
*/
|
||||
// syntax preferences
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quotes": [2, "single", {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}],
|
||||
"no-extra-semi": 2,
|
||||
"@typescript-eslint/semi": [2],
|
||||
"comma-style": [2, "last"],
|
||||
"wrap-iife": [2, "inside"],
|
||||
"spaced-comment": [2, "always", {
|
||||
"markers": ["*"]
|
||||
}],
|
||||
"eqeqeq": [2],
|
||||
"accessor-pairs": [2, {
|
||||
"getWithoutSet": false,
|
||||
"setWithoutGet": false
|
||||
}],
|
||||
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
|
||||
"curly": [2, "multi-or-nest", "consistent"],
|
||||
"new-parens": 2,
|
||||
"arrow-parens": [2, "as-needed"],
|
||||
"prefer-const": 2,
|
||||
"quote-props": [2, "consistent"],
|
||||
|
||||
// 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"),
|
||||
}],
|
||||
}
|
||||
};
|
||||
73
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
73
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Something doesn't work like it should? Tell us!
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- ⚠️⚠️ Do not delete this template ⚠️⚠️ -->
|
||||
|
||||
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
|
||||
<!-- 🧪 Test using the latest Playwright release to see if your issue has already been fixed -->
|
||||
<!-- 💡 Provide enough information for us to be able to reproduce your issue locally -->
|
||||
|
||||
### System info
|
||||
- Playwright Version: [v1.XX]
|
||||
- Operating System: [All, Windows 11, Ubuntu 20, macOS 13.2, etc.]
|
||||
- Browser: [All, Chromium, Firefox, WebKit]
|
||||
- Other info:
|
||||
|
||||
### Source code
|
||||
|
||||
- [ ] I provided exact source code that allows reproducing the issue locally.
|
||||
|
||||
<!-- For simple cases, please provide a self-contained test file along with the config file -->
|
||||
<!-- For larger cases, you can provide a GitHub repo you created for this issue -->
|
||||
<!-- If we can not reproduce the problem locally, we won't be able to act on it -->
|
||||
<!-- You can still file without the exact code and we will try to help, but if we can't repro, it will be closed -->
|
||||
|
||||
**Link to the GitHub repository with the repro**
|
||||
|
||||
[https://github.com/your_profile/playwright_issue_title]
|
||||
|
||||
or
|
||||
|
||||
**Config file**
|
||||
|
||||
```js
|
||||
// playwright.config.ts
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], },
|
||||
},
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
**Test file (self-contained)**
|
||||
|
||||
```js
|
||||
it('should check the box using setChecked', async ({ page }) => {
|
||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||
await page.getByRole('checkbox').check();
|
||||
await expect(page.getByRole('checkbox')).toBeChecked();
|
||||
});
|
||||
```
|
||||
|
||||
**Steps**
|
||||
- [Run the test]
|
||||
- [...]
|
||||
|
||||
**Expected**
|
||||
|
||||
[Describe expected behavior]
|
||||
|
||||
**Actual**
|
||||
|
||||
[Describe actual behavior]
|
||||
101
.github/ISSUE_TEMPLATE/bug.yml
vendored
101
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -1,101 +0,0 @@
|
|||
name: Bug Report 🪲
|
||||
description: Create a bug report to help us improve
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Please follow these steps first:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Troubleshoot
|
||||
If Playwright is not behaving the way you expect, we'd ask you to look at the [documentation](https://playwright.dev/docs/intro) and search the issue tracker for evidence supporting your expectation.
|
||||
Please make reasonable efforts to troubleshoot and rule out issues with your code, the configuration, or any 3rd party libraries you might be using.
|
||||
Playwright offers [several debugging tools](https://playwright.dev/docs/debug) that you can use to troubleshoot your issues.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Ask for help through appropriate channels
|
||||
If you feel unsure about the cause of the problem, consider asking for help on for example [StackOverflow](https://stackoverflow.com/questions/ask) or our [Discord channel](https://aka.ms/playwright/discord) before posting a bug report. The issue tracker is not a help forum.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Make a minimal reproduction
|
||||
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
|
||||
The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
|
||||
Please make sure you only include the code and the dependencies absolutely necessary for your repro. Due to the security considerations, we can only run the code we trust. Major web frameworks are Ok to use, but smaller convenience libraries are not.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> Bug reports without a minimal reproduction will be rejected.
|
||||
|
||||
---
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: |
|
||||
The version of Playwright you are using.
|
||||
Is it the [latest](https://github.com/microsoft/playwright/releases)? Test and see if the bug has already been fixed.
|
||||
placeholder: ex. 1.41.1
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Please link to a repository with a minimal reproduction and describe accurately how we can reproduce/verify the bug.
|
||||
placeholder: |
|
||||
Example steps (replace with your own):
|
||||
1. Clone my repo at https://github.com/<myuser>/example
|
||||
2. npm install
|
||||
3. npm run test
|
||||
4. You should see the error come up
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A description of what you expect to happen.
|
||||
placeholder: I expect to see X or Y
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: |
|
||||
A clear and concise description of the unexpected behavior.
|
||||
Please include any relevant output here, especially any error messages.
|
||||
placeholder: A bug happened!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Anything else that might be relevant
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: envinfo
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Please paste the output of running `npx envinfo --preset playwright`.
|
||||
This will be automatically formatted as a code block, so no need for backticks.
|
||||
placeholder: |
|
||||
System:
|
||||
OS: Linux 6.2 Ubuntu 22.04.3 LTS 22.04.3 LTS (Jammy Jellyfish)
|
||||
CPU: (8) arm64
|
||||
Binaries:
|
||||
Node: 18.19.0 - ~/.nvm/versions/node/v18.19.0/bin/node
|
||||
npm: 10.2.3 - ~/.nvm/versions/node/v18.19.0/bin/npm
|
||||
npmPackages:
|
||||
@playwright/test: 1.41.1 => 1.41.1
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,4 +1,3 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Join our Discord Server
|
||||
url: https://aka.ms/playwright/discord
|
||||
|
|
|
|||
29
.github/ISSUE_TEMPLATE/documentation.yml
vendored
29
.github/ISSUE_TEMPLATE/documentation.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
name: Documentation 📖
|
||||
description: Submit a request to add or update documentation
|
||||
title: '[Docs]: '
|
||||
labels: ['Documentation :book:']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thank you for helping us improve our documentation!
|
||||
Please be sure you are looking at [the Next version of the documentation](https://playwright.dev/docs/next/intro) before opening an issue here.
|
||||
- type: textarea
|
||||
id: links
|
||||
attributes:
|
||||
label: Page(s)
|
||||
description: |
|
||||
Links to one or more documentation pages that should be modified.
|
||||
If you are reporting an issue with a specific section of a page, try to link directly to the nearest anchor.
|
||||
If you are suggesting that a new page be created, link to the parent of the proposed page.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Describe the change you are requesting.
|
||||
If the issue pertains to a single function or matcher, be sure to specify the entire call signature.
|
||||
validations:
|
||||
required: true
|
||||
30
.github/ISSUE_TEMPLATE/feature.yml
vendored
30
.github/ISSUE_TEMPLATE/feature.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
name: Feature Request 🚀
|
||||
description: Submit a proposal for a new feature
|
||||
title: '[Feature]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thank you for taking the time to suggest a new feature!
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: '🚀 Feature Request'
|
||||
description: A clear and concise description of what the feature is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: example
|
||||
attributes:
|
||||
label: Example
|
||||
description: Describe how this feature would be used.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
Outline your motivation for the proposal. How will it make Playwright better?
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Request new features to be added
|
||||
title: "[Feature]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Let us know what functionality you'd like to see in Playwright and what your use case is.
|
||||
Do you think others might benefit from this as well?
|
||||
27
.github/ISSUE_TEMPLATE/question.yml
vendored
27
.github/ISSUE_TEMPLATE/question.yml
vendored
|
|
@ -1,27 +0,0 @@
|
|||
name: 'Questions / Help 💬'
|
||||
description: If you have questions, please check StackOverflow or Discord
|
||||
title: '[Please read the message below]'
|
||||
labels: [':speech_balloon: Question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Questions and Help 💬
|
||||
|
||||
This issue tracker is reserved for bug reports and feature requests.
|
||||
|
||||
For anything else, such as questions or getting help, please see:
|
||||
|
||||
- [The Playwright documentation](https://playwright.dev)
|
||||
- [Our Discord server](https://aka.ms/playwright/discord)
|
||||
- type: checkboxes
|
||||
id: no-post
|
||||
attributes:
|
||||
label: |
|
||||
Please do not submit this issue.
|
||||
description: |
|
||||
> [!IMPORTANT]
|
||||
> This issue will be closed.
|
||||
options:
|
||||
- label: I understand
|
||||
required: true
|
||||
32
.github/ISSUE_TEMPLATE/regression.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/regression.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Report regression
|
||||
about: Functionality that used to work and does not any more
|
||||
title: "[REGRESSION]: "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Context:**
|
||||
- GOOD Playwright Version: [what Playwright version worked nicely?]
|
||||
- BAD Playwright Version: [what Playwright version doesn't work any more?]
|
||||
- Operating System: [e.g. Windows, Linux or Mac]
|
||||
- Extra: [any specific details about your environment]
|
||||
|
||||
**Code Snippet**
|
||||
|
||||
Help us help you! Put down a short code snippet that illustrates your bug and
|
||||
that we can run and debug locally. For example:
|
||||
|
||||
```javascript
|
||||
const {chromium, webkit, firefox} = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
// ...
|
||||
})();
|
||||
```
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
Add any other details about the problem here.
|
||||
96
.github/ISSUE_TEMPLATE/regression.yml
vendored
96
.github/ISSUE_TEMPLATE/regression.yml
vendored
|
|
@ -1,96 +0,0 @@
|
|||
name: Report regression
|
||||
description: Functionality that used to work and does not any more
|
||||
title: "[Regression]: "
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Please follow these steps first:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Make a minimal reproduction
|
||||
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the regression.
|
||||
The simpler you can make it, the more likely we are to successfully verify and fix the regression.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> Regression reports without a minimal reproduction will be rejected.
|
||||
|
||||
---
|
||||
- type: input
|
||||
id: goodVersion
|
||||
attributes:
|
||||
label: Last Good Version
|
||||
description: |
|
||||
Last version of Playwright where the feature was working.
|
||||
placeholder: ex. 1.40.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: badVersion
|
||||
attributes:
|
||||
label: First Bad Version
|
||||
description: |
|
||||
First version of Playwright where the feature was broken.
|
||||
Is it the [latest](https://github.com/microsoft/playwright/releases)? Test and see if the regression has already been fixed.
|
||||
placeholder: ex. 1.41.1
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Please link to a repository with a minimal reproduction and describe accurately how we can reproduce/verify the bug.
|
||||
placeholder: |
|
||||
Example steps (replace with your own):
|
||||
1. Clone my repo at https://github.com/<myuser>/example
|
||||
2. npm install
|
||||
3. npm run test
|
||||
4. You should see the error come up
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A description of what you expect to happen.
|
||||
placeholder: I expect to see X or Y
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: A clear and concise description of the unexpected behavior.
|
||||
placeholder: A bug happened!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Anything else that might be relevant
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: envinfo
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Please paste the output of running `npx envinfo --preset playwright`.
|
||||
This will be automatically formatted as a code block, so no need for backticks.
|
||||
placeholder: |
|
||||
System:
|
||||
OS: Linux 6.2 Ubuntu 22.04.3 LTS 22.04.3 LTS (Jammy Jellyfish)
|
||||
CPU: (8) arm64
|
||||
Binaries:
|
||||
Node: 18.19.0 - ~/.nvm/versions/node/v18.19.0/bin/node
|
||||
npm: 10.2.3 - ~/.nvm/versions/node/v18.19.0/bin/npm
|
||||
npmPackages:
|
||||
@playwright/test: 1.41.1 => 1.41.1
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
26
.github/ISSUE_TEMPLATE/vscode-extension.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/vscode-extension.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: VSCode extension bug
|
||||
about: Something doesn't work like it should inside the Visual Studio Code extension or you have a feature request? Tell us!
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Context:**
|
||||
- Playwright Version: [what Playwright version do you use?]
|
||||
- Operating System: [e.g. Windows, Linux or Mac]
|
||||
- Node.js version: [e.g. 12.22, 14.6]
|
||||
- Visual Studio Code version: [e.g. 1.65]
|
||||
- Playwright for VSCode extension version: [e.g. 1.2.3]
|
||||
- Browser: [e.g. All, Chromium, Firefox, WebKit]
|
||||
- Extra: [any specific details about your environment]
|
||||
|
||||
**Code Snippet**
|
||||
|
||||
Help us help you! Put down a short code snippet that illustrates your bug and
|
||||
that we can run and debug locally. For example:
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
Add any other details about the problem here.
|
||||
42
.github/actions/download-artifact/action.yml
vendored
42
.github/actions/download-artifact/action.yml
vendored
|
|
@ -1,44 +1,40 @@
|
|||
name: 'Download artifacts'
|
||||
description: 'Download artifacts from GitHub'
|
||||
name: 'Download blob report'
|
||||
description: 'Download blob report from GitHub artifacts'
|
||||
inputs:
|
||||
namePrefix:
|
||||
description: 'Name prefix of the artifacts to download'
|
||||
name:
|
||||
description: 'Name of the artifact to download'
|
||||
required: true
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
path:
|
||||
description: 'Directory with downloaded artifacts'
|
||||
required: true
|
||||
default: '.'
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Create temp downloads dir
|
||||
shell: bash
|
||||
run: mkdir -p '${{ inputs.path }}/artifacts'
|
||||
- name: Download artifacts
|
||||
uses: actions/github-script@v7
|
||||
- name: Download blob report
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
console.log(`downloading artifacts for workflow_run: ${context.payload.workflow_run.id}`);
|
||||
console.log(`workflow_run: ${JSON.stringify(context.payload.workflow_run, null, 2)}`);
|
||||
const allArtifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
...context.repo,
|
||||
run_id: context.payload.workflow_run.id
|
||||
});
|
||||
console.log('total = ', allArtifacts.length);
|
||||
const artifacts = allArtifacts.filter(a => a.name.startsWith('${{ inputs.namePrefix }}'));
|
||||
const fs = require('fs');
|
||||
for (const artifact of artifacts) {
|
||||
console.log('total = ', data.total_count);
|
||||
const name = '${{ inputs.name }}';
|
||||
const report = data.artifacts.filter(a => a.name === name)[0];
|
||||
const result = await github.rest.actions.downloadArtifact({
|
||||
...context.repo,
|
||||
artifact_id: artifact.id,
|
||||
artifact_id: report.id,
|
||||
archive_format: 'zip'
|
||||
});
|
||||
console.log(`Downloaded ${artifact.name}.zip (${result.data.byteLength} bytes)`);
|
||||
fs.writeFileSync(`${{ inputs.path }}/artifacts/${artifact.name}.zip`, Buffer.from(result.data));
|
||||
}
|
||||
- name: Unzip artifacts
|
||||
console.log('download result', result);
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync(`${name}.zip`, Buffer.from(result.data));
|
||||
- name: Unzip blob report
|
||||
shell: bash
|
||||
run: |
|
||||
unzip -n '${{ inputs.path }}/artifacts/*.zip' -d ${{ inputs.path }}
|
||||
rm -rf '${{ inputs.path }}/artifacts'
|
||||
run: unzip ${{ inputs.name }}.zip -d ${{ inputs.path }}
|
||||
|
|
|
|||
29
.github/actions/download-blob-report-from-azure/action.yml
vendored
Normal file
29
.github/actions/download-blob-report-from-azure/action.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: 'Download blob report from Azure'
|
||||
description: 'Download blob report from Azure blob storage'
|
||||
inputs:
|
||||
blob_prefix:
|
||||
description: 'Name of the Azure blob storage directory containing blob report'
|
||||
required: true
|
||||
type: string
|
||||
output_dir:
|
||||
description: 'Output directory where downloaded blobs will be stored'
|
||||
required: true
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
connection_string:
|
||||
description: 'Azure connection string'
|
||||
required: true
|
||||
type: string
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download Blob Reports from Azure Blob Storage
|
||||
shell: bash
|
||||
run: |
|
||||
OUTPUT_DIR='${{ inputs.output_dir }}'
|
||||
mkdir -p $OUTPUT_DIR
|
||||
LIST=$(az storage blob list -c '$web' --prefix ${{ inputs.blob_prefix }} --connection-string "${{ inputs.connection_string }}")
|
||||
for name in $(echo $LIST | jq --raw-output '.[].name | select(test("report-.*\\.zip$"))');
|
||||
do
|
||||
az storage blob download -c '$web' --name $name -f $OUTPUT_DIR/$(basename $name) --connection-string "${{ inputs.connection_string }}"
|
||||
done
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
name: Enable Microphone Access (macOS)
|
||||
description: 'Allow microphone access to all apps on macOS'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# https://github.com/actions/runner-images/issues/9330
|
||||
- name: Allow microphone access to all apps
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "Not macOS, exiting"
|
||||
exit 0
|
||||
fi
|
||||
echo "Allowing microphone access to all apps"
|
||||
version=$(sw_vers -productVersion | cut -d. -f1)
|
||||
if [[ "$version" == "14" || "$version" == "15" ]]; then
|
||||
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159,NULL,NULL,'UNUSED',1687786159);"
|
||||
elif [[ "$version" == "12" || "$version" == "13" ]]; then
|
||||
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR REPLACE INTO access VALUES('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159);"
|
||||
else
|
||||
echo "Skipping unsupported macOS version $version"
|
||||
exit 0
|
||||
fi
|
||||
echo "Successfully allowed microphone access"
|
||||
93
.github/actions/run-test/action.yml
vendored
93
.github/actions/run-test/action.yml
vendored
|
|
@ -1,93 +0,0 @@
|
|||
name: 'Run browser tests'
|
||||
description: 'Run browser tests'
|
||||
inputs:
|
||||
command:
|
||||
description: 'Command to run tests'
|
||||
required: true
|
||||
node-version:
|
||||
description: 'Node.js version to use'
|
||||
required: false
|
||||
default: '18'
|
||||
browsers-to-install:
|
||||
description: 'Browser to install. Default is all browsers.'
|
||||
required: false
|
||||
default: ''
|
||||
bot-name:
|
||||
description: 'Bot name'
|
||||
required: true
|
||||
shell:
|
||||
description: 'Shell to use'
|
||||
required: false
|
||||
default: 'bash'
|
||||
flakiness-client-id:
|
||||
description: 'Azure Flakiness Dashboard Client ID'
|
||||
required: false
|
||||
flakiness-tenant-id:
|
||||
description: 'Azure Flakiness Dashboard Tenant ID'
|
||||
required: false
|
||||
flakiness-subscription-id:
|
||||
description: 'Azure Flakiness Dashboard Subscription ID'
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
- uses: ./.github/actions/enable-microphone-access
|
||||
- run: |
|
||||
echo "::group::npm ci"
|
||||
npm ci
|
||||
echo "::endgroup::"
|
||||
shell: bash
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
- run: |
|
||||
echo "::group::npm run build"
|
||||
npm run build
|
||||
echo "::endgroup::"
|
||||
shell: bash
|
||||
- run: |
|
||||
echo "::group::npx playwright install --with-deps"
|
||||
npx playwright install --with-deps ${{ inputs.browsers-to-install }}
|
||||
echo "::endgroup::"
|
||||
shell: bash
|
||||
- name: Run tests
|
||||
if: inputs.shell == 'bash'
|
||||
run: |
|
||||
if [[ "$(uname)" == "Linux" ]]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- ${{ inputs.command }}
|
||||
else
|
||||
${{ inputs.command }}
|
||||
fi
|
||||
shell: bash
|
||||
env:
|
||||
PWTEST_BOT_NAME: ${{ inputs.bot-name }}
|
||||
- name: Run tests
|
||||
if: inputs.shell != 'bash'
|
||||
run: ${{ inputs.command }}
|
||||
shell: ${{ inputs.shell }}
|
||||
env:
|
||||
PWTEST_BOT_NAME: ${{ inputs.bot-name }}
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
|
||||
with:
|
||||
client-id: ${{ inputs.flakiness-client-id }}
|
||||
tenant-id: ${{ inputs.flakiness-tenant-id }}
|
||||
subscription-id: ${{ inputs.flakiness-subscription-id }}
|
||||
- run: |
|
||||
echo "::group::./utils/upload_flakiness_dashboard.sh"
|
||||
./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
echo "::endgroup::"
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
# We only merge reports for PRs as per .github/workflows/create_test_report.yml.
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
job_name: ${{ inputs.bot-name }}
|
||||
26
.github/actions/upload-blob-report/action.yml
vendored
26
.github/actions/upload-blob-report/action.yml
vendored
|
|
@ -4,31 +4,25 @@ inputs:
|
|||
report_dir:
|
||||
description: 'Directory containing blob report'
|
||||
required: true
|
||||
default: 'test-results/blob-report'
|
||||
job_name:
|
||||
description: 'Unique job name'
|
||||
required: true
|
||||
default: ''
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Integrity check
|
||||
shell: bash
|
||||
run: find "${{ inputs.report_dir }}" -name "*.zip" -exec unzip -t {} \;
|
||||
- name: Upload blob report to GitHub
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: blob-report-${{ inputs.job_name }}
|
||||
path: ${{ inputs.report_dir }}/**
|
||||
name: all-blob-reports
|
||||
path: ${{ inputs.report_dir }}
|
||||
retention-days: 7
|
||||
- name: Write triggering pull request number in a file
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: echo '${{ github.event.number }}' > pull_request_number.txt;
|
||||
- name: Upload artifact with the pull request number
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: pull-request-${{ inputs.job_name }}
|
||||
name: pull-request
|
||||
path: pull_request_number.txt
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
|
|
@ -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:
|
||||
- "*"
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright-chromium/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright-chromium/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright-core/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright-core/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright-firefox/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright-firefox/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright-test/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright-test/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright-webkit/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright-webkit/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
7
.github/dummy-package-files-for-dependents-analytics/playwright/package.json
vendored
Normal file
7
.github/dummy-package-files-for-dependents-analytics/playwright/package.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.0.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
@ -12,9 +12,6 @@ on:
|
|||
description: Comma-separated list of commit hashes to cherry-pick
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
roll:
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
@ -26,7 +23,7 @@ jobs:
|
|||
echo "Version is not a two digit semver version"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: release-${{ github.event.inputs.version }}
|
||||
fetch-depth: 0
|
||||
|
|
@ -60,7 +57,7 @@ jobs:
|
|||
git checkout -b "$BRANCH_NAME"
|
||||
git push origin $BRANCH_NAME
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||
script: |
|
||||
|
|
|
|||
54
.github/workflows/create_test_report.yml
vendored
54
.github/workflows/create_test_report.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
name: Publish Test Results
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["tests 1", "tests 2", "tests others"]
|
||||
workflows: ["tests 1", "tests 2"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
|
|
@ -9,40 +9,31 @@ jobs:
|
|||
permissions:
|
||||
pull-requests: write
|
||||
checks: write
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
|
||||
- name: Download blob report artifact
|
||||
uses: ./.github/actions/download-artifact
|
||||
with:
|
||||
namePrefix: 'blob-report'
|
||||
path: 'all-blob-reports'
|
||||
name: all-blob-reports
|
||||
path: all-blob-reports
|
||||
|
||||
- name: Merge reports
|
||||
run: |
|
||||
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
|
||||
npx playwright merge-reports --reporter markdown,html ./all-blob-reports
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
- name: Upload HTML report to Azure
|
||||
run: |
|
||||
|
|
@ -50,16 +41,19 @@ jobs:
|
|||
azcopy cp --recursive "./playwright-report/*" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR"
|
||||
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html"
|
||||
env:
|
||||
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
|
||||
AZCOPY_AUTO_LOGIN_TYPE: SPN
|
||||
AZCOPY_SPA_APPLICATION_ID: '${{ secrets.AZCOPY_SPA_APPLICATION_ID }}'
|
||||
AZCOPY_SPA_CLIENT_SECRET: '${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}'
|
||||
AZCOPY_TENANT_ID: '${{ secrets.AZCOPY_TENANT_ID }}'
|
||||
|
||||
- name: Read pull request number
|
||||
uses: ./.github/actions/download-artifact
|
||||
with:
|
||||
namePrefix: 'pull-request'
|
||||
path: '.'
|
||||
name: 'pull-request'
|
||||
path: './'
|
||||
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
@ -121,3 +115,21 @@ jobs:
|
|||
]),
|
||||
});
|
||||
core.info('Posted comment: ' + response.html_url);
|
||||
|
||||
const check = await github.rest.checks.create({
|
||||
...context.repo,
|
||||
name: 'Merge report (${{ github.event.workflow_run.name }})',
|
||||
head_sha: '${{ github.event.workflow_run.head_sha }}',
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
details_url: reportUrl,
|
||||
output: {
|
||||
title: 'Test results for "${{ github.event.workflow_run.name }}"',
|
||||
summary: [
|
||||
reportMd,
|
||||
'',
|
||||
'---',
|
||||
`Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).`
|
||||
].join('\n'),
|
||||
}
|
||||
});
|
||||
|
|
|
|||
24
.github/workflows/infra.yml
vendored
24
.github/workflows/infra.yml
vendored
|
|
@ -16,10 +16,10 @@ 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
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
|
|
@ -35,27 +35,21 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
- name: Audit prod NPM dependencies
|
||||
run: node utils/check_audit.js
|
||||
run: npm audit --omit dev
|
||||
lint-snippets:
|
||||
name: "Lint snippets"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- uses: actions/setup-dotnet@v4
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
- run: npm ci
|
||||
- run: pip install -r utils/doclint/linting-code-snippets/python/requirements.txt
|
||||
- run: mvn package
|
||||
working-directory: utils/doclint/linting-code-snippets/java
|
||||
- run: node utils/doclint/linting-code-snippets/cli.js
|
||||
|
|
|
|||
4
.github/workflows/merge.config.ts
vendored
4
.github/workflows/merge.config.ts
vendored
|
|
@ -1,4 +0,0 @@
|
|||
export default {
|
||||
testDir: '../../tests',
|
||||
reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']]
|
||||
};
|
||||
|
|
@ -4,39 +4,31 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/src/api/**/*'
|
||||
- 'packages/playwright-core/src/client/**/*'
|
||||
- 'packages/playwright-core/src/utils/isomorphic/**/*'
|
||||
- 'packages/playwright/src/matchers/matchers.ts'
|
||||
- 'packages/protocol/src/protocol.yml'
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create GitHub issue
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||
script: |
|
||||
const currentPlaywrightVersion = require('./package.json').version.match(/\d+\.\d+/)[0];
|
||||
const { data } = await github.rest.git.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha,
|
||||
});
|
||||
const commitHeader = data.message.split('\n')[0];
|
||||
const prMatch = commitHeader.match(/#(\d+)/);
|
||||
const formattedCommit = prMatch
|
||||
? `https://github.com/microsoft/playwright/pull/${prMatch[1]}`
|
||||
: `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${context.sha} (${commitHeader})`;
|
||||
|
||||
const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion;
|
||||
const title = '[Ports]: Backport client side changes';
|
||||
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
|
||||
const { data: issuesData } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}" author:playwrightmachine`
|
||||
q: `is:issue is:open repo:microsoft/${repo} in:title "${title}"`
|
||||
})
|
||||
let issueNumber = null;
|
||||
let issueBody = '';
|
||||
|
|
@ -54,7 +46,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,
|
||||
|
|
|
|||
38
.github/workflows/publish_canary.yml
vendored
38
.github/workflows/publish_canary.yml
vendored
|
|
@ -13,16 +13,15 @@ env:
|
|||
|
||||
jobs:
|
||||
publish-canary:
|
||||
name: "publish canary NPM"
|
||||
runs-on: ubuntu-24.04
|
||||
name: "publish canary NPM & Publish canary Docker"
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login)
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
|
@ -50,26 +49,33 @@ jobs:
|
|||
utils/publish_all_packages.sh --beta
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_PW_CDN_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_PW_CDN_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }}
|
||||
- name: build & publish driver
|
||||
env:
|
||||
AZ_UPLOAD_FOLDER: driver/next
|
||||
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
|
||||
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}
|
||||
run: |
|
||||
utils/build/build-playwright-driver.sh
|
||||
utils/build/upload-playwright-driver.sh
|
||||
- uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: playwright.azurecr.io
|
||||
username: playwright
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Set up Docker QEMU for arm64 docker builds
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: arm64
|
||||
- name: publish docker canary
|
||||
run: ./utils/docker/publish_docker.sh canary
|
||||
|
||||
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
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Deploy Canary
|
||||
|
|
|
|||
34
.github/workflows/publish_release_docker.yml
vendored
34
.github/workflows/publish_release_docker.yml
vendored
|
|
@ -2,6 +2,12 @@ name: "publish release - Docker"
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
is_release:
|
||||
required: true
|
||||
type: boolean
|
||||
description: "Is this a release image?"
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
|
@ -11,31 +17,27 @@ env:
|
|||
jobs:
|
||||
publish-docker-release:
|
||||
name: "publish to DockerHub"
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
environment: allow-publishing-docker-to-acr
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: playwright.azurecr.io
|
||||
username: playwright
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Set up Docker QEMU for arm64 docker builds
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: arm64
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx playwright install-deps
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
|
||||
- name: Login to ACR via OIDC
|
||||
run: az acr login --name playwright
|
||||
- run: ./utils/docker/publish_docker.sh stable
|
||||
if: (github.event_name != 'workflow_dispatch' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release == 'true')
|
||||
- run: ./utils/docker/publish_docker.sh canary
|
||||
if: (github.event_name != 'workflow_dispatch' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release != 'true')
|
||||
|
|
|
|||
18
.github/workflows/publish_release_driver.yml
vendored
18
.github/workflows/publish_release_driver.yml
vendored
|
|
@ -10,15 +10,11 @@ env:
|
|||
jobs:
|
||||
publish-driver-release:
|
||||
name: "publish playwright driver to CDN"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
|
@ -26,12 +22,8 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: npx playwright install-deps
|
||||
- run: utils/build/build-playwright-driver.sh
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_PW_CDN_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_PW_CDN_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }}
|
||||
- run: utils/build/upload-playwright-driver.sh
|
||||
env:
|
||||
AZ_UPLOAD_FOLDER: driver
|
||||
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
|
||||
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}
|
||||
|
|
|
|||
10
.github/workflows/publish_release_npm.yml
vendored
10
.github/workflows/publish_release_npm.yml
vendored
|
|
@ -10,14 +10,14 @@ 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
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
|
@ -25,10 +25,10 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: npx playwright install-deps
|
||||
- run: utils/publish_all_packages.sh --release-candidate
|
||||
if: ${{ github.event.release.prerelease }}
|
||||
if: "github.event.release.prerelease"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: utils/publish_all_packages.sh --release
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
if: "!github.event.release.prerelease"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ 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
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Deploy Stable
|
||||
|
|
|
|||
|
|
@ -3,31 +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
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
|
|
@ -36,21 +21,21 @@ jobs:
|
|||
run: npx playwright install-deps
|
||||
- name: Roll to new revision
|
||||
run: |
|
||||
./utils/roll_browser.js $BROWSER $REVISION
|
||||
./utils/roll_browser.js ${{ github.event.client_payload.browser }} ${{ github.event.client_payload.revision }}
|
||||
npm run build
|
||||
- name: Prepare branch
|
||||
id: prepare-branch
|
||||
run: |
|
||||
BRANCH_NAME="roll-into-pw-${BROWSER}/${REVISION}"
|
||||
BRANCH_NAME="roll-into-pw-${{ github.event.client_payload.browser }}/${{ github.event.client_payload.revision }}"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git config --global user.name github-actions
|
||||
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add .
|
||||
git commit -m "feat(${BROWSER}): roll to r${REVISION}"
|
||||
git push origin $BRANCH_NAME --force
|
||||
git commit -m "feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}"
|
||||
git push origin $BRANCH_NAME
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||
script: |
|
||||
|
|
@ -59,7 +44,7 @@ jobs:
|
|||
repo: 'playwright',
|
||||
head: 'microsoft:${{ steps.prepare-branch.outputs.BRANCH_NAME }}',
|
||||
base: 'main',
|
||||
title: 'feat(${{ env.BROWSER }}): roll to r${{ env.REVISION }}',
|
||||
title: 'feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}',
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: 'microsoft',
|
||||
|
|
|
|||
8
.github/workflows/roll_driver_nodejs.yml
vendored
8
.github/workflows/roll_driver_nodejs.yml
vendored
|
|
@ -9,11 +9,9 @@ jobs:
|
|||
name: Trigger Roll
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository == 'microsoft/playwright'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: node utils/build/update-playwright-driver-version.mjs
|
||||
|
|
@ -35,7 +33,7 @@ jobs:
|
|||
git push origin $BRANCH_NAME
|
||||
- name: Create Pull Request
|
||||
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||
script: |
|
||||
|
|
|
|||
71
.github/workflows/tests_bidi.yml
vendored
71
.github/workflows/tests_bidi.yml
vendored
|
|
@ -1,71 +0,0 @@
|
|||
name: tests BiDi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/tests_bidi.yml
|
||||
- packages/playwright-core/src/server/bidi/**
|
||||
- tests/bidi/**
|
||||
schedule:
|
||||
# Run every day at midnight
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
test_bidi:
|
||||
name: BiDi
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
channel: [bidi-chromium, bidi-firefox-nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: matrix.channel == 'bidi-chromium'
|
||||
- run: npx -y @puppeteer/browsers install firefox@nightly
|
||||
if: matrix.channel == 'bidi-firefox-nightly'
|
||||
- name: Run tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
||||
env:
|
||||
PWTEST_USE_BIDI_EXPECTATIONS: '1'
|
||||
- name: Upload csv report to GitHub
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: csv-report-${{ matrix.channel }}
|
||||
path: test-results/report.csv
|
||||
retention-days: 7
|
||||
|
||||
- name: Azure Login
|
||||
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Upload report.csv to Azure
|
||||
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
REPORT_DIR='bidi-reports'
|
||||
azcopy cp "./test-results/report.csv" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR/${{ matrix.channel }}.csv"
|
||||
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/${{ matrix.channel }}.csv"
|
||||
env:
|
||||
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
|
||||
8
.github/workflows/tests_components.yml
vendored
8
.github/workflows/tests_components.yml
vendored
|
|
@ -27,13 +27,13 @@ jobs:
|
|||
node-version: [18]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
- os: ubuntu-latest
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
|
|
|
|||
48
.github/workflows/tests_electron.yml
vendored
Normal file
48
.github/workflows/tests_electron.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: "electron"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'browser_patches/**'
|
||||
- 'docs/**'
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
env:
|
||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||
FORCE_COLOR: 1
|
||||
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||
|
||||
jobs:
|
||||
test_electron:
|
||||
name: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run etest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: npm run etest
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- run: node tests/config/checkCoverage.js electron
|
||||
if: always() && matrix.os == 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
166
.github/workflows/tests_others.yml
vendored
166
.github/workflows/tests_others.yml
vendored
|
|
@ -1,166 +0,0 @@
|
|||
name: tests others
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'browser_patches/**'
|
||||
- 'docs/**'
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
test_stress:
|
||||
name: Stress - ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm run stest contexts -- --project=chromium
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=chromium
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=chromium
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest contexts -- --project=webkit
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=webkit
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=webkit
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest contexts -- --project=firefox
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=firefox
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=firefox
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest heap -- --project=chromium
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
test_webview2:
|
||||
name: WebView2
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- run: dotnet build
|
||||
working-directory: tests/webview2/webview2-app/
|
||||
- name: Update to Evergreen WebView2 Runtime
|
||||
shell: pwsh
|
||||
run: |
|
||||
# See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
|
||||
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
|
||||
Start-Process -FilePath setup.exe -Verb RunAs -Wait
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
node-version: 20
|
||||
browsers-to-install: chromium
|
||||
command: npm run webview2test
|
||||
bot-name: "webview2-chromium-windows"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
|
||||
test_clock_frozen_time_linux:
|
||||
name: time library - ${{ matrix.clock }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
clock: [frozen, realtime]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
node-version: 20
|
||||
browsers-to-install: chromium
|
||||
command: npm run test -- --project=chromium-*
|
||||
bot-name: "${{ matrix.clock }}-time-library-chromium-linux"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PW_CLOCK: ${{ matrix.clock }}
|
||||
|
||||
test_clock_frozen_time_test_runner:
|
||||
name: time test runner - ${{ matrix.clock }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
clock: [frozen, realtime]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
node-version: 20
|
||||
command: npm run ttest
|
||||
bot-name: "${{ matrix.clock }}-time-runner-chromium-linux"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PW_CLOCK: ${{ matrix.clock }}
|
||||
|
||||
test_electron:
|
||||
name: Electron - ${{ matrix.os }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
if grep -q "Ubuntu 24" /etc/os-release; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
shell: bash
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
browsers-to-install: chromium
|
||||
command: npm run etest
|
||||
bot-name: "electron-${{ matrix.os }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD:
|
||||
197
.github/workflows/tests_primary.yml
vendored
197
.github/workflows/tests_primary.yml
vendored
|
|
@ -22,12 +22,12 @@ concurrency:
|
|||
env:
|
||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||
FORCE_COLOR: 1
|
||||
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }})
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -36,101 +36,122 @@ jobs:
|
|||
node-version: [18]
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
browser: chromium
|
||||
- os: ubuntu-22.04
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
browser: chromium
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
browsers-to-install: ${{ matrix.browser }} chromium
|
||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
|
||||
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
||||
test_linux_chromium_tot:
|
||||
name: ${{ matrix.os }} (chromium tip-of-tree)
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
browsers-to-install: chromium-tip-of-tree
|
||||
command: npm run test -- --project=chromium-*
|
||||
bot-name: "${{ matrix.os }}-chromium-tip-of-tree"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium-tip-of-tree
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium
|
||||
env:
|
||||
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||
PWTEST_BLOB_REPORT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
||||
test_test_runner:
|
||||
name: Test Runner
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
node-version: [18]
|
||||
shardIndex: [1, 2]
|
||||
shardTotal: [2]
|
||||
shard: [1/2, 2/2]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
node-version: 20
|
||||
shardIndex: 1
|
||||
shardTotal: 2
|
||||
node-version: 16
|
||||
shard: 1/2
|
||||
- os: ubuntu-latest
|
||||
node-version: 16
|
||||
shard: 2/2
|
||||
- os: ubuntu-latest
|
||||
node-version: 20
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
shard: 1/2
|
||||
- os: ubuntu-latest
|
||||
node-version: 22
|
||||
shardIndex: 1
|
||||
shardTotal: 2
|
||||
- os: ubuntu-latest
|
||||
node-version: 22
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
node-version: 20
|
||||
shard: 2/2
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{matrix.node-version}}
|
||||
command: npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
bot-name: "${{ matrix.os }}-node${{ matrix.node-version }}-${{ matrix.shardIndex }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
- run: npm ci
|
||||
env:
|
||||
PWTEST_CHANNEL: firefox-beta
|
||||
DEBUG: pw:install
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm run ttest -- --shard ${{ matrix.shard }}
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "${{ matrix.os }}-node${{ matrix.node-version }}"
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- run: xvfb-run npm run ttest -- --shard ${{ matrix.shard }}
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "${{ matrix.os }}-node${{ matrix.node-version }}"
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
||||
test_web_components:
|
||||
name: Web Components
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
|
|
@ -141,33 +162,29 @@ jobs:
|
|||
- run: npx playwright install --with-deps
|
||||
- run: npm run test-html-reporter
|
||||
env:
|
||||
PWTEST_BOT_NAME: "web-components-html-reporter"
|
||||
PWTEST_BLOB_REPORT_NAME: "web-components-html-reporter"
|
||||
- name: Upload blob report
|
||||
if: ${{ !cancelled() }}
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: packages/html-reporter/blob-report
|
||||
job_name: "web-components-html-reporter"
|
||||
|
||||
- run: npm run test-web
|
||||
if: ${{ !cancelled() }}
|
||||
if: always()
|
||||
env:
|
||||
PWTEST_BOT_NAME: "web-components-web"
|
||||
PWTEST_BLOB_REPORT_NAME: "web-components-web"
|
||||
- name: Upload blob report
|
||||
if: ${{ !cancelled() }}
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: packages/web/blob-report
|
||||
job_name: "web-components-web"
|
||||
|
||||
test_vscode_extension:
|
||||
name: VSCode Extension
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PWTEST_BOT_NAME: "vscode-extension"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
|
|
@ -188,17 +205,18 @@ jobs:
|
|||
working-directory: ./playwright-vscode
|
||||
- name: Run extension tests
|
||||
run: npm run test -- --workers=1
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "vscode-extension"
|
||||
working-directory: ./playwright-vscode
|
||||
- name: Upload blob report
|
||||
if: ${{ !cancelled() }}
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: playwright-vscode/blob-report
|
||||
job_name: ${{ env.PWTEST_BOT_NAME }}
|
||||
report_dir: ./playwright-vscode/blob-report
|
||||
|
||||
test_package_installations:
|
||||
name: "Installation Test ${{ matrix.os }}"
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -206,27 +224,32 @@ jobs:
|
|||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm install -g yarn@1
|
||||
- run: npm install -g pnpm@8
|
||||
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
if grep -q "Ubuntu 24" /etc/os-release; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
- run: npm run itest
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "package-installations-${{ matrix.os }}"
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
PWTEST_BLOB_REPORT_NAME: "package-installations-${{ matrix.os }}"
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: ./.github/actions/run-test
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
command: npm run itest
|
||||
bot-name: "package-installations-${{ matrix.os }}"
|
||||
shell: ${{ matrix.os == 'windows-latest' && 'pwsh' || 'bash' }}
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
report_dir: blob-report
|
||||
|
|
|
|||
883
.github/workflows/tests_secondary.yml
vendored
883
.github/workflows/tests_secondary.yml
vendored
File diff suppressed because it is too large
Load diff
20
.github/workflows/tests_service.yml
vendored
20
.github/workflows/tests_service.yml
vendored
|
|
@ -17,24 +17,24 @@ jobs:
|
|||
browser: [chromium, firefox, webkit]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- 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
|
||||
- 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"
|
||||
PWTEST_BLOB_REPORT_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
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: all-blob-reports
|
||||
path: blob-report
|
||||
|
|
@ -43,17 +43,17 @@ jobs:
|
|||
merge_reports:
|
||||
name: "Merge reports"
|
||||
needs: [test]
|
||||
if: ${{ !cancelled() }}
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- name: Download blob report artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: all-blob-reports
|
||||
path: all-blob-reports
|
||||
|
|
|
|||
52
.github/workflows/tests_stress.yml
vendored
Normal file
52
.github/workflows/tests_stress.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: "stress"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'browser_patches/**'
|
||||
- 'docs/**'
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
test_components:
|
||||
name: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npx playwright install firefox-asan
|
||||
if: matrix.os != 'windows-latest'
|
||||
- run: npm run stest contexts -- --project=chromium
|
||||
if: always()
|
||||
- run: npm run stest browsers -- --project=chromium
|
||||
if: always()
|
||||
- run: npm run stest contexts -- --project=webkit
|
||||
if: always()
|
||||
- run: npm run stest browsers -- --project=webkit
|
||||
if: always()
|
||||
- run: npm run stest contexts -- --project=firefox
|
||||
if: always()
|
||||
- run: npm run stest browsers -- --project=firefox
|
||||
if: always()
|
||||
- run: npm run stest heap -- --project=chromium
|
||||
if: always()
|
||||
26
.github/workflows/tests_video.yml
vendored
26
.github/workflows/tests_video.yml
vendored
|
|
@ -9,30 +9,32 @@ on:
|
|||
env:
|
||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||
FORCE_COLOR: 1
|
||||
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
video_linux:
|
||||
name: "Video Linux"
|
||||
environment: allow-uploading-flakiness-results
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chromium, firefox, webkit]
|
||||
os: [ubuntu-20.04, ubuntu-22.04]
|
||||
permissions:
|
||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
||||
contents: read # This is required for actions/checkout to succeed
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
browsers-to-install: ${{ matrix.browser }} chromium
|
||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
|
||||
env:
|
||||
PWTEST_VIDEO: 1
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
|
|
|
|||
44
.github/workflows/tests_webview2.yml
vendored
Normal file
44
.github/workflows/tests_webview2.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: "WebView2 Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'browser_patches/**'
|
||||
- 'docs/**'
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
env:
|
||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||
FORCE_COLOR: 1
|
||||
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
|
||||
jobs:
|
||||
test_webview2:
|
||||
name: WebView2
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run build
|
||||
- run: dotnet build
|
||||
working-directory: tests/webview2/webview2-app/
|
||||
- run: npm run webview2test
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
shell: bash
|
||||
30
.github/workflows/trigger_build_chromium_with_symbols.yml
vendored
Normal file
30
.github/workflows/trigger_build_chromium_with_symbols.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: "Trigger: Chromium with Symbols Builds"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
name: "trigger"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Get Chromium revision
|
||||
id: chromium-version
|
||||
run: |
|
||||
REVISION=$(node -e "console.log(require('./packages/playwright-core/browsers.json').browsers.find(b => b.name === 'chromium-with-symbols').revision)")
|
||||
echo "REVISION=$REVISION" >> $GITHUB_OUTPUT
|
||||
- run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
--data "{\"event_type\": \"build_chromium_with_symbols\", \"client_payload\": {\"revision\": \"${CHROMIUM_REVISION}\"}}" \
|
||||
https://api.github.com/repos/microsoft/playwright-browsers/dispatches
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||
CHROMIUM_REVISION: ${{ steps.chromium-version.outputs.REVISION }}
|
||||
2
.github/workflows/trigger_tests.yml
vendored
2
.github/workflows/trigger_tests.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
jobs:
|
||||
trigger:
|
||||
name: "trigger"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- run: |
|
||||
curl -X POST \
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -7,11 +7,9 @@ node_modules/
|
|||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
.mono
|
||||
.idea
|
||||
yarn.lock
|
||||
/packages/playwright-core/src/generated
|
||||
/packages/playwright-ct-core/src/generated
|
||||
/packages/playwright-core/src/generated/*
|
||||
packages/*/lib/
|
||||
drivers/
|
||||
.android-sdk/
|
||||
|
|
@ -34,5 +32,3 @@ test-results
|
|||
/tests/installation/.registry.json
|
||||
.cache/
|
||||
.eslintcache
|
||||
playwright.env
|
||||
/firefox/
|
||||
|
|
|
|||
244
CONTRIBUTING.md
244
CONTRIBUTING.md
|
|
@ -1,87 +1,88 @@
|
|||
# Contributing
|
||||
|
||||
## Choose an issue
|
||||
- [How to Contribute](#how-to-contribute)
|
||||
* [Getting Code](#getting-code)
|
||||
* [Code reviews](#code-reviews)
|
||||
* [Code Style](#code-style)
|
||||
* [API guidelines](#api-guidelines)
|
||||
* [Commit Messages](#commit-messages)
|
||||
* [Writing Documentation](#writing-documentation)
|
||||
* [Adding New Dependencies](#adding-new-dependencies)
|
||||
* [Running & Writing Tests](#running--writing-tests)
|
||||
* [Public API Coverage](#public-api-coverage)
|
||||
- [Contributor License Agreement](#contributor-license-agreement)
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
|
||||
Playwright **requires an issue** for every contribution, except for minor documentation updates. We strongly recommend to pick an issue labeled `open-to-a-pull-request` for your first contribution to the project.
|
||||
## How to Contribute
|
||||
|
||||
If you are passioned about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will facilitate the discussion and you might get some early feedback from project maintainers before spending your time on creating a pull request.
|
||||
We strongly recommend that you open an issue before beginning any code modifications. This is particularly important if the changes involve complex logic or if the existing code isn't immediately clear. By doing so, we can discuss and agree upon the best approach to address a bug or implement a feature, ensuring that our efforts are aligned.
|
||||
|
||||
## Make a change
|
||||
### Getting Code
|
||||
|
||||
Make sure you're running Node.js 16+ and NPM 8+, to verify and upgrade NPM do:
|
||||
|
||||
Make sure you're running Node.js 20 or later.
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
npm i -g npm@latest
|
||||
```
|
||||
|
||||
Clone the repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first.
|
||||
1. Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/microsoft/playwright
|
||||
cd playwright
|
||||
```
|
||||
|
||||
Install dependencies and run the build in watch mode.
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run watch
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode**
|
||||
3. Build Playwright
|
||||
|
||||
```
|
||||
PW_HMR=1 npm run watch
|
||||
PW_HMR=1 npx playwright show-trace
|
||||
PW_HMR=1 npm run ctest -- --ui
|
||||
PW_HMR=1 npx playwright codegen
|
||||
PW_HMR=1 npx playwright show-report
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright).
|
||||
4. Run all Playwright tests locally. For more information about tests, read [Running & Writing Tests](#running--writing-tests).
|
||||
|
||||
Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src).
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
### Code reviews
|
||||
|
||||
Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
### Write documentation
|
||||
### Code Style
|
||||
|
||||
Every part of the public API should be documented in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src), in the same change that adds/changes the API. We use markdown files with custom structure to specify the API. Take a look around for an example.
|
||||
- Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js)
|
||||
- Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
|
||||
Various other files are generated from the API specification. If you are running `npm run watch`, these will be re-generated automatically.
|
||||
To run code linter, use:
|
||||
|
||||
Larger changes will require updates to the documentation guides as well. This will be made clear during the code review.
|
||||
```bash
|
||||
npm run eslint
|
||||
```
|
||||
|
||||
## Add a test
|
||||
### API guidelines
|
||||
|
||||
Playwright requires a test for almost any new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that.
|
||||
When authoring new API methods, consider the following:
|
||||
|
||||
There are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. The two most important that you need to run locally are:
|
||||
- Expose as little information as needed. When in doubt, don’t expose new information.
|
||||
- Methods are used in favor of getters/setters.
|
||||
- The only exception is namespaces, e.g. `page.keyboard` and `page.coverage`
|
||||
- All string literals must be lowercase. This includes event names and option values.
|
||||
- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** common.
|
||||
|
||||
- Library tests cover APIs not related to the test runner.
|
||||
```bash
|
||||
# fast path runs all tests in Chromium
|
||||
npm run ctest
|
||||
### Commit Messages
|
||||
|
||||
# slow path runs all tests in three browsers
|
||||
npm run test
|
||||
```
|
||||
|
||||
- Test runner tests.
|
||||
```bash
|
||||
npm run ttest
|
||||
```
|
||||
|
||||
Since Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests).
|
||||
|
||||
Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.
|
||||
|
||||
## Write a commit message
|
||||
|
||||
Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:
|
||||
Commit messages should follow the Semantic Commit Messages format:
|
||||
|
||||
```
|
||||
label(namespace): title
|
||||
|
|
@ -92,57 +93,144 @@ footer
|
|||
```
|
||||
|
||||
1. *label* is one of the following:
|
||||
- `fix` - bug fixes
|
||||
- `feat` - new features
|
||||
- `docs` - documentation-only changes
|
||||
- `test` - test-only changes
|
||||
- `devops` - changes to the CI or build
|
||||
- `fix` - playwright bug fixes.
|
||||
- `feat` - playwright features.
|
||||
- `docs` - changes to docs, e.g. `docs(api.md): ..` to change documentation.
|
||||
- `test` - changes to playwright tests infrastructure.
|
||||
- `devops` - build-related work, e.g. CI related patches and general changes to the browser build infrastructure
|
||||
- `chore` - everything that doesn't fall under previous categories
|
||||
1. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
||||
1. *title* is a brief summary of changes.
|
||||
1. *description* is **optional**, new-line separated from title and is in present tense.
|
||||
1. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
|
||||
2. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
||||
3. *title* is a brief summary of changes.
|
||||
4. *description* is **optional**, new-line separated from title and is in present tense.
|
||||
5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
feat(trace viewer): network panel filtering
|
||||
fix(firefox): make sure session cookies work
|
||||
|
||||
This patch adds a filtering toolbar to the network panel.
|
||||
<link to a screenshot>
|
||||
This patch fixes session cookies in the firefox browser.
|
||||
|
||||
Fixes #123, references #234.
|
||||
Fixes #123, fixes #234
|
||||
```
|
||||
|
||||
## Send a pull request
|
||||
### Writing Documentation
|
||||
|
||||
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
|
||||
All API classes, methods, and events should have a description in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). There's a [documentation linter](https://github.com/microsoft/playwright/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase.
|
||||
|
||||
After a successful code review, one of the maintainers will merge your pull request. Congratulations!
|
||||
To run the documentation linter, use:
|
||||
|
||||
## More details
|
||||
```bash
|
||||
npm run doc
|
||||
```
|
||||
|
||||
**No new dependencies**
|
||||
To build the documentation site locally and test how your changes will look in practice:
|
||||
|
||||
There is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies.
|
||||
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo
|
||||
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress
|
||||
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes
|
||||
|
||||
**Custom browser build**
|
||||
### Adding New Dependencies
|
||||
|
||||
For all dependencies (both installation and development):
|
||||
- **Do not add** a dependency if the desired functionality is easily implementable.
|
||||
- If adding a dependency, it should be well-maintained and trustworthy.
|
||||
|
||||
A barrier for introducing new installation dependencies is especially high:
|
||||
- **Do not add** installation dependency unless it's critical to project success.
|
||||
|
||||
### Running & Writing Tests
|
||||
|
||||
- Every feature should be accompanied by a test.
|
||||
- Every public api event/method should be accompanied by a test.
|
||||
- Tests should be *hermetic*. Tests should not depend on external services.
|
||||
- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests.
|
||||
|
||||
Playwright tests are located in [`tests`](https://github.com/microsoft/playwright/blob/main/tests) and use `@playwright/test` test runner.
|
||||
These are integration tests, making sure public API methods and events work as expected.
|
||||
|
||||
- To run all tests:
|
||||
|
||||
```bash
|
||||
npx playwright install
|
||||
npm run test
|
||||
```
|
||||
|
||||
Be sure to run `npm run build` or let `npm run watch` run before you re-run the
|
||||
tests after making your changes to check them.
|
||||
|
||||
- To run all tests in Chromium
|
||||
```bash
|
||||
npm run ctest # also `ftest` for firefox and `wtest` for WebKit
|
||||
```
|
||||
|
||||
- To run the Playwright test runner tests
|
||||
```bash
|
||||
npm run ttest
|
||||
npm run ttest -- --grep "specific test"
|
||||
```
|
||||
|
||||
- To run a specific test, substitute `it` with `it.only`, or use the `--grep 'My test'` CLI parameter:
|
||||
|
||||
```js
|
||||
...
|
||||
// Using "it.only" to run a specific test
|
||||
it.only('should work', async ({server, page}) => {
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok).toBe(true);
|
||||
});
|
||||
// or
|
||||
playwright test --config=xxx --grep 'should work'
|
||||
```
|
||||
|
||||
- To disable a specific test, substitute `it` with `it.skip`:
|
||||
|
||||
```js
|
||||
...
|
||||
// Using "it.skip" to skip a specific test
|
||||
it.skip('should work', async ({server, page}) => {
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
- To run tests in non-headless (headed) mode:
|
||||
|
||||
```bash
|
||||
npm run ctest -- --headed
|
||||
```
|
||||
|
||||
- To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
|
||||
|
||||
To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
|
||||
```bash
|
||||
CRPATH=<path-to-executable> npm run ctest
|
||||
```
|
||||
|
||||
You will also find `DEBUG=pw:browser` useful for debugging custom builds.
|
||||
- To run tests in slow-mode:
|
||||
|
||||
**Building documentation site**
|
||||
```bash
|
||||
SLOW_MO=500 npm run wtest -- --headed
|
||||
```
|
||||
|
||||
The [playwright.dev](https://playwright.dev/) documentation site lives in a separate repository, and documentation from [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src) is frequently rolled there.
|
||||
- When should a test be marked with `skip` or `fail`?
|
||||
|
||||
Most of the time this should not concern you. However, if you are doing something unusual in the docs, you can build locally and test how your changes will look in practice:
|
||||
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo.
|
||||
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress.
|
||||
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes.
|
||||
- **`skip(condition)`**: This test *should ***never*** work* for `condition`
|
||||
where `condition` is usually a certain browser like `FFOX` (for Firefox),
|
||||
`WEBKIT` (for WebKit), and `CHROMIUM` (for Chromium).
|
||||
|
||||
For example, the [alt-click downloads test](https://github.com/microsoft/playwright/blob/471ccc72d3f0847caa36f629b394a028c7750d93/test/download.spec.js#L86) is marked
|
||||
with `skip(FFOX)` since an alt-click in Firefox will not produce a download
|
||||
even if a person was driving the browser.
|
||||
|
||||
|
||||
- **`fail(condition)`**: This test *should ***eventually*** work* for `condition`
|
||||
where `condition` is usually a certain browser like `FFOX` (for Firefox),
|
||||
`WEBKIT` (for WebKit), and `CHROMIUM` (for Chromium).
|
||||
|
||||
For example, the [alt-click downloads test](https://github.com/microsoft/playwright/blob/471ccc72d3f0847caa36f629b394a028c7750d93/test/download.spec.js#L86) is marked
|
||||
with `fail(CHROMIUM || WEBKIT)` since Playwright performing these actions
|
||||
currently diverges from what a user would experience driving a Chromium or
|
||||
WebKit.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# How to File a Bug Report That Actually Gets Resolved
|
||||
|
||||
Make sure you’re 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 can’t reproduce them ourselves.
|
||||
- We can’t 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 Overflow’s 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 it’s unlikely we can assist.
|
||||
11
README.md
11
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->134.0.6998.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->119.0.6045.9<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->118.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
14
SECURITY.md
14
SECURITY.md
|
|
@ -1,18 +1,18 @@
|
|||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ Please include the requested information listed below (as much as you can provid
|
|||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
|
|
@ -36,6 +36,6 @@ We prefer all communications to be in English.
|
|||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
|
|
|
|||
17
SUPPORT.md
17
SUPPORT.md
|
|
@ -1,17 +0,0 @@
|
|||
# Support
|
||||
|
||||
## How to file issues and get help
|
||||
|
||||
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
|
||||
|
||||
For help and questions about using this project, please see the [docs site for Playwright][docs].
|
||||
|
||||
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
|
||||
|
||||
## Microsoft Support Policy
|
||||
|
||||
Support for Playwright is limited to the resources listed above.
|
||||
|
||||
[gh-issues]: https://github.com/microsoft/playwright/issues/
|
||||
[docs]: https://playwright.dev/
|
||||
[discord-server]: https://aka.ms/playwright/discord
|
||||
|
|
@ -4,11 +4,11 @@
|
|||
},
|
||||
"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-proposal-export-namespace-from",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-transform-modules-commonjs"
|
||||
],
|
||||
"ignore": [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||
BASE_BRANCH="release"
|
||||
BASE_REVISION="5cfa81898f6eef8fb1abe463e5253cea5bc17f3f"
|
||||
BASE_REVISION="fdbb85992450ee14d23efe08c0dd655eedab0e1d"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
class Helper {
|
||||
decorateAsEventEmitter(objectToDecorate) {
|
||||
|
|
@ -23,16 +24,6 @@ class Helper {
|
|||
return allBrowsingContexts;
|
||||
}
|
||||
|
||||
awaitTopic(topic) {
|
||||
return new Promise(resolve => {
|
||||
const listener = () => {
|
||||
Services.obs.removeObserver(listener, topic);
|
||||
resolve();
|
||||
}
|
||||
Services.obs.addObserver(listener, topic);
|
||||
});
|
||||
}
|
||||
|
||||
toProtocolNavigationId(loadIdentifier) {
|
||||
return `nav-${loadIdentifier}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm");
|
||||
|
||||
|
|
@ -145,13 +146,9 @@ class NetworkRequest {
|
|||
}
|
||||
this._expectingInterception = false;
|
||||
this._expectingResumedRequest = undefined; // { method, headers, postData }
|
||||
this._overriddenHeadersForRedirect = redirectedFrom?._overriddenHeadersForRedirect;
|
||||
this._sentOnResponse = false;
|
||||
this._fulfilled = false;
|
||||
|
||||
if (this._overriddenHeadersForRedirect)
|
||||
overrideRequestHeaders(httpChannel, this._overriddenHeadersForRedirect);
|
||||
else if (this._pageNetwork)
|
||||
if (this._pageNetwork)
|
||||
appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
||||
|
||||
this._responseBodyChunks = [];
|
||||
|
|
@ -198,7 +195,6 @@ class NetworkRequest {
|
|||
|
||||
// Public interception API.
|
||||
fulfill(status, statusText, headers, base64body) {
|
||||
this._fulfilled = true;
|
||||
this._interceptedChannel.synthesizeStatus(status, statusText);
|
||||
for (const header of headers) {
|
||||
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
||||
|
|
@ -233,13 +229,16 @@ 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))
|
||||
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)
|
||||
|
|
@ -364,6 +363,13 @@ class NetworkRequest {
|
|||
return;
|
||||
}
|
||||
|
||||
const browserContext = pageNetwork._target.browserContext();
|
||||
if (browserContext.crossProcessCookie.settings.onlineOverride === 'offline') {
|
||||
// Implement offline.
|
||||
this.abort(Cr.NS_ERROR_OFFLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ok, so now we have intercepted the request, let's issue onRequest.
|
||||
// If interception has been disabled while we were intercepting, resume and forget.
|
||||
const interceptionEnabled = this._shouldIntercept();
|
||||
|
|
@ -453,6 +459,8 @@ class NetworkRequest {
|
|||
const browserContext = pageNetwork._target.browserContext();
|
||||
if (browserContext.requestInterceptionEnabled)
|
||||
return true;
|
||||
if (browserContext.crossProcessCookie.settings.onlineOverride === 'offline')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -551,11 +559,7 @@ class NetworkRequest {
|
|||
|
||||
_sendOnRequestFinished() {
|
||||
const pageNetwork = this._pageNetwork;
|
||||
// Undefined |responseEndTime| means there has been no response yet.
|
||||
// This happens when request interception API is used to redirect
|
||||
// the request to a different URL.
|
||||
// In this case, we should not emit "requestFinished" event.
|
||||
if (pageNetwork && this.httpChannel.responseEndTime !== undefined) {
|
||||
if (pageNetwork) {
|
||||
let protocolVersion = undefined;
|
||||
try {
|
||||
protocolVersion = this.httpChannel.protocolVersion;
|
||||
|
|
@ -598,8 +602,6 @@ class NetworkObserver {
|
|||
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
||||
return;
|
||||
}
|
||||
if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy))
|
||||
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
||||
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
||||
proxy.type,
|
||||
proxy.host,
|
||||
|
|
@ -769,20 +771,6 @@ function requestHeaders(httpChannel) {
|
|||
return headers;
|
||||
}
|
||||
|
||||
function clearRequestHeaders(httpChannel) {
|
||||
for (const header of requestHeaders(httpChannel)) {
|
||||
// We cannot remove the "host" header.
|
||||
if (header.name.toLowerCase() === 'host')
|
||||
continue;
|
||||
httpChannel.setRequestHeader(header.name, '', false /* merge */);
|
||||
}
|
||||
}
|
||||
|
||||
function overrideRequestHeaders(httpChannel, headers) {
|
||||
clearRequestHeaders(httpChannel);
|
||||
appendExtraHTTPHeaders(httpChannel, headers);
|
||||
}
|
||||
|
||||
function causeTypeToString(causeType) {
|
||||
for (let key in Ci.nsIContentPolicy) {
|
||||
if (Ci.nsIContentPolicy[key] === causeType)
|
||||
|
|
@ -815,8 +803,7 @@ class ResponseStorage {
|
|||
return;
|
||||
}
|
||||
let encodings = [];
|
||||
// Note: fulfilled request comes with decoded body right away.
|
||||
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) {
|
||||
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) {
|
||||
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
|
||||
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
|
||||
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
|
||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
|
|
@ -21,8 +22,6 @@ const ALL_PERMISSIONS = [
|
|||
'desktop-notification',
|
||||
];
|
||||
|
||||
let globalTabAndWindowActivationChain = Promise.resolve();
|
||||
|
||||
class DownloadInterceptor {
|
||||
constructor(registry) {
|
||||
this._registry = registry
|
||||
|
|
@ -116,7 +115,6 @@ class TargetRegistry {
|
|||
this._browserToTarget = new Map();
|
||||
this._browserIdToTarget = new Map();
|
||||
|
||||
this._proxiesWithClashingAuthCacheKeys = new Set();
|
||||
this._browserProxy = null;
|
||||
|
||||
// Cleanup containers from previous runs (if any)
|
||||
|
|
@ -186,7 +184,7 @@ class TargetRegistry {
|
|||
domWindow = appWindow;
|
||||
appWindow = null;
|
||||
}
|
||||
if (!domWindow.isChromeWindow)
|
||||
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
|
||||
return;
|
||||
// In persistent mode, window might be opened long ago and might be
|
||||
// already initialized.
|
||||
|
|
@ -214,7 +212,7 @@ class TargetRegistry {
|
|||
|
||||
const onCloseWindow = window => {
|
||||
const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
||||
if (!domWindow.isChromeWindow)
|
||||
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
|
||||
return;
|
||||
if (!domWindow.gBrowser)
|
||||
return;
|
||||
|
|
@ -235,50 +233,12 @@ class TargetRegistry {
|
|||
onOpenWindow(win);
|
||||
}
|
||||
|
||||
// Firefox uses nsHttpAuthCache to cache authentication to the proxy.
|
||||
// If we're provided with a single proxy with a multiple different authentications, then
|
||||
// we should clear the nsHttpAuthCache on every request.
|
||||
shouldBustHTTPAuthCacheForProxy(proxy) {
|
||||
return this._proxiesWithClashingAuthCacheKeys.has(proxy);
|
||||
}
|
||||
|
||||
_updateProxiesWithSameAuthCacheAndDifferentCredentials() {
|
||||
const proxyIdToCredentials = new Map();
|
||||
const allProxies = [...this._browserContextIdToBrowserContext.values()].map(bc => bc._proxy).filter(Boolean);
|
||||
if (this._browserProxy)
|
||||
allProxies.push(this._browserProxy);
|
||||
const proxyAuthCacheKeyAndProxy = allProxies.map(proxy => [
|
||||
JSON.stringify({
|
||||
type: proxy.type,
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
}),
|
||||
proxy,
|
||||
]);
|
||||
this._proxiesWithClashingAuthCacheKeys.clear();
|
||||
|
||||
proxyAuthCacheKeyAndProxy.sort(([cacheKey1], [cacheKey2]) => cacheKey1 < cacheKey2 ? -1 : 1);
|
||||
for (let i = 0; i < proxyAuthCacheKeyAndProxy.length - 1; ++i) {
|
||||
const [cacheKey1, proxy1] = proxyAuthCacheKeyAndProxy[i];
|
||||
const [cacheKey2, proxy2] = proxyAuthCacheKeyAndProxy[i + 1];
|
||||
if (cacheKey1 !== cacheKey2)
|
||||
continue;
|
||||
if (proxy1.username === proxy2.username && proxy1.password === proxy2.password)
|
||||
continue;
|
||||
// `proxy1` and `proxy2` have the same caching key, but serve different credentials.
|
||||
// We have to bust HTTP Auth Cache everytime there's a request that will use either of the proxies.
|
||||
this._proxiesWithClashingAuthCacheKeys.add(proxy1);
|
||||
this._proxiesWithClashingAuthCacheKeys.add(proxy2);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelDownload(options) {
|
||||
this._downloadInterceptor.cancelDownload(options.uuid);
|
||||
}
|
||||
|
||||
setBrowserProxy(proxy) {
|
||||
this._browserProxy = proxy;
|
||||
this._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||
}
|
||||
|
||||
getProxyInfo(channel) {
|
||||
|
|
@ -393,8 +353,7 @@ class PageTarget {
|
|||
this._videoRecordingInfo = undefined;
|
||||
this._screencastRecordingInfo = undefined;
|
||||
this._dialogs = new Map();
|
||||
this.forcedColors = 'none';
|
||||
this.disableCache = false;
|
||||
this.forcedColors = 'no-override';
|
||||
this.mediumOverride = '';
|
||||
this.crossProcessCookie = {
|
||||
initScripts: [],
|
||||
|
|
@ -407,7 +366,7 @@ class PageTarget {
|
|||
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
|
||||
};
|
||||
this._eventListeners = [
|
||||
helper.addObserver(this._updateModalDialogs.bind(this), 'common-dialog-loaded'),
|
||||
helper.addObserver(this._updateModalDialogs.bind(this), 'tabmodal-dialog-loaded'),
|
||||
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
|
||||
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
|
||||
helper.addEventListener(this._linkedBrowser, 'WillChangeBrowserRemoteness', event => this._willChangeBrowserRemoteness()),
|
||||
|
|
@ -426,7 +385,7 @@ class PageTarget {
|
|||
const tabBrowser = ownerWindow.gBrowser;
|
||||
// Serialize all tab-switching commands per tabbed browser
|
||||
// to disallow concurrent tab switching.
|
||||
const result = globalTabAndWindowActivationChain.then(async () => {
|
||||
const result = (tabBrowser.__serializedChain ?? Promise.resolve()).then(async () => {
|
||||
this._window.focus();
|
||||
if (tabBrowser.selectedTab !== this._tab) {
|
||||
const promise = helper.awaitEvent(ownerWindow, 'TabSwitchDone');
|
||||
|
|
@ -441,7 +400,7 @@ class PageTarget {
|
|||
notificationsPopup?.style.removeProperty('pointer-events');
|
||||
}
|
||||
});
|
||||
globalTabAndWindowActivationChain = result.catch(error => { /* swallow errors to keep chain running */ });
|
||||
tabBrowser.__serializedChain = result.catch(error => { /* swallow errors to keep chain running */ });
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -500,25 +459,6 @@ class PageTarget {
|
|||
this.updateColorSchemeOverride(browsingContext);
|
||||
this.updateReducedMotionOverride(browsingContext);
|
||||
this.updateForcedColorsOverride(browsingContext);
|
||||
this.updateForceOffline(browsingContext);
|
||||
this.updateCacheDisabled(browsingContext);
|
||||
}
|
||||
|
||||
updateForceOffline(browsingContext = undefined) {
|
||||
(browsingContext || this._linkedBrowser.browsingContext).forceOffline = this._browserContext.forceOffline;
|
||||
}
|
||||
|
||||
setCacheDisabled(disabled) {
|
||||
this.disableCache = disabled;
|
||||
this.updateCacheDisabled();
|
||||
}
|
||||
|
||||
updateCacheDisabled(browsingContext = this._linkedBrowser.browsingContext) {
|
||||
const enableFlags = Ci.nsIRequest.LOAD_NORMAL;
|
||||
const disableFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
||||
Ci.nsIRequest.INHIBIT_CACHING;
|
||||
|
||||
browsingContext.defaultLoadFlags = (this._browserContext.disableCache || this.disableCache) ? disableFlags : enableFlags;
|
||||
}
|
||||
|
||||
updateTouchOverride(browsingContext = undefined) {
|
||||
|
|
@ -538,7 +478,7 @@ class PageTarget {
|
|||
}
|
||||
|
||||
_updateModalDialogs() {
|
||||
const prompts = new Set(this._linkedBrowser.tabDialogBox.getContentDialogManager().dialogs.map(dialog => dialog.frameContentWindow.Dialog));
|
||||
const prompts = new Set(this._linkedBrowser.tabModalPromptBox ? this._linkedBrowser.tabModalPromptBox.listPrompts() : []);
|
||||
for (const dialog of this._dialogs.values()) {
|
||||
if (!prompts.has(dialog.prompt())) {
|
||||
this._dialogs.delete(dialog.id());
|
||||
|
|
@ -635,8 +575,7 @@ class PageTarget {
|
|||
}
|
||||
|
||||
updateForcedColorsOverride(browsingContext = undefined) {
|
||||
const isActive = this.forcedColors === 'active' || this._browserContext.forcedColors === 'active';
|
||||
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = isActive ? 'active' : 'none';
|
||||
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = (this.forcedColors !== 'no-override' ? this.forcedColors : this._browserContext.forcedColors) || 'no-override';
|
||||
}
|
||||
|
||||
async setInterceptFileChooserDialog(enabled) {
|
||||
|
|
@ -859,8 +798,8 @@ function fromProtocolReducedMotion(reducedMotion) {
|
|||
function fromProtocolForcedColors(forcedColors) {
|
||||
if (forcedColors === 'active' || forcedColors === 'none')
|
||||
return forcedColors;
|
||||
if (!forcedColors)
|
||||
return 'none';
|
||||
if (forcedColors === null)
|
||||
return undefined;
|
||||
throw new Error('Unknown forced colors: ' + forcedColors);
|
||||
}
|
||||
|
||||
|
|
@ -891,10 +830,8 @@ class BrowserContext {
|
|||
this.defaultUserAgent = null;
|
||||
this.defaultPlatform = null;
|
||||
this.touchOverride = false;
|
||||
this.forceOffline = false;
|
||||
this.disableCache = false;
|
||||
this.colorScheme = 'none';
|
||||
this.forcedColors = 'none';
|
||||
this.forcedColors = 'no-override';
|
||||
this.reducedMotion = 'none';
|
||||
this.videoRecordingOptions = undefined;
|
||||
this.crossProcessCookie = {
|
||||
|
|
@ -946,14 +883,12 @@ class BrowserContext {
|
|||
}
|
||||
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
|
||||
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
|
||||
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||
}
|
||||
|
||||
setProxy(proxy) {
|
||||
// Clear AuthCache.
|
||||
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
||||
this._proxy = proxy;
|
||||
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||
}
|
||||
|
||||
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
|
||||
|
|
@ -990,18 +925,6 @@ class BrowserContext {
|
|||
page.updateTouchOverride();
|
||||
}
|
||||
|
||||
setForceOffline(forceOffline) {
|
||||
this.forceOffline = forceOffline;
|
||||
for (const page of this.pages)
|
||||
page.updateForceOffline();
|
||||
}
|
||||
|
||||
setCacheDisabled(disabled) {
|
||||
this.disableCache = disabled;
|
||||
for (const page of this.pages)
|
||||
page.updateCacheDisabled();
|
||||
}
|
||||
|
||||
async setDefaultViewport(viewport) {
|
||||
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
|
||||
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"];
|
|||
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
|
||||
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
|
||||
const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
|
||||
|
|
@ -105,10 +106,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;
|
||||
}
|
||||
|
||||
let pipeStopped = false;
|
||||
let browserHandler;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { initialize } = ChromeUtils.import('chrome://juggler/content/content/main.js');
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
|
|
@ -8,8 +9,6 @@ const helper = new Helper();
|
|||
|
||||
let sameProcessInstanceNumber = 0;
|
||||
|
||||
const topBrowingContextToAgents = new Map();
|
||||
|
||||
class JugglerFrameChild extends JSWindowActorChild {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -18,66 +17,46 @@ class JugglerFrameChild extends JSWindowActorChild {
|
|||
}
|
||||
|
||||
handleEvent(aEvent) {
|
||||
const agents = this._agents();
|
||||
if (!agents)
|
||||
return;
|
||||
if (aEvent.type === 'DOMWillOpenModalDialog') {
|
||||
agents.channel.pause();
|
||||
if (this._agents && aEvent.type === 'DOMWillOpenModalDialog') {
|
||||
this._agents.channel.pause();
|
||||
return;
|
||||
}
|
||||
if (aEvent.type === 'DOMModalDialogClosed') {
|
||||
agents.channel.resumeSoon();
|
||||
if (this._agents && aEvent.type === 'DOMModalDialogClosed') {
|
||||
this._agents.channel.resumeSoon();
|
||||
return;
|
||||
}
|
||||
if (aEvent.target === this.document) {
|
||||
agents.pageAgent.onWindowEvent(aEvent);
|
||||
agents.frameTree.onWindowEvent(aEvent);
|
||||
}
|
||||
}
|
||||
|
||||
_agents() {
|
||||
return topBrowingContextToAgents.get(this.browsingContext.top);
|
||||
if (this._agents && aEvent.target === this.document)
|
||||
this._agents.pageAgent.onWindowEvent(aEvent);
|
||||
if (this._agents && aEvent.target === this.document)
|
||||
this._agents.frameTree.onWindowEvent(aEvent);
|
||||
}
|
||||
|
||||
actorCreated() {
|
||||
this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
|
||||
|
||||
this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => {
|
||||
this._agents()?.pageAgent.onWindowEvent(event);
|
||||
this._agents?.pageAgent.onWindowEvent(event);
|
||||
}));
|
||||
|
||||
if (this.document.documentURI.startsWith('moz-extension://'))
|
||||
return;
|
||||
|
||||
// Child frame events will be forwarded to related top-level agents.
|
||||
if (this.browsingContext.parent)
|
||||
return;
|
||||
|
||||
let agents = topBrowingContextToAgents.get(this.browsingContext);
|
||||
if (!agents) {
|
||||
agents = initialize(this.browsingContext, this.docShell);
|
||||
topBrowingContextToAgents.set(this.browsingContext, agents);
|
||||
this._agents = initialize(this.browsingContext, this.docShell, this);
|
||||
}
|
||||
agents.channel.bindToActor(this);
|
||||
agents.actor = this;
|
||||
|
||||
_dispose() {
|
||||
helper.removeListeners(this._eventListeners);
|
||||
// We do not cleanup since agents are shared for all frames in the process.
|
||||
|
||||
// TODO: restore the cleanup.
|
||||
// Reset transport so that all messages will be pending and will not throw any errors.
|
||||
// this._channel.resetTransport();
|
||||
// this._agents.pageAgent.dispose();
|
||||
// this._agents.frameTree.dispose();
|
||||
// this._agents = undefined;
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
helper.removeListeners(this._eventListeners);
|
||||
|
||||
if (this.browsingContext.parent)
|
||||
return;
|
||||
|
||||
const agents = topBrowingContextToAgents.get(this.browsingContext);
|
||||
// The agents are already re-bound to a new actor.
|
||||
if (agents?.actor !== this)
|
||||
return;
|
||||
|
||||
topBrowingContextToAgents.delete(this.browsingContext);
|
||||
|
||||
agents.channel.resetTransport();
|
||||
agents.pageAgent.dispose();
|
||||
agents.frameTree.dispose();
|
||||
this._dispose();
|
||||
}
|
||||
|
||||
receiveMessage() { }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
|
|
@ -62,6 +63,7 @@ class PageAgent {
|
|||
|
||||
const docShell = frameTree.mainFrame().docShell();
|
||||
this._docShell = docShell;
|
||||
this._initialDPPX = docShell.contentViewer.overrideDPPX;
|
||||
|
||||
// Dispatch frameAttached events for all initial frames
|
||||
for (const frame of this._frameTree.frames()) {
|
||||
|
|
@ -120,8 +122,7 @@ class PageAgent {
|
|||
// After the dragStart event is dispatched and handled by Web,
|
||||
// it might or might not create a new drag session, depending on its preventing default.
|
||||
setTimeout(() => {
|
||||
const session = this._getCurrentDragSession();
|
||||
this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!session });
|
||||
this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!dragService.getCurrentSession() });
|
||||
}, 0);
|
||||
}
|
||||
}),
|
||||
|
|
@ -153,6 +154,7 @@ class PageAgent {
|
|||
getFullAXTree: this._getFullAXTree.bind(this),
|
||||
insertText: this._insertText.bind(this),
|
||||
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
|
||||
setCacheDisabled: this._setCacheDisabled.bind(this),
|
||||
setFileInputFiles: this._setFileInputFiles.bind(this),
|
||||
evaluate: this._runtime.evaluate.bind(this._runtime),
|
||||
callFunction: this._runtime.callFunction.bind(this._runtime),
|
||||
|
|
@ -162,6 +164,15 @@ class PageAgent {
|
|||
];
|
||||
}
|
||||
|
||||
_setCacheDisabled({cacheDisabled}) {
|
||||
const enable = Ci.nsIRequest.LOAD_NORMAL;
|
||||
const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
||||
Ci.nsIRequest.INHIBIT_CACHING;
|
||||
|
||||
const docShell = this._frameTree.mainFrame().docShell();
|
||||
docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
|
||||
}
|
||||
|
||||
_emitAllEvents(frame) {
|
||||
this._browserPage.emit('pageEventFired', {
|
||||
frameId: frame.id(),
|
||||
|
|
@ -371,19 +382,8 @@ class PageAgent {
|
|||
const unsafeObject = frame.unsafeObject(objectId);
|
||||
if (!unsafeObject)
|
||||
throw new Error('Object is not input!');
|
||||
let nsFiles;
|
||||
if (unsafeObject.webkitdirectory) {
|
||||
nsFiles = await new Directory(files[0]).getFiles(true);
|
||||
} else {
|
||||
nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
||||
}
|
||||
const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
||||
unsafeObject.mozSetFileArray(nsFiles);
|
||||
const events = [
|
||||
new (frame.domWindow().Event)('input', { bubbles: true, cancelable: true, composed: true }),
|
||||
new (frame.domWindow().Event)('change', { bubbles: true, cancelable: true, composed: true }),
|
||||
];
|
||||
for (const event of events)
|
||||
unsafeObject.dispatchEvent(event);
|
||||
}
|
||||
|
||||
_getContentQuads({objectId, frameId}) {
|
||||
|
|
@ -464,6 +464,10 @@ class PageAgent {
|
|||
async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
|
||||
const frame = this._frameTree.mainFrame();
|
||||
const tip = frame.textInputProcessor();
|
||||
if (key === 'Meta' && Services.appinfo.OS !== 'Darwin')
|
||||
key = 'OS';
|
||||
else if (key === 'OS' && Services.appinfo.OS === 'Darwin')
|
||||
key = 'Meta';
|
||||
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
|
||||
key,
|
||||
code,
|
||||
|
|
@ -515,26 +519,75 @@ class PageAgent {
|
|||
false /* aIgnoreRootScrollFrame */,
|
||||
true /* aFlushLayout */);
|
||||
|
||||
await this._dispatchTouchEvent({
|
||||
const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({
|
||||
type: 'touchstart',
|
||||
modifiers,
|
||||
touchPoints: [{x, y}]
|
||||
});
|
||||
await this._dispatchTouchEvent({
|
||||
const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({
|
||||
type: 'touchend',
|
||||
modifiers,
|
||||
touchPoints: [{x, y}]
|
||||
});
|
||||
}
|
||||
if (startPrevented || endPrevented)
|
||||
return;
|
||||
|
||||
_getCurrentDragSession() {
|
||||
const frame = this._frameTree.mainFrame();
|
||||
const domWindow = frame?.domWindow();
|
||||
return domWindow ? dragService.getCurrentSession(domWindow) : undefined;
|
||||
const winUtils = frame.domWindow().windowUtils;
|
||||
winUtils.jugglerSendMouseEvent(
|
||||
'mousemove',
|
||||
x,
|
||||
y,
|
||||
0 /*button*/,
|
||||
0 /*clickCount*/,
|
||||
modifiers,
|
||||
false /*aIgnoreRootScrollFrame*/,
|
||||
0.0 /*pressure*/,
|
||||
5 /*inputSource*/,
|
||||
true /*isDOMEventSynthesized*/,
|
||||
false /*isWidgetEventSynthesized*/,
|
||||
0 /*buttons*/,
|
||||
winUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
||||
true /*disablePointerEvent*/
|
||||
);
|
||||
|
||||
winUtils.jugglerSendMouseEvent(
|
||||
'mousedown',
|
||||
x,
|
||||
y,
|
||||
0 /*button*/,
|
||||
1 /*clickCount*/,
|
||||
modifiers,
|
||||
false /*aIgnoreRootScrollFrame*/,
|
||||
0.0 /*pressure*/,
|
||||
5 /*inputSource*/,
|
||||
true /*isDOMEventSynthesized*/,
|
||||
false /*isWidgetEventSynthesized*/,
|
||||
1 /*buttons*/,
|
||||
winUtils.DEFAULT_MOUSE_POINTER_ID /*pointerIdentifier*/,
|
||||
true /*disablePointerEvent*/,
|
||||
);
|
||||
|
||||
winUtils.jugglerSendMouseEvent(
|
||||
'mouseup',
|
||||
x,
|
||||
y,
|
||||
0 /*button*/,
|
||||
1 /*clickCount*/,
|
||||
modifiers,
|
||||
false /*aIgnoreRootScrollFrame*/,
|
||||
0.0 /*pressure*/,
|
||||
5 /*inputSource*/,
|
||||
true /*isDOMEventSynthesized*/,
|
||||
false /*isWidgetEventSynthesized*/,
|
||||
0 /*buttons*/,
|
||||
winUtils.DEFAULT_MOUSE_POINTER_ID /*pointerIdentifier*/,
|
||||
true /*disablePointerEvent*/,
|
||||
);
|
||||
}
|
||||
|
||||
async _dispatchDragEvent({type, x, y, modifiers}) {
|
||||
const session = this._getCurrentDragSession();
|
||||
const session = dragService.getCurrentSession();
|
||||
const dropEffect = session.dataTransfer.dropEffect;
|
||||
|
||||
if ((type === 'drop' && dropEffect !== 'none') || type === 'dragover') {
|
||||
|
|
@ -558,8 +611,9 @@ class PageAgent {
|
|||
return;
|
||||
}
|
||||
if (type === 'dragend') {
|
||||
const session = this._getCurrentDragSession();
|
||||
session?.endDragSession(true);
|
||||
const session = dragService.getCurrentSession();
|
||||
if (session)
|
||||
dragService.endDragSession(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class Runtime {
|
|||
if (isWorker) {
|
||||
this._registerWorkerConsoleHandler();
|
||||
} else {
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
this._registerConsoleServiceListener(Services);
|
||||
this._registerConsoleAPIListener(Services);
|
||||
}
|
||||
|
|
@ -239,8 +240,8 @@ class Runtime {
|
|||
return {success: true, obj: obj.promiseValue};
|
||||
if (obj.promiseState === 'rejected') {
|
||||
const debuggee = executionContext._debuggee;
|
||||
exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
|
||||
exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
|
||||
return {success: false, obj: null};
|
||||
}
|
||||
let resolve, reject;
|
||||
|
|
@ -267,8 +268,8 @@ class Runtime {
|
|||
return;
|
||||
};
|
||||
const debuggee = pendingPromise.executionContext._debuggee;
|
||||
pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
|
||||
pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
|
||||
pendingPromise.resolve({success: false, obj: null});
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +442,7 @@ class ExecutionContext {
|
|||
_instanceOf(debuggerObj, rawObj, className) {
|
||||
if (this._domWindow)
|
||||
return rawObj instanceof this._domWindow[className];
|
||||
return this._debuggee.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._debuggee.makeDebuggeeValue(className)}, {useInnerBindings: true}).return;
|
||||
return this._debuggee.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._debuggee.makeDebuggeeValue(className)}).return;
|
||||
}
|
||||
|
||||
_createRemoteObject(debuggerObj) {
|
||||
|
|
@ -531,7 +532,7 @@ class ExecutionContext {
|
|||
}
|
||||
|
||||
_serialize(obj) {
|
||||
const result = this._debuggee.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject}, {useInnerBindings: true});
|
||||
const result = this._debuggee.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
|
||||
if (result.throw)
|
||||
throw new Error('Object is not serializable');
|
||||
return result.return === undefined ? undefined : JSON.parse(result.return);
|
||||
|
|
@ -560,12 +561,15 @@ class ExecutionContext {
|
|||
}
|
||||
|
||||
_getResult(completionValue, exceptionDetails = {}) {
|
||||
if (!completionValue)
|
||||
throw new Error('evaluation terminated');
|
||||
if (!completionValue) {
|
||||
exceptionDetails.text = 'Evaluation terminated!';
|
||||
exceptionDetails.stack = '';
|
||||
return {success: false, obj: null};
|
||||
}
|
||||
if (completionValue.throw) {
|
||||
if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}, {useInnerBindings: true}).return) {
|
||||
exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}, {useInnerBindings: true}).return;
|
||||
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}, {useInnerBindings: true}).return;
|
||||
if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
|
||||
exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
|
||||
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
|
||||
} else {
|
||||
exceptionDetails.value = this._serialize(completionValue.throw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,30 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
|
||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
||||
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
|
||||
|
||||
const browsingContextToAgents = new Map();
|
||||
const helper = new Helper();
|
||||
|
||||
function initialize(browsingContext, docShell) {
|
||||
const data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
|
||||
function initialize(browsingContext, docShell, actor) {
|
||||
if (browsingContext.parent) {
|
||||
// For child frames, return agents from the main frame.
|
||||
return browsingContextToAgents.get(browsingContext.top);
|
||||
}
|
||||
|
||||
let data = browsingContextToAgents.get(browsingContext);
|
||||
if (data) {
|
||||
// Rebind from one main frame actor to another one.
|
||||
data.channel.bindToActor(actor);
|
||||
return data;
|
||||
}
|
||||
|
||||
data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
|
||||
browsingContextToAgents.set(browsingContext, data);
|
||||
|
||||
const applySetting = {
|
||||
geolocation: (geolocation) => {
|
||||
|
|
@ -33,6 +48,15 @@ function initialize(browsingContext, docShell) {
|
|||
}
|
||||
},
|
||||
|
||||
onlineOverride: (onlineOverride) => {
|
||||
if (!onlineOverride) {
|
||||
docShell.onlineOverride = Ci.nsIDocShell.ONLINE_OVERRIDE_NONE;
|
||||
return;
|
||||
}
|
||||
docShell.onlineOverride = onlineOverride === 'online' ?
|
||||
Ci.nsIDocShell.ONLINE_OVERRIDE_ONLINE : Ci.nsIDocShell.ONLINE_OVERRIDE_OFFLINE;
|
||||
},
|
||||
|
||||
bypassCSP: (bypassCSP) => {
|
||||
docShell.bypassCSPEnabled = bypassCSP;
|
||||
},
|
||||
|
|
@ -45,6 +69,10 @@ function initialize(browsingContext, docShell) {
|
|||
docShell.languageOverride = locale;
|
||||
},
|
||||
|
||||
scrollbarsHidden: (hidden) => {
|
||||
data.frameTree.setScrollbarsHidden(hidden);
|
||||
},
|
||||
|
||||
javaScriptDisabled: (javaScriptDisabled) => {
|
||||
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
|
||||
},
|
||||
|
|
@ -66,6 +94,7 @@ function initialize(browsingContext, docShell) {
|
|||
data.frameTree.addBinding(worldName, name, script);
|
||||
data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]);
|
||||
data.channel = new SimpleChannel('', 'process-' + Services.appinfo.processID);
|
||||
data.channel.bindToActor(actor);
|
||||
data.pageAgent = new PageAgent(data.channel, data.frameTree);
|
||||
docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
|
||||
|
|
@ -186,10 +187,6 @@ class BrowserHandler {
|
|||
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
|
||||
}
|
||||
|
||||
['Browser.setCacheDisabled']({browserContextId, cacheDisabled}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).setCacheDisabled(cacheDisabled);
|
||||
}
|
||||
|
||||
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
|
||||
}
|
||||
|
|
@ -203,8 +200,7 @@ class BrowserHandler {
|
|||
}
|
||||
|
||||
async ['Browser.setOnlineOverride']({browserContextId, override}) {
|
||||
const forceOffline = override === 'offline';
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setForceOffline(forceOffline);
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override));
|
||||
}
|
||||
|
||||
async ['Browser.setColorScheme']({browserContextId, colorScheme}) {
|
||||
|
|
@ -255,6 +251,10 @@ class BrowserHandler {
|
|||
await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
|
||||
}
|
||||
|
||||
async ['Browser.setScrollbarsHidden']({browserContextId, hidden}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
||||
}
|
||||
|
||||
async ['Browser.setInitScripts']({browserContextId, scripts}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
const {Helper, EventWatcher} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
|
||||
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
||||
|
|
@ -256,13 +257,6 @@ class PageHandler {
|
|||
return await this._contentPage.send('disposeObject', options);
|
||||
}
|
||||
|
||||
async ['Heap.collectGarbage']() {
|
||||
Services.obs.notifyObservers(null, "child-gc-request");
|
||||
Cu.forceGC();
|
||||
Services.obs.notifyObservers(null, "child-cc-request");
|
||||
Cu.forceCC();
|
||||
}
|
||||
|
||||
async ['Network.getResponseBody']({requestId}) {
|
||||
return this._pageNetwork.getResponseBody(requestId);
|
||||
}
|
||||
|
|
@ -309,8 +303,8 @@ class PageHandler {
|
|||
await this._pageTarget.activateAndRun(() => {});
|
||||
}
|
||||
|
||||
async ['Page.setCacheDisabled']({cacheDisabled}) {
|
||||
return await this._pageTarget.setCacheDisabled(cacheDisabled);
|
||||
async ['Page.setCacheDisabled'](options) {
|
||||
return await this._contentPage.send('setCacheDisabled', options);
|
||||
}
|
||||
|
||||
async ['Page.addBinding']({ worldName, name, script }) {
|
||||
|
|
@ -401,7 +395,7 @@ class PageHandler {
|
|||
'nsIReferrerInfo',
|
||||
'init'
|
||||
);
|
||||
referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.UNSAFE_URL, true, referrerURI);
|
||||
referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid referer: "${referer}"`);
|
||||
}
|
||||
|
|
@ -495,9 +489,6 @@ class PageHandler {
|
|||
this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0);
|
||||
// 2. Get element's bounding box in the browser after the scroll is completed.
|
||||
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
||||
// 3. Make sure compositor is flushed after scrolling.
|
||||
if (win.windowUtils.flushApzRepaints())
|
||||
await helper.awaitTopic('apz-repaints-flushed');
|
||||
|
||||
const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers);
|
||||
const promises = [];
|
||||
|
|
@ -618,7 +609,7 @@ class PageHandler {
|
|||
const lineOrPageDeltaX = deltaX > 0 ? Math.floor(deltaX) : Math.ceil(deltaX);
|
||||
const lineOrPageDeltaY = deltaY > 0 ? Math.floor(deltaY) : Math.ceil(deltaY);
|
||||
|
||||
await this._pageTarget.activateAndRun(async () => {
|
||||
await this._pageTarget.activateAndRun(() => {
|
||||
this._pageTarget.ensureContextMenuClosed();
|
||||
|
||||
// 1. Scroll element to the desired location first; the coordinates are relative to the element.
|
||||
|
|
@ -627,10 +618,6 @@ class PageHandler {
|
|||
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
||||
|
||||
const win = this._pageTarget._window;
|
||||
// 3. Make sure compositor is flushed after scrolling.
|
||||
if (win.windowUtils.flushApzRepaints())
|
||||
await helper.awaitTopic('apz-repaints-flushed');
|
||||
|
||||
win.windowUtils.sendWheelEvent(
|
||||
x + boundingBox.left,
|
||||
y + boundingBox.top,
|
||||
|
|
|
|||
|
|
@ -322,12 +322,6 @@ const Browser = {
|
|||
enabled: t.Boolean,
|
||||
},
|
||||
},
|
||||
'setCacheDisabled': {
|
||||
params: {
|
||||
browserContextId: t.Optional(t.String),
|
||||
cacheDisabled: t.Boolean,
|
||||
},
|
||||
},
|
||||
'setGeolocationOverride': {
|
||||
params: {
|
||||
browserContextId: t.Optional(t.String),
|
||||
|
|
@ -394,6 +388,12 @@ const Browser = {
|
|||
viewport: t.Nullable(pageTypes.Viewport),
|
||||
}
|
||||
},
|
||||
'setScrollbarsHidden': {
|
||||
params: {
|
||||
browserContextId: t.Optional(t.String),
|
||||
hidden: t.Boolean,
|
||||
}
|
||||
},
|
||||
'setInitScripts': {
|
||||
params: {
|
||||
browserContextId: t.Optional(t.String),
|
||||
|
|
@ -481,17 +481,6 @@ const Browser = {
|
|||
},
|
||||
};
|
||||
|
||||
const Heap = {
|
||||
targets: ['page'],
|
||||
types: {},
|
||||
events: {},
|
||||
methods: {
|
||||
'collectGarbage': {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Network = {
|
||||
targets: ['page'],
|
||||
types: networkTypes,
|
||||
|
|
@ -1007,7 +996,7 @@ const Accessibility = {
|
|||
}
|
||||
|
||||
this.protocol = {
|
||||
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
|
||||
domains: {Browser, Page, Runtime, Network, Accessibility},
|
||||
};
|
||||
this.checkScheme = checkScheme;
|
||||
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ public:
|
|||
* amount of lines and columns to retain semantic of U and V planes which
|
||||
* contain only 1/4 of pixel information.
|
||||
*/
|
||||
int yuvTopOffset = m_margin.top + (m_margin.top & 1);
|
||||
int yuvLeftOffset = m_margin.left + (m_margin.left & 1);
|
||||
int yuvTopOffset = m_margin.top & 1 ? m_margin.top + 1 : m_margin.top;
|
||||
int yuvLeftOffset = m_margin.left & 1 ? m_margin.left + 1 : m_margin.left;
|
||||
|
||||
double src_width = src->width() - yuvLeftOffset;
|
||||
double src_height = src->height() - yuvTopOffset;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
|||
capability.height = 960;
|
||||
capability.maxFPS = ScreencastEncoder::fps;
|
||||
capability.videoType = webrtc::VideoType::kI420;
|
||||
int error = mCaptureModule->StartCaptureCounted(capability);
|
||||
int error = mCaptureModule->StartCapture(capability);
|
||||
if (error) {
|
||||
fprintf(stderr, "StartCapture error %d\n", error);
|
||||
return false;
|
||||
|
|
@ -152,7 +152,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
|||
mCaptureModule->DeRegisterCaptureDataCallback(this);
|
||||
else
|
||||
mCaptureModule->DeRegisterRawFrameCallback(this);
|
||||
mCaptureModule->StopCaptureCounted();
|
||||
mCaptureModule->StopCapture();
|
||||
if (mEncoder) {
|
||||
mEncoder->finish([this, protect = RefPtr{this}] {
|
||||
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,10 +3,6 @@
|
|||
// =================================================================
|
||||
// THESE ARE THE PROPERTIES THAT MUST BE ENABLED FOR JUGGLER TO WORK
|
||||
// =================================================================
|
||||
pref("dom.input_events.security.minNumTicks", 0);
|
||||
pref("dom.input_events.security.minTimeElapsedInMS", 0);
|
||||
|
||||
pref("dom.iframe_lazy_loading.enabled", false);
|
||||
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
|
||||
|
|
@ -15,9 +11,6 @@ pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
|||
// Force pdfs into downloads.
|
||||
pref("pdfjs.disabled", true);
|
||||
|
||||
// This preference breaks our authentication flow.
|
||||
pref("network.auth.use_redirect_for_retries", false);
|
||||
|
||||
// Disable cross-process iframes, but not cross-process navigations.
|
||||
pref("fission.webContentIsolationStrategy", 0);
|
||||
|
||||
|
|
@ -47,9 +40,6 @@ pref("permissions.isolateBy.userContext", true);
|
|||
// |Page.setFileInputFiles| protocol method.
|
||||
pref("dom.file.createInChild", true);
|
||||
|
||||
// Allow uploading directorys in content process.
|
||||
pref("dom.filesystem.pathcheck.disabled", true);
|
||||
|
||||
// Do not warn when closing all open tabs
|
||||
pref("browser.tabs.warnOnClose", false);
|
||||
|
||||
|
|
@ -100,16 +90,14 @@ 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.
|
||||
pref("toolkit.shutdown.fastShutdownStage", 3);
|
||||
|
||||
// @see https://github.com/microsoft/playwright/issues/8178
|
||||
pref("dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", true);
|
||||
|
||||
// Use light theme by default.
|
||||
pref("ui.systemUsesDarkTheme", 0);
|
||||
|
||||
|
|
@ -120,20 +108,9 @@ pref("prompts.contentPromptSubDialog", false);
|
|||
// Do not use system colors - they are affected by themes.
|
||||
pref("ui.use_standins_for_native_colors", true);
|
||||
|
||||
// Turn off the Push service.
|
||||
pref("dom.push.serverURL", "");
|
||||
// Prevent Remote Settings (firefox.settings.services.mozilla.com) to issue non local connections.
|
||||
// This setting breaks settings loading.
|
||||
pref("services.settings.server", "");
|
||||
// Prevent location.services.mozilla.com to issue non local connections.
|
||||
pref("browser.region.network.url", "");
|
||||
pref("browser.pocket.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.feeds.topsites", false);
|
||||
// Disable sponsored tiles from "Mozilla Tiles Service"
|
||||
pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
|
||||
// required to prevent non-local access to push.services.mozilla.com
|
||||
pref("dom.push.connection.enabled", false);
|
||||
// Prevent contile.services.mozilla.com to issue non local connections.
|
||||
pref("browser.topsites.contile.enabled", false);
|
||||
pref("browser.safebrowsing.provider.mozilla.updateURL", "");
|
||||
pref("browser.library.activity-stream.enabled", false);
|
||||
pref("browser.search.geoSpecificDefaults", false);
|
||||
|
|
@ -328,8 +305,5 @@ pref("extensions.blocklist.enabled", false);
|
|||
// Force Firefox Devtools to open in a separate window.
|
||||
pref("devtools.toolbox.host", "window");
|
||||
|
||||
// Disable auto translations
|
||||
pref("browser.translations.enable", false);
|
||||
|
||||
// Disable spell check
|
||||
pref("layout.spellcheckDefault", 0);
|
||||
// Prevent shortcut creation
|
||||
pref("browser.privacySegmentation.createdShortcut", true);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
||||
BASE_BRANCH="main"
|
||||
BASE_REVISION="76c95d6131edd36775a5eac01e297926fc974be8"
|
||||
BASE_REVISION="e50b54c0585e5846b51d94711b75515282bc3bd8"
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -1,14 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function getWebkitCheckoutPath() {
|
||||
echo ${WK_CHECKOUT_PATH:-"$HOME/webkit"}
|
||||
}
|
||||
|
||||
function runOSX() {
|
||||
# if script is run as-is
|
||||
WK_CHECKOUT_PATH=$(getWebkitCheckoutPath)
|
||||
if [[ -f "${SCRIPT_PATH}/EXPECTED_BUILDS" && -d "$WK_CHECKOUT_PATH/WebKitBuild/Release/Playwright.app" ]]; then
|
||||
if [[ -f "${SCRIPT_PATH}/EXPECTED_BUILDS" && -n "$WK_CHECKOUT_PATH" && -d "$WK_CHECKOUT_PATH/WebKitBuild/Release/Playwright.app" ]]; then
|
||||
DYLIB_PATH="$WK_CHECKOUT_PATH/WebKitBuild/Release"
|
||||
elif [[ -f "${SCRIPT_PATH}/EXPECTED_BUILDS" && -d "$HOME/webkit/WebKitBuild/Release/Playwright.app" ]]; then
|
||||
DYLIB_PATH="$HOME/webkit/WebKitBuild/Release"
|
||||
elif [[ -d $SCRIPT_PATH/Playwright.app ]]; then
|
||||
DYLIB_PATH="$SCRIPT_PATH"
|
||||
elif [[ -d $SCRIPT_PATH/WebKitBuild/Release/Playwright.app ]]; then
|
||||
|
|
@ -26,30 +23,29 @@ function runLinux() {
|
|||
GIO_DIR="";
|
||||
LD_PATH="";
|
||||
BUNDLE_DIR="";
|
||||
DEPENDENCIES_FOLDER="WebKitBuild/DependenciesGTK";
|
||||
DEPENDENCIES_FOLDER="DependenciesGTK";
|
||||
MINIBROWSER_FOLDER="minibrowser-gtk";
|
||||
BUILD_FOLDER="WebKitBuild/GTK";
|
||||
if [[ "$*" == *--headless* ]]; then
|
||||
DEPENDENCIES_FOLDER="WebKitBuild/DependenciesWPE";
|
||||
DEPENDENCIES_FOLDER="DependenciesWPE";
|
||||
MINIBROWSER_FOLDER="minibrowser-wpe";
|
||||
BUILD_FOLDER="WebKitBuild/WPE";
|
||||
fi
|
||||
# Setting extra environment variables like LD_LIBRARY_PATH or WEBKIT_INJECTED_BUNDLE_PATH
|
||||
# is only needed when calling MiniBrowser from the build folder. The MiniBrowser from
|
||||
# the zip bundle wrapper already sets itself the needed env variables.
|
||||
WK_CHECKOUT_PATH=$(getWebkitCheckoutPath)
|
||||
if [[ -d $SCRIPT_PATH/$MINIBROWSER_FOLDER ]]; then
|
||||
MINIBROWSER="$SCRIPT_PATH/$MINIBROWSER_FOLDER/MiniBrowser"
|
||||
elif [[ -d $WK_CHECKOUT_PATH/$BUILD_FOLDER ]]; then
|
||||
LD_PATH="$WK_CHECKOUT_PATH/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/checkout/$BUILD_FOLDER/Release/bin"
|
||||
GIO_DIR="$WK_CHECKOUT_PATH/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||
BUNDLE_DIR="$WK_CHECKOUT_PATH/$BUILD_FOLDER/Release/lib"
|
||||
MINIBROWSER="$WK_CHECKOUT_PATH/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
||||
elif [[ -d $HOME/webkit/$BUILD_FOLDER ]]; then
|
||||
LD_PATH="$HOME/webkit/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/checkout/$BUILD_FOLDER/Release/bin"
|
||||
GIO_DIR="$HOME/webkit/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||
BUNDLE_DIR="$HOME/webkit/$BUILD_FOLDER/Release/lib"
|
||||
MINIBROWSER="$HOME/webkit/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
||||
elif [[ -f $SCRIPT_PATH/MiniBrowser ]]; then
|
||||
MINIBROWSER="$SCRIPT_PATH/MiniBrowser"
|
||||
elif [[ -d $SCRIPT_PATH/$BUILD_FOLDER ]]; then
|
||||
LD_PATH="$SCRIPT_PATH/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/$BUILD_FOLDER/Release/bin"
|
||||
GIO_DIR="$SCRIPT_PATH/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||
LD_PATH="$SCRIPT_PATH/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/$BUILD_FOLDER/Release/bin"
|
||||
GIO_DIR="$SCRIPT_PATH/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||
BUNDLE_DIR="$SCRIPT_PATH/$BUILD_FOLDER/Release/lib"
|
||||
MINIBROWSER="$SCRIPT_PATH/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -39,4 +39,5 @@ fi
|
|||
|
||||
# create a TMP directory to copy all necessary files
|
||||
cd ./x64/Release
|
||||
7z a "$ZIP_PATH" ./PrintDeps.exe
|
||||
zip $ZIP_PATH ./PrintDeps.exe
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ A few examples of problems this can catch include:
|
|||
|
||||
The following examples rely on the [`com.deque.html.axe-core/playwright`](https://mvnrepository.com/artifact/com.deque.html.axe-core/playwright) Maven package which adds support for running the [axe accessibility testing engine](https://www.deque.com/axe/) as part of your Playwright tests.
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
|
||||
|
|
@ -70,9 +72,8 @@ 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 {
|
||||
@Test
|
||||
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
|
||||
page.navigate("https://your-site.com/");
|
||||
|
||||
page.locator("button[aria-label=\"Navigation Menu\"]").click();
|
||||
|
|
@ -87,7 +88,6 @@ public class HomepageTests {
|
|||
.analyze();
|
||||
|
||||
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ If the element in question is used repeatedly in many pages, consider [using a t
|
|||
|
||||
### Disabling individual scan rules
|
||||
|
||||
If your application contains many different preexisting violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-maven-html/blob/develop/playwright/README.md#axebuilderdisablerulesliststring-rules) to temporarily disable individual rules until you're able to fix the issues.
|
||||
If your application contains many different pre-existing violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-maven-html/blob/develop/playwright/README.md#axebuilderdisablerulesliststring-rules) to temporarily disable individual rules until you're able to fix the issues.
|
||||
|
||||
You can find the rule IDs to pass to `disableRules()` in the `id` property of the violations you want to suppress. A [complete list of axe's rules](https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md) can be found in `axe-core`'s documentation.
|
||||
|
||||
|
|
@ -160,9 +160,8 @@ 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 {
|
||||
@Test
|
||||
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
|
||||
page.navigate("https://your-site.com/");
|
||||
|
||||
AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();
|
||||
|
|
@ -174,15 +173,15 @@ public class HomepageTests {
|
|||
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) {
|
||||
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()
|
||||
|
|
@ -193,7 +192,6 @@ public class HomepageTests {
|
|||
node.getTarget().toString()
|
||||
)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -214,8 +212,8 @@ This example fixture creates an `AxeBuilder` object which is pre-configured with
|
|||
class AxeTestFixtures extends TestFixtures {
|
||||
AxeBuilder makeAxeBuilder() {
|
||||
return new AxeBuilder(page)
|
||||
.withTags(new String[]{"wcag2a", "wcag2aa", "wcag21a", "wcag21aa"})
|
||||
.exclude("#commonly-reused-element-with-known-issue");
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.exclude('#commonly-reused-element-with-known-issue');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -233,12 +231,10 @@ 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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ A few examples of problems this can catch include:
|
|||
|
||||
The following examples rely on the [`@axe-core/playwright`](https://npmjs.org/@axe-core/playwright) package which adds support for running the [axe accessibility testing engine](https://www.deque.com/axe/) as part of your Playwright tests.
|
||||
|
||||
:::note[Disclaimer]
|
||||
:::note Disclaimer
|
||||
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
|
||||
|
||||
For manual assessments, we recommend [Accessibility Insights for Web](https://accessibilityinsights.io/docs/web/overview/?referrer=playwright-accessibility-testing-js), a free and open source dev tool that walks you through assessing a website for [WCAG 2.1 AA](https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&levels=aaa) coverage.
|
||||
|
|
@ -147,7 +147,7 @@ If the element in question is used repeatedly in many pages, consider [using a t
|
|||
|
||||
### Disabling individual scan rules
|
||||
|
||||
If your application contains many different preexisting violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderdisablerulesrules-stringarray) to temporarily disable individual rules until you're able to fix the issues.
|
||||
If your application contains many different pre-existing violations of a specific rule, you can use [`AxeBuilder.disableRules()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderdisablerulesrules-stringarray) to temporarily disable individual rules until you're able to fix the issues.
|
||||
|
||||
You can find the rule IDs to pass to `disableRules()` in the `id` property of the violations you want to suppress. A [complete list of axe's rules](https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md) can be found in `axe-core`'s documentation.
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ test('should not have any accessibility violations outside of rules with known i
|
|||
|
||||
### Using snapshots to allow specific known issues
|
||||
|
||||
If you would like to allow for a more granular set of known issues, you can use [Snapshots](./test-snapshots.md) to verify that a set of preexisting violations has not changed. This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost of slightly more complexity and fragility.
|
||||
If you would like to allow for a more granular set of known issues, you can use [Snapshots](./test-snapshots.md) to verify that a set of pre-existing violations has not changed. This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost of slightly more complexity and fragility.
|
||||
|
||||
Do not use a snapshot of the entire `accessibilityScanResults.violations` array. It contains implementation details of the elements in question, such as a snippet of their rendered HTML; if you include these in your snapshots, it will make your tests prone to breaking every time one of the components in question changes for an unrelated reason:
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ title: "Auto-waiting"
|
|||
Playwright performs a range of actionability checks on the elements before making actions to ensure these actions
|
||||
behave as expected. It auto-waits for all the relevant checks to pass and only then performs the requested action. If the required checks do not pass within the given `timeout`, action fails with the `TimeoutError`.
|
||||
|
||||
For example, for [`method: Locator.click`], Playwright will ensure that:
|
||||
- locator resolves to exactly one element
|
||||
For example, for [`method: Page.click`], Playwright will ensure that:
|
||||
- element is [Attached] to the DOM
|
||||
- element is [Visible]
|
||||
- element is [Stable], as in not animating or completed animation
|
||||
- element [Receives Events], as in not obscured by other elements
|
||||
|
|
@ -17,75 +17,72 @@ For example, for [`method: Locator.click`], Playwright will ensure that:
|
|||
|
||||
Here is the complete list of actionability checks performed for each action:
|
||||
|
||||
| Action | [Visible] | [Stable] | [Receives Events] | [Enabled] | [Editable] |
|
||||
| :- | :-: | :-: | :-: | :-: | :-: |
|
||||
| [`method: Locator.check`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.click`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.dblclick`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.setChecked`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.tap`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.uncheck`] | Yes | Yes | Yes | Yes | - |
|
||||
| [`method: Locator.hover`] | Yes | Yes | Yes | - | - |
|
||||
| [`method: Locator.dragTo`] | Yes | Yes | Yes | - | - |
|
||||
| [`method: Locator.screenshot`] | Yes | Yes | - | - | - |
|
||||
| [`method: Locator.fill`] | Yes | - | - | Yes | Yes |
|
||||
| [`method: Locator.clear`] | Yes | - | - | Yes | Yes |
|
||||
| [`method: Locator.selectOption`] | Yes | - | - | Yes | - |
|
||||
| [`method: Locator.selectText`] | Yes | - | - | - | - |
|
||||
| [`method: Locator.scrollIntoViewIfNeeded`] | - | Yes | - | - | - |
|
||||
| [`method: Locator.blur`] | - | - | - | - | - |
|
||||
| [`method: Locator.dispatchEvent`] | - | - | - | - | - |
|
||||
| [`method: Locator.focus`] | - | - | - | - | - |
|
||||
| [`method: Locator.press`] | - | - | - | - | - |
|
||||
| [`method: Locator.pressSequentially`] | - | - | - | - | - |
|
||||
| [`method: Locator.setInputFiles`] | - | - | - | - | - |
|
||||
| Action | [Attached] | [Visible] | [Stable] | [Receives Events] | [Enabled] | [Editable] |
|
||||
| :- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||
| check | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| click | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| dblclick | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| setChecked | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| tap | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| uncheck | Yes | Yes | Yes | Yes | Yes | - |
|
||||
| hover | Yes | Yes | Yes | Yes | - | - |
|
||||
| scrollIntoViewIfNeeded | Yes | - | Yes | - | - | - |
|
||||
| screenshot | Yes | Yes | Yes | - | - | - |
|
||||
| fill | Yes | Yes | - | - | Yes | Yes |
|
||||
| selectText | Yes | Yes | - | - | - | - |
|
||||
| dispatchEvent | Yes | - | - | - | - | - |
|
||||
| focus | Yes | - | - | - | - | - |
|
||||
| getAttribute | Yes | - | - | - | - | - |
|
||||
| innerText | Yes | - | - | - | - | - |
|
||||
| innerHTML | Yes | - | - | - | - | - |
|
||||
| press | Yes | - | - | - | - | - |
|
||||
| setInputFiles | Yes | - | - | - | - | - |
|
||||
| selectOption | Yes | Yes | - | - | Yes | - |
|
||||
| textContent | Yes | - | - | - | - | - |
|
||||
| type | Yes | - | - | - | - | - |
|
||||
|
||||
<br/>
|
||||
|
||||
## Forcing actions
|
||||
|
||||
Some actions like [`method: Locator.click`] support `force` option that disables non-essential actionability checks,
|
||||
for example passing truthy `force` to [`method: Locator.click`] method will not check that the target element actually
|
||||
Some actions like [`method: Page.click`] support `force` option that disables non-essential actionability checks,
|
||||
for example passing truthy `force` to [`method: Page.click`] method will not check that the target element actually
|
||||
receives click events.
|
||||
|
||||
## Assertions
|
||||
|
||||
Playwright includes auto-retrying assertions that remove flakiness by waiting until the condition is met, similarly to auto-waiting before actions.
|
||||
You can check the actionability state of the element using one of the following methods as well. This is typically
|
||||
not necessary, but it helps writing assertive tests that ensure that after certain actions, elements reach
|
||||
actionable state:
|
||||
|
||||
| Assertion | Description |
|
||||
| :- | :- |
|
||||
| [`method: LocatorAssertions.toBeAttached`] | Element is attached |
|
||||
| [`method: LocatorAssertions.toBeChecked`] | Checkbox is checked |
|
||||
| [`method: LocatorAssertions.toBeDisabled`] | Element is disabled |
|
||||
| [`method: LocatorAssertions.toBeEditable`] | Element is editable |
|
||||
| [`method: LocatorAssertions.toBeEmpty`] | Container is empty |
|
||||
| [`method: LocatorAssertions.toBeEnabled`] | Element is enabled |
|
||||
| [`method: LocatorAssertions.toBeFocused`] | Element is focused |
|
||||
| [`method: LocatorAssertions.toBeHidden`] | Element is not visible |
|
||||
| [`method: LocatorAssertions.toBeInViewport`] | Element intersects viewport |
|
||||
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
|
||||
| [`method: LocatorAssertions.toContainText`] | Element contains text |
|
||||
| [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute |
|
||||
| [`method: LocatorAssertions.toHaveClass`] | Element has a class property |
|
||||
| [`method: LocatorAssertions.toHaveCount`] | List has exact number of children |
|
||||
| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property |
|
||||
| [`method: LocatorAssertions.toHaveId`] | Element has an ID |
|
||||
| [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property |
|
||||
| [`method: LocatorAssertions.toHaveText`] | Element matches text |
|
||||
| [`method: LocatorAssertions.toHaveValue`] | Input has a value |
|
||||
| [`method: LocatorAssertions.toHaveValues`] | Select has options selected |
|
||||
| [`method: PageAssertions.toHaveTitle`] | Page has a title |
|
||||
| [`method: PageAssertions.toHaveURL`] | Page has a URL |
|
||||
| [`method: APIResponseAssertions.toBeOK`] | Response has an OK status |
|
||||
- [`method: ElementHandle.isChecked`]
|
||||
- [`method: ElementHandle.isDisabled`]
|
||||
- [`method: ElementHandle.isEditable`]
|
||||
- [`method: ElementHandle.isEnabled`]
|
||||
- [`method: ElementHandle.isHidden`]
|
||||
- [`method: ElementHandle.isVisible`]
|
||||
- [`method: Page.isChecked`]
|
||||
- [`method: Page.isDisabled`]
|
||||
- [`method: Page.isEditable`]
|
||||
- [`method: Page.isEnabled`]
|
||||
- [`method: Page.isHidden`]
|
||||
- [`method: Page.isVisible`]
|
||||
- [`method: Locator.isChecked`]
|
||||
- [`method: Locator.isDisabled`]
|
||||
- [`method: Locator.isEditable`]
|
||||
- [`method: Locator.isEnabled`]
|
||||
- [`method: Locator.isHidden`]
|
||||
- [`method: Locator.isVisible`]
|
||||
|
||||
Learn more in the [assertions guide](./test-assertions.md).
|
||||
<br/>
|
||||
|
||||
## Attached
|
||||
|
||||
Element is considered attached when it is [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
|
||||
|
||||
## Visible
|
||||
|
||||
Element is considered visible when it has non-empty bounding box and does not have `visibility:hidden` computed style.
|
||||
|
||||
Note that according to this definition:
|
||||
* Elements of zero size **are not** considered visible.
|
||||
* Elements with `display:none` **are not** considered visible.
|
||||
* Elements with `opacity:0` **are** considered visible.
|
||||
Element is considered visible when it has non-empty bounding box and does not have `visibility:hidden` computed style. Note that elements of zero size or with `display:none` are not considered visible.
|
||||
|
||||
## Stable
|
||||
|
||||
|
|
@ -93,27 +90,18 @@ 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
|
||||
|
||||
Element is considered receiving pointer events when it is the hit target of the pointer event at the action point. For example, when clicking at the point `(10;10)`, Playwright checks whether some other element (usually an overlay) will instead capture the click at `(10;10)`.
|
||||
|
||||
|
||||
For example, consider a scenario where Playwright will click `Sign Up` button regardless of when the [`method: Locator.click`] call was made:
|
||||
For example, consider a scenario where Playwright will click `Sign Up` button regardless of when the [`method: Page.click`] call was made:
|
||||
- page is checking that user name is unique and `Sign Up` button is disabled;
|
||||
- after checking with the server, the disabled `Sign Up` button is replaced with another one that is now enabled.
|
||||
|
||||
|
|
@ -122,3 +110,4 @@ For example, consider a scenario where Playwright will click `Sign Up` button re
|
|||
[Enabled]: #enabled "Enabled"
|
||||
[Editable]: #editable "Editable"
|
||||
[Receives Events]: #receives-events "Receives Events"
|
||||
[Attached]: #attached "Attached"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ A few examples where it may come in handy:
|
|||
|
||||
All of that could be achieved via [APIRequestContext] methods.
|
||||
|
||||
The following examples rely on the [`Microsoft.Playwright.MSTest`](./test-runners.md) package which creates a Playwright and Page instance for each test.
|
||||
The following examples rely on the [`Microsoft.Playwright.NUnit`](./test-runners.md) package which creates a Playwright and Page instance for each test.
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
## Writing API Test
|
||||
|
||||
|
|
@ -32,19 +34,22 @@ The following example demonstrates how to use Playwright to test issues creation
|
|||
GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the `baseURL` to simplify the tests.
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.MSTest;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[TestClass]
|
||||
public class TestGitHubAPI : PlaywrightTest
|
||||
{
|
||||
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
|
||||
private IAPIRequestContext Request = null!;
|
||||
private IAPIRequestContext Request = null;
|
||||
|
||||
[TestInitialize]
|
||||
[SetUp]
|
||||
public async Task SetUpAPITesting()
|
||||
{
|
||||
await CreateAPIRequestContext();
|
||||
|
|
@ -66,7 +71,7 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
});
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
[TearDown]
|
||||
public async Task TearDownAPITesting()
|
||||
{
|
||||
await Request.DisposeAsync();
|
||||
|
|
@ -78,34 +83,36 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
|
||||
Now that we initialized request object we can add a few tests that will create new issues in the repository.
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.MSTest;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[TestClass]
|
||||
[TestFixture]
|
||||
public class TestGitHubAPI : PlaywrightTest
|
||||
{
|
||||
static string REPO = "test";
|
||||
static string REPO = "test-repo-2";
|
||||
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
|
||||
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
|
||||
private IAPIRequestContext Request = null!;
|
||||
private IAPIRequestContext Request = null;
|
||||
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task ShouldCreateBugReport()
|
||||
{
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "title", "[Bug] report 1" },
|
||||
{ "body", "Bug description" }
|
||||
};
|
||||
var data = new Dictionary<string, string>();
|
||||
data.Add("title", "[Bug] report 1");
|
||||
data.Add("body", "Bug description");
|
||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
|
||||
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(issues.Ok);
|
||||
var issuesJsonResponse = await issues.JsonAsync();
|
||||
JsonElement? issue = null;
|
||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||
|
|
@ -118,24 +125,23 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
}
|
||||
}
|
||||
}
|
||||
Assert.IsNotNull(issue);
|
||||
Assert.NotNull(issue);
|
||||
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task ShouldCreateFeatureRequests()
|
||||
{
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "title", "[Feature] request 1" },
|
||||
{ "body", "Feature description" }
|
||||
};
|
||||
var data = new Dictionary<string, string>();
|
||||
data.Add("title", "[Feature] request 1");
|
||||
data.Add("body", "Feature description");
|
||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
|
||||
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(issues.Ok);
|
||||
var issuesJsonResponse = await issues.JsonAsync();
|
||||
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
|
||||
|
||||
JsonElement? issue = null;
|
||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||
|
|
@ -148,7 +154,7 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
}
|
||||
}
|
||||
}
|
||||
Assert.IsNotNull(issue);
|
||||
Assert.NotNull(issue);
|
||||
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
||||
}
|
||||
|
||||
|
|
@ -161,17 +167,11 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that.
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.MSTest;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[TestClass]
|
||||
public class TestGitHubAPI : PlaywrightTest
|
||||
{
|
||||
// ...
|
||||
[TestInitialize]
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUpAPITesting()
|
||||
{
|
||||
await CreateAPIRequestContext();
|
||||
|
|
@ -187,10 +187,10 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
["name"] = REPO,
|
||||
},
|
||||
});
|
||||
await Expect(resp).ToBeOKAsync();
|
||||
Assert.True(resp.Ok);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
[TearDown]
|
||||
public async Task TearDownAPITesting()
|
||||
{
|
||||
await DeleteTestRepository();
|
||||
|
|
@ -200,7 +200,7 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
private async Task DeleteTestRepository()
|
||||
{
|
||||
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
||||
await Expect(resp).ToBeOKAsync();
|
||||
Assert.True(resp.Ok);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -210,34 +210,36 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
Here is the complete example of an API test:
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.MSTest;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[TestClass]
|
||||
[TestFixture]
|
||||
public class TestGitHubAPI : PlaywrightTest
|
||||
{
|
||||
static string REPO = "test-repo-2";
|
||||
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
|
||||
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");
|
||||
|
||||
private IAPIRequestContext Request = null!;
|
||||
private IAPIRequestContext Request = null;
|
||||
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task ShouldCreateBugReport()
|
||||
{
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "title", "[Bug] report 1" },
|
||||
{ "body", "Bug description" }
|
||||
};
|
||||
var data = new Dictionary<string, string>();
|
||||
data.Add("title", "[Bug] report 1");
|
||||
data.Add("body", "Bug description");
|
||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
|
||||
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(issues.Ok);
|
||||
var issuesJsonResponse = await issues.JsonAsync();
|
||||
JsonElement? issue = null;
|
||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||
|
|
@ -250,24 +252,23 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
}
|
||||
}
|
||||
}
|
||||
Assert.IsNotNull(issue);
|
||||
Assert.NotNull(issue);
|
||||
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task ShouldCreateFeatureRequests()
|
||||
{
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "title", "[Feature] request 1" },
|
||||
{ "body", "Feature description" }
|
||||
};
|
||||
var data = new Dictionary<string, string>();
|
||||
data.Add("title", "[Feature] request 1");
|
||||
data.Add("body", "Feature description");
|
||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
|
||||
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(issues.Ok);
|
||||
var issuesJsonResponse = await issues.JsonAsync();
|
||||
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
|
||||
|
||||
JsonElement? issue = null;
|
||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||
|
|
@ -280,11 +281,11 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
}
|
||||
}
|
||||
}
|
||||
Assert.IsNotNull(issue);
|
||||
Assert.NotNull(issue);
|
||||
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
[SetUp]
|
||||
public async Task SetUpAPITesting()
|
||||
{
|
||||
await CreateAPIRequestContext();
|
||||
|
|
@ -293,16 +294,14 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
|
||||
private async Task CreateAPIRequestContext()
|
||||
{
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
var headers = new Dictionary<string, string>();
|
||||
// We set this header per GitHub guidelines.
|
||||
{ "Accept", "application/vnd.github.v3+json" },
|
||||
headers.Add("Accept", "application/vnd.github.v3+json");
|
||||
// Add authorization token to all requests.
|
||||
// Assuming personal access token available in the environment.
|
||||
{ "Authorization", "token " + API_TOKEN }
|
||||
};
|
||||
headers.Add("Authorization", "token " + API_TOKEN);
|
||||
|
||||
Request = await Playwright.APIRequest.NewContextAsync(new()
|
||||
Request = await this.Playwright.APIRequest.NewContextAsync(new()
|
||||
{
|
||||
// All requests we send go to this API endpoint.
|
||||
BaseURL = "https://api.github.com",
|
||||
|
|
@ -319,10 +318,10 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
["name"] = REPO,
|
||||
},
|
||||
});
|
||||
await Expect(resp).ToBeOKAsync();
|
||||
Assert.True(resp.Ok);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
[TearDown]
|
||||
public async Task TearDownAPITesting()
|
||||
{
|
||||
await DeleteTestRepository();
|
||||
|
|
@ -332,7 +331,7 @@ public class TestGitHubAPI : PlaywrightTest
|
|||
private async Task DeleteTestRepository()
|
||||
{
|
||||
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
||||
await Expect(resp).ToBeOKAsync();
|
||||
Assert.True(resp.Ok);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -345,16 +344,14 @@ project to check that it appears at the top of the list. The check is performed
|
|||
```csharp
|
||||
class TestGitHubAPI : PageTest
|
||||
{
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task LastCreatedIssueShouldBeFirstInTheList()
|
||||
{
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "title", "[Feature] request 1" },
|
||||
{ "body", "Feature description" }
|
||||
};
|
||||
var data = new Dictionary<string, string>();
|
||||
data.Add("title", "[Feature] request 1");
|
||||
data.Add("body", "Feature description");
|
||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
|
||||
// When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start
|
||||
// a browser, context, and page manually or inherit from 'PageTest' which will launch it for you.
|
||||
|
|
@ -371,10 +368,9 @@ The following test creates a new issue via user interface in the browser and the
|
|||
it was created:
|
||||
|
||||
```csharp
|
||||
// Make sure to extend from PageTest if you want to use the Page class.
|
||||
class GitHubTests : PageTest
|
||||
{
|
||||
[TestMethod]
|
||||
[Test]
|
||||
public async Task LastCreatedIssueShouldBeOnTheServer()
|
||||
{
|
||||
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
|
||||
|
|
@ -382,10 +378,10 @@ class GitHubTests : PageTest
|
|||
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
|
||||
await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description");
|
||||
await Page.Locator("text=Submit new issue").ClickAsync();
|
||||
var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));
|
||||
String issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));
|
||||
|
||||
var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
|
||||
await Expect(newIssue).ToBeOKAsync();
|
||||
Assert.True(newIssue.Ok);
|
||||
StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ A few examples where it may come in handy:
|
|||
|
||||
All of that could be achieved via [APIRequestContext] methods.
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
## Writing API Test
|
||||
|
||||
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||
|
|
@ -194,7 +196,6 @@ public class TestGitHubAPI {
|
|||
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `@BeforeAll` and `@AfterAll` hooks for that.
|
||||
|
||||
```java
|
||||
public class TestGitHubAPI {
|
||||
// ...
|
||||
|
||||
void createTestRepository() {
|
||||
|
|
@ -224,7 +225,6 @@ public class TestGitHubAPI {
|
|||
disposeAPIRequestContext();
|
||||
closePlaywright();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete test example
|
||||
|
|
@ -375,17 +375,14 @@ public class TestGitHubAPI {
|
|||
}
|
||||
```
|
||||
|
||||
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
|
||||
|
||||
## Prepare server state via API calls
|
||||
|
||||
The following test creates a new issue via API and then navigates to the list of all issues in the
|
||||
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() {
|
||||
@Test
|
||||
void lastCreatedIssueShouldBeFirstInTheList() {
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("title", "[Feature] request 1");
|
||||
data.put("body", "Feature description");
|
||||
|
|
@ -396,7 +393,6 @@ public class TestGitHubAPI {
|
|||
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,9 +402,8 @@ 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() {
|
||||
@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");
|
||||
|
|
@ -419,7 +414,6 @@ public class TestGitHubAPI {
|
|||
APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
|
||||
assertThat(newIssue).isOK();
|
||||
assertTrue(newIssue.text().contains("Bug report 1"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ A few examples where it may come in handy:
|
|||
|
||||
All of that could be achieved via [APIRequestContext] methods.
|
||||
|
||||
<!-- TOC3 -->
|
||||
|
||||
## Writing API Test
|
||||
|
||||
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||
|
|
@ -46,24 +48,6 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
**Proxy configuration**
|
||||
|
||||
If your tests need to run behind a proxy, you can specify this in the config and the `request` fixture
|
||||
will pick it up automatically:
|
||||
|
||||
```js title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
export default defineConfig({
|
||||
use: {
|
||||
proxy: {
|
||||
server: 'http://my-proxy:8080',
|
||||
username: 'user',
|
||||
password: 'secret'
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Writing tests
|
||||
|
||||
Playwright Test comes with the built-in `request` fixture that respects configuration options like `baseURL` or `extraHTTPHeaders` we specified and is ready to send some requests.
|
||||
|
|
@ -329,7 +313,7 @@ test('context request will share cookie storage with its browser context', async
|
|||
[name, value])
|
||||
)).toEqual(responseCookies);
|
||||
|
||||
await route.fulfill({
|
||||
route.fulfill({
|
||||
response,
|
||||
headers: { ...responseHeaders, foo: 'bar' },
|
||||
});
|
||||
|
|
@ -373,7 +357,7 @@ test('global context request has isolated cookie storage', async ({
|
|||
new Map(contextCookies2.map(({ name, value }) => [name, value]))
|
||||
).toEqual(responseCookies);
|
||||
|
||||
await route.fulfill({
|
||||
route.fulfill({
|
||||
response,
|
||||
headers: { ...responseHeaders, foo: 'bar' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ All of that could be achieved via [APIRequestContext] methods.
|
|||
|
||||
The following examples rely on the [`pytest-playwright`](./test-runners.md) package which add Playwright fixtures to the Pytest test-runner.
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
## Writing API Test
|
||||
|
||||
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,12 @@ const { _android: android } = require('playwright');
|
|||
})();
|
||||
```
|
||||
|
||||
Note that since you don't need Playwright to install web browsers when testing Android, you can omit browser download via setting the following environment variable when installing Playwright:
|
||||
|
||||
```bash js
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright
|
||||
```
|
||||
|
||||
## async method: Android.connect
|
||||
* since: v1.28
|
||||
- returns: <[AndroidDevice]>
|
||||
|
|
@ -202,12 +208,6 @@ Prevents automatic playwright driver installation on attach. Assumes that the dr
|
|||
Optional device serial number to launch the browser on. If not specified, it will
|
||||
throw if multiple devices are connected.
|
||||
|
||||
### option: Android.launchServer.host
|
||||
* since: v1.45
|
||||
- `host` <[string]>
|
||||
|
||||
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
|
||||
|
||||
### option: Android.launchServer.port
|
||||
* since: v1.28
|
||||
- `port` <[int]>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ Launches Chrome browser on the device, and returns its persistent context.
|
|||
|
||||
### option: AndroidDevice.launchBrowser.pkg
|
||||
* since: v1.9
|
||||
- `pkg` <[string]>
|
||||
- `command` <[string]>
|
||||
|
||||
Optional package name to launch instead of default Chrome for Android.
|
||||
|
||||
|
|
@ -177,9 +177,7 @@ Launches a process in the shell on the device and returns a socket to communicat
|
|||
|
||||
### param: AndroidDevice.open.command
|
||||
* since: v1.9
|
||||
- `command` <[string]>
|
||||
|
||||
Shell command to execute.
|
||||
- `command` <[string]> Shell command to execute.
|
||||
|
||||
## async method: AndroidDevice.pinchClose
|
||||
* since: v1.9
|
||||
|
|
@ -447,7 +445,7 @@ Either a predicate that receives an event or an options object. Optional.
|
|||
* since: v1.9
|
||||
- returns: <[AndroidWebView]>
|
||||
|
||||
This method waits until [AndroidWebView] matching the [`param: selector`] is opened and returns it. If there is already an open [AndroidWebView] matching the [`param: selector`], returns immediately.
|
||||
This method waits until [AndroidWebView] matching the [`option: selector`] is opened and returns it. If there is already an open [AndroidWebView] matching the [`option: selector`], returns immediately.
|
||||
|
||||
### param: AndroidDevice.webView.selector
|
||||
* since: v1.9
|
||||
|
|
|
|||
|
|
@ -12,22 +12,12 @@ see [APIRequestContext].
|
|||
|
||||
Creates new instances of [APIRequestContext].
|
||||
|
||||
### option: APIRequest.newContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||
* since: 1.46
|
||||
|
||||
### option: APIRequest.newContext.useragent = %%-context-option-useragent-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequest.newContext.failOnStatusCode
|
||||
* since: v1.51
|
||||
- `failOnStatusCode` <[boolean]>
|
||||
|
||||
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
|
||||
for all status codes.
|
||||
|
||||
### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%%
|
||||
* since: v1.16
|
||||
|
||||
|
|
@ -71,7 +61,6 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` ?<[Array]<[unknown]>> indexedDB to set for context
|
||||
|
||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
||||
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the
|
||||
|
|
|
|||
|
|
@ -138,40 +138,28 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### param: APIRequestContext.delete.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.delete.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.delete.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.delete.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.delete.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.delete.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.delete.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.delete.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.delete.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.delete.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.delete.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -189,31 +177,21 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.delete.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.delete.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.dispose
|
||||
* since: v1.16
|
||||
|
||||
All responses returned by [`method: APIRequestContext.get`] and similar methods are stored in the memory, so that you can later call [`method: APIResponse.body`].This method discards all its resources, calling any method on disposed [APIRequestContext] will throw an exception.
|
||||
|
||||
### option: APIRequestContext.dispose.reason
|
||||
* since: v1.45
|
||||
- `reason` <[string]>
|
||||
|
||||
The reason to be reported to the operations interrupted by the context disposal.
|
||||
All responses returned by [`method: APIRequestContext.get`] and similar methods are stored in the memory, so that you can later call [`method: APIResponse.body`]. This method
|
||||
discards all stored responses, and makes [`method: APIResponse.body`] throw "Response disposed" error.
|
||||
|
||||
## async method: APIRequestContext.fetch
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
||||
Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
|
||||
context cookies from the response. The method will automatically follow redirects.
|
||||
context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly to the request.
|
||||
|
||||
**Usage**
|
||||
|
||||
JSON objects can be passed directly to the request:
|
||||
|
||||
```js
|
||||
await request.fetch('https://example.com/api/createBook', {
|
||||
method: 'post',
|
||||
|
|
@ -247,17 +225,28 @@ var data = new Dictionary<string, object>() {
|
|||
await Request.FetchAsync("https://example.com/api/createBook", new() { Method = "post", DataObject = data });
|
||||
```
|
||||
|
||||
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding, by specifiying the `multipart` parameter:
|
||||
The common way to send file(s) in the body of a request is to encode it as form fields with `multipart/form-data` encoding. You can achieve that with Playwright API like this:
|
||||
|
||||
```js
|
||||
const form = new FormData();
|
||||
form.set('name', 'John');
|
||||
form.append('name', 'Doe');
|
||||
// Send two file fields with the same name.
|
||||
form.append('file', new File(['console.log(2024);'], 'f1.js', { type: 'text/javascript' }));
|
||||
form.append('file', new File(['hello'], 'f2.txt', { type: 'text/plain' }));
|
||||
await request.fetch('https://example.com/api/uploadForm', {
|
||||
multipart: form
|
||||
// Open file as a stream and pass it to the request:
|
||||
const stream = fs.createReadStream('team.csv');
|
||||
await request.fetch('https://example.com/api/uploadTeamList', {
|
||||
method: 'post',
|
||||
multipart: {
|
||||
fileField: stream
|
||||
}
|
||||
});
|
||||
|
||||
// Or you can pass the file content directly as an object:
|
||||
await request.fetch('https://example.com/api/uploadScript', {
|
||||
method: 'post',
|
||||
multipart: {
|
||||
fileField: {
|
||||
name: 'f.js',
|
||||
mimeType: 'text/javascript',
|
||||
buffer: Buffer.from('console.log(2022);')
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -271,14 +260,15 @@ APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
|
|||
// Or you can pass the file content directly as FilePayload object:
|
||||
FilePayload filePayload = new FilePayload("f.js", "text/javascript",
|
||||
"console.log(2022);".getBytes(StandardCharsets.UTF_8));
|
||||
APIResponse response = request.fetch("https://example.com/api/uploadScript",
|
||||
APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
|
||||
RequestOptions.create().setMethod("post").setMultipart(
|
||||
FormData.create().set("fileField", filePayload)));
|
||||
```
|
||||
|
||||
```python
|
||||
api_request_context.fetch(
|
||||
"https://example.com/api/uploadScript", method="post",
|
||||
"https://example.com/api/uploadScrip'",
|
||||
method="post",
|
||||
multipart={
|
||||
"fileField": {
|
||||
"name": "f.js",
|
||||
|
|
@ -300,28 +290,21 @@ multipart.Set("fileField", file);
|
|||
await Request.FetchAsync("https://example.com/api/uploadScript", new() { Method = "post", Multipart = multipart });
|
||||
```
|
||||
|
||||
|
||||
### param: APIRequestContext.fetch.urlOrRequest
|
||||
* since: v1.16
|
||||
- `urlOrRequest` <[string]|[Request]>
|
||||
|
||||
Target URL or Request to get all parameters from.
|
||||
|
||||
### option: APIRequestContext.fetch.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.fetch.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.fetch.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.fetch.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.fetch.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.fetch.method
|
||||
* since: v1.16
|
||||
* langs: js, python, csharp
|
||||
|
|
@ -336,19 +319,13 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
|
|||
### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.fetch.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.fetch.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -366,9 +343,6 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
|
|||
### option: APIRequestContext.fetch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.fetch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.get
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
@ -382,24 +356,12 @@ context cookies from the response. The method will automatically follow redirect
|
|||
Request parameters can be configured with `params` option, they will be serialized into the URL search parameters:
|
||||
|
||||
```js
|
||||
// Passing params as object
|
||||
await request.get('https://example.com/api/getText', {
|
||||
params: {
|
||||
'isbn': '1234',
|
||||
'page': 23,
|
||||
}
|
||||
});
|
||||
|
||||
// Passing params as URLSearchParams
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('isbn', '1234');
|
||||
searchParams.append('page', 23);
|
||||
searchParams.append('page', 24);
|
||||
await request.get('https://example.com/api/getText', { params: searchParams });
|
||||
|
||||
// Passing params as string
|
||||
const queryString = 'isbn=1234&page=23&page=24';
|
||||
await request.get('https://example.com/api/getText', { params: queryString });
|
||||
```
|
||||
|
||||
```java
|
||||
|
|
@ -428,40 +390,28 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
|
|||
### param: APIRequestContext.get.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.get.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.get.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.get.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.get.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.get.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.get.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.get.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.get.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.get.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.get.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -479,9 +429,6 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
|
|||
### option: APIRequestContext.get.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.head
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
@ -493,40 +440,28 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### param: APIRequestContext.head.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.head.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.head.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.head.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.head.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.head.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.head.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.head.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.head.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-js-fetch-option-form-%%
|
||||
### option: APIRequestContext.head.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.head.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -544,9 +479,6 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.head.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.patch
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
@ -558,40 +490,28 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### param: APIRequestContext.patch.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.patch.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.patch.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.patch.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.patch.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.patch.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.patch.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.patch.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -609,9 +529,6 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.patch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.patch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.post
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
@ -650,7 +567,7 @@ api_request_context.post("https://example.com/api/createBook", data=data)
|
|||
|
||||
```csharp
|
||||
var data = new Dictionary<string, object>() {
|
||||
{ "firstName", "John" },
|
||||
{ "firstNam", "John" },
|
||||
{ "lastName", "Doe" }
|
||||
};
|
||||
await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data });
|
||||
|
|
@ -688,17 +605,26 @@ formData.Set("body", "John Doe");
|
|||
await request.PostAsync("https://example.com/api/findBook", new() { Form = formData });
|
||||
```
|
||||
|
||||
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding. Use [FormData] to construct request body and pass it to the request as `multipart` parameter:
|
||||
The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` encoding. You can achieve that with Playwright API like this:
|
||||
|
||||
```js
|
||||
const form = new FormData();
|
||||
form.set('name', 'John');
|
||||
form.append('name', 'Doe');
|
||||
// Send two file fields with the same name.
|
||||
form.append('file', new File(['console.log(2024);'], 'f1.js', { type: 'text/javascript' }));
|
||||
form.append('file', new File(['hello'], 'f2.txt', { type: 'text/plain' }));
|
||||
await request.post('https://example.com/api/uploadForm', {
|
||||
multipart: form
|
||||
// Open file as a stream and pass it to the request:
|
||||
const stream = fs.createReadStream('team.csv');
|
||||
await request.post('https://example.com/api/uploadTeamList', {
|
||||
multipart: {
|
||||
fileField: stream
|
||||
}
|
||||
});
|
||||
|
||||
// Or you can pass the file content directly as an object:
|
||||
await request.post('https://example.com/api/uploadScript', {
|
||||
multipart: {
|
||||
fileField: {
|
||||
name: 'f.js',
|
||||
mimeType: 'text/javascript',
|
||||
buffer: Buffer.from('console.log(2022);')
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -710,16 +636,16 @@ APIResponse response = request.post("https://example.com/api/uploadTeamList",
|
|||
FormData.create().set("fileField", file)));
|
||||
|
||||
// Or you can pass the file content directly as FilePayload object:
|
||||
FilePayload filePayload1 = new FilePayload("f1.js", "text/javascript",
|
||||
FilePayload filePayload = new FilePayload("f.js", "text/javascript",
|
||||
"console.log(2022);".getBytes(StandardCharsets.UTF_8));
|
||||
APIResponse response = request.post("https://example.com/api/uploadScript",
|
||||
APIResponse response = request.post("https://example.com/api/uploadTeamList",
|
||||
RequestOptions.create().setMultipart(
|
||||
FormData.create().set("fileField", filePayload)));
|
||||
```
|
||||
|
||||
```python
|
||||
api_request_context.post(
|
||||
"https://example.com/api/uploadScript'",
|
||||
"https://example.com/api/uploadScrip'",
|
||||
multipart={
|
||||
"fileField": {
|
||||
"name": "f.js",
|
||||
|
|
@ -744,40 +670,28 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
|
|||
### param: APIRequestContext.post.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.post.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.post.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.post.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.post.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.post.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.post.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.post.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -795,9 +709,6 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
|
|||
### option: APIRequestContext.post.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.post.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.put
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
@ -809,40 +720,28 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### param: APIRequestContext.put.url = %%-fetch-param-url-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.params = %%-js-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### param: APIRequestContext.put.params = %%-java-fetch-params-%%
|
||||
### param: APIRequestContext.put.params = %%-java-csharp-fetch-params-%%
|
||||
* since: v1.18
|
||||
|
||||
### option: APIRequestContext.put.params = %%-python-fetch-option-params-%%
|
||||
### option: APIRequestContext.put.params = %%-js-python-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.params = %%-csharp-fetch-option-params-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.paramsString = %%-csharp-fetch-option-paramsString-%%
|
||||
* since: v1.47
|
||||
|
||||
### option: APIRequestContext.put.headers = %%-js-python-csharp-fetch-option-headers-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-js-fetch-option-form-%%
|
||||
### option: APIRequestContext.put.form = %%-js-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-js-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-python-fetch-option-multipart-%%
|
||||
### option: APIRequestContext.put.multipart = %%-js-python-fetch-option-multipart-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.multipart = %%-csharp-fetch-option-multipart-%%
|
||||
|
|
@ -860,9 +759,6 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.put.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.put.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.storageState
|
||||
* since: v1.16
|
||||
- returns: <[Object]>
|
||||
|
|
@ -880,7 +776,6 @@ context cookies from the response. The method will automatically follow redirect
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[unknown]>>
|
||||
|
||||
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
||||
|
||||
|
|
@ -891,9 +786,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.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
|
|||
- `name` <[string]> Name of the header.
|
||||
- `value` <[string]> Value of the header.
|
||||
|
||||
An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
|
||||
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
|
||||
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
|
||||
|
||||
## async method: APIResponse.json
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# class: Browser
|
||||
* since: v1.8
|
||||
* extends: [EventEmitter]
|
||||
|
||||
A Browser is created via [`method: BrowserType.launch`]. An example of using a [Browser] to create a [Page]:
|
||||
|
||||
|
|
@ -20,10 +21,10 @@ import com.microsoft.playwright.*;
|
|||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
BrowserType firefox = playwright.firefox();
|
||||
BrowserType firefox = playwright.firefox()
|
||||
Browser browser = firefox.launch();
|
||||
Page page = browser.newPage();
|
||||
page.navigate("https://example.com");
|
||||
page.navigate('https://example.com');
|
||||
browser.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -96,17 +97,11 @@ 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.
|
||||
|
||||
### option: Browser.close.reason
|
||||
* since: v1.40
|
||||
- `reason` <[string]>
|
||||
|
||||
The reason to be reported to the operations interrupted by the browser closure.
|
||||
|
||||
## method: Browser.contexts
|
||||
* since: v1.8
|
||||
- returns: <[Array]<[BrowserContext]>>
|
||||
|
|
@ -132,16 +127,16 @@ System.out.println(browser.contexts().size()); // prints "1"
|
|||
|
||||
```python async
|
||||
browser = await pw.webkit.launch()
|
||||
print(len(browser.contexts)) # prints `0`
|
||||
print(len(browser.contexts())) # prints `0`
|
||||
context = await browser.new_context()
|
||||
print(len(browser.contexts)) # prints `1`
|
||||
print(len(browser.contexts())) # prints `1`
|
||||
```
|
||||
|
||||
```python sync
|
||||
browser = pw.webkit.launch()
|
||||
print(len(browser.contexts)) # prints `0`
|
||||
print(len(browser.contexts())) # prints `0`
|
||||
context = browser.new_context()
|
||||
print(len(browser.contexts)) # prints `1`
|
||||
print(len(browser.contexts())) # prints `1`
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -202,7 +197,7 @@ Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
|
|||
BrowserContext context = browser.newContext();
|
||||
// Create a new page in a pristine context.
|
||||
Page page = context.newPage();
|
||||
page.navigate("https://example.com");
|
||||
page.navigate('https://example.com');
|
||||
|
||||
// Graceful close up everything
|
||||
context.close();
|
||||
|
|
@ -255,9 +250,6 @@ await browser.CloseAsync();
|
|||
### option: Browser.newContext.proxy = %%-context-option-proxy-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: Browser.newContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||
* since: 1.46
|
||||
|
||||
### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%%
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -283,9 +275,6 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
|
|||
### option: Browser.newPage.proxy = %%-context-option-proxy-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: Browser.newPage.clientCertificates = %%-context-option-clientCertificates-%%
|
||||
* since: 1.46
|
||||
|
||||
### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%%
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -295,20 +284,6 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
|
|||
### option: Browser.newPage.storageStatePath = %%-csharp-java-context-option-storage-state-path-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Browser.removeAllListeners
|
||||
* since: v1.47
|
||||
* langs: js
|
||||
|
||||
Removes all the listeners of the given type (or all registered listeners if no type given).
|
||||
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
|
||||
|
||||
### param: Browser.removeAllListeners.type
|
||||
* since: v1.47
|
||||
- `type` ?<[string]>
|
||||
|
||||
### option: Browser.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
|
||||
* since: v1.47
|
||||
|
||||
## async method: Browser.startTracing
|
||||
* since: v1.11
|
||||
* langs: java, js, python
|
||||
|
|
@ -331,7 +306,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();
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# class: BrowserContext
|
||||
* since: v1.8
|
||||
* extends: [EventEmitter]
|
||||
|
||||
BrowserContexts provide a way to operate multiple independent browser sessions.
|
||||
|
||||
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
|
||||
context.
|
||||
|
||||
Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser
|
||||
Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser
|
||||
contexts don't write any browsing data to disk.
|
||||
|
||||
```js
|
||||
|
|
@ -63,6 +64,7 @@ await context.CloseAsync();
|
|||
|
||||
## event: BrowserContext.backgroundPage
|
||||
* since: v1.11
|
||||
* langs: js, python
|
||||
- argument: <[Page]>
|
||||
|
||||
:::note
|
||||
|
|
@ -71,12 +73,6 @@ Only works with Chromium browser's persistent context.
|
|||
|
||||
Emitted when new background page is created in the context.
|
||||
|
||||
```java
|
||||
context.onBackgroundPage(backgroundPage -> {
|
||||
System.out.println(backgroundPage.url());
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
const backgroundPage = await context.waitForEvent('backgroundpage');
|
||||
```
|
||||
|
|
@ -89,20 +85,6 @@ background_page = await context.wait_for_event("backgroundpage")
|
|||
background_page = context.wait_for_event("backgroundpage")
|
||||
```
|
||||
|
||||
```csharp
|
||||
context.BackgroundPage += (_, backgroundPage) =>
|
||||
{
|
||||
Console.WriteLine(backgroundPage.Url);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## property: BrowserContext.clock
|
||||
* since: v1.45
|
||||
- type: <[Clock]>
|
||||
|
||||
Playwright has ability to mock clock and passage of time.
|
||||
|
||||
## event: BrowserContext.close
|
||||
* since: v1.8
|
||||
- argument: <[BrowserContext]>
|
||||
|
|
@ -112,19 +94,13 @@ Emitted when Browser context gets closed. This might happen because of one of th
|
|||
* Browser application is closed or crashed.
|
||||
* The [`method: Browser.close`] method was called.
|
||||
|
||||
### option: BrowserContext.close.reason
|
||||
* since: v1.40
|
||||
- `reason` <[string]>
|
||||
|
||||
The reason to be reported to the operations interrupted by the context closure.
|
||||
|
||||
## event: BrowserContext.console
|
||||
* since: v1.34
|
||||
* langs:
|
||||
- alias-java: consoleMessage
|
||||
- argument: <[ConsoleMessage]>
|
||||
|
||||
Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`.
|
||||
Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning.
|
||||
|
||||
The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
|
||||
|
||||
|
|
@ -204,10 +180,7 @@ context.on("dialog", lambda dialog: dialog.accept())
|
|||
```
|
||||
|
||||
```csharp
|
||||
Context.Dialog += async (_, dialog) =>
|
||||
{
|
||||
await dialog.AcceptAsync();
|
||||
};
|
||||
context.Dialog += (_, dialog) => dialog.AcceptAsync();
|
||||
```
|
||||
|
||||
:::note
|
||||
|
|
@ -223,7 +196,7 @@ also fire for popup pages. See also [`event: Page.popup`] to receive events abou
|
|||
|
||||
The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a
|
||||
popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is
|
||||
done and its response has started loading in the popup. If you would like to route/listen to this network request, use [`method: BrowserContext.route`] and [`event: BrowserContext.request`] respectively instead of similar methods on the [Page].
|
||||
done and its response has started loading in the popup.
|
||||
|
||||
```js
|
||||
const newPagePromise = context.waitForEvent('page');
|
||||
|
|
@ -356,14 +329,18 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 });
|
|||
- `cookies` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `url` ?<[string]> Either url or domain / path are required. Optional.
|
||||
- `domain` ?<[string]> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or domain / path are required. Optional.
|
||||
- `path` ?<[string]> Either url or domain / path are required Optional.
|
||||
- `url` ?<[string]> either url or domain / path are required. Optional.
|
||||
- `domain` ?<[string]> either url or domain / path are required Optional.
|
||||
- `path` ?<[string]> either url or domain / path are required Optional.
|
||||
- `expires` ?<[float]> Unix time in seconds. Optional.
|
||||
- `httpOnly` ?<[boolean]> Optional.
|
||||
- `secure` ?<[boolean]> Optional.
|
||||
- `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional.
|
||||
|
||||
Adds cookies to the browser context.
|
||||
|
||||
For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com".
|
||||
|
||||
## async method: BrowserContext.addInitScript
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -407,7 +384,7 @@ browser_context.add_init_script(path="preload.js")
|
|||
```
|
||||
|
||||
```csharp
|
||||
await Context.AddInitScriptAsync(scriptPath: "preload.js");
|
||||
await context.AddInitScriptAsync(scriptPath: "preload.js");
|
||||
```
|
||||
|
||||
:::note
|
||||
|
|
@ -455,6 +432,7 @@ Script to be evaluated in all pages in the browser context. Optional.
|
|||
|
||||
## method: BrowserContext.backgroundPages
|
||||
* since: v1.11
|
||||
* langs: js, python
|
||||
- returns: <[Array]<[Page]>>
|
||||
|
||||
:::note
|
||||
|
|
@ -472,71 +450,7 @@ Returns the browser instance of the context. If it was launched as a persistent
|
|||
## async method: BrowserContext.clearCookies
|
||||
* since: v1.8
|
||||
|
||||
Removes cookies from context. Accepts optional filter.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await context.clearCookies();
|
||||
await context.clearCookies({ name: 'session-id' });
|
||||
await context.clearCookies({ domain: 'my-origin.com' });
|
||||
await context.clearCookies({ domain: /.*my-origin\.com/ });
|
||||
await context.clearCookies({ path: '/api/v1' });
|
||||
await context.clearCookies({ name: 'session-id', domain: 'my-origin.com' });
|
||||
```
|
||||
|
||||
|
||||
```java
|
||||
context.clearCookies();
|
||||
context.clearCookies(new BrowserContext.ClearCookiesOptions().setName("session-id"));
|
||||
context.clearCookies(new BrowserContext.ClearCookiesOptions().setDomain("my-origin.com"));
|
||||
context.clearCookies(new BrowserContext.ClearCookiesOptions().setPath("/api/v1"));
|
||||
context.clearCookies(new BrowserContext.ClearCookiesOptions()
|
||||
.setName("session-id")
|
||||
.setDomain("my-origin.com"));
|
||||
```
|
||||
|
||||
```python async
|
||||
await context.clear_cookies()
|
||||
await context.clear_cookies(name="session-id")
|
||||
await context.clear_cookies(domain="my-origin.com")
|
||||
await context.clear_cookies(path="/api/v1")
|
||||
await context.clear_cookies(name="session-id", domain="my-origin.com")
|
||||
```
|
||||
|
||||
```python sync
|
||||
context.clear_cookies()
|
||||
context.clear_cookies(name="session-id")
|
||||
context.clear_cookies(domain="my-origin.com")
|
||||
context.clear_cookies(path="/api/v1")
|
||||
context.clear_cookies(name="session-id", domain="my-origin.com")
|
||||
```
|
||||
|
||||
```csharp
|
||||
await context.ClearCookiesAsync();
|
||||
await context.ClearCookiesAsync(new() { Name = "session-id" });
|
||||
await context.ClearCookiesAsync(new() { Domain = "my-origin.com" });
|
||||
await context.ClearCookiesAsync(new() { Path = "/api/v1" });
|
||||
await context.ClearCookiesAsync(new() { Name = "session-id", Domain = "my-origin.com" });
|
||||
```
|
||||
|
||||
### option: BrowserContext.clearCookies.name
|
||||
* since: v1.43
|
||||
- `name` <[string]|[RegExp]>
|
||||
|
||||
Only removes cookies with the given name.
|
||||
|
||||
### option: BrowserContext.clearCookies.domain
|
||||
* since: v1.43
|
||||
- `domain` <[string]|[RegExp]>
|
||||
|
||||
Only removes cookies with the given domain.
|
||||
|
||||
### option: BrowserContext.clearCookies.path
|
||||
* since: v1.43
|
||||
- `path` <[string]|[RegExp]>
|
||||
|
||||
Only removes cookies with the given path.
|
||||
Clears context cookies.
|
||||
|
||||
## async method: BrowserContext.clearPermissions
|
||||
* since: v1.8
|
||||
|
|
@ -655,7 +569,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());
|
||||
|
|
@ -679,7 +593,7 @@ from playwright.async_api import async_playwright, Playwright
|
|||
|
||||
async def run(playwright: Playwright):
|
||||
webkit = playwright.webkit
|
||||
browser = await webkit.launch(headless=False)
|
||||
browser = await webkit.launch(headless=false)
|
||||
context = await browser.new_context()
|
||||
await context.expose_binding("pageURL", lambda source: source["page"].url)
|
||||
page = await context.new_page()
|
||||
|
|
@ -705,7 +619,7 @@ from playwright.sync_api import sync_playwright, Playwright
|
|||
|
||||
def run(playwright: Playwright):
|
||||
webkit = playwright.webkit
|
||||
browser = webkit.launch(headless=False)
|
||||
browser = webkit.launch(headless=false)
|
||||
context = browser.new_context()
|
||||
context.expose_binding("pageURL", lambda source: source["page"].url)
|
||||
page = context.new_page()
|
||||
|
|
@ -743,6 +657,83 @@ await page.SetContentAsync("<script>\n" +
|
|||
await page.GetByRole(AriaRole.Button).ClickAsync();
|
||||
```
|
||||
|
||||
An example of passing an element handle:
|
||||
|
||||
```js
|
||||
await context.exposeBinding('clicked', async (source, element) => {
|
||||
console.log(await element.textContent());
|
||||
}, { handle: true });
|
||||
await page.setContent(`
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
`);
|
||||
```
|
||||
|
||||
```java
|
||||
context.exposeBinding("clicked", (source, args) -> {
|
||||
ElementHandle element = (ElementHandle) args[0];
|
||||
System.out.println(element.textContent());
|
||||
return null;
|
||||
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
|
||||
page.setContent("" +
|
||||
"<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
```
|
||||
|
||||
```python async
|
||||
async def print(source, element):
|
||||
print(await element.text_content())
|
||||
|
||||
await context.expose_binding("clicked", print, handle=true)
|
||||
await page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```python sync
|
||||
def print(source, element):
|
||||
print(element.text_content())
|
||||
|
||||
context.expose_binding("clicked", print, handle=true)
|
||||
page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var result = new TaskCompletionSource<string>();
|
||||
var page = await Context.NewPageAsync();
|
||||
await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
|
||||
{
|
||||
return result.TrySetResult(await t.AsElement().TextContentAsync());
|
||||
});
|
||||
|
||||
await page.SetContentAsync("<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
// Note: it makes sense to await the result here, because otherwise, the context
|
||||
// gets closed and the binding function will throw an exception.
|
||||
Assert.AreEqual("Click me", await result.Task);
|
||||
```
|
||||
|
||||
### param: BrowserContext.exposeBinding.name
|
||||
* since: v1.8
|
||||
- `name` <[string]>
|
||||
|
|
@ -757,7 +748,6 @@ Callback function that will be called in the Playwright's context.
|
|||
|
||||
### option: BrowserContext.exposeBinding.handle
|
||||
* since: v1.8
|
||||
* deprecated: This option will be removed in the future.
|
||||
- `handle` <[boolean]>
|
||||
|
||||
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
|
||||
|
|
@ -813,9 +803,8 @@ import java.util.Base64;
|
|||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
BrowserType webkit = playwright.webkit();
|
||||
BrowserType webkit = playwright.webkit()
|
||||
Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
|
||||
BrowserContext context = browser.newContext();
|
||||
context.exposeFunction("sha256", args -> {
|
||||
String text = (String) args[0];
|
||||
MessageDigest crypto;
|
||||
|
|
@ -963,28 +952,22 @@ specified.
|
|||
* since: v1.8
|
||||
- `permissions` <[Array]<[string]>>
|
||||
|
||||
A list of permissions to grant.
|
||||
|
||||
:::danger
|
||||
Supported permissions differ between browsers, and even between different versions of the same browser. Any permission may stop working after an update.
|
||||
:::
|
||||
|
||||
Here are some permissions that may be supported by some browsers:
|
||||
* `'accelerometer'`
|
||||
* `'ambient-light-sensor'`
|
||||
* `'background-sync'`
|
||||
* `'camera'`
|
||||
* `'clipboard-read'`
|
||||
* `'clipboard-write'`
|
||||
A permission or an array of permissions to grant. Permissions can be one of the following values:
|
||||
* `'geolocation'`
|
||||
* `'midi'`
|
||||
* `'midi-sysex'` (system-exclusive midi)
|
||||
* `'notifications'`
|
||||
* `'camera'`
|
||||
* `'microphone'`
|
||||
* `'background-sync'`
|
||||
* `'ambient-light-sensor'`
|
||||
* `'accelerometer'`
|
||||
* `'gyroscope'`
|
||||
* `'magnetometer'`
|
||||
* `'microphone'`
|
||||
* `'midi-sysex'` (system-exclusive midi)
|
||||
* `'midi'`
|
||||
* `'notifications'`
|
||||
* `'accessibility-events'`
|
||||
* `'clipboard-read'`
|
||||
* `'clipboard-write'`
|
||||
* `'payment-handler'`
|
||||
* `'storage-access'`
|
||||
|
||||
### option: BrowserContext.grantPermissions.origin
|
||||
* since: v1.8
|
||||
|
|
@ -1021,20 +1004,6 @@ Creates a new page in the browser context.
|
|||
|
||||
Returns all open pages in the context.
|
||||
|
||||
## async method: BrowserContext.removeAllListeners
|
||||
* since: v1.47
|
||||
* langs: js
|
||||
|
||||
Removes all the listeners of the given type (or all registered listeners if no type given).
|
||||
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
|
||||
|
||||
### param: BrowserContext.removeAllListeners.type
|
||||
* since: v1.47
|
||||
- `type` ?<[string]>
|
||||
|
||||
### option: BrowserContext.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
|
||||
* since: v1.47
|
||||
|
||||
## property: BrowserContext.request
|
||||
* since: v1.16
|
||||
* langs:
|
||||
|
|
@ -1145,11 +1114,11 @@ await browser.CloseAsync();
|
|||
It is possible to examine the request to decide the route action. For example, mocking all requests that contain some post data, and leaving all other requests as is:
|
||||
|
||||
```js
|
||||
await context.route('/api/**', async route => {
|
||||
await context.route('/api/**', route => {
|
||||
if (route.request().postData().includes('my-string'))
|
||||
await route.fulfill({ body: 'mocked-data' });
|
||||
route.fulfill({ body: 'mocked-data' });
|
||||
else
|
||||
await route.continue();
|
||||
route.continue();
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -1163,16 +1132,16 @@ context.route("/api/**", route -> {
|
|||
```
|
||||
|
||||
```python async
|
||||
async def handle_route(route: Route):
|
||||
def handle_route(route):
|
||||
if ("my-string" in route.request.post_data):
|
||||
await route.fulfill(body="mocked-data")
|
||||
route.fulfill(body="mocked-data")
|
||||
else:
|
||||
await route.continue_()
|
||||
route.continue_()
|
||||
await context.route("/api/**", handle_route)
|
||||
```
|
||||
|
||||
```python sync
|
||||
def handle_route(route: Route):
|
||||
def handle_route(route):
|
||||
if ("my-string" in route.request.post_data):
|
||||
route.fulfill(body="mocked-data")
|
||||
else:
|
||||
|
|
@ -1184,7 +1153,7 @@ context.route("/api/**", handle_route)
|
|||
await page.RouteAsync("/api/**", async r =>
|
||||
{
|
||||
if (r.Request.PostData.Contains("my-string"))
|
||||
await r.FulfillAsync(new() { Body = "mocked-data" });
|
||||
await r.FulfillAsync(body: "mocked-data");
|
||||
else
|
||||
await r.ContinueAsync();
|
||||
});
|
||||
|
|
@ -1204,7 +1173,7 @@ Enabling routing disables http cache.
|
|||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||
|
||||
A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
|
||||
When a [`option: Browser.newContext.baseURL`] via the context options was provided and the passed URL is a path,
|
||||
When a [`option: baseURL`] via the context options was provided and the passed URL is a path,
|
||||
it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
|
||||
### param: BrowserContext.route.handler
|
||||
|
|
@ -1272,99 +1241,6 @@ When set to `minimal`, only record information necessary for routing from HAR. T
|
|||
|
||||
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
|
||||
|
||||
|
||||
## async method: BrowserContext.routeWebSocket
|
||||
* since: v1.48
|
||||
|
||||
This method allows to modify websocket connections that are made by any page in the browser context.
|
||||
|
||||
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before creating any pages.
|
||||
|
||||
**Usage**
|
||||
|
||||
Below is an example of a simple handler that blocks some websocket messages.
|
||||
See [WebSocketRoute] for more details and examples.
|
||||
|
||||
```js
|
||||
await context.routeWebSocket('/ws', async ws => {
|
||||
ws.routeSend(message => {
|
||||
if (message === 'to-be-blocked')
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
await ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
context.routeWebSocket("/ws", ws -> {
|
||||
ws.routeSend(message -> {
|
||||
if ("to-be-blocked".equals(message))
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```python async
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
async def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
await ws.connect()
|
||||
|
||||
await context.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```python sync
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
ws.connect()
|
||||
|
||||
context.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```csharp
|
||||
await context.RouteWebSocketAsync("/ws", async ws => {
|
||||
ws.RouteSend(message => {
|
||||
if (message == "to-be-blocked")
|
||||
return;
|
||||
ws.Send(message);
|
||||
});
|
||||
await ws.ConnectAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### param: BrowserContext.routeWebSocket.url
|
||||
* since: v1.48
|
||||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||
|
||||
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: Browser.newContext.baseURL`] context option.
|
||||
|
||||
### param: BrowserContext.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: js, python
|
||||
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
### param: BrowserContext.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: csharp, java
|
||||
- `handler` <[function]\([WebSocketRoute]\)>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
|
||||
## method: BrowserContext.serviceWorkers
|
||||
* since: v1.11
|
||||
* langs: js, python
|
||||
|
|
@ -1412,7 +1288,7 @@ This setting will change the default maximum time for all the methods accepting
|
|||
* since: v1.8
|
||||
- `timeout` <[float]>
|
||||
|
||||
Maximum time in milliseconds. Pass `0` to disable timeout.
|
||||
Maximum time in milliseconds
|
||||
|
||||
## async method: BrowserContext.setExtraHTTPHeaders
|
||||
* since: v1.8
|
||||
|
|
@ -1511,9 +1387,8 @@ Whether to emulate network being offline for the browser context.
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[unknown]>>
|
||||
|
||||
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
||||
Returns storage state for this browser context, contains current cookies and local storage snapshot.
|
||||
|
||||
## async method: BrowserContext.storageState
|
||||
* since: v1.8
|
||||
|
|
@ -1523,29 +1398,10 @@ Returns storage state for this browser context, contains current cookies, local
|
|||
### option: BrowserContext.storageState.path = %%-storagestate-option-path-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: BrowserContext.storageState.indexedDB
|
||||
* since: v1.51
|
||||
- `indexedDB` ?<boolean>
|
||||
|
||||
Set to `true` to include IndexedDB in the storage state snapshot.
|
||||
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this.
|
||||
|
||||
:::note
|
||||
IndexedDBs with typed arrays are currently not supported.
|
||||
:::
|
||||
|
||||
## property: BrowserContext.tracing
|
||||
* since: v1.12
|
||||
- type: <[Tracing]>
|
||||
|
||||
## async method: BrowserContext.unrouteAll
|
||||
* since: v1.41
|
||||
|
||||
Removes all routes created with [`method: BrowserContext.route`] and [`method: BrowserContext.routeFromHAR`].
|
||||
|
||||
### option: BrowserContext.unrouteAll.behavior = %%-unroute-all-options-behavior-%%
|
||||
* since: v1.41
|
||||
|
||||
## async method: BrowserContext.unroute
|
||||
* since: v1.8
|
||||
|
||||
|
|
|
|||
|
|
@ -31,5 +31,3 @@ Browser websocket url.
|
|||
|
||||
Browser websocket endpoint which can be used as an argument to [`method: BrowserType.connect`] to establish connection
|
||||
to the browser.
|
||||
|
||||
Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway, even if the actual listening address is an unspecified address.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -345,15 +337,6 @@ use a temporary directory instead.
|
|||
### option: BrowserType.launchPersistentContext.-inline- = %%-shared-context-params-list-v1.8-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: BrowserType.launchPersistentContext.firefoxUserPrefs = %%-js-python-browser-option-firefoxuserprefs-%%
|
||||
* since: v1.40
|
||||
|
||||
### option: BrowserType.launchPersistentContext.firefoxUserPrefs2 = %%-csharp-java-browser-option-firefoxuserprefs-%%
|
||||
* since: v1.40
|
||||
|
||||
### option: BrowserType.launchPersistentContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||
* since: 1.46
|
||||
|
||||
## async method: BrowserType.launchServer
|
||||
* since: v1.8
|
||||
* langs: js
|
||||
|
|
@ -391,12 +374,6 @@ const { chromium } = require('playwright'); // Or 'webkit' or 'firefox'.
|
|||
### option: BrowserType.launchServer.logger = %%-browser-option-logger-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: BrowserType.launchServer.host
|
||||
* since: v1.45
|
||||
- `host` <[string]>
|
||||
|
||||
Host to use for the web socket. It is optional and if it is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. Consider hardening it with picking a specific interface.
|
||||
|
||||
### option: BrowserType.launchServer.port
|
||||
* since: v1.8
|
||||
- `port` <[int]>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# class: CDPSession
|
||||
* since: v1.8
|
||||
* extends: [EventEmitter]
|
||||
|
||||
The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
|
||||
* protocol methods can be called with `session.send` method.
|
||||
|
|
@ -29,7 +30,7 @@ client.on("Animation.animationCreated", lambda: print("animation created!"))
|
|||
response = await client.send("Animation.getPlaybackRate")
|
||||
print("playback rate is " + str(response["playbackRate"]))
|
||||
await client.send("Animation.setPlaybackRate", {
|
||||
"playbackRate": response["playbackRate"] / 2
|
||||
playbackRate: response["playbackRate"] / 2
|
||||
})
|
||||
```
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ client.on("Animation.animationCreated", lambda: print("animation created!"))
|
|||
response = client.send("Animation.getPlaybackRate")
|
||||
print("playback rate is " + str(response["playbackRate"]))
|
||||
client.send("Animation.setPlaybackRate", {
|
||||
"playbackRate": response["playbackRate"] / 2
|
||||
playbackRate: response["playbackRate"] / 2
|
||||
})
|
||||
```
|
||||
```csharp
|
||||
|
|
|
|||
|
|
@ -1,342 +0,0 @@
|
|||
# class: Clock
|
||||
* since: v1.45
|
||||
|
||||
Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn more about [clock emulation](../clock.md).
|
||||
|
||||
Note that clock is installed for the entire [BrowserContext], so the time
|
||||
in all the pages and iframes is controlled by the same clock.
|
||||
|
||||
## async method: Clock.fastForward
|
||||
* since: v1.45
|
||||
|
||||
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
|
||||
reopening it later, after given time.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await page.clock.fastForward(1000);
|
||||
await page.clock.fastForward('30:00');
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.clock.fast_forward(1000)
|
||||
await page.clock.fast_forward("30:00")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.clock.fast_forward(1000)
|
||||
page.clock.fast_forward("30:00")
|
||||
```
|
||||
|
||||
```java
|
||||
page.clock().fastForward(1000);
|
||||
page.clock().fastForward("30:00");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Clock.FastForwardAsync(1000);
|
||||
await page.Clock.FastForwardAsync("30:00");
|
||||
```
|
||||
|
||||
### param: Clock.fastForward.ticks
|
||||
* since: v1.45
|
||||
- `ticks` <[long]|[string]>
|
||||
|
||||
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||
|
||||
## async method: Clock.install
|
||||
* since: v1.45
|
||||
|
||||
Install fake implementations for the following time-related functions:
|
||||
|
||||
* `Date`
|
||||
* `setTimeout`
|
||||
* `clearTimeout`
|
||||
* `setInterval`
|
||||
* `clearInterval`
|
||||
* `requestAnimationFrame`
|
||||
* `cancelAnimationFrame`
|
||||
* `requestIdleCallback`
|
||||
* `cancelIdleCallback`
|
||||
* `performance`
|
||||
|
||||
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information.
|
||||
|
||||
### option: Clock.install.time
|
||||
* langs: js, java
|
||||
* since: v1.45
|
||||
- `time` <[long]|[string]|[Date]>
|
||||
|
||||
Time to initialize with, current system time by default.
|
||||
|
||||
### option: Clock.install.time
|
||||
* langs: python
|
||||
* since: v1.45
|
||||
- `time` <[float]|[string]|[Date]>
|
||||
|
||||
Time to initialize with, current system time by default.
|
||||
|
||||
### option: Clock.install.time
|
||||
* langs: csharp
|
||||
* since: v1.45
|
||||
- `time` <[string]|[Date]>
|
||||
|
||||
Time to initialize with, current system time by default.
|
||||
|
||||
## async method: Clock.runFor
|
||||
* since: v1.45
|
||||
|
||||
Advance the clock, firing all the time-related callbacks.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await page.clock.runFor(1000);
|
||||
await page.clock.runFor('30:00');
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.clock.run_for(1000);
|
||||
await page.clock.run_for("30:00")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.clock.run_for(1000);
|
||||
page.clock.run_for("30:00")
|
||||
```
|
||||
|
||||
```java
|
||||
page.clock().runFor(1000);
|
||||
page.clock().runFor("30:00");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Clock.RunForAsync(1000);
|
||||
await page.Clock.RunForAsync("30:00");
|
||||
```
|
||||
|
||||
### param: Clock.runFor.ticks
|
||||
* since: v1.45
|
||||
- `ticks` <[long]|[string]>
|
||||
|
||||
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||
|
||||
|
||||
## async method: Clock.pauseAt
|
||||
* since: v1.45
|
||||
|
||||
Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers
|
||||
are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.pauseAt`] or [`method: Clock.resume`] is called.
|
||||
|
||||
Only fires due timers at most once.
|
||||
This is equivalent to user closing the laptop lid for a while and reopening it at the specified time and
|
||||
pausing.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await page.clock.pauseAt(new Date('2020-02-02'));
|
||||
await page.clock.pauseAt('2020-02-02');
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.clock.pause_at(datetime.datetime(2020, 2, 2))
|
||||
await page.clock.pause_at("2020-02-02")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.clock.pause_at(datetime.datetime(2020, 2, 2))
|
||||
page.clock.pause_at("2020-02-02")
|
||||
```
|
||||
|
||||
```java
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
|
||||
page.clock().pauseAt(format.parse("2020-02-02"));
|
||||
page.clock().pauseAt("2020-02-02");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
|
||||
await page.Clock.PauseAtAsync("2020-02-02");
|
||||
```
|
||||
|
||||
For best results, install the clock before navigating the page and set it to a time slightly before the intended test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the page has fully loaded, you can safely use [`method: Clock.pauseAt`] to pause the clock.
|
||||
|
||||
```js
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
|
||||
await page.goto('http://localhost:3333');
|
||||
await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
|
||||
```
|
||||
|
||||
```python async
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
await page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
|
||||
await page.goto("http://localhost:3333")
|
||||
await page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
|
||||
```
|
||||
|
||||
```python sync
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
|
||||
page.goto("http://localhost:3333")
|
||||
page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
|
||||
```
|
||||
|
||||
```java
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
|
||||
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
|
||||
page.navigate("http://localhost:3333");
|
||||
page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
|
||||
```
|
||||
|
||||
### param: Clock.pauseAt.time
|
||||
* langs: js, java
|
||||
* since: v1.45
|
||||
- `time` <[long]|[string]|[Date]>
|
||||
|
||||
Time to pause at.
|
||||
|
||||
### param: Clock.pauseAt.time
|
||||
* langs: python
|
||||
* since: v1.45
|
||||
- `time` <[float]|[string]|[Date]>
|
||||
|
||||
Time to pause at.
|
||||
|
||||
### param: Clock.pauseAt.time
|
||||
* langs: csharp
|
||||
* since: v1.45
|
||||
- `time` <[Date]|[string]>
|
||||
|
||||
Time to pause at.
|
||||
|
||||
## async method: Clock.resume
|
||||
* since: v1.45
|
||||
|
||||
Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
|
||||
|
||||
## async method: Clock.setFixedTime
|
||||
* since: v1.45
|
||||
|
||||
Makes `Date.now` and `new Date()` return fixed fake time at all times,
|
||||
keeps all the timers running.
|
||||
|
||||
Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios, use [`method: Clock.install`] instead. Read docs on [clock emulation](../clock.md) to learn more.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await page.clock.setFixedTime(Date.now());
|
||||
await page.clock.setFixedTime(new Date('2020-02-02'));
|
||||
await page.clock.setFixedTime('2020-02-02');
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.clock.set_fixed_time(datetime.datetime.now())
|
||||
await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
|
||||
await page.clock.set_fixed_time("2020-02-02")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.clock.set_fixed_time(datetime.datetime.now())
|
||||
page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
|
||||
page.clock.set_fixed_time("2020-02-02")
|
||||
```
|
||||
|
||||
```java
|
||||
page.clock().setFixedTime(new Date());
|
||||
page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
|
||||
page.clock().setFixedTime("2020-02-02");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Clock.SetFixedTimeAsync(DateTime.Now);
|
||||
await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
|
||||
await page.Clock.SetFixedTimeAsync("2020-02-02");
|
||||
```
|
||||
|
||||
### param: Clock.setFixedTime.time
|
||||
* langs: js, java
|
||||
* since: v1.45
|
||||
- `time` <[long]|[string]|[Date]>
|
||||
|
||||
Time to be set in milliseconds.
|
||||
|
||||
### param: Clock.setFixedTime.time
|
||||
* langs: python
|
||||
* since: v1.45
|
||||
- `time` <[float]|[string]|[Date]>
|
||||
|
||||
Time to be set.
|
||||
|
||||
### param: Clock.setFixedTime.time
|
||||
* langs: csharp
|
||||
* since: v1.45
|
||||
- `time` <[string]|[Date]>
|
||||
|
||||
Time to be set.
|
||||
|
||||
## async method: Clock.setSystemTime
|
||||
* since: v1.45
|
||||
|
||||
Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example switching from summer to winter time, or changing time zones.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await page.clock.setSystemTime(Date.now());
|
||||
await page.clock.setSystemTime(new Date('2020-02-02'));
|
||||
await page.clock.setSystemTime('2020-02-02');
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.clock.set_system_time(datetime.datetime.now())
|
||||
await page.clock.set_system_time(datetime.datetime(2020, 2, 2))
|
||||
await page.clock.set_system_time("2020-02-02")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.clock.set_system_time(datetime.datetime.now())
|
||||
page.clock.set_system_time(datetime.datetime(2020, 2, 2))
|
||||
page.clock.set_system_time("2020-02-02")
|
||||
```
|
||||
|
||||
```java
|
||||
page.clock().setSystemTime(new Date());
|
||||
page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
|
||||
page.clock().setSystemTime("2020-02-02");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Clock.SetSystemTimeAsync(DateTime.Now);
|
||||
await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
|
||||
await page.Clock.SetSystemTimeAsync("2020-02-02");
|
||||
```
|
||||
|
||||
### param: Clock.setSystemTime.time
|
||||
* langs: js, java
|
||||
* since: v1.45
|
||||
- `time` <[long]|[string]|[Date]>
|
||||
|
||||
Time to be set in milliseconds.
|
||||
|
||||
### param: Clock.setSystemTime.time
|
||||
* langs: python
|
||||
* since: v1.45
|
||||
- `time` <[float]|[string]|[Date]>
|
||||
|
||||
Time to be set.
|
||||
|
||||
### param: Clock.setSystemTime.time
|
||||
* langs: csharp
|
||||
* since: v1.45
|
||||
- `time` <[string]|[Date]>
|
||||
|
||||
Time to be set.
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* since: v1.8
|
||||
|
||||
[ConsoleMessage] objects are dispatched by page via the [`event: Page.console`] event.
|
||||
For each console message logged in the page there will be corresponding event in the Playwright
|
||||
For each console messages logged in the page there will be corresponding event in the Playwright
|
||||
context.
|
||||
|
||||
```js
|
||||
|
|
@ -44,8 +44,8 @@ ConsoleMessage msg = page.waitForConsoleMessage(() -> {
|
|||
});
|
||||
|
||||
// Deconstruct console.log arguments
|
||||
msg.args().get(0).jsonValue(); // hello
|
||||
msg.args().get(1).jsonValue(); // 42
|
||||
msg.args().get(0).jsonValue() // hello
|
||||
msg.args().get(1).jsonValue() // 42
|
||||
```
|
||||
|
||||
```python async
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ Upon successful cancellations, `download.failure()` would resolve to `'canceled'
|
|||
## async method: Download.createReadStream
|
||||
* since: v1.8
|
||||
* langs: java, js, csharp
|
||||
- returns: <[Readable]>
|
||||
- returns: <[null]|[Readable]>
|
||||
|
||||
Returns a readable stream for a successful download, or throws for a failed/canceled download.
|
||||
Returns readable stream for current download or `null` if download failed.
|
||||
|
||||
## async method: Download.delete
|
||||
* since: v1.8
|
||||
|
|
@ -93,9 +93,10 @@ Get the page that the download belongs to.
|
|||
|
||||
## async method: Download.path
|
||||
* since: v1.8
|
||||
- returns: <[path]>
|
||||
- returns: <[null]|[path]>
|
||||
|
||||
Returns path to the downloaded file for a successful download, or throws for a failed/canceled download. The method will wait for the download to finish if necessary. The method throws when connected remotely.
|
||||
Returns path to the downloaded file in case of successful download. The method will
|
||||
wait for the download to finish if necessary. The method throws when connected remotely.
|
||||
|
||||
Note that the download's file name is a random GUID, use [`method: Download.suggestedFilename`]
|
||||
to get suggested file name.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ const { _electron: electron } = require('playwright');
|
|||
})();
|
||||
```
|
||||
|
||||
Note that since you don't need Playwright to install web browsers when testing Electron, you can omit browser download via setting the following environment variable when installing Playwright:
|
||||
|
||||
```bash js
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright
|
||||
```
|
||||
|
||||
**Supported Electron versions are:**
|
||||
* v12.2.0+
|
||||
* v13.4.0+
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue