Compare commits
40 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
679ee37d0f | ||
|
|
710674a0fe | ||
|
|
cb419e75cd | ||
|
|
ce33ec23c7 | ||
|
|
ed7a560ad2 | ||
|
|
b139ffc174 | ||
|
|
d59972aeb7 | ||
|
|
c132756306 | ||
|
|
847b546794 | ||
|
|
1869bd28d6 | ||
|
|
8bc3e0b6cf | ||
|
|
c7d84f5f37 | ||
|
|
e169cd394a | ||
|
|
72382faddc | ||
|
|
6b2858f0fb | ||
|
|
a0b4bd178d | ||
|
|
f622457b33 | ||
|
|
bc3ec153d3 | ||
|
|
3f2640336c | ||
|
|
08422f0651 | ||
|
|
8693fd4743 | ||
|
|
98ff2a891a | ||
|
|
75b429d143 | ||
|
|
b8f802910c | ||
|
|
39c3482980 | ||
|
|
0646773e85 | ||
|
|
e75fe015cf | ||
|
|
497c89dcfb | ||
|
|
b6e9f1fa53 | ||
|
|
620310ffb2 | ||
|
|
eed74036e8 | ||
|
|
80081692cd | ||
|
|
2fed4b6073 | ||
|
|
940078e06c | ||
|
|
f4f2bdd2ac | ||
|
|
53c40e24d2 | ||
|
|
ec76a817ed | ||
|
|
82cd1789b2 | ||
|
|
90de09668e | ||
|
|
3e8b14031b |
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
19
.eslintignore
Normal file
19
.eslintignore
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
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/
|
||||||
|
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"),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
};
|
||||||
72
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
72
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
---
|
||||||
|
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
|
|
||||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Join our Discord Server
|
- name: Join our GitHub Discussions community
|
||||||
url: https://aka.ms/playwright/discord
|
url: https://github.com/microsoft/playwright/discussions
|
||||||
about: Ask questions and discuss with other community members
|
about: Ask questions and discuss with other community members
|
||||||
|
|
|
||||||
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.
|
||||||
44
.github/actions/download-artifact/action.yml
vendored
44
.github/actions/download-artifact/action.yml
vendored
|
|
@ -1,44 +0,0 @@
|
||||||
name: 'Download artifacts'
|
|
||||||
description: 'Download artifacts from GitHub'
|
|
||||||
inputs:
|
|
||||||
namePrefix:
|
|
||||||
description: 'Name prefix of the artifacts to download'
|
|
||||||
required: true
|
|
||||||
default: 'blob-report'
|
|
||||||
path:
|
|
||||||
description: 'Directory with downloaded artifacts'
|
|
||||||
required: true
|
|
||||||
default: '.'
|
|
||||||
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
|
|
||||||
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, {
|
|
||||||
...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) {
|
|
||||||
const result = await github.rest.actions.downloadArtifact({
|
|
||||||
...context.repo,
|
|
||||||
artifact_id: artifact.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
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
unzip -n '${{ inputs.path }}/artifacts/*.zip' -d ${{ inputs.path }}
|
|
||||||
rm -rf '${{ inputs.path }}/artifacts'
|
|
||||||
|
|
@ -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 }}
|
|
||||||
34
.github/actions/upload-blob-report/action.yml
vendored
34
.github/actions/upload-blob-report/action.yml
vendored
|
|
@ -1,34 +0,0 @@
|
||||||
name: 'Upload blob report'
|
|
||||||
description: 'Upload blob report to GitHub artifacts (for pull requests)'
|
|
||||||
inputs:
|
|
||||||
report_dir:
|
|
||||||
description: 'Directory containing blob report'
|
|
||||||
required: true
|
|
||||||
default: 'test-results/blob-report'
|
|
||||||
job_name:
|
|
||||||
description: 'Unique job name'
|
|
||||||
required: true
|
|
||||||
default: ''
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Integrity check
|
|
||||||
shell: bash
|
|
||||||
run: find "${{ inputs.report_dir }}" -name "*.zip" -exec unzip -t {} \;
|
|
||||||
- name: Upload blob report to GitHub
|
|
||||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: blob-report-${{ inputs.job_name }}
|
|
||||||
path: ${{ inputs.report_dir }}/**
|
|
||||||
retention-days: 7
|
|
||||||
- name: Write triggering pull request number in a file
|
|
||||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
|
||||||
shell: bash
|
|
||||||
run: echo '${{ github.event.number }}' > pull_request_number.txt;
|
|
||||||
- name: Upload artifact with the pull request number
|
|
||||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: pull-request-${{ inputs.job_name }}
|
|
||||||
path: pull_request_number.txt
|
|
||||||
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
|
description: Comma-separated list of commit hashes to cherry-pick
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
roll:
|
roll:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
@ -26,7 +23,7 @@ jobs:
|
||||||
echo "Version is not a two digit semver version"
|
echo "Version is not a two digit semver version"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: release-${{ github.event.inputs.version }}
|
ref: release-${{ github.event.inputs.version }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
@ -60,7 +57,7 @@ jobs:
|
||||||
git checkout -b "$BRANCH_NAME"
|
git checkout -b "$BRANCH_NAME"
|
||||||
git push origin $BRANCH_NAME
|
git push origin $BRANCH_NAME
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|
|
||||||
123
.github/workflows/create_test_report.yml
vendored
123
.github/workflows/create_test_report.yml
vendored
|
|
@ -1,123 +0,0 @@
|
||||||
name: Publish Test Results
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["tests 1", "tests 2", "tests others"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
jobs:
|
|
||||||
merge-reports:
|
|
||||||
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
|
|
||||||
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: Merge reports
|
|
||||||
run: |
|
|
||||||
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
|
||||||
|
|
||||||
- name: Azure Login
|
|
||||||
uses: azure/login@v2
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- name: Upload HTML report to Azure
|
|
||||||
run: |
|
|
||||||
REPORT_DIR='run-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}-${{ github.sha }}'
|
|
||||||
azcopy cp --recursive "./playwright-report/*" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR"
|
|
||||||
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html"
|
|
||||||
env:
|
|
||||||
AZCOPY_AUTO_LOGIN_TYPE: AZCLI
|
|
||||||
|
|
||||||
- name: Read pull request number
|
|
||||||
uses: ./.github/actions/download-artifact
|
|
||||||
with:
|
|
||||||
namePrefix: 'pull-request'
|
|
||||||
path: '.'
|
|
||||||
|
|
||||||
- name: Comment on PR
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
let prNumber;
|
|
||||||
if (context.payload.workflow_run.event === 'pull_request') {
|
|
||||||
const prs = context.payload.workflow_run.pull_requests;
|
|
||||||
if (prs.length) {
|
|
||||||
prNumber = prs[0].number;
|
|
||||||
} else {
|
|
||||||
prNumber = parseInt(fs.readFileSync('pull_request_number.txt').toString());
|
|
||||||
console.log('Read pull request number from file: ' + prNumber);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
core.error('Unsupported workflow trigger event: ' + context.payload.workflow_run.event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!prNumber) {
|
|
||||||
core.error('No pull request found for commit ' + context.sha + ' and workflow triggered by: ' + context.payload.workflow_run.event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// Mark previous comments as outdated by minimizing them.
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
|
||||||
...context.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
});
|
|
||||||
for (const comment of comments) {
|
|
||||||
if (comment.user.login === 'github-actions[bot]' && /\[Test results\]\(https:\/\/.+?\) for "${{ github.event.workflow_run.name }}"/.test(comment.body)) {
|
|
||||||
await github.graphql(`
|
|
||||||
mutation {
|
|
||||||
minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) {
|
|
||||||
clientMutationId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const reportDir = 'run-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}-${{ github.sha }}';
|
|
||||||
const reportUrl = `https://mspwblobreport.z1.web.core.windows.net/${reportDir}/index.html#?q=s%3Afailed%20s%3Aflaky`;
|
|
||||||
core.notice('Report url: ' + reportUrl);
|
|
||||||
const mergeWorkflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
||||||
const reportMd = await fs.promises.readFile('report.md', 'utf8');
|
|
||||||
function formatComment(lines) {
|
|
||||||
let body = lines.join('\n');
|
|
||||||
if (body.length > 65535)
|
|
||||||
body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`;
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
const { data: response } = await github.rest.issues.createComment({
|
|
||||||
...context.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: formatComment([
|
|
||||||
`### [Test results](${reportUrl}) for "${{ github.event.workflow_run.name }}"`,
|
|
||||||
reportMd,
|
|
||||||
'',
|
|
||||||
`Merge [workflow run](${mergeWorkflowUrl}).`
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
core.info('Posted comment: ' + response.html_url);
|
|
||||||
37
.github/workflows/infra.yml
vendored
37
.github/workflows/infra.yml
vendored
|
|
@ -10,18 +10,16 @@ on:
|
||||||
- main
|
- main
|
||||||
- release-*
|
- release-*
|
||||||
|
|
||||||
env:
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
doc-and-lint:
|
doc-and-lint:
|
||||||
name: "docs & lint"
|
name: "docs & lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- run: npx playwright install-deps
|
||||||
|
|
@ -35,27 +33,4 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Audit prod NPM dependencies
|
- 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
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
- uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
dotnet-version: 8.0.x
|
|
||||||
- uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '21'
|
|
||||||
- run: npm ci
|
|
||||||
- run: pip install -r utils/doclint/linting-code-snippets/python/requirements.txt
|
|
||||||
- run: mvn package
|
|
||||||
working-directory: utils/doclint/linting-code-snippets/java
|
|
||||||
- run: node utils/doclint/linting-code-snippets/cli.js
|
|
||||||
|
|
|
||||||
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,30 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'docs/src/api/**/*'
|
|
||||||
- 'packages/playwright-core/src/client/**/*'
|
- 'packages/playwright-core/src/client/**/*'
|
||||||
- 'packages/playwright-core/src/utils/isomorphic/**/*'
|
- 'packages/playwright-test/src/matchers/matchers.ts'
|
||||||
- 'packages/playwright/src/matchers/matchers.ts'
|
|
||||||
- 'packages/protocol/src/protocol.yml'
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Create GitHub issue
|
- name: Create GitHub issue
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const currentPlaywrightVersion = require('./package.json').version.match(/\d+\.\d+/)[0];
|
|
||||||
const { data } = await github.rest.git.getCommit({
|
const { data } = await github.rest.git.getCommit({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
commit_sha: context.sha,
|
commit_sha: context.sha,
|
||||||
});
|
});
|
||||||
const commitHeader = data.message.split('\n')[0];
|
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']) {
|
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {
|
||||||
const { data: issuesData } = await github.rest.search.issuesAndPullRequests({
|
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 issueNumber = null;
|
||||||
let issueBody = '';
|
let issueBody = '';
|
||||||
|
|
@ -54,11 +45,11 @@ jobs:
|
||||||
issueBody = issueCreateData.body;
|
issueBody = issueCreateData.body;
|
||||||
}
|
}
|
||||||
const newBody = issueBody.trimEnd() + `
|
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({
|
const data = await github.rest.issues.update({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
issue_number: issueNumber,
|
issue_number: issueNumber,
|
||||||
body: newBody
|
body: newBody
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
.github/workflows/publish_canary.yml
vendored
46
.github/workflows/publish_canary.yml
vendored
|
|
@ -8,24 +8,18 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- release-*
|
- release-*
|
||||||
|
|
||||||
env:
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-canary:
|
publish-canary:
|
||||||
name: "publish canary NPM"
|
name: "publish canary NPM & Publish canary Docker"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- run: npx playwright install-deps
|
||||||
|
|
@ -50,28 +44,36 @@ jobs:
|
||||||
utils/publish_all_packages.sh --beta
|
utils/publish_all_packages.sh --beta
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
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
|
- name: build & publish driver
|
||||||
env:
|
env:
|
||||||
AZ_UPLOAD_FOLDER: driver/next
|
AZ_UPLOAD_FOLDER: driver/next
|
||||||
|
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
|
||||||
|
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}
|
||||||
run: |
|
run: |
|
||||||
utils/build/build-playwright-driver.sh
|
utils/build/build-playwright-driver.sh
|
||||||
utils/build/upload-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:
|
publish-trace-viewer:
|
||||||
name: "publish Trace Viewer to trace.playwright.dev"
|
name: "publish Trace Viewer to trace.playwright.dev"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- name: Deploy Canary
|
- name: Deploy Canary
|
||||||
run: bash utils/build/deploy-trace-viewer.sh --canary
|
run: bash utils/build/deploy-trace-viewer.sh --canary
|
||||||
if: contains(github.ref, 'main')
|
if: contains(github.ref, 'main')
|
||||||
|
|
|
||||||
40
.github/workflows/publish_release_docker.yml
vendored
40
.github/workflows/publish_release_docker.yml
vendored
|
|
@ -2,40 +2,40 @@ name: "publish release - Docker"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
is_release:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
description: "Is this a release image?"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
env:
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-docker-release:
|
publish-docker-release:
|
||||||
name: "publish to DockerHub"
|
name: "publish to DockerHub"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
|
||||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
|
||||||
contents: read # This is required for actions/checkout to succeed
|
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
environment: allow-publishing-docker-to-acr
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
registry-url: 'https://registry.npmjs.org'
|
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
|
- name: Set up Docker QEMU for arm64 docker builds
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- 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
|
- 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')
|
||||||
|
|
|
||||||
24
.github/workflows/publish_release_driver.yml
vendored
24
.github/workflows/publish_release_driver.yml
vendored
|
|
@ -4,34 +4,24 @@ on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
env:
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-driver-release:
|
publish-driver-release:
|
||||||
name: "publish playwright driver to CDN"
|
name: "publish playwright driver to CDN"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- run: npx playwright install-deps
|
||||||
- run: utils/build/build-playwright-driver.sh
|
- 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
|
- run: utils/build/upload-playwright-driver.sh
|
||||||
env:
|
env:
|
||||||
AZ_UPLOAD_FOLDER: driver
|
AZ_UPLOAD_FOLDER: driver
|
||||||
|
AZ_ACCOUNT_KEY: ${{ secrets.AZ_ACCOUNT_KEY }}
|
||||||
|
AZ_ACCOUNT_NAME: ${{ secrets.AZ_ACCOUNT_NAME }}
|
||||||
|
|
|
||||||
19
.github/workflows/publish_release_npm.yml
vendored
19
.github/workflows/publish_release_npm.yml
vendored
|
|
@ -4,31 +4,26 @@ on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
env:
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-npm-release:
|
publish-npm-release:
|
||||||
name: "publish to NPM"
|
name: "publish to NPM"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- run: npx playwright install-deps
|
||||||
- run: utils/publish_all_packages.sh --release-candidate
|
- run: utils/publish_all_packages.sh --release-candidate
|
||||||
if: ${{ github.event.release.prerelease }}
|
if: "github.event.release.prerelease"
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
- run: utils/publish_all_packages.sh --release
|
- run: utils/publish_all_packages.sh --release
|
||||||
if: ${{ !github.event.release.prerelease }}
|
if: "!github.event.release.prerelease"
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
publish-trace-viewer:
|
publish-trace-viewer:
|
||||||
name: "publish Trace Viewer to trace.playwright.dev"
|
name: "publish Trace Viewer to trace.playwright.dev"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- name: Deploy Stable
|
- name: Deploy Stable
|
||||||
run: bash utils/build/deploy-trace-viewer.sh --stable
|
run: bash utils/build/deploy-trace-viewer.sh --stable
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -3,54 +3,37 @@ name: Roll Browser into Playwright
|
||||||
on:
|
on:
|
||||||
repository_dispatch:
|
repository_dispatch:
|
||||||
types: [roll_into_pw]
|
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:
|
jobs:
|
||||||
roll:
|
roll:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npx playwright install-deps
|
run: npx playwright install-deps
|
||||||
- name: Roll to new revision
|
- name: Roll to new revision
|
||||||
run: |
|
run: |
|
||||||
./utils/roll_browser.js $BROWSER $REVISION
|
./utils/roll_browser.js ${{ github.event.client_payload.browser }} ${{ github.event.client_payload.revision }}
|
||||||
npm run build
|
npm run build
|
||||||
- name: Prepare branch
|
- name: Prepare branch
|
||||||
id: prepare-branch
|
id: prepare-branch
|
||||||
run: |
|
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
|
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
git config --global user.name github-actions
|
git config --global user.name github-actions
|
||||||
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
|
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||||
git checkout -b "$BRANCH_NAME"
|
git checkout -b "$BRANCH_NAME"
|
||||||
git add .
|
git add .
|
||||||
git commit -m "feat(${BROWSER}): roll to r${REVISION}"
|
git commit -m "feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}"
|
||||||
git push origin $BRANCH_NAME --force
|
git push origin $BRANCH_NAME
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|
@ -59,7 +42,7 @@ jobs:
|
||||||
repo: 'playwright',
|
repo: 'playwright',
|
||||||
head: 'microsoft:${{ steps.prepare-branch.outputs.BRANCH_NAME }}',
|
head: 'microsoft:${{ steps.prepare-branch.outputs.BRANCH_NAME }}',
|
||||||
base: 'main',
|
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({
|
await github.rest.issues.addLabels({
|
||||||
owner: 'microsoft',
|
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
|
name: Trigger Roll
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.repository == 'microsoft/playwright'
|
if: github.repository == 'microsoft/playwright'
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- run: node utils/build/update-playwright-driver-version.mjs
|
- run: node utils/build/update-playwright-driver-version.mjs
|
||||||
|
|
@ -35,7 +33,7 @@ jobs:
|
||||||
git push origin $BRANCH_NAME
|
git push origin $BRANCH_NAME
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
|
if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }}
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
github-token: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
|
||||||
script: |
|
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
|
|
||||||
17
.github/workflows/tests_components.yml
vendored
17
.github/workflows/tests_components.yml
vendored
|
|
@ -15,27 +15,22 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_components:
|
test_components:
|
||||||
name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }}
|
name: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
node-version: [18]
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
node-version: 20
|
|
||||||
- os: ubuntu-latest
|
|
||||||
node-version: 22
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
# Component tests require Node.js 16+ (they require ESM via TS)
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install --with-deps
|
- run: npx playwright install --with-deps
|
||||||
|
|
|
||||||
54
.github/workflows/tests_electron.yml
vendored
Normal file
54
.github/workflows/tests_electron.yml
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
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: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- 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
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && matrix.os == 'ubuntu-latest'
|
||||||
|
with:
|
||||||
|
name: electron-linux-test-results
|
||||||
|
path: test-results
|
||||||
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:
|
|
||||||
226
.github/workflows/tests_primary.yml
vendored
226
.github/workflows/tests_primary.yml
vendored
|
|
@ -22,183 +22,133 @@ concurrency:
|
||||||
env:
|
env:
|
||||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_linux:
|
test_linux:
|
||||||
name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }})
|
name: ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }})
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
os: [ubuntu-22.04]
|
os: [ubuntu-22.04]
|
||||||
node-version: [18]
|
node-version: [14]
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
node-version: 20
|
node-version: 16
|
||||||
browser: chromium
|
browser: chromium
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
node-version: 22
|
node-version: 18
|
||||||
browser: chromium
|
browser: chromium
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
- run: npm i -g npm@8
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
- run: npm ci
|
||||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}-node${{ matrix.node-version }}"
|
env:
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
|
||||||
|
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.browser }}-${{ matrix.os }}-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
test_linux_chromium_tot:
|
test_linux_chromium_tot:
|
||||||
name: ${{ matrix.os }} (chromium tip-of-tree)
|
name: ${{ matrix.os }} (chromium tip-of-tree)
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04]
|
os: [ubuntu-20.04]
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: chromium-tip-of-tree
|
node-version: 16
|
||||||
command: npm run test -- --project=chromium-*
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.os }}-chromium-tip-of-tree"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
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:
|
env:
|
||||||
PWTEST_CHANNEL: chromium-tip-of-tree
|
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.browser }}-chromium-tip-of-tree-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
test_test_runner:
|
test_test_runner:
|
||||||
name: Test Runner
|
name: Test Runner
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
node-version: [18]
|
node-version: [16]
|
||||||
shardIndex: [1, 2]
|
|
||||||
shardTotal: [2]
|
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node-version: 20
|
node-version: 14
|
||||||
shardIndex: 1
|
|
||||||
shardTotal: 2
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node-version: 20
|
node-version: 18
|
||||||
shardIndex: 2
|
|
||||||
shardTotal: 2
|
|
||||||
- os: ubuntu-latest
|
|
||||||
node-version: 22
|
|
||||||
shardIndex: 1
|
|
||||||
shardTotal: 2
|
|
||||||
- os: ubuntu-latest
|
|
||||||
node-version: 22
|
|
||||||
shardIndex: 2
|
|
||||||
shardTotal: 2
|
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{matrix.node-version}}
|
node-version: ${{matrix.node-version}}
|
||||||
command: npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.os }}-node${{ matrix.node-version }}-${{ matrix.shardIndex }}"
|
- run: npm ci
|
||||||
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:
|
env:
|
||||||
PWTEST_CHANNEL: firefox-beta
|
DEBUG: pw:install
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps
|
||||||
|
- run: npm run ttest
|
||||||
|
if: matrix.os != 'ubuntu-latest'
|
||||||
|
- run: xvfb-run npm run ttest
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
|
||||||
test_web_components:
|
test_web_components:
|
||||||
name: Web Components
|
name: Web Components
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
# Component tests require Node.js 16+ (they require ESM via TS)
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
env:
|
env:
|
||||||
DEBUG: pw:install
|
DEBUG: pw:install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|
||||||
- run: npx playwright install --with-deps
|
- run: npx playwright install --with-deps
|
||||||
- run: npm run test-html-reporter
|
- run: npm run test-html-reporter
|
||||||
env:
|
|
||||||
PWTEST_BOT_NAME: "web-components-html-reporter"
|
|
||||||
- name: Upload blob report
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
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
|
- run: npm run test-web
|
||||||
if: ${{ !cancelled() }}
|
if: always()
|
||||||
env:
|
|
||||||
PWTEST_BOT_NAME: "web-components-web"
|
|
||||||
- name: Upload blob report
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
uses: ./.github/actions/upload-blob-report
|
|
||||||
with:
|
|
||||||
report_dir: packages/web/blob-report
|
|
||||||
job_name: "web-components-web"
|
|
||||||
|
|
||||||
test_vscode_extension:
|
test-package-installations:
|
||||||
name: VSCode Extension
|
name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
|
||||||
PWTEST_BOT_NAME: "vscode-extension"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- run: npm ci
|
|
||||||
env:
|
|
||||||
DEBUG: pw:install
|
|
||||||
- run: npm run build
|
|
||||||
- run: npx playwright install chromium
|
|
||||||
- name: Checkout extension
|
|
||||||
run: git clone https://github.com/microsoft/playwright-vscode.git
|
|
||||||
- name: Print extension revision
|
|
||||||
run: git rev-parse HEAD
|
|
||||||
working-directory: ./playwright-vscode
|
|
||||||
- name: Remove @playwright/test from extension dependencies
|
|
||||||
run: node -e "const p = require('./package.json'); delete p.devDependencies['@playwright/test']; fs.writeFileSync('./package.json', JSON.stringify(p, null, 2));"
|
|
||||||
working-directory: ./playwright-vscode
|
|
||||||
- name: Build extension
|
|
||||||
run: npm install && npm run build
|
|
||||||
working-directory: ./playwright-vscode
|
|
||||||
- name: Run extension tests
|
|
||||||
run: npm run test -- --workers=1
|
|
||||||
working-directory: ./playwright-vscode
|
|
||||||
- name: Upload blob report
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
uses: ./.github/actions/upload-blob-report
|
|
||||||
with:
|
|
||||||
report_dir: playwright-vscode/blob-report
|
|
||||||
job_name: ${{ env.PWTEST_BOT_NAME }}
|
|
||||||
|
|
||||||
test_package_installations:
|
|
||||||
name: "Installation Test ${{ matrix.os }}"
|
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -206,27 +156,25 @@ jobs:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
runs-on: ${{ matrix.os }}
|
node_version:
|
||||||
|
- "^14.1.0" # pre 14.1, zip extraction was broken (https://github.com/microsoft/playwright/issues/1988)
|
||||||
timeout-minutes: 30
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- run: npm install -g yarn@1
|
- uses: actions/setup-node@v3
|
||||||
- run: npm install -g pnpm@8
|
|
||||||
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
|
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
run: |
|
|
||||||
if grep -q "Ubuntu 24" /etc/os-release; then
|
|
||||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
- uses: ./.github/actions/run-test
|
|
||||||
with:
|
with:
|
||||||
command: npm run itest
|
node-version: ${{ matrix.node_version }}
|
||||||
bot-name: "package-installations-${{ matrix.os }}"
|
- run: npm i -g npm@8
|
||||||
shell: ${{ matrix.os == 'windows-latest' && 'pwsh' || 'bash' }}
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
- run: npm run build
|
||||||
|
- run: npx playwright install-deps
|
||||||
|
- run: npm run itest
|
||||||
|
if: matrix.os != 'ubuntu-latest'
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
|
|
||||||
843
.github/workflows/tests_secondary.yml
vendored
843
.github/workflows/tests_secondary.yml
vendored
|
|
@ -17,162 +17,197 @@ on:
|
||||||
env:
|
env:
|
||||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
|
||||||
contents: read # This is required for actions/checkout to succeed
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_linux:
|
test_linux:
|
||||||
name: ${{ matrix.os }} (${{ matrix.browser }})
|
name: ${{ matrix.os }} (${{ matrix.browser }})
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
os: [ubuntu-20.04, ubuntu-24.04]
|
os: [ubuntu-20.04, ubuntu-22.04]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
|
||||||
|
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.browser }}-${{ matrix.os }}-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
test_mac:
|
test_mac:
|
||||||
name: ${{ matrix.os }} (${{ matrix.browser }})
|
name: ${{ matrix.os }} (${{ matrix.browser }})
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Intel: *-large
|
os: [macos-11, macos-12]
|
||||||
# Arm64: *-xlarge
|
|
||||||
os: [macos-13-large, macos-13-xlarge, macos-14-large, macos-14-xlarge]
|
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
include:
|
|
||||||
- os: macos-15-large
|
|
||||||
browser: webkit
|
|
||||||
- os: macos-15-xlarge
|
|
||||||
browser: webkit
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||||
|
- run: npm run test -- --project=${{ matrix.browser }}
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.browser }}-${{ matrix.os }}-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
test_win:
|
test_win:
|
||||||
name: "Windows"
|
name: "Windows"
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-* ${{ matrix.browser == 'firefox' && '--workers 1' || '' }}
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.browser }}-windows-latest"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||||
|
- run: npm run test -- --project=${{ matrix.browser }}
|
||||||
|
shell: bash
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.browser }}-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
test-package-installations-other-node-versions:
|
test-package-installations-other-node-versions:
|
||||||
name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})"
|
name: "Installation Test ${{ matrix.os }} (${{ matrix.node_version }})"
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node_version: 20
|
node_version: "^18.0.0"
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
node_version: 22
|
node_version: "^16.0.0"
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- run: npm install -g yarn@1
|
- uses: actions/setup-node@v3
|
||||||
- run: npm install -g pnpm@8
|
|
||||||
- name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed
|
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
run: |
|
|
||||||
if grep -q "Ubuntu 24" /etc/os-release; then
|
|
||||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
- uses: ./.github/actions/run-test
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node_version }}
|
node-version: ${{ matrix.node_version }}
|
||||||
command: npm run itest
|
- run: npm i -g npm@8
|
||||||
bot-name: "package-installations-${{ matrix.os }}-node${{ matrix.node_version }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
- run: npm run build
|
||||||
|
- run: npx playwright install-deps
|
||||||
|
- run: npm run itest
|
||||||
|
if: matrix.os != 'ubuntu-latest'
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
|
||||||
headed_tests:
|
headed_tests:
|
||||||
name: "headed ${{ matrix.browser }} (${{ matrix.os }})"
|
name: "headed ${{ matrix.browser }} (${{ matrix.os }})"
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
os: [ubuntu-24.04, macos-14-xlarge, windows-latest]
|
os: [ubuntu-20.04, ubuntu-22.04, macos-latest, windows-latest]
|
||||||
include:
|
|
||||||
# We have different binaries per Ubuntu version for WebKit.
|
|
||||||
- browser: webkit
|
|
||||||
os: ubuntu-20.04
|
|
||||||
- browser: webkit
|
|
||||||
os: ubuntu-22.04
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-* --headed
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.browser }}-headed-${{ matrix.os }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
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 }} --headed
|
||||||
|
if: always() && startsWith(matrix.os, 'ubuntu-')
|
||||||
|
- run: npm run test -- --project=${{ matrix.browser }} --headed
|
||||||
|
if: always() && !startsWith(matrix.os, 'ubuntu-')
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always() && startsWith(matrix.os, 'ubuntu-')
|
||||||
|
with:
|
||||||
|
name: headful-${{ matrix.browser }}-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
transport_linux:
|
transport_linux:
|
||||||
name: "Transport"
|
name: "Transport"
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
mode: [driver, service]
|
mode: [driver, service]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: chromium
|
node-version: 16
|
||||||
command: npm run ctest
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.mode }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
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 ctest
|
||||||
env:
|
env:
|
||||||
PWTEST_MODE: ${{ matrix.mode }}
|
PWTEST_MODE: ${{ matrix.mode }}
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: mode-${{ matrix.mode }}-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
tracing_linux:
|
tracing_linux:
|
||||||
name: Tracing ${{ matrix.browser }} ${{ matrix.channel }}
|
name: Tracing ${{ matrix.browser }} ${{ matrix.channel }}
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -184,137 +219,581 @@ jobs:
|
||||||
channel: chromium-tip-of-tree
|
channel: chromium-tip-of-tree
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium ${{ matrix.channel }}
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
- run: npm i -g npm@8
|
||||||
bot-name: "tracing-${{ matrix.channel || matrix.browser }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium ${{ matrix.channel }}
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
|
||||||
env:
|
env:
|
||||||
PWTEST_TRACE: 1
|
PWTEST_TRACE: 1
|
||||||
PWTEST_CHANNEL: ${{ matrix.channel }}
|
PWTEST_CHANNEL: ${{ matrix.channel }}
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
|
||||||
test_chromium_channels:
|
chrome_stable_linux:
|
||||||
name: Test ${{ matrix.channel }} on ${{ matrix.runs-on }}
|
name: "Chrome Stable (Linux)"
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
channel: [chrome, chrome-beta, msedge, msedge-beta, msedge-dev]
|
|
||||||
runs-on: [ubuntu-20.04, macos-latest, windows-latest]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.channel }}
|
node-version: 16
|
||||||
command: npm run ctest
|
- run: npm i -g npm@8
|
||||||
bot-name: ${{ matrix.channel }}-${{ matrix.runs-on }}
|
- run: npm ci
|
||||||
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:
|
env:
|
||||||
PWTEST_CHANNEL: ${{ matrix.channel }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-stable-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
chrome_stable_win:
|
||||||
|
name: "Chrome Stable (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome
|
||||||
|
- run: npm run ctest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-stable-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
chrome_stable_mac:
|
||||||
|
name: "Chrome Stable (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome
|
||||||
|
- run: npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-stable-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
chromium_tot:
|
chromium_tot:
|
||||||
name: Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }}
|
name: Chromium TOT ${{ matrix.os }}
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, macos-13, windows-latest]
|
os: [ubuntu-20.04, macos-12, windows-latest]
|
||||||
headed: ['--headed', '']
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: chromium-tip-of-tree
|
node-version: 16
|
||||||
command: npm run ctest -- ${{ matrix.headed }}
|
- run: npm i -g npm@8
|
||||||
bot-name: "chromium-tip-of-tree-${{ matrix.os }}${{ matrix.headed }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
- 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 ctest
|
||||||
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
env:
|
env:
|
||||||
PWTEST_CHANNEL: chromium-tip-of-tree
|
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||||
|
- run: npm run ctest
|
||||||
chromium_tot_headless_shell:
|
if: matrix.os != 'ubuntu-20.04'
|
||||||
name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
|
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-20.04]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: ./.github/actions/run-test
|
|
||||||
with:
|
|
||||||
browsers-to-install: chromium-tip-of-tree-headless-shell
|
|
||||||
command: npm run ctest
|
|
||||||
bot-name: "chromium-tip-of-tree-headless-shell-${{ matrix.os }}"
|
|
||||||
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:
|
env:
|
||||||
PWTEST_CHANNEL: chromium-tip-of-tree-headless-shell
|
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: chromium-tot-${{ matrix.os }}-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
firefox_beta:
|
chromium_tot_headed:
|
||||||
name: Firefox Beta ${{ matrix.os }}
|
name: Chromium TOT headed ${{ matrix.os }}
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: firefox-beta chromium
|
node-version: 16
|
||||||
command: npm run ftest
|
- run: npm i -g npm@8
|
||||||
bot-name: "firefox-beta-${{ matrix.os }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
- 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 ctest -- --headed
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||||
|
- run: npm run ctest -- --headed
|
||||||
|
if: matrix.os != 'ubuntu-latest'
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: chromium-tot-headed-${{ matrix.os }}-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
firefox_beta_linux:
|
||||||
|
name: "Firefox Beta (Linux)"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps firefox-beta chromium
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ftest
|
||||||
env:
|
env:
|
||||||
PWTEST_CHANNEL: firefox-beta
|
PWTEST_CHANNEL: firefox-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: firefox-beta-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
firefox_beta_win:
|
||||||
|
name: "Firefox Beta (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps firefox-beta chromium
|
||||||
|
- run: npm run ftest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: firefox-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: firefox-beta-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
firefox_beta_mac:
|
||||||
|
name: "Firefox Beta (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps firefox-beta chromium
|
||||||
|
- run: npm run ftest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: firefox-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: firefox-beta-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_stable_mac:
|
||||||
|
name: "Edge Stable (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge
|
||||||
|
- run: npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: msedge-stable-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
|
||||||
|
edge_stable_win:
|
||||||
|
name: "Edge Stable (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge
|
||||||
|
- run: npm run ctest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-stable-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_stable_linux:
|
||||||
|
name: "Edge Stable (Linux)"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-stable-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_beta_mac:
|
||||||
|
name: "Edge Beta (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-beta
|
||||||
|
- run: npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: msedge-beta-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_beta_win:
|
||||||
|
name: "Edge Beta (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-beta
|
||||||
|
- run: npm run ctest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-beta-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_beta_linux:
|
||||||
|
name: "Edge Beta (Linux)"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-beta
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-beta-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_dev_mac:
|
||||||
|
name: "Edge Dev (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-dev
|
||||||
|
- run: npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-dev
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: msedge-dev-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_dev_win:
|
||||||
|
name: "Edge Dev (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-dev
|
||||||
|
- run: npm run ctest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-dev
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-dev-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
edge_dev_linux:
|
||||||
|
name: "Edge Dev (Linux)"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps msedge-dev
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: msedge-dev
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: edge-dev-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
chrome_beta_linux:
|
||||||
|
name: "Chrome Beta (Linux)"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome-beta
|
||||||
|
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-beta-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
chrome_beta_win:
|
||||||
|
name: "Chrome Beta (Win)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome-beta
|
||||||
|
- run: npm run ctest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-beta-win-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
chrome_beta_mac:
|
||||||
|
name: "Chrome Beta (Mac)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps chrome-beta
|
||||||
|
- run: npm run ctest
|
||||||
|
env:
|
||||||
|
PWTEST_CHANNEL: chrome-beta
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: chrome-beta-mac-test-results
|
||||||
|
path: test-results
|
||||||
|
|
||||||
build-playwright-driver:
|
build-playwright-driver:
|
||||||
name: "build-playwright-driver"
|
name: "build-playwright-driver"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
|
- run: npm i -g npm@8
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install-deps
|
- run: npx playwright install-deps
|
||||||
- run: utils/build/build-playwright-driver.sh
|
- run: utils/build/build-playwright-driver.sh
|
||||||
|
|
||||||
test_channel_chromium:
|
|
||||||
name: Test channel=chromium
|
|
||||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
runs-on: [ubuntu-latest, windows-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: ./.github/actions/run-test
|
|
||||||
with:
|
|
||||||
# TODO: this should pass --no-shell.
|
|
||||||
# However, codegen tests do not inherit the channel and try to launch headless shell.
|
|
||||||
browsers-to-install: chromium
|
|
||||||
command: npm run ctest
|
|
||||||
bot-name: "channel-chromium-${{ matrix.runs-on }}"
|
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
|
||||||
env:
|
|
||||||
PWTEST_CHANNEL: chromium
|
|
||||||
|
|
|
||||||
70
.github/workflows/tests_service.yml
vendored
70
.github/workflows/tests_service.yml
vendored
|
|
@ -1,70 +0,0 @@
|
||||||
name: "tests service"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: "Service"
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
service-os: [linux, windows]
|
|
||||||
browser: [chromium, firefox, webkit]
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
- run: npm ci
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
|
||||||
- run: npm run build
|
|
||||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --workers=10 --retries=0
|
|
||||||
env:
|
|
||||||
PWTEST_MODE: service2
|
|
||||||
PWTEST_TRACE: 1
|
|
||||||
PWTEST_BOT_NAME: "${{ matrix.browser }}-${{ matrix.service-os }}-service"
|
|
||||||
PLAYWRIGHT_SERVICE_ACCESS_KEY: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_KEY }}
|
|
||||||
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
|
|
||||||
PLAYWRIGHT_SERVICE_OS: ${{ matrix.service-os }}
|
|
||||||
PLAYWRIGHT_SERVICE_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}
|
|
||||||
- name: Upload blob report to GitHub
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: all-blob-reports
|
|
||||||
path: blob-report
|
|
||||||
retention-days: 2
|
|
||||||
|
|
||||||
merge_reports:
|
|
||||||
name: "Merge reports"
|
|
||||||
needs: [test]
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
- run: npm ci
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
|
||||||
- run: npm run build
|
|
||||||
- name: Download blob report artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: all-blob-reports
|
|
||||||
path: all-blob-reports
|
|
||||||
- run: npx playwright merge-reports --reporter markdown,html ./all-blob-reports
|
|
||||||
- name: Upload HTML report to Azure
|
|
||||||
run: |
|
|
||||||
REPORT_DIR='run-service-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}'
|
|
||||||
azcopy cp --recursive "./playwright-report/*" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR"
|
|
||||||
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/index.html#?q=s:failed"
|
|
||||||
env:
|
|
||||||
AZCOPY_AUTO_LOGIN_TYPE: SPN
|
|
||||||
AZCOPY_SPA_APPLICATION_ID: '${{ secrets.AZCOPY_SPA_APPLICATION_ID }}'
|
|
||||||
AZCOPY_SPA_CLIENT_SECRET: '${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}'
|
|
||||||
AZCOPY_TENANT_ID: '${{ secrets.AZCOPY_TENANT_ID }}'
|
|
||||||
46
.github/workflows/tests_stress.yml
vendored
Normal file
46
.github/workflows/tests_stress.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: "stress"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release-*
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- 'browser_patches/**'
|
||||||
|
- 'docs/**'
|
||||||
|
types: [ labeled ]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release-*
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 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
|
||||||
|
- run: npm i -g npm@8
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright install --with-deps
|
||||||
|
- 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()
|
||||||
33
.github/workflows/tests_video.yml
vendored
33
.github/workflows/tests_video.yml
vendored
|
|
@ -9,30 +9,37 @@ on:
|
||||||
env:
|
env:
|
||||||
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
# Force terminal colors. @see https://www.npmjs.com/package/colors
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
FLAKINESS_CONNECTION_STRING: ${{ secrets.FLAKINESS_CONNECTION_STRING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
video_linux:
|
video_linux:
|
||||||
name: "Video Linux"
|
name: "Video Linux"
|
||||||
environment: allow-uploading-flakiness-results
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
os: [ubuntu-20.04, ubuntu-22.04]
|
os: [ubuntu-20.04, ubuntu-22.04]
|
||||||
permissions:
|
|
||||||
id-token: write # This is required for OIDC login (azure/login) to succeed
|
|
||||||
contents: read # This is required for actions/checkout to succeed
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/run-test
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
browsers-to-install: ${{ matrix.browser }} chromium
|
node-version: 16
|
||||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
- run: npm i -g npm@8
|
||||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
|
- run: npm ci
|
||||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
env:
|
||||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
DEBUG: pw:install
|
||||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
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:
|
env:
|
||||||
PWTEST_VIDEO: 1
|
PWTEST_VIDEO: 1
|
||||||
|
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: video-${{ matrix.browser }}-linux-test-results
|
||||||
|
path: test-results
|
||||||
|
|
|
||||||
49
.github/workflows/tests_webview2.yml
vendored
Normal file
49
.github/workflows/tests_webview2.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
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 i -g npm@8
|
||||||
|
- 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
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: webview2-test-results
|
||||||
|
path: test-results
|
||||||
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: '16'
|
||||||
|
- 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:
|
jobs:
|
||||||
trigger:
|
trigger:
|
||||||
name: "trigger"
|
name: "trigger"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
curl -X POST \
|
curl -X POST \
|
||||||
|
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -7,11 +7,9 @@ node_modules/
|
||||||
*.swp
|
*.swp
|
||||||
*.pyc
|
*.pyc
|
||||||
.vscode
|
.vscode
|
||||||
.mono
|
|
||||||
.idea
|
.idea
|
||||||
yarn.lock
|
yarn.lock
|
||||||
/packages/playwright-core/src/generated
|
/packages/playwright-core/src/generated/*
|
||||||
/packages/playwright-ct-core/src/generated
|
|
||||||
packages/*/lib/
|
packages/*/lib/
|
||||||
drivers/
|
drivers/
|
||||||
.android-sdk/
|
.android-sdk/
|
||||||
|
|
@ -20,19 +18,15 @@ nohup.out
|
||||||
.trace
|
.trace
|
||||||
.tmp
|
.tmp
|
||||||
allure*
|
allure*
|
||||||
blob-report
|
|
||||||
playwright-report
|
playwright-report
|
||||||
test-results
|
test-results
|
||||||
/demo/
|
/demo/
|
||||||
/packages/*/LICENSE
|
/packages/*/LICENSE
|
||||||
/packages/*/NOTICE
|
/packages/*/NOTICE
|
||||||
/packages/playwright/README.md
|
/packages/playwright/README.md
|
||||||
/packages/playwright-test/README.md
|
|
||||||
/packages/playwright-core/api.json
|
/packages/playwright-core/api.json
|
||||||
.env
|
.env
|
||||||
/tests/installation/output/
|
/tests/installation/output/
|
||||||
/tests/installation/.registry.json
|
/tests/installation/.registry.json
|
||||||
.cache/
|
.cache/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
playwright.env
|
|
||||||
/firefox/
|
|
||||||
232
CONTRIBUTING.md
232
CONTRIBUTING.md
|
|
@ -1,87 +1,86 @@
|
||||||
# Contributing
|
# 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.
|
### Getting Code
|
||||||
|
|
||||||
## Make a change
|
Make sure you're running Node.js 14+ and NPM 8+, to verify and upgrade NPM do:
|
||||||
|
|
||||||
Make sure you're running Node.js 20 or later.
|
|
||||||
```bash
|
```bash
|
||||||
node --version
|
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
|
```bash
|
||||||
git clone https://github.com/microsoft/playwright
|
git clone https://github.com/microsoft/playwright
|
||||||
cd playwright
|
cd playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
Install dependencies and run the build in watch mode.
|
2. Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
npm run watch
|
|
||||||
npx playwright install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode**
|
3. Build Playwright
|
||||||
|
|
||||||
```
|
```bash
|
||||||
PW_HMR=1 npm run watch
|
npm run build
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
### Code reviews
|
||||||
```bash
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
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.
|
### Commit Messages
|
||||||
```bash
|
|
||||||
# fast path runs all tests in Chromium
|
|
||||||
npm run ctest
|
|
||||||
|
|
||||||
# slow path runs all tests in three browsers
|
Commit messages should follow the Semantic Commit Messages format:
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
label(namespace): title
|
label(namespace): title
|
||||||
|
|
@ -92,57 +91,134 @@ footer
|
||||||
```
|
```
|
||||||
|
|
||||||
1. *label* is one of the following:
|
1. *label* is one of the following:
|
||||||
- `fix` - bug fixes
|
- `fix` - playwright bug fixes.
|
||||||
- `feat` - new features
|
- `feat` - playwright features.
|
||||||
- `docs` - documentation-only changes
|
- `docs` - changes to docs, e.g. `docs(api.md): ..` to change documentation.
|
||||||
- `test` - test-only changes
|
- `test` - changes to playwright tests infrastructure.
|
||||||
- `devops` - changes to the CI or build
|
- `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
|
- `chore` - everything that doesn't fall under previous categories
|
||||||
1. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
2. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
||||||
1. *title* is a brief summary of changes.
|
3. *title* is a brief summary of changes.
|
||||||
1. *description* is **optional**, new-line separated from title and is in present tense.
|
4. *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.
|
5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(trace viewer): network panel filtering
|
fix(firefox): make sure session cookies work
|
||||||
|
|
||||||
This patch adds a filtering toolbar to the network panel.
|
This patch fixes session cookies in the firefox browser.
|
||||||
<link to a screenshot>
|
|
||||||
|
|
||||||
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
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
- To run all tests in Chromium
|
||||||
|
```bash
|
||||||
|
npm run ctest # also `ftest` for firefox and `wtest` for WebKit
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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
|
```bash
|
||||||
CRPATH=<path-to-executable> npm run ctest
|
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:
|
- **`skip(condition)`**: This test *should ***never*** work* for `condition`
|
||||||
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo.
|
where `condition` is usually a certain browser like `FFOX` (for Firefox),
|
||||||
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.
|
`WEBKIT` (for WebKit), and `CHROMIUM` (for Chromium).
|
||||||
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes.
|
|
||||||
|
For 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
|
## 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.
|
|
||||||
19
README.md
19
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 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)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,11 +8,11 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->134.0.6998.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->112.0.5615.29<!-- 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: |
|
| WebKit <!-- GEN:webkit-version -->16.4<!-- 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: |
|
| Firefox <!-- GEN:firefox-version -->111.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/troubleshooting#system-requirements) for details.
|
||||||
|
|
||||||
Looking for Playwright for [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)?
|
Looking for Playwright for [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)?
|
||||||
|
|
||||||
|
|
@ -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).
|
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)
|
* [Getting started](https://playwright.dev/docs/intro)
|
||||||
|
* [Installation configuration](https://playwright.dev/docs/installation)
|
||||||
* [API reference](https://playwright.dev/docs/api/class-playwright)
|
* [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
@ -90,13 +91,13 @@ To learn how to run these Playwright Test examples, check out our [getting start
|
||||||
|
|
||||||
#### Page screenshot
|
#### Page screenshot
|
||||||
|
|
||||||
This code snippet navigates to Playwright homepage and saves a screenshot.
|
This code snippet navigates to whatsmyuseragent.org and saves a screenshot.
|
||||||
|
|
||||||
```TypeScript
|
```TypeScript
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
|
|
||||||
test('Page Screenshot', async ({ page }) => {
|
test('Page Screenshot', async ({ page }) => {
|
||||||
await page.goto('https://playwright.dev/');
|
await page.goto('http://whatsmyuseragent.org/');
|
||||||
await page.screenshot({ path: `example.png` });
|
await page.screenshot({ path: `example.png` });
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -117,7 +118,7 @@ test.use({
|
||||||
|
|
||||||
test('Mobile and geolocation', async ({ page }) => {
|
test('Mobile and geolocation', async ({ page }) => {
|
||||||
await page.goto('https://maps.google.com');
|
await page.goto('https://maps.google.com');
|
||||||
await page.getByText('Your location').click();
|
await page.locator('text="Your location"').click();
|
||||||
await page.waitForRequest(/.*preview\/pwa/);
|
await page.waitForRequest(/.*preview\/pwa/);
|
||||||
await page.screenshot({ path: 'colosseum-iphone.png' });
|
await page.screenshot({ path: 'colosseum-iphone.png' });
|
||||||
});
|
});
|
||||||
|
|
@ -162,7 +163,7 @@ test('Intercept network requests', async ({ page }) => {
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
* [Documentation](https://playwright.dev)
|
* [Documentation](https://playwright.dev/docs/intro)
|
||||||
* [API reference](https://playwright.dev/docs/api/class-playwright/)
|
* [API reference](https://playwright.dev/docs/api/class-playwright/)
|
||||||
* [Contribution guide](CONTRIBUTING.md)
|
* [Contribution guide](CONTRIBUTING.md)
|
||||||
* [Changelog](https://github.com/microsoft/playwright/releases)
|
* [Changelog](https://github.com/microsoft/playwright/releases)
|
||||||
|
|
|
||||||
16
SECURITY.md
16
SECURITY.md
|
|
@ -1,20 +1,20 @@
|
||||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->
|
||||||
|
|
||||||
## Security
|
## 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
|
## Reporting Security Issues
|
||||||
|
|
||||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
|
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
|
||||||
|
|
||||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
|
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
|
||||||
|
|
||||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||||
|
|
||||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ Please include the requested information listed below (as much as you can provid
|
||||||
|
|
||||||
This information will help us triage your report more quickly.
|
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
|
## Preferred Languages
|
||||||
|
|
||||||
|
|
@ -36,6 +36,6 @@ We prefer all communications to be in English.
|
||||||
|
|
||||||
## Policy
|
## 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 -->
|
<!-- 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": [
|
"plugins": [
|
||||||
["@babel/plugin-transform-typescript", { "allowDeclareFields": true } ],
|
["@babel/plugin-transform-typescript", { "allowDeclareFields": true } ],
|
||||||
"@babel/plugin-transform-export-namespace-from",
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
"@babel/plugin-transform-class-properties",
|
"@babel/plugin-proposal-class-properties",
|
||||||
"@babel/plugin-transform-logical-assignment-operators",
|
"@babel/plugin-proposal-logical-assignment-operators",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator",
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
"@babel/plugin-transform-optional-chaining",
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
"@babel/plugin-transform-modules-commonjs"
|
"@babel/plugin-transform-modules-commonjs"
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||||
BASE_BRANCH="release"
|
BASE_BRANCH="release"
|
||||||
BASE_REVISION="5cfa81898f6eef8fb1abe463e5253cea5bc17f3f"
|
BASE_REVISION="e2956def6c181ca7375897992c5c821a5a6c886d"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||||
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
class Helper {
|
class Helper {
|
||||||
decorateAsEventEmitter(objectToDecorate) {
|
decorateAsEventEmitter(objectToDecorate) {
|
||||||
|
|
@ -16,27 +17,6 @@ class Helper {
|
||||||
objectToDecorate.emit = emitter.emit.bind(emitter);
|
objectToDecorate.emit = emitter.emit.bind(emitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
collectAllBrowsingContexts(rootBrowsingContext, allBrowsingContexts = []) {
|
|
||||||
allBrowsingContexts.push(rootBrowsingContext);
|
|
||||||
for (const child of rootBrowsingContext.children)
|
|
||||||
this.collectAllBrowsingContexts(child, allBrowsingContexts);
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
addObserver(handler, topic) {
|
addObserver(handler, topic) {
|
||||||
Services.obs.addObserver(handler, topic);
|
Services.obs.addObserver(handler, topic);
|
||||||
return () => Services.obs.removeObserver(handler, topic);
|
return () => Services.obs.removeObserver(handler, topic);
|
||||||
|
|
@ -47,15 +27,15 @@ class Helper {
|
||||||
return () => receiver.removeMessageListener(eventName, handler);
|
return () => receiver.removeMessageListener(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(receiver, eventName, handler, options) {
|
addEventListener(receiver, eventName, handler) {
|
||||||
receiver.addEventListener(eventName, handler, options);
|
receiver.addEventListener(eventName, handler);
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
try {
|
||||||
receiver.removeEventListener(eventName, handler, options);
|
receiver.removeEventListener(eventName, handler);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This could fail when window has navigated cross-process
|
// This could fail when window has navigated cross-process
|
||||||
// and we remove the listener from WindowProxy.
|
// and we remove the listener from WindowProxy.
|
||||||
// Nothing we can do here - so ignore the error.
|
dump(`WARNING: removeEventListener throws ${e} at ${new Error().stack}\n`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -69,11 +49,11 @@ class Helper {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
on(receiver, eventName, handler, options) {
|
on(receiver, eventName, handler) {
|
||||||
// The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
|
// The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
|
||||||
// Fire event listeners without it for convenience.
|
// Fire event listeners without it for convenience.
|
||||||
const handlerWrapper = (_, ...args) => handler(...args);
|
const handlerWrapper = (_, ...args) => handler(...args);
|
||||||
receiver.on(eventName, handlerWrapper, options);
|
receiver.on(eventName, handlerWrapper);
|
||||||
return () => receiver.off(eventName, handlerWrapper);
|
return () => receiver.off(eventName, handlerWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,76 +144,10 @@ class Helper {
|
||||||
browsingContextToFrameId(browsingContext) {
|
browsingContextToFrameId(browsingContext) {
|
||||||
if (!browsingContext)
|
if (!browsingContext)
|
||||||
return undefined;
|
return undefined;
|
||||||
if (!browsingContext.parent)
|
return 'frame-' + browsingContext.id;
|
||||||
return 'mainframe-' + browsingContext.browserId;
|
|
||||||
return 'subframe-' + browsingContext.id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const helper = new Helper();
|
var EXPORTED_SYMBOLS = [ "Helper" ];
|
||||||
|
|
||||||
class EventWatcher {
|
|
||||||
constructor(receiver, eventNames, pendingEventWatchers = new Set()) {
|
|
||||||
this._pendingEventWatchers = pendingEventWatchers;
|
|
||||||
this._pendingEventWatchers.add(this);
|
|
||||||
|
|
||||||
this._events = [];
|
|
||||||
this._pendingPromises = [];
|
|
||||||
this._eventListeners = eventNames.map(eventName =>
|
|
||||||
helper.on(receiver, eventName, this._onEvent.bind(this, eventName)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onEvent(eventName, eventObject) {
|
|
||||||
this._events.push({eventName, eventObject});
|
|
||||||
for (const promise of this._pendingPromises)
|
|
||||||
promise.resolve();
|
|
||||||
this._pendingPromises = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureEvent(aEventName, predicate) {
|
|
||||||
if (typeof aEventName !== 'string')
|
|
||||||
throw new Error('ERROR: ensureEvent expects a "string" as its first argument');
|
|
||||||
while (true) {
|
|
||||||
const result = this.getEvent(aEventName, predicate);
|
|
||||||
if (result)
|
|
||||||
return result;
|
|
||||||
await new Promise((resolve, reject) => this._pendingPromises.push({resolve, reject}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureEvents(eventNames, predicate) {
|
|
||||||
if (!Array.isArray(eventNames))
|
|
||||||
throw new Error('ERROR: ensureEvents expects an array of event names as its first argument');
|
|
||||||
return await Promise.all(eventNames.map(eventName => this.ensureEvent(eventName, predicate)));
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureEventsAndDispose(eventNames, predicate) {
|
|
||||||
if (!Array.isArray(eventNames))
|
|
||||||
throw new Error('ERROR: ensureEventsAndDispose expects an array of event names as its first argument');
|
|
||||||
const result = await this.ensureEvents(eventNames, predicate);
|
|
||||||
this.dispose();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEvent(aEventName, predicate = (eventObject) => true) {
|
|
||||||
return this._events.find(({eventName, eventObject}) => eventName === aEventName && predicate(eventObject))?.eventObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEvent(aEventName, predicate) {
|
|
||||||
return !!this.getEvent(aEventName, predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this._pendingEventWatchers.delete(this);
|
|
||||||
for (const promise of this._pendingPromises)
|
|
||||||
promise.reject(new Error('EventWatcher is being disposed'));
|
|
||||||
this._pendingPromises = [];
|
|
||||||
helper.removeListeners(this._eventListeners);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var EXPORTED_SYMBOLS = [ "Helper", "EventWatcher" ];
|
|
||||||
this.Helper = Helper;
|
this.Helper = Helper;
|
||||||
this.EventWatcher = EventWatcher;
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,6 @@ class JugglerFrameParent extends JSWindowActorParent {
|
||||||
receiveMessage() { }
|
receiveMessage() { }
|
||||||
|
|
||||||
async actorCreated() {
|
async actorCreated() {
|
||||||
// Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only
|
|
||||||
// interested in those WindowGlobalParent actors that are matching current browsingContext
|
|
||||||
// window global.
|
|
||||||
// See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15
|
|
||||||
if (!this.manager?.isCurrentGlobal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Only interested in main frames for now.
|
// Only interested in main frames for now.
|
||||||
if (this.browsingContext.parent)
|
if (this.browsingContext.parent)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
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 {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||||
const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm");
|
const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm");
|
||||||
|
|
||||||
|
|
@ -30,8 +31,6 @@ const pageNetworkSymbol = Symbol('PageNetwork');
|
||||||
|
|
||||||
class PageNetwork {
|
class PageNetwork {
|
||||||
static forPageTarget(target) {
|
static forPageTarget(target) {
|
||||||
if (!target)
|
|
||||||
return undefined;
|
|
||||||
let result = target[pageNetworkSymbol];
|
let result = target[pageNetworkSymbol];
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = new PageNetwork(target);
|
result = new PageNetwork(target);
|
||||||
|
|
@ -106,12 +105,21 @@ class NetworkRequest {
|
||||||
this.httpChannel = httpChannel;
|
this.httpChannel = httpChannel;
|
||||||
|
|
||||||
const loadInfo = this.httpChannel.loadInfo;
|
const loadInfo = this.httpChannel.loadInfo;
|
||||||
const browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.workerAssociatedBrowsingContext || loadInfo?.browsingContext;
|
let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext;
|
||||||
|
// TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or
|
||||||
|
// browsingContext.
|
||||||
|
//
|
||||||
|
// We fail to attribute them to the original frames on the browser side, but we
|
||||||
|
// can use load context top frame to attribute them to the top frame at least.
|
||||||
|
if (!browsingContext) {
|
||||||
|
const loadContext = helper.getLoadContext(this.httpChannel);
|
||||||
|
browsingContext = loadContext?.topFrameElement?.browsingContext;
|
||||||
|
}
|
||||||
|
|
||||||
this._frameId = helper.browsingContextToFrameId(browsingContext);
|
this._frameId = helper.browsingContextToFrameId(browsingContext);
|
||||||
|
|
||||||
this.requestId = httpChannel.channelId + '';
|
this.requestId = httpChannel.channelId + '';
|
||||||
this.navigationId = httpChannel.isMainDocumentChannel && loadInfo ? helper.toProtocolNavigationId(loadInfo.jugglerLoadIdentifier) : undefined;
|
this.navigationId = httpChannel.isMainDocumentChannel ? this.requestId : undefined;
|
||||||
|
|
||||||
this._redirectedIndex = 0;
|
this._redirectedIndex = 0;
|
||||||
if (redirectedFrom) {
|
if (redirectedFrom) {
|
||||||
|
|
@ -137,21 +145,12 @@ class NetworkRequest {
|
||||||
throw new Error(`Internal Error: invariant is broken for _channelToRequest map`);
|
throw new Error(`Internal Error: invariant is broken for _channelToRequest map`);
|
||||||
this._networkObserver._channelToRequest.set(this.httpChannel, this);
|
this._networkObserver._channelToRequest.set(this.httpChannel, this);
|
||||||
|
|
||||||
if (redirectedFrom) {
|
this._pageNetwork = redirectedFrom ? redirectedFrom._pageNetwork : networkObserver._findPageNetwork(httpChannel);
|
||||||
this._pageNetwork = redirectedFrom._pageNetwork;
|
|
||||||
} else if (browsingContext) {
|
|
||||||
const target = this._networkObserver._targetRegistry.targetForBrowserId(browsingContext.browserId);
|
|
||||||
this._pageNetwork = PageNetwork.forPageTarget(target);
|
|
||||||
}
|
|
||||||
this._expectingInterception = false;
|
this._expectingInterception = false;
|
||||||
this._expectingResumedRequest = undefined; // { method, headers, postData }
|
this._expectingResumedRequest = undefined; // { method, headers, postData }
|
||||||
this._overriddenHeadersForRedirect = redirectedFrom?._overriddenHeadersForRedirect;
|
|
||||||
this._sentOnResponse = false;
|
this._sentOnResponse = false;
|
||||||
this._fulfilled = false;
|
|
||||||
|
|
||||||
if (this._overriddenHeadersForRedirect)
|
if (this._pageNetwork)
|
||||||
overrideRequestHeaders(httpChannel, this._overriddenHeadersForRedirect);
|
|
||||||
else if (this._pageNetwork)
|
|
||||||
appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
||||||
|
|
||||||
this._responseBodyChunks = [];
|
this._responseBodyChunks = [];
|
||||||
|
|
@ -198,7 +197,6 @@ class NetworkRequest {
|
||||||
|
|
||||||
// Public interception API.
|
// Public interception API.
|
||||||
fulfill(status, statusText, headers, base64body) {
|
fulfill(status, statusText, headers, base64body) {
|
||||||
this._fulfilled = true;
|
|
||||||
this._interceptedChannel.synthesizeStatus(status, statusText);
|
this._interceptedChannel.synthesizeStatus(status, statusText);
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
||||||
|
|
@ -233,13 +231,16 @@ class NetworkRequest {
|
||||||
if (!this._expectingResumedRequest)
|
if (!this._expectingResumedRequest)
|
||||||
return;
|
return;
|
||||||
const { method, headers, postData } = this._expectingResumedRequest;
|
const { method, headers, postData } = this._expectingResumedRequest;
|
||||||
this._overriddenHeadersForRedirect = headers;
|
|
||||||
this._expectingResumedRequest = undefined;
|
this._expectingResumedRequest = undefined;
|
||||||
|
|
||||||
if (headers)
|
if (headers) {
|
||||||
overrideRequestHeaders(this.httpChannel, headers);
|
for (const header of requestHeaders(this.httpChannel))
|
||||||
else if (this._pageNetwork)
|
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());
|
appendExtraHTTPHeaders(this.httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
||||||
|
}
|
||||||
if (method)
|
if (method)
|
||||||
this.httpChannel.requestMethod = method;
|
this.httpChannel.requestMethod = method;
|
||||||
if (postData !== undefined)
|
if (postData !== undefined)
|
||||||
|
|
@ -299,9 +300,6 @@ class NetworkRequest {
|
||||||
}
|
}
|
||||||
if (!credentials)
|
if (!credentials)
|
||||||
return false;
|
return false;
|
||||||
const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort;
|
|
||||||
if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase())
|
|
||||||
return false;
|
|
||||||
authInfo.username = credentials.username;
|
authInfo.username = credentials.username;
|
||||||
authInfo.password = credentials.password;
|
authInfo.password = credentials.password;
|
||||||
// This will produce a new request with respective auth header set.
|
// This will produce a new request with respective auth header set.
|
||||||
|
|
@ -364,6 +362,13 @@ class NetworkRequest {
|
||||||
return;
|
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.
|
// Ok, so now we have intercepted the request, let's issue onRequest.
|
||||||
// If interception has been disabled while we were intercepting, resume and forget.
|
// If interception has been disabled while we were intercepting, resume and forget.
|
||||||
const interceptionEnabled = this._shouldIntercept();
|
const interceptionEnabled = this._shouldIntercept();
|
||||||
|
|
@ -453,15 +458,15 @@ class NetworkRequest {
|
||||||
const browserContext = pageNetwork._target.browserContext();
|
const browserContext = pageNetwork._target.browserContext();
|
||||||
if (browserContext.requestInterceptionEnabled)
|
if (browserContext.requestInterceptionEnabled)
|
||||||
return true;
|
return true;
|
||||||
|
if (browserContext.crossProcessCookie.settings.onlineOverride === 'offline')
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fallThroughInterceptController() {
|
_fallThroughInterceptController() {
|
||||||
try {
|
if (!this._previousCallbacks || !(this._previousCallbacks instanceof Ci.nsINetworkInterceptController))
|
||||||
return this._previousCallbacks?.getInterface(Ci.nsINetworkInterceptController);
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
return this._previousCallbacks.getInterface(Ci.nsINetworkInterceptController);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendOnRequest(isIntercepted) {
|
_sendOnRequest(isIntercepted) {
|
||||||
|
|
@ -551,11 +556,7 @@ class NetworkRequest {
|
||||||
|
|
||||||
_sendOnRequestFinished() {
|
_sendOnRequestFinished() {
|
||||||
const pageNetwork = this._pageNetwork;
|
const pageNetwork = this._pageNetwork;
|
||||||
// Undefined |responseEndTime| means there has been no response yet.
|
if (pageNetwork) {
|
||||||
// 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) {
|
|
||||||
let protocolVersion = undefined;
|
let protocolVersion = undefined;
|
||||||
try {
|
try {
|
||||||
protocolVersion = this.httpChannel.protocolVersion;
|
protocolVersion = this.httpChannel.protocolVersion;
|
||||||
|
|
@ -598,8 +599,6 @@ class NetworkObserver {
|
||||||
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy))
|
|
||||||
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
|
||||||
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
||||||
proxy.type,
|
proxy.type,
|
||||||
proxy.host,
|
proxy.host,
|
||||||
|
|
@ -655,6 +654,16 @@ class NetworkObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findPageNetwork(httpChannel) {
|
||||||
|
let loadContext = helper.getLoadContext(httpChannel);
|
||||||
|
if (!loadContext)
|
||||||
|
return;
|
||||||
|
const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement);
|
||||||
|
if (!target)
|
||||||
|
return;
|
||||||
|
return PageNetwork.forPageTarget(target);
|
||||||
|
}
|
||||||
|
|
||||||
_onRequest(channel, topic) {
|
_onRequest(channel, topic) {
|
||||||
if (!(channel instanceof Ci.nsIHttpChannel))
|
if (!(channel instanceof Ci.nsIHttpChannel))
|
||||||
return;
|
return;
|
||||||
|
|
@ -725,7 +734,6 @@ function readRequestPostData(httpChannel) {
|
||||||
if (!iStream)
|
if (!iStream)
|
||||||
return undefined;
|
return undefined;
|
||||||
const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
|
const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
|
||||||
const isTellableStream = iStream instanceof Ci.nsITellableStream;
|
|
||||||
|
|
||||||
// For some reason, we cannot rewind back big streams,
|
// For some reason, we cannot rewind back big streams,
|
||||||
// so instead we should clone them.
|
// so instead we should clone them.
|
||||||
|
|
@ -734,9 +742,7 @@ function readRequestPostData(httpChannel) {
|
||||||
iStream = iStream.clone();
|
iStream = iStream.clone();
|
||||||
|
|
||||||
let prevOffset;
|
let prevOffset;
|
||||||
// Surprisingly, stream might implement `nsITellableStream` without
|
if (isSeekableStream) {
|
||||||
// implementing the `tell` method.
|
|
||||||
if (isSeekableStream && isTellableStream && iStream.tell) {
|
|
||||||
prevOffset = iStream.tell();
|
prevOffset = iStream.tell();
|
||||||
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -769,20 +775,6 @@ function requestHeaders(httpChannel) {
|
||||||
return headers;
|
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) {
|
function causeTypeToString(causeType) {
|
||||||
for (let key in Ci.nsIContentPolicy) {
|
for (let key in Ci.nsIContentPolicy) {
|
||||||
if (Ci.nsIContentPolicy[key] === causeType)
|
if (Ci.nsIContentPolicy[key] === causeType)
|
||||||
|
|
@ -815,8 +807,7 @@ class ResponseStorage {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let encodings = [];
|
let encodings = [];
|
||||||
// Note: fulfilled request comes with decoded body right away.
|
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) {
|
||||||
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) {
|
|
||||||
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
|
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
|
||||||
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,29 @@
|
||||||
const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
|
const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
|
||||||
|
|
||||||
class SimpleChannel {
|
class SimpleChannel {
|
||||||
constructor(name, uid) {
|
static createForActor(actor) {
|
||||||
|
const channel = new SimpleChannel('');
|
||||||
|
channel.bindToActor(actor);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createForMessageManager(name, mm) {
|
||||||
|
const channel = new SimpleChannel(name);
|
||||||
|
|
||||||
|
const messageListener = {
|
||||||
|
receiveMessage: message => channel._onMessage(message.data)
|
||||||
|
};
|
||||||
|
mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
|
||||||
|
|
||||||
|
channel.setTransport({
|
||||||
|
sendMessage: obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj),
|
||||||
|
dispose: () => mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener),
|
||||||
|
});
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(name) {
|
||||||
this._name = name;
|
this._name = name;
|
||||||
this._messageId = 0;
|
this._messageId = 0;
|
||||||
this._connectorId = 0;
|
this._connectorId = 0;
|
||||||
|
|
@ -21,15 +43,7 @@ class SimpleChannel {
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
};
|
};
|
||||||
this._ready = false;
|
this._ready = false;
|
||||||
this._paused = false;
|
|
||||||
this._disposed = false;
|
this._disposed = false;
|
||||||
|
|
||||||
this._bufferedResponses = new Map();
|
|
||||||
// This is a "unique" identifier of this end of the channel. Two SimpleChannel instances
|
|
||||||
// on the same end of the channel (e.g. two content processes) must not have the same id.
|
|
||||||
// This way, the other end can distinguish between the old peer with a new transport and a new peer.
|
|
||||||
this._uid = uid;
|
|
||||||
this._connectedToUID = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindToActor(actor) {
|
bindToActor(actor) {
|
||||||
|
|
@ -58,29 +72,12 @@ class SimpleChannel {
|
||||||
// 1. There are two channel ends in different processes.
|
// 1. There are two channel ends in different processes.
|
||||||
// 2. Both ends start in the `ready = false` state, meaning that they will
|
// 2. Both ends start in the `ready = false` state, meaning that they will
|
||||||
// not send any messages over transport.
|
// not send any messages over transport.
|
||||||
// 3. Once channel end is created, it sends { ack: `READY` } message to the other end.
|
// 3. Once channel end is created, it sends `READY` message to the other end.
|
||||||
// 4. Eventually, at least one of the ends receives { ack: `READY` } message and responds with
|
// 4. Eventually, at least one of the ends receives `READY` message and responds with
|
||||||
// { ack: `READY_ACK` }. We assume at least one of the ends will receive { ack: "READY" } event from the other, since
|
// `READY_ACK`. We assume at least one of the ends will receive "READY" event from the other, since
|
||||||
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
|
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
|
||||||
// 5. Once channel end receives either { ack: `READY` } or { ack: `READY_ACK` }, it transitions to `ready` state.
|
// 5. Once channel end receives either `READY` or `READY_ACK`, it transitions to `ready` state.
|
||||||
this.transport.sendMessage({ ack: 'READY', uid: this._uid });
|
this.transport.sendMessage('READY');
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this._paused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeSoon() {
|
|
||||||
if (!this._paused)
|
|
||||||
return;
|
|
||||||
this._paused = false;
|
|
||||||
this._setTimeout(() => this._deliverBufferedIncomingMessages(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setTimeout(cb, timeout) {
|
|
||||||
// Lazy load on first call.
|
|
||||||
this._setTimeout = ChromeUtils.import('resource://gre/modules/Timer.jsm').setTimeout;
|
|
||||||
this._setTimeout(cb, timeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_markAsReady() {
|
_markAsReady() {
|
||||||
|
|
@ -124,16 +121,13 @@ class SimpleChannel {
|
||||||
if (this._handlers.has(namespace))
|
if (this._handlers.has(namespace))
|
||||||
throw new Error('ERROR: double-register for namespace ' + namespace);
|
throw new Error('ERROR: double-register for namespace ' + namespace);
|
||||||
this._handlers.set(namespace, handler);
|
this._handlers.set(namespace, handler);
|
||||||
this._deliverBufferedIncomingMessages();
|
// Try to re-deliver all pending messages.
|
||||||
return () => this.unregister(namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deliverBufferedIncomingMessages() {
|
|
||||||
const bufferedRequests = this._bufferedIncomingMessages;
|
const bufferedRequests = this._bufferedIncomingMessages;
|
||||||
this._bufferedIncomingMessages = [];
|
this._bufferedIncomingMessages = [];
|
||||||
for (const data of bufferedRequests) {
|
for (const data of bufferedRequests) {
|
||||||
this._onMessage(data);
|
this._onMessage(data);
|
||||||
}
|
}
|
||||||
|
return () => this.unregister(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
unregister(namespace) {
|
unregister(namespace) {
|
||||||
|
|
@ -160,47 +154,23 @@ class SimpleChannel {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(data) {
|
async _onMessage(data) {
|
||||||
if (data?.ack === 'READY') {
|
if (data === 'READY') {
|
||||||
// The "READY" and "READY_ACK" messages are a part of initialization sequence.
|
this.transport.sendMessage('READY_ACK');
|
||||||
// This sequence happens when:
|
|
||||||
// 1. A new SimpleChannel instance is getting initialized on the other end.
|
|
||||||
// In this case, it will have a different UID and we must clear
|
|
||||||
// `this._bufferedResponses` since they are no longer relevant.
|
|
||||||
// 2. A new transport is assigned to communicate between 2 SimpleChannel instances.
|
|
||||||
// In this case, we MUST NOT clear `this._bufferedResponses` since they are used
|
|
||||||
// to address the double-dispatch issue.
|
|
||||||
if (this._connectedToUID !== data.uid)
|
|
||||||
this._bufferedResponses.clear();
|
|
||||||
this._connectedToUID = data.uid;
|
|
||||||
this.transport.sendMessage({ ack: 'READY_ACK', uid: this._uid });
|
|
||||||
this._markAsReady();
|
this._markAsReady();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data?.ack === 'READY_ACK') {
|
if (data === 'READY_ACK') {
|
||||||
if (this._connectedToUID !== data.uid)
|
|
||||||
this._bufferedResponses.clear();
|
|
||||||
this._connectedToUID = data.uid;
|
|
||||||
this._markAsReady();
|
this._markAsReady();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data?.ack === 'RESPONSE_ACK') {
|
|
||||||
this._bufferedResponses.delete(data.responseId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._paused)
|
|
||||||
this._bufferedIncomingMessages.push(data);
|
|
||||||
else
|
|
||||||
this._onMessageInternal(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onMessageInternal(data) {
|
|
||||||
if (data.responseId) {
|
if (data.responseId) {
|
||||||
this.transport.sendMessage({ ack: 'RESPONSE_ACK', responseId: data.responseId });
|
|
||||||
const message = this._pendingMessages.get(data.responseId);
|
const message = this._pendingMessages.get(data.responseId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
// During cross-process navigation, we might receive a response for
|
// During corss-process navigation, we might receive a response for
|
||||||
// the message sent by another process.
|
// the message sent by another process.
|
||||||
|
// TODO: consider events that are marked as "no-response" to avoid
|
||||||
|
// unneeded responses altogether.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._pendingMessages.delete(data.responseId);
|
this._pendingMessages.delete(data.responseId);
|
||||||
|
|
@ -209,17 +179,6 @@ class SimpleChannel {
|
||||||
else
|
else
|
||||||
message.resolve(data.result);
|
message.resolve(data.result);
|
||||||
} else if (data.requestId) {
|
} else if (data.requestId) {
|
||||||
// When the underlying transport gets replaced, some responses might
|
|
||||||
// not get delivered. As a result, sender will repeat the same request once
|
|
||||||
// a new transport gets set.
|
|
||||||
//
|
|
||||||
// If this request was already processed, we can fulfill it with the cached response
|
|
||||||
// and fast-return.
|
|
||||||
if (this._bufferedResponses.has(data.requestId)) {
|
|
||||||
this.transport.sendMessage(this._bufferedResponses.get(data.requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const namespace = data.namespace;
|
const namespace = data.namespace;
|
||||||
const handler = this._handlers.get(namespace);
|
const handler = this._handlers.get(namespace);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
|
|
@ -231,20 +190,12 @@ class SimpleChannel {
|
||||||
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
|
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let response;
|
|
||||||
const connectedToUID = this._connectedToUID;
|
|
||||||
try {
|
try {
|
||||||
const result = await method.call(handler, ...data.params);
|
const result = await method.call(handler, ...data.params);
|
||||||
response = {responseId: data.requestId, result};
|
this.transport.sendMessage({responseId: data.requestId, result});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
response = {responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`};
|
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
|
||||||
}
|
return;
|
||||||
// The connection might have changed during the ASYNCHRONOUS handler execution.
|
|
||||||
// We only need to buffer & send response if we are connected to the same
|
|
||||||
// end.
|
|
||||||
if (connectedToUID === this._connectedToUID) {
|
|
||||||
this._bufferedResponses.set(data.requestId, response);
|
|
||||||
this.transport.sendMessage(response);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dump(`WARNING: unknown message in channel "${this._name}": ${JSON.stringify(data)}\n`);
|
dump(`WARNING: unknown message in channel "${this._name}": ${JSON.stringify(data)}\n`);
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
|
|
||||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.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 {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
|
||||||
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
|
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
|
||||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||||
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||||
|
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
const Cr = Components.results;
|
const Cr = Components.results;
|
||||||
|
|
||||||
|
|
@ -21,8 +23,6 @@ const ALL_PERMISSIONS = [
|
||||||
'desktop-notification',
|
'desktop-notification',
|
||||||
];
|
];
|
||||||
|
|
||||||
let globalTabAndWindowActivationChain = Promise.resolve();
|
|
||||||
|
|
||||||
class DownloadInterceptor {
|
class DownloadInterceptor {
|
||||||
constructor(registry) {
|
constructor(registry) {
|
||||||
this._registry = registry
|
this._registry = registry
|
||||||
|
|
@ -116,7 +116,6 @@ class TargetRegistry {
|
||||||
this._browserToTarget = new Map();
|
this._browserToTarget = new Map();
|
||||||
this._browserIdToTarget = new Map();
|
this._browserIdToTarget = new Map();
|
||||||
|
|
||||||
this._proxiesWithClashingAuthCacheKeys = new Set();
|
|
||||||
this._browserProxy = null;
|
this._browserProxy = null;
|
||||||
|
|
||||||
// Cleanup containers from previous runs (if any)
|
// Cleanup containers from previous runs (if any)
|
||||||
|
|
@ -142,6 +141,15 @@ class TargetRegistry {
|
||||||
}
|
}
|
||||||
}, 'oop-frameloader-crashed');
|
}, 'oop-frameloader-crashed');
|
||||||
|
|
||||||
|
helper.addObserver((browsingContext, topic, why) => {
|
||||||
|
if (why === 'replace') {
|
||||||
|
// Top-level browsingContext is replaced on cross-process navigations.
|
||||||
|
const target = this._browserIdToTarget.get(browsingContext.browserId);
|
||||||
|
if (target)
|
||||||
|
target.replaceTopBrowsingContext(browsingContext);
|
||||||
|
}
|
||||||
|
}, 'browsing-context-attached');
|
||||||
|
|
||||||
const onTabOpenListener = (appWindow, window, event) => {
|
const onTabOpenListener = (appWindow, window, event) => {
|
||||||
const tab = event.target;
|
const tab = event.target;
|
||||||
const userContextId = tab.userContextId;
|
const userContextId = tab.userContextId;
|
||||||
|
|
@ -186,7 +194,7 @@ class TargetRegistry {
|
||||||
domWindow = appWindow;
|
domWindow = appWindow;
|
||||||
appWindow = null;
|
appWindow = null;
|
||||||
}
|
}
|
||||||
if (!domWindow.isChromeWindow)
|
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
|
||||||
return;
|
return;
|
||||||
// In persistent mode, window might be opened long ago and might be
|
// In persistent mode, window might be opened long ago and might be
|
||||||
// already initialized.
|
// already initialized.
|
||||||
|
|
@ -214,7 +222,7 @@ class TargetRegistry {
|
||||||
|
|
||||||
const onCloseWindow = window => {
|
const onCloseWindow = window => {
|
||||||
const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
||||||
if (!domWindow.isChromeWindow)
|
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
|
||||||
return;
|
return;
|
||||||
if (!domWindow.gBrowser)
|
if (!domWindow.gBrowser)
|
||||||
return;
|
return;
|
||||||
|
|
@ -235,50 +243,12 @@ class TargetRegistry {
|
||||||
onOpenWindow(win);
|
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) {
|
async cancelDownload(options) {
|
||||||
this._downloadInterceptor.cancelDownload(options.uuid);
|
this._downloadInterceptor.cancelDownload(options.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBrowserProxy(proxy) {
|
setBrowserProxy(proxy) {
|
||||||
this._browserProxy = proxy;
|
this._browserProxy = proxy;
|
||||||
this._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getProxyInfo(channel) {
|
getProxyInfo(channel) {
|
||||||
|
|
@ -389,12 +359,11 @@ class PageTarget {
|
||||||
this._openerId = opener ? opener.id() : undefined;
|
this._openerId = opener ? opener.id() : undefined;
|
||||||
this._actor = undefined;
|
this._actor = undefined;
|
||||||
this._actorSequenceNumber = 0;
|
this._actorSequenceNumber = 0;
|
||||||
this._channel = new SimpleChannel(`browser::page[${this._targetId}]`, 'target-' + this._targetId);
|
this._channel = new SimpleChannel(`browser::page[${this._targetId}]`);
|
||||||
this._videoRecordingInfo = undefined;
|
this._videoRecordingInfo = undefined;
|
||||||
this._screencastRecordingInfo = undefined;
|
this._screencastRecordingInfo = undefined;
|
||||||
this._dialogs = new Map();
|
this._dialogs = new Map();
|
||||||
this.forcedColors = 'none';
|
this.forcedColors = 'no-override';
|
||||||
this.disableCache = false;
|
|
||||||
this.mediumOverride = '';
|
this.mediumOverride = '';
|
||||||
this.crossProcessCookie = {
|
this.crossProcessCookie = {
|
||||||
initScripts: [],
|
initScripts: [],
|
||||||
|
|
@ -407,10 +376,9 @@ class PageTarget {
|
||||||
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
|
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
|
||||||
};
|
};
|
||||||
this._eventListeners = [
|
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.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
|
||||||
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
|
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
|
||||||
helper.addEventListener(this._linkedBrowser, 'WillChangeBrowserRemoteness', event => this._willChangeBrowserRemoteness()),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this._disposed = false;
|
this._disposed = false;
|
||||||
|
|
@ -421,34 +389,6 @@ class PageTarget {
|
||||||
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
|
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async activateAndRun(callback = () => {}, { muteNotificationsPopup = false } = {}) {
|
|
||||||
const ownerWindow = this._tab.linkedBrowser.ownerGlobal;
|
|
||||||
const tabBrowser = ownerWindow.gBrowser;
|
|
||||||
// Serialize all tab-switching commands per tabbed browser
|
|
||||||
// to disallow concurrent tab switching.
|
|
||||||
const result = globalTabAndWindowActivationChain.then(async () => {
|
|
||||||
this._window.focus();
|
|
||||||
if (tabBrowser.selectedTab !== this._tab) {
|
|
||||||
const promise = helper.awaitEvent(ownerWindow, 'TabSwitchDone');
|
|
||||||
tabBrowser.selectedTab = this._tab;
|
|
||||||
await promise;
|
|
||||||
}
|
|
||||||
const notificationsPopup = muteNotificationsPopup ? this._linkedBrowser?.ownerDocument.getElementById('notification-popup') : null;
|
|
||||||
notificationsPopup?.style.setProperty('pointer-events', 'none');
|
|
||||||
try {
|
|
||||||
await callback();
|
|
||||||
} finally {
|
|
||||||
notificationsPopup?.style.removeProperty('pointer-events');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
globalTabAndWindowActivationChain = result.catch(error => { /* swallow errors to keep chain running */ });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
frameIdToBrowsingContext(frameId) {
|
|
||||||
return helper.collectAllBrowsingContexts(this._linkedBrowser.browsingContext).find(bc => helper.browsingContextToFrameId(bc) === frameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextActorSequenceNumber() {
|
nextActorSequenceNumber() {
|
||||||
return ++this._actorSequenceNumber;
|
return ++this._actorSequenceNumber;
|
||||||
}
|
}
|
||||||
|
|
@ -467,8 +407,13 @@ class PageTarget {
|
||||||
this._channel.resetTransport();
|
this._channel.resetTransport();
|
||||||
}
|
}
|
||||||
|
|
||||||
_willChangeBrowserRemoteness() {
|
replaceTopBrowsingContext(browsingContext) {
|
||||||
this.removeActor(this._actor);
|
if (this._actor && this._actor.browsingContext !== browsingContext) {
|
||||||
|
// Disconnect early to avoid receiving protocol messages from the old actor.
|
||||||
|
this.removeActor(this._actor);
|
||||||
|
}
|
||||||
|
this.emit(PageTarget.Events.TopBrowsingContextReplaced);
|
||||||
|
this.updateOverridesForBrowsingContext(browsingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog(dialogId) {
|
dialog(dialogId) {
|
||||||
|
|
@ -500,25 +445,6 @@ class PageTarget {
|
||||||
this.updateColorSchemeOverride(browsingContext);
|
this.updateColorSchemeOverride(browsingContext);
|
||||||
this.updateReducedMotionOverride(browsingContext);
|
this.updateReducedMotionOverride(browsingContext);
|
||||||
this.updateForcedColorsOverride(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) {
|
updateTouchOverride(browsingContext = undefined) {
|
||||||
|
|
@ -538,7 +464,7 @@ class PageTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateModalDialogs() {
|
_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()) {
|
for (const dialog of this._dialogs.values()) {
|
||||||
if (!prompts.has(dialog.prompt())) {
|
if (!prompts.has(dialog.prompt())) {
|
||||||
this._dialogs.delete(dialog.id());
|
this._dialogs.delete(dialog.id());
|
||||||
|
|
@ -557,9 +483,6 @@ class PageTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateViewportSize() {
|
async updateViewportSize() {
|
||||||
await waitForWindowReady(this._window);
|
|
||||||
this.updateDPPXOverride();
|
|
||||||
|
|
||||||
// Viewport size is defined by three arguments:
|
// Viewport size is defined by three arguments:
|
||||||
// 1. default size. Could be explicit if set as part of `window.open` call, e.g.
|
// 1. default size. Could be explicit if set as part of `window.open` call, e.g.
|
||||||
// `window.open(url, title, 'width=400,height=400')`
|
// `window.open(url, title, 'width=400,height=400')`
|
||||||
|
|
@ -570,36 +493,13 @@ class PageTarget {
|
||||||
// Otherwise, explicitly set page viewport prevales over browser context
|
// Otherwise, explicitly set page viewport prevales over browser context
|
||||||
// default viewport.
|
// default viewport.
|
||||||
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
|
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
|
||||||
if (viewportSize) {
|
const actualSize = await setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window);
|
||||||
const {width, height} = viewportSize;
|
this.updateDPPXOverride();
|
||||||
this._linkedBrowser.style.setProperty('width', width + 'px');
|
await this._channel.connect('').send('awaitViewportDimensions', {
|
||||||
this._linkedBrowser.style.setProperty('height', height + 'px');
|
width: actualSize.width,
|
||||||
this._linkedBrowser.style.setProperty('box-sizing', 'content-box');
|
height: actualSize.height,
|
||||||
this._linkedBrowser.closest('.browserStack').style.setProperty('overflow', 'auto');
|
deviceSizeIsPageSize: !!this._browserContext.deviceScaleFactor,
|
||||||
this._linkedBrowser.closest('.browserStack').style.setProperty('contain', 'size');
|
});
|
||||||
this._linkedBrowser.closest('.browserStack').style.setProperty('scrollbar-width', 'none');
|
|
||||||
this._linkedBrowser.browsingContext.inRDMPane = true;
|
|
||||||
|
|
||||||
const stackRect = this._linkedBrowser.closest('.browserStack').getBoundingClientRect();
|
|
||||||
const toolbarTop = stackRect.y;
|
|
||||||
this._window.resizeBy(width - this._window.innerWidth, height + toolbarTop - this._window.innerHeight);
|
|
||||||
|
|
||||||
await this._channel.connect('').send('awaitViewportDimensions', { width, height });
|
|
||||||
} else {
|
|
||||||
this._linkedBrowser.style.removeProperty('width');
|
|
||||||
this._linkedBrowser.style.removeProperty('height');
|
|
||||||
this._linkedBrowser.style.removeProperty('box-sizing');
|
|
||||||
this._linkedBrowser.closest('.browserStack').style.removeProperty('overflow');
|
|
||||||
this._linkedBrowser.closest('.browserStack').style.removeProperty('contain');
|
|
||||||
this._linkedBrowser.closest('.browserStack').style.removeProperty('scrollbar-width');
|
|
||||||
this._linkedBrowser.browsingContext.inRDMPane = false;
|
|
||||||
|
|
||||||
const actualSize = this._linkedBrowser.getBoundingClientRect();
|
|
||||||
await this._channel.connect('').send('awaitViewportDimensions', {
|
|
||||||
width: actualSize.width,
|
|
||||||
height: actualSize.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEmulatedMedia(mediumOverride) {
|
setEmulatedMedia(mediumOverride) {
|
||||||
|
|
@ -635,8 +535,7 @@ class PageTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateForcedColorsOverride(browsingContext = undefined) {
|
updateForcedColorsOverride(browsingContext = undefined) {
|
||||||
const isActive = this.forcedColors === 'active' || this._browserContext.forcedColors === 'active';
|
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = (this.forcedColors !== 'no-override' ? this.forcedColors : this._browserContext.forcedColors) || 'no-override';
|
||||||
(browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = isActive ? 'active' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInterceptFileChooserDialog(enabled) {
|
async setInterceptFileChooserDialog(enabled) {
|
||||||
|
|
@ -716,7 +615,7 @@ class PageTarget {
|
||||||
// NSWindow.windowNumber may be -1, so we wait until the window is known
|
// NSWindow.windowNumber may be -1, so we wait until the window is known
|
||||||
// to be initialized and visible.
|
// to be initialized and visible.
|
||||||
await this.windowReady();
|
await this.windowReady();
|
||||||
const file = PathUtils.join(dir, helper.generateId() + '.webm');
|
const file = OS.Path.join(dir, helper.generateId() + '.webm');
|
||||||
if (width < 10 || width > 10000 || height < 10 || height > 10000)
|
if (width < 10 || width > 10000 || height < 10 || height > 10000)
|
||||||
throw new Error("Invalid size");
|
throw new Error("Invalid size");
|
||||||
|
|
||||||
|
|
@ -795,23 +694,7 @@ class PageTarget {
|
||||||
screencastService.stopVideoRecording(screencastId);
|
screencastService.stopVideoRecording(screencastId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureContextMenuClosed() {
|
|
||||||
// Close context menu, if any, since it might capture mouse events on Linux
|
|
||||||
// and prevent browser shutdown on MacOS.
|
|
||||||
const doc = this._linkedBrowser.ownerDocument;
|
|
||||||
const contextMenu = doc.getElementById('contentAreaContextMenu');
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.hidePopup();
|
|
||||||
const autocompletePopup = doc.getElementById('PopupAutoComplete');
|
|
||||||
if (autocompletePopup)
|
|
||||||
autocompletePopup.hidePopup();
|
|
||||||
const selectPopup = doc.getElementById('ContentSelectDropdown')?.menupopup;
|
|
||||||
if (selectPopup)
|
|
||||||
selectPopup.hidePopup()
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.ensureContextMenuClosed();
|
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
if (this._videoRecordingInfo)
|
if (this._videoRecordingInfo)
|
||||||
this._stopVideoRecording();
|
this._stopVideoRecording();
|
||||||
|
|
@ -838,6 +721,7 @@ PageTarget.Events = {
|
||||||
Crashed: Symbol('PageTarget.Crashed'),
|
Crashed: Symbol('PageTarget.Crashed'),
|
||||||
DialogOpened: Symbol('PageTarget.DialogOpened'),
|
DialogOpened: Symbol('PageTarget.DialogOpened'),
|
||||||
DialogClosed: Symbol('PageTarget.DialogClosed'),
|
DialogClosed: Symbol('PageTarget.DialogClosed'),
|
||||||
|
TopBrowsingContextReplaced: Symbol('PageTarget.TopBrowsingContextReplaced'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function fromProtocolColorScheme(colorScheme) {
|
function fromProtocolColorScheme(colorScheme) {
|
||||||
|
|
@ -859,8 +743,8 @@ function fromProtocolReducedMotion(reducedMotion) {
|
||||||
function fromProtocolForcedColors(forcedColors) {
|
function fromProtocolForcedColors(forcedColors) {
|
||||||
if (forcedColors === 'active' || forcedColors === 'none')
|
if (forcedColors === 'active' || forcedColors === 'none')
|
||||||
return forcedColors;
|
return forcedColors;
|
||||||
if (!forcedColors)
|
if (forcedColors === null)
|
||||||
return 'none';
|
return undefined;
|
||||||
throw new Error('Unknown forced colors: ' + forcedColors);
|
throw new Error('Unknown forced colors: ' + forcedColors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -891,10 +775,8 @@ class BrowserContext {
|
||||||
this.defaultUserAgent = null;
|
this.defaultUserAgent = null;
|
||||||
this.defaultPlatform = null;
|
this.defaultPlatform = null;
|
||||||
this.touchOverride = false;
|
this.touchOverride = false;
|
||||||
this.forceOffline = false;
|
|
||||||
this.disableCache = false;
|
|
||||||
this.colorScheme = 'none';
|
this.colorScheme = 'none';
|
||||||
this.forcedColors = 'none';
|
this.forcedColors = 'no-override';
|
||||||
this.reducedMotion = 'none';
|
this.reducedMotion = 'none';
|
||||||
this.videoRecordingOptions = undefined;
|
this.videoRecordingOptions = undefined;
|
||||||
this.crossProcessCookie = {
|
this.crossProcessCookie = {
|
||||||
|
|
@ -946,14 +828,12 @@ class BrowserContext {
|
||||||
}
|
}
|
||||||
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
|
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
|
||||||
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
|
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
|
||||||
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setProxy(proxy) {
|
setProxy(proxy) {
|
||||||
// Clear AuthCache.
|
// Clear AuthCache.
|
||||||
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
||||||
this._proxy = proxy;
|
this._proxy = proxy;
|
||||||
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
|
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
|
||||||
|
|
@ -990,18 +870,6 @@ class BrowserContext {
|
||||||
page.updateTouchOverride();
|
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) {
|
async setDefaultViewport(viewport) {
|
||||||
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
|
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
|
||||||
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;
|
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;
|
||||||
|
|
@ -1229,6 +1097,26 @@ async function waitForWindowReady(window) {
|
||||||
await helper.awaitEvent(window, 'load');
|
await helper.awaitEvent(window, 'load');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setViewportSizeForBrowser(viewportSize, browser, window) {
|
||||||
|
await waitForWindowReady(window);
|
||||||
|
if (viewportSize) {
|
||||||
|
const {width, height} = viewportSize;
|
||||||
|
const rect = browser.getBoundingClientRect();
|
||||||
|
window.resizeBy(width - rect.width, height - rect.height);
|
||||||
|
browser.style.setProperty('min-width', width + 'px');
|
||||||
|
browser.style.setProperty('min-height', height + 'px');
|
||||||
|
browser.style.setProperty('max-width', width + 'px');
|
||||||
|
browser.style.setProperty('max-height', height + 'px');
|
||||||
|
} else {
|
||||||
|
browser.style.removeProperty('min-width');
|
||||||
|
browser.style.removeProperty('min-height');
|
||||||
|
browser.style.removeProperty('max-width');
|
||||||
|
browser.style.removeProperty('max-height');
|
||||||
|
}
|
||||||
|
const rect = browser.getBoundingClientRect();
|
||||||
|
return { width: rect.width, height: rect.height };
|
||||||
|
}
|
||||||
|
|
||||||
TargetRegistry.Events = {
|
TargetRegistry.Events = {
|
||||||
TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
|
TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
|
||||||
TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
|
TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"];
|
||||||
|
|
||||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.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 {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
|
||||||
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
|
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
|
||||||
const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
|
const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
|
||||||
|
|
@ -34,8 +35,6 @@ ActorManagerParent.addJSWindowActors({
|
||||||
DOMDocElementInserted: {},
|
DOMDocElementInserted: {},
|
||||||
// Also, listening to DOMContentLoaded.
|
// Also, listening to DOMContentLoaded.
|
||||||
DOMContentLoaded: {},
|
DOMContentLoaded: {},
|
||||||
DOMWillOpenModalDialog: {},
|
|
||||||
DOMModalDialogClosed: {},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
|
|
@ -105,10 +104,7 @@ class Juggler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force create hidden window here, otherwise its creation later closes the web socket!
|
// 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.
|
Services.appShell.hiddenDOMWindow;
|
||||||
if (Services.appShell.hasHiddenWindow) {
|
|
||||||
Services.appShell.hiddenDOMWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pipeStopped = false;
|
let pipeStopped = false;
|
||||||
let browserHandler;
|
let browserHandler;
|
||||||
|
|
@ -135,13 +131,13 @@ class Juggler {
|
||||||
};
|
};
|
||||||
pipe.init(connection);
|
pipe.init(connection);
|
||||||
const dispatcher = new Dispatcher(connection);
|
const dispatcher = new Dispatcher(connection);
|
||||||
browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, browserStartupFinishedPromise, () => {
|
browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
|
||||||
if (this._silent)
|
if (this._silent)
|
||||||
Services.startup.exitLastWindowClosingSurvivalArea();
|
Services.startup.exitLastWindowClosingSurvivalArea();
|
||||||
connection.onclose();
|
connection.onclose();
|
||||||
pipe.stop();
|
pipe.stop();
|
||||||
pipeStopped = true;
|
pipeStopped = true;
|
||||||
});
|
}, () => browserStartupFinishedPromise);
|
||||||
dispatcher.rootSession().setHandler(browserHandler);
|
dispatcher.rootSession().setHandler(browserHandler);
|
||||||
loadStyleSheet();
|
loadStyleSheet();
|
||||||
dump(`\nJuggler listening to the pipe\n`);
|
dump(`\nJuggler listening to the pipe\n`);
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,10 @@ const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.j
|
||||||
const helper = new Helper();
|
const helper = new Helper();
|
||||||
|
|
||||||
class FrameTree {
|
class FrameTree {
|
||||||
constructor(rootBrowsingContext) {
|
constructor(rootDocShell) {
|
||||||
helper.decorateAsEventEmitter(this);
|
helper.decorateAsEventEmitter(this);
|
||||||
|
|
||||||
this._rootBrowsingContext = rootBrowsingContext;
|
this._browsingContextGroup = rootDocShell.browsingContext.group;
|
||||||
|
|
||||||
this._browsingContextGroup = rootBrowsingContext.group;
|
|
||||||
if (!this._browsingContextGroup.__jugglerFrameTrees)
|
if (!this._browsingContextGroup.__jugglerFrameTrees)
|
||||||
this._browsingContextGroup.__jugglerFrameTrees = new Set();
|
this._browsingContextGroup.__jugglerFrameTrees = new Set();
|
||||||
this._browsingContextGroup.__jugglerFrameTrees.add(this);
|
this._browsingContextGroup.__jugglerFrameTrees.add(this);
|
||||||
|
|
@ -31,14 +29,12 @@ class FrameTree {
|
||||||
|
|
||||||
this._runtime = new Runtime(false /* isWorker */);
|
this._runtime = new Runtime(false /* isWorker */);
|
||||||
this._workers = new Map();
|
this._workers = new Map();
|
||||||
|
this._docShellToFrame = new Map();
|
||||||
this._frameIdToFrame = new Map();
|
this._frameIdToFrame = new Map();
|
||||||
this._pageReady = false;
|
this._pageReady = false;
|
||||||
this._javaScriptDisabled = false;
|
this._javaScriptDisabled = false;
|
||||||
for (const browsingContext of helper.collectAllBrowsingContexts(rootBrowsingContext))
|
this._mainFrame = this._createFrame(rootDocShell);
|
||||||
this._createFrame(browsingContext);
|
const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
this._mainFrame = this.frameForBrowsingContext(rootBrowsingContext);
|
|
||||||
|
|
||||||
const webProgress = rootBrowsingContext.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
||||||
.getInterface(Ci.nsIWebProgress);
|
.getInterface(Ci.nsIWebProgress);
|
||||||
this.QueryInterface = ChromeUtils.generateQI([
|
this.QueryInterface = ChromeUtils.generateQI([
|
||||||
Ci.nsIWebProgressListener,
|
Ci.nsIWebProgressListener,
|
||||||
|
|
@ -46,6 +42,8 @@ class FrameTree {
|
||||||
Ci.nsISupportsWeakReference,
|
Ci.nsISupportsWeakReference,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
this._addedScrollbarsStylesheetSymbol = Symbol('_addedScrollbarsStylesheetSymbol');
|
||||||
|
|
||||||
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
|
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
|
||||||
this._wdmListener = {
|
this._wdmListener = {
|
||||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
|
||||||
|
|
@ -59,29 +57,12 @@ class FrameTree {
|
||||||
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
|
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
|
||||||
Ci.nsIWebProgress.NOTIFY_LOCATION;
|
Ci.nsIWebProgress.NOTIFY_LOCATION;
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
helper.addObserver((docShell, topic, loadIdentifier) => {
|
|
||||||
const frame = this.frameForDocShell(docShell);
|
|
||||||
if (!frame)
|
|
||||||
return;
|
|
||||||
frame._pendingNavigationId = helper.toProtocolNavigationId(loadIdentifier);
|
|
||||||
this.emit(FrameTree.Events.NavigationStarted, frame);
|
|
||||||
}, 'juggler-navigation-started-renderer'),
|
|
||||||
helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
|
helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
|
||||||
helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'),
|
helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'),
|
||||||
helper.addObserver((browsingContext, topic, why) => {
|
helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'),
|
||||||
this._onBrowsingContextAttached(browsingContext);
|
helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'),
|
||||||
}, 'browsing-context-attached'),
|
|
||||||
helper.addObserver((browsingContext, topic, why) => {
|
|
||||||
this._onBrowsingContextDetached(browsingContext);
|
|
||||||
}, 'browsing-context-discarded'),
|
|
||||||
helper.addObserver((subject, topic, eventInfo) => {
|
|
||||||
const [type, jugglerEventId] = eventInfo.split(' ');
|
|
||||||
this.emit(FrameTree.Events.InputEvent, { type, jugglerEventId: +(jugglerEventId ?? '0') });
|
|
||||||
}, 'juggler-mouse-event-hit-renderer'),
|
|
||||||
helper.addProgressListener(webProgress, this, flags),
|
helper.addProgressListener(webProgress, this, flags),
|
||||||
];
|
];
|
||||||
|
|
||||||
this._dragEventListeners = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workers() {
|
workers() {
|
||||||
|
|
@ -124,16 +105,29 @@ class FrameTree {
|
||||||
return null;
|
return null;
|
||||||
if (!workerDebugger.window)
|
if (!workerDebugger.window)
|
||||||
return null;
|
return null;
|
||||||
return this.frameForDocShell(workerDebugger.window.docShell);
|
const docShell = workerDebugger.window.docShell;
|
||||||
|
return this._docShellToFrame.get(docShell) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDOMWindowCreated(window) {
|
_onDOMWindowCreated(window) {
|
||||||
const frame = this.frameForDocShell(window.docShell);
|
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._docShellToFrame.get(window.docShell) || null;
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
frame._onGlobalObjectCleared();
|
frame._onGlobalObjectCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setScrollbarsHidden(hidden) {
|
||||||
|
this.scrollbarsHidden = hidden;
|
||||||
|
}
|
||||||
|
|
||||||
setJavaScriptDisabled(javaScriptDisabled) {
|
setJavaScriptDisabled(javaScriptDisabled) {
|
||||||
this._javaScriptDisabled = javaScriptDisabled;
|
this._javaScriptDisabled = javaScriptDisabled;
|
||||||
for (const frame of this.frames())
|
for (const frame of this.frames())
|
||||||
|
|
@ -199,18 +193,8 @@ class FrameTree {
|
||||||
frame._addBinding(worldName, name, script);
|
frame._addBinding(worldName, name, script);
|
||||||
}
|
}
|
||||||
|
|
||||||
frameForBrowsingContext(browsingContext) {
|
|
||||||
if (!browsingContext)
|
|
||||||
return null;
|
|
||||||
const frameId = helper.browsingContextToFrameId(browsingContext);
|
|
||||||
return this._frameIdToFrame.get(frameId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
frameForDocShell(docShell) {
|
frameForDocShell(docShell) {
|
||||||
if (!docShell)
|
return this._docShellToFrame.get(docShell) || null;
|
||||||
return null;
|
|
||||||
const frameId = helper.browsingContextToFrameId(docShell.browsingContext);
|
|
||||||
return this._frameIdToFrame.get(frameId) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(frameId) {
|
frame(frameId) {
|
||||||
|
|
@ -238,51 +222,6 @@ class FrameTree {
|
||||||
this._wdm.removeListener(this._wdmListener);
|
this._wdm.removeListener(this._wdmListener);
|
||||||
this._runtime.dispose();
|
this._runtime.dispose();
|
||||||
helper.removeListeners(this._eventListeners);
|
helper.removeListeners(this._eventListeners);
|
||||||
helper.removeListeners(this._dragEventListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
onWindowEvent(event) {
|
|
||||||
if (event.type !== 'DOMDocElementInserted' || !event.target.ownerGlobal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const docShell = event.target.ownerGlobal.docShell;
|
|
||||||
const frame = this.frameForDocShell(docShell);
|
|
||||||
if (!frame) {
|
|
||||||
dump(`WARNING: ${event.type} for unknown frame ${helper.browsingContextToFrameId(docShell.browsingContext)}\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame._pendingNavigationId) {
|
|
||||||
docShell.QueryInterface(Ci.nsIWebNavigation);
|
|
||||||
this._frameNavigationCommitted(frame, docShell.currentURI.spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame === this._mainFrame) {
|
|
||||||
helper.removeListeners(this._dragEventListeners);
|
|
||||||
const chromeEventHandler = docShell.chromeEventHandler;
|
|
||||||
const options = {
|
|
||||||
mozSystemGroup: true,
|
|
||||||
capture: true,
|
|
||||||
};
|
|
||||||
const emitInputEvent = (event) => this.emit(FrameTree.Events.InputEvent, { type: event.type, jugglerEventId: 0 });
|
|
||||||
// Drag events are dispatched from content process, so these we don't see in the
|
|
||||||
// `juggler-mouse-event-hit-renderer` instrumentation.
|
|
||||||
this._dragEventListeners = [
|
|
||||||
helper.addEventListener(chromeEventHandler, 'dragstart', emitInputEvent, options),
|
|
||||||
helper.addEventListener(chromeEventHandler, 'dragover', emitInputEvent, options),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_frameNavigationCommitted(frame, url) {
|
|
||||||
for (const subframe of frame._children)
|
|
||||||
this._detachFrame(subframe);
|
|
||||||
const navigationId = frame._pendingNavigationId;
|
|
||||||
frame._pendingNavigationId = null;
|
|
||||||
frame._lastCommittedNavigationId = navigationId;
|
|
||||||
frame._url = url;
|
|
||||||
this.emit(FrameTree.Events.NavigationCommitted, frame);
|
|
||||||
if (frame === this._mainFrame)
|
|
||||||
this.forcePageReady();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStateChange(progress, request, flag, status) {
|
onStateChange(progress, request, flag, status) {
|
||||||
|
|
@ -290,9 +229,11 @@ class FrameTree {
|
||||||
return;
|
return;
|
||||||
const channel = request.QueryInterface(Ci.nsIChannel);
|
const channel = request.QueryInterface(Ci.nsIChannel);
|
||||||
const docShell = progress.DOMWindow.docShell;
|
const docShell = progress.DOMWindow.docShell;
|
||||||
const frame = this.frameForDocShell(docShell);
|
const frame = this._docShellToFrame.get(docShell);
|
||||||
if (!frame)
|
if (!frame) {
|
||||||
|
dump(`WARNING: got a state changed event for un-tracked docshell!\n`);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!channel.isDocument) {
|
if (!channel.isDocument) {
|
||||||
// Somehow, we can get worker requests here,
|
// Somehow, we can get worker requests here,
|
||||||
|
|
@ -300,11 +241,32 @@ class FrameTree {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
|
||||||
|
const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
|
||||||
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
|
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
|
||||||
if (isStop && frame._pendingNavigationId && status) {
|
|
||||||
|
if (isStart) {
|
||||||
|
// Starting a new navigation.
|
||||||
|
frame._pendingNavigationId = channelId(channel);
|
||||||
|
frame._pendingNavigationURL = channel.URI.spec;
|
||||||
|
this.emit(FrameTree.Events.NavigationStarted, frame);
|
||||||
|
} else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) {
|
||||||
|
// Navigation is committed.
|
||||||
|
for (const subframe of frame._children)
|
||||||
|
this._detachFrame(subframe);
|
||||||
|
const navigationId = frame._pendingNavigationId;
|
||||||
|
frame._pendingNavigationId = null;
|
||||||
|
frame._pendingNavigationURL = null;
|
||||||
|
frame._lastCommittedNavigationId = navigationId;
|
||||||
|
frame._url = channel.URI.spec;
|
||||||
|
this.emit(FrameTree.Events.NavigationCommitted, frame);
|
||||||
|
if (frame === this._mainFrame)
|
||||||
|
this.forcePageReady();
|
||||||
|
} else if (isStop && frame._pendingNavigationId && status) {
|
||||||
// Navigation is aborted.
|
// Navigation is aborted.
|
||||||
const navigationId = frame._pendingNavigationId;
|
const navigationId = frame._pendingNavigationId;
|
||||||
frame._pendingNavigationId = null;
|
frame._pendingNavigationId = null;
|
||||||
|
frame._pendingNavigationURL = null;
|
||||||
// Always report download navigation as failure to match other browsers.
|
// Always report download navigation as failure to match other browsers.
|
||||||
const errorText = helper.getNetworkErrorStatusText(status);
|
const errorText = helper.getNetworkErrorStatusText(status);
|
||||||
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
|
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
|
||||||
|
|
@ -315,7 +277,7 @@ class FrameTree {
|
||||||
|
|
||||||
onLocationChange(progress, request, location, flags) {
|
onLocationChange(progress, request, location, flags) {
|
||||||
const docShell = progress.DOMWindow.docShell;
|
const docShell = progress.DOMWindow.docShell;
|
||||||
const frame = this.frameForDocShell(docShell);
|
const frame = this._docShellToFrame.get(docShell);
|
||||||
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
|
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
|
||||||
if (frame && sameDocumentNavigation) {
|
if (frame && sameDocumentNavigation) {
|
||||||
frame._url = location.spec;
|
frame._url = location.spec;
|
||||||
|
|
@ -323,29 +285,28 @@ class FrameTree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onBrowsingContextAttached(browsingContext) {
|
_onDocShellCreated(docShell) {
|
||||||
// If this browsing context doesn't belong to our frame tree - do nothing.
|
// Bug 1142752: sometimes, the docshell appears to be immediately
|
||||||
if (browsingContext.top !== this._rootBrowsingContext)
|
// destroyed, bailout early to prevent random exceptions.
|
||||||
|
if (docShell.isBeingDestroyed())
|
||||||
return;
|
return;
|
||||||
this._createFrame(browsingContext);
|
// If this docShell doesn't belong to our frame tree - do nothing.
|
||||||
|
let root = docShell;
|
||||||
|
while (root.parent)
|
||||||
|
root = root.parent;
|
||||||
|
if (root === this._mainFrame._docShell)
|
||||||
|
this._createFrame(docShell);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onBrowsingContextDetached(browsingContext) {
|
_createFrame(docShell) {
|
||||||
const frame = this.frameForBrowsingContext(browsingContext);
|
const parentFrame = this._docShellToFrame.get(docShell.parent) || null;
|
||||||
if (frame)
|
|
||||||
this._detachFrame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
_createFrame(browsingContext) {
|
|
||||||
const parentFrame = this.frameForBrowsingContext(browsingContext.parent);
|
|
||||||
if (!parentFrame && this._mainFrame) {
|
if (!parentFrame && this._mainFrame) {
|
||||||
dump(`WARNING: found docShell with the same root, but no parent!\n`);
|
dump(`WARNING: found docShell with the same root, but no parent!\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const frame = new Frame(this, this._runtime, browsingContext, parentFrame);
|
const frame = new Frame(this, this._runtime, docShell, parentFrame);
|
||||||
|
this._docShellToFrame.set(docShell, frame);
|
||||||
this._frameIdToFrame.set(frame.id(), frame);
|
this._frameIdToFrame.set(frame.id(), frame);
|
||||||
if (browsingContext.docShell?.domWindow && browsingContext.docShell?.domWindow.location)
|
|
||||||
frame._url = browsingContext.docShell.domWindow.location.href;
|
|
||||||
this.emit(FrameTree.Events.FrameAttached, frame);
|
this.emit(FrameTree.Events.FrameAttached, frame);
|
||||||
// Create execution context **after** reporting frame.
|
// Create execution context **after** reporting frame.
|
||||||
// This is our protocol contract.
|
// This is our protocol contract.
|
||||||
|
|
@ -354,6 +315,12 @@ class FrameTree {
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDocShellDestroyed(docShell) {
|
||||||
|
const frame = this._docShellToFrame.get(docShell);
|
||||||
|
if (frame)
|
||||||
|
this._detachFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
_detachFrame(frame) {
|
_detachFrame(frame) {
|
||||||
// Detach all children first
|
// Detach all children first
|
||||||
for (const subframe of frame._children)
|
for (const subframe of frame._children)
|
||||||
|
|
@ -363,6 +330,7 @@ class FrameTree {
|
||||||
// as it confuses the client.
|
// as it confuses the client.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._docShellToFrame.delete(frame._docShell);
|
||||||
this._frameIdToFrame.delete(frame.id());
|
this._frameIdToFrame.delete(frame.id());
|
||||||
if (frame._parentFrame)
|
if (frame._parentFrame)
|
||||||
frame._parentFrame._children.delete(frame);
|
frame._parentFrame._children.delete(frame);
|
||||||
|
|
@ -387,7 +355,6 @@ FrameTree.Events = {
|
||||||
NavigationAborted: 'navigationaborted',
|
NavigationAborted: 'navigationaborted',
|
||||||
SameDocumentNavigation: 'samedocumentnavigation',
|
SameDocumentNavigation: 'samedocumentnavigation',
|
||||||
PageReady: 'pageready',
|
PageReady: 'pageready',
|
||||||
InputEvent: 'inputevent',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class IsolatedWorld {
|
class IsolatedWorld {
|
||||||
|
|
@ -399,14 +366,16 @@ class IsolatedWorld {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Frame {
|
class Frame {
|
||||||
constructor(frameTree, runtime, browsingContext, parentFrame) {
|
constructor(frameTree, runtime, docShell, parentFrame) {
|
||||||
this._frameTree = frameTree;
|
this._frameTree = frameTree;
|
||||||
this._runtime = runtime;
|
this._runtime = runtime;
|
||||||
this._browsingContext = browsingContext;
|
this._docShell = docShell;
|
||||||
this._children = new Set();
|
this._children = new Set();
|
||||||
this._frameId = helper.browsingContextToFrameId(browsingContext);
|
this._frameId = helper.browsingContextToFrameId(this._docShell.browsingContext);
|
||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
this._url = '';
|
this._url = '';
|
||||||
|
if (docShell.domWindow && docShell.domWindow.location)
|
||||||
|
this._url = docShell.domWindow.location.href;
|
||||||
if (parentFrame) {
|
if (parentFrame) {
|
||||||
this._parentFrame = parentFrame;
|
this._parentFrame = parentFrame;
|
||||||
parentFrame._children.add(this);
|
parentFrame._children.add(this);
|
||||||
|
|
@ -414,6 +383,7 @@ class Frame {
|
||||||
|
|
||||||
this._lastCommittedNavigationId = null;
|
this._lastCommittedNavigationId = null;
|
||||||
this._pendingNavigationId = null;
|
this._pendingNavigationId = null;
|
||||||
|
this._pendingNavigationURL = null;
|
||||||
|
|
||||||
this._textInputProcessor = null;
|
this._textInputProcessor = null;
|
||||||
|
|
||||||
|
|
@ -546,7 +516,7 @@ class Frame {
|
||||||
|
|
||||||
_onGlobalObjectCleared() {
|
_onGlobalObjectCleared() {
|
||||||
const webSocketService = this._frameTree._webSocketEventService;
|
const webSocketService = this._frameTree._webSocketEventService;
|
||||||
if (this._webSocketListenerInnerWindowId && webSocketService.hasListenerFor(this._webSocketListenerInnerWindowId))
|
if (this._webSocketListenerInnerWindowId)
|
||||||
webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
||||||
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
|
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
|
||||||
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
||||||
|
|
@ -581,8 +551,8 @@ class Frame {
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateJavaScriptDisabled() {
|
_updateJavaScriptDisabled() {
|
||||||
if (this._browsingContext.currentWindowContext)
|
if (this._docShell.browsingContext.currentWindowContext)
|
||||||
this._browsingContext.currentWindowContext.allowJavascript = !this._frameTree._javaScriptDisabled;
|
this._docShell.browsingContext.currentWindowContext.allowJavascript = !this._frameTree._javaScriptDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainExecutionContext() {
|
mainExecutionContext() {
|
||||||
|
|
@ -593,7 +563,7 @@ class Frame {
|
||||||
if (!this._textInputProcessor) {
|
if (!this._textInputProcessor) {
|
||||||
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
|
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
|
||||||
}
|
}
|
||||||
this._textInputProcessor.beginInputTransactionForTests(this.docShell().DOMWindow);
|
this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow);
|
||||||
return this._textInputProcessor;
|
return this._textInputProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -601,20 +571,24 @@ class Frame {
|
||||||
return this._pendingNavigationId;
|
return this._pendingNavigationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingNavigationURL() {
|
||||||
|
return this._pendingNavigationURL;
|
||||||
|
}
|
||||||
|
|
||||||
lastCommittedNavigationId() {
|
lastCommittedNavigationId() {
|
||||||
return this._lastCommittedNavigationId;
|
return this._lastCommittedNavigationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
docShell() {
|
docShell() {
|
||||||
return this._browsingContext.docShell;
|
return this._docShell;
|
||||||
}
|
}
|
||||||
|
|
||||||
domWindow() {
|
domWindow() {
|
||||||
return this.docShell()?.domWindow;
|
return this._docShell.domWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
name() {
|
name() {
|
||||||
const frameElement = this.domWindow()?.frameElement;
|
const frameElement = this._docShell.domWindow.frameElement;
|
||||||
let name = '';
|
let name = '';
|
||||||
if (frameElement)
|
if (frameElement)
|
||||||
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
|
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
|
||||||
|
|
@ -643,7 +617,7 @@ class Worker {
|
||||||
|
|
||||||
workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
|
workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
|
||||||
|
|
||||||
this._channel = new SimpleChannel(`content::worker[${this._workerId}]`, 'worker-' + this._workerId);
|
this._channel = new SimpleChannel(`content::worker[${this._workerId}]`);
|
||||||
this._channel.setTransport({
|
this._channel.setTransport({
|
||||||
sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
|
sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
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 { initialize } = ChromeUtils.import('chrome://juggler/content/content/main.js');
|
||||||
|
|
||||||
const Ci = Components.interfaces;
|
const Ci = Components.interfaces;
|
||||||
|
|
@ -8,8 +9,6 @@ const helper = new Helper();
|
||||||
|
|
||||||
let sameProcessInstanceNumber = 0;
|
let sameProcessInstanceNumber = 0;
|
||||||
|
|
||||||
const topBrowingContextToAgents = new Map();
|
|
||||||
|
|
||||||
class JugglerFrameChild extends JSWindowActorChild {
|
class JugglerFrameChild extends JSWindowActorChild {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -18,66 +17,36 @@ class JugglerFrameChild extends JSWindowActorChild {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(aEvent) {
|
handleEvent(aEvent) {
|
||||||
const agents = this._agents();
|
if (this._agents && aEvent.target === this.document)
|
||||||
if (!agents)
|
this._agents.pageAgent.onWindowEvent(aEvent);
|
||||||
return;
|
|
||||||
if (aEvent.type === 'DOMWillOpenModalDialog') {
|
|
||||||
agents.channel.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (aEvent.type === 'DOMModalDialogClosed') {
|
|
||||||
agents.channel.resumeSoon();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (aEvent.target === this.document) {
|
|
||||||
agents.pageAgent.onWindowEvent(aEvent);
|
|
||||||
agents.frameTree.onWindowEvent(aEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_agents() {
|
|
||||||
return topBrowingContextToAgents.get(this.browsingContext.top);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actorCreated() {
|
actorCreated() {
|
||||||
this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
|
this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
|
||||||
|
|
||||||
this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => {
|
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://'))
|
if (this.document.documentURI.startsWith('moz-extension://'))
|
||||||
return;
|
return;
|
||||||
|
this._agents = initialize(this.browsingContext, this.docShell, this);
|
||||||
|
}
|
||||||
|
|
||||||
// Child frame events will be forwarded to related top-level agents.
|
_dispose() {
|
||||||
if (this.browsingContext.parent)
|
helper.removeListeners(this._eventListeners);
|
||||||
return;
|
// We do not cleanup since agents are shared for all frames in the process.
|
||||||
|
|
||||||
let agents = topBrowingContextToAgents.get(this.browsingContext);
|
// TODO: restore the cleanup.
|
||||||
if (!agents) {
|
// Reset transport so that all messages will be pending and will not throw any errors.
|
||||||
agents = initialize(this.browsingContext, this.docShell);
|
// this._channel.resetTransport();
|
||||||
topBrowingContextToAgents.set(this.browsingContext, agents);
|
// this._agents.pageAgent.dispose();
|
||||||
}
|
// this._agents.frameTree.dispose();
|
||||||
agents.channel.bindToActor(this);
|
// this._agents = undefined;
|
||||||
agents.actor = this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
didDestroy() {
|
didDestroy() {
|
||||||
helper.removeListeners(this._eventListeners);
|
this._dispose();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveMessage() { }
|
receiveMessage() { }
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,13 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
const Ci = Components.interfaces;
|
const Ci = Components.interfaces;
|
||||||
const Cr = Components.results;
|
const Cr = Components.results;
|
||||||
const Cu = Components.utils;
|
const Cu = Components.utils;
|
||||||
|
|
||||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||||
const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
|
||||||
|
|
||||||
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
|
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
|
||||||
Ci.nsIDragService
|
Ci.nsIDragService
|
||||||
);
|
);
|
||||||
|
|
@ -62,6 +60,8 @@ class PageAgent {
|
||||||
|
|
||||||
const docShell = frameTree.mainFrame().docShell();
|
const docShell = frameTree.mainFrame().docShell();
|
||||||
this._docShell = docShell;
|
this._docShell = docShell;
|
||||||
|
this._initialDPPX = docShell.contentViewer.overrideDPPX;
|
||||||
|
this._dragging = false;
|
||||||
|
|
||||||
// Dispatch frameAttached events for all initial frames
|
// Dispatch frameAttached events for all initial frames
|
||||||
for (const frame of this._frameTree.frames()) {
|
for (const frame of this._frameTree.frames()) {
|
||||||
|
|
@ -114,17 +114,6 @@ class PageAgent {
|
||||||
helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)),
|
helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)),
|
||||||
helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)),
|
helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)),
|
||||||
helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)),
|
helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)),
|
||||||
helper.on(this._frameTree, 'inputevent', inputEvent => {
|
|
||||||
this._browserPage.emit('pageInputEvent', inputEvent);
|
|
||||||
if (inputEvent.type === 'dragstart') {
|
|
||||||
// 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 });
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
|
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
|
||||||
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
|
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
|
||||||
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
|
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
|
||||||
|
|
@ -146,13 +135,15 @@ class PageAgent {
|
||||||
crash: this._crash.bind(this),
|
crash: this._crash.bind(this),
|
||||||
describeNode: this._describeNode.bind(this),
|
describeNode: this._describeNode.bind(this),
|
||||||
dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
|
dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
|
||||||
dispatchDragEvent: this._dispatchDragEvent.bind(this),
|
dispatchMouseEvent: this._dispatchMouseEvent.bind(this),
|
||||||
dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
|
dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
|
||||||
dispatchTapEvent: this._dispatchTapEvent.bind(this),
|
dispatchTapEvent: this._dispatchTapEvent.bind(this),
|
||||||
getContentQuads: this._getContentQuads.bind(this),
|
getContentQuads: this._getContentQuads.bind(this),
|
||||||
getFullAXTree: this._getFullAXTree.bind(this),
|
getFullAXTree: this._getFullAXTree.bind(this),
|
||||||
insertText: this._insertText.bind(this),
|
insertText: this._insertText.bind(this),
|
||||||
|
navigate: this._navigate.bind(this),
|
||||||
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
|
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
|
||||||
|
setCacheDisabled: this._setCacheDisabled.bind(this),
|
||||||
setFileInputFiles: this._setFileInputFiles.bind(this),
|
setFileInputFiles: this._setFileInputFiles.bind(this),
|
||||||
evaluate: this._runtime.evaluate.bind(this._runtime),
|
evaluate: this._runtime.evaluate.bind(this._runtime),
|
||||||
callFunction: this._runtime.callFunction.bind(this._runtime),
|
callFunction: this._runtime.callFunction.bind(this._runtime),
|
||||||
|
|
@ -162,6 +153,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) {
|
_emitAllEvents(frame) {
|
||||||
this._browserPage.emit('pageEventFired', {
|
this._browserPage.emit('pageEventFired', {
|
||||||
frameId: frame.id(),
|
frameId: frame.id(),
|
||||||
|
|
@ -286,6 +286,7 @@ class PageAgent {
|
||||||
this._browserPage.emit('pageNavigationStarted', {
|
this._browserPage.emit('pageNavigationStarted', {
|
||||||
frameId: frame.id(),
|
frameId: frame.id(),
|
||||||
navigationId: frame.pendingNavigationId(),
|
navigationId: frame.pendingNavigationId(),
|
||||||
|
url: frame.pendingNavigationURL(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,6 +346,39 @@ class PageAgent {
|
||||||
helper.removeListeners(this._eventListeners);
|
helper.removeListeners(this._eventListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _navigate({frameId, url, referer}) {
|
||||||
|
try {
|
||||||
|
const uri = NetUtil.newURI(url);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid url: "${url}"`);
|
||||||
|
}
|
||||||
|
let referrerURI = null;
|
||||||
|
let referrerInfo = null;
|
||||||
|
if (referer) {
|
||||||
|
try {
|
||||||
|
referrerURI = NetUtil.newURI(referer);
|
||||||
|
const ReferrerInfo = Components.Constructor(
|
||||||
|
'@mozilla.org/referrer-info;1',
|
||||||
|
'nsIReferrerInfo',
|
||||||
|
'init'
|
||||||
|
);
|
||||||
|
referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid referer: "${referer}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const frame = this._frameTree.frame(frameId);
|
||||||
|
const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
|
||||||
|
docShell.loadURI(url, {
|
||||||
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
||||||
|
referrerInfo,
|
||||||
|
postData: null,
|
||||||
|
headers: null,
|
||||||
|
});
|
||||||
|
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
|
||||||
|
}
|
||||||
|
|
||||||
async _adoptNode({frameId, objectId, executionContextId}) {
|
async _adoptNode({frameId, objectId, executionContextId}) {
|
||||||
const frame = this._frameTree.frame(frameId);
|
const frame = this._frameTree.frame(frameId);
|
||||||
if (!frame)
|
if (!frame)
|
||||||
|
|
@ -371,19 +405,8 @@ class PageAgent {
|
||||||
const unsafeObject = frame.unsafeObject(objectId);
|
const unsafeObject = frame.unsafeObject(objectId);
|
||||||
if (!unsafeObject)
|
if (!unsafeObject)
|
||||||
throw new Error('Object is not input!');
|
throw new Error('Object is not input!');
|
||||||
let nsFiles;
|
const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
||||||
if (unsafeObject.webkitdirectory) {
|
|
||||||
nsFiles = await new Directory(files[0]).getFiles(true);
|
|
||||||
} else {
|
|
||||||
nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
|
||||||
}
|
|
||||||
unsafeObject.mozSetFileArray(nsFiles);
|
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}) {
|
_getContentQuads({objectId, frameId}) {
|
||||||
|
|
@ -462,8 +485,18 @@ class PageAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
|
async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
|
||||||
|
// key events don't fire if we are dragging.
|
||||||
|
if (this._dragging) {
|
||||||
|
if (type === 'keydown' && key === 'Escape')
|
||||||
|
this._cancelDragIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const frame = this._frameTree.mainFrame();
|
const frame = this._frameTree.mainFrame();
|
||||||
const tip = frame.textInputProcessor();
|
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)("", {
|
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
|
||||||
key,
|
key,
|
||||||
code,
|
code,
|
||||||
|
|
@ -499,9 +532,7 @@ class PageAgent {
|
||||||
touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
|
touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
|
||||||
touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
|
touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
|
||||||
touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
|
touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
|
||||||
touchPoints.map(point => 0),
|
touchPoints.length,
|
||||||
touchPoints.map(point => 0),
|
|
||||||
touchPoints.map(point => 0),
|
|
||||||
modifiers);
|
modifiers);
|
||||||
return {defaultPrevented};
|
return {defaultPrevented};
|
||||||
}
|
}
|
||||||
|
|
@ -515,31 +546,83 @@ class PageAgent {
|
||||||
false /* aIgnoreRootScrollFrame */,
|
false /* aIgnoreRootScrollFrame */,
|
||||||
true /* aFlushLayout */);
|
true /* aFlushLayout */);
|
||||||
|
|
||||||
await this._dispatchTouchEvent({
|
const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({
|
||||||
type: 'touchstart',
|
type: 'touchstart',
|
||||||
modifiers,
|
modifiers,
|
||||||
touchPoints: [{x, y}]
|
touchPoints: [{x, y}]
|
||||||
});
|
});
|
||||||
await this._dispatchTouchEvent({
|
const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({
|
||||||
type: 'touchend',
|
type: 'touchend',
|
||||||
modifiers,
|
modifiers,
|
||||||
touchPoints: [{x, y}]
|
touchPoints: [{x, y}]
|
||||||
});
|
});
|
||||||
}
|
if (startPrevented || endPrevented)
|
||||||
|
return;
|
||||||
|
|
||||||
_getCurrentDragSession() {
|
|
||||||
const frame = this._frameTree.mainFrame();
|
const frame = this._frameTree.mainFrame();
|
||||||
const domWindow = frame?.domWindow();
|
frame.domWindow().windowUtils.sendMouseEvent(
|
||||||
return domWindow ? dragService.getCurrentSession(domWindow) : undefined;
|
'mousemove',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
0 /*button*/,
|
||||||
|
0 /*clickCount*/,
|
||||||
|
modifiers,
|
||||||
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
|
undefined /*pressure*/,
|
||||||
|
5 /*inputSource*/,
|
||||||
|
undefined /*isDOMEventSynthesized*/,
|
||||||
|
false /*isWidgetEventSynthesized*/,
|
||||||
|
0 /*buttons*/,
|
||||||
|
undefined /*pointerIdentifier*/,
|
||||||
|
true /*disablePointerEvent*/);
|
||||||
|
|
||||||
|
frame.domWindow().windowUtils.sendMouseEvent(
|
||||||
|
'mousedown',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
0 /*button*/,
|
||||||
|
1 /*clickCount*/,
|
||||||
|
modifiers,
|
||||||
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
|
undefined /*pressure*/,
|
||||||
|
5 /*inputSource*/,
|
||||||
|
undefined /*isDOMEventSynthesized*/,
|
||||||
|
false /*isWidgetEventSynthesized*/,
|
||||||
|
1 /*buttons*/,
|
||||||
|
undefined /*pointerIdentifier*/,
|
||||||
|
true /*disablePointerEvent*/);
|
||||||
|
|
||||||
|
frame.domWindow().windowUtils.sendMouseEvent(
|
||||||
|
'mouseup',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
0 /*button*/,
|
||||||
|
1 /*clickCount*/,
|
||||||
|
modifiers,
|
||||||
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
|
undefined /*pressure*/,
|
||||||
|
5 /*inputSource*/,
|
||||||
|
undefined /*isDOMEventSynthesized*/,
|
||||||
|
false /*isWidgetEventSynthesized*/,
|
||||||
|
0 /*buttons*/,
|
||||||
|
undefined /*pointerIdentifier*/,
|
||||||
|
true /*disablePointerEvent*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _dispatchDragEvent({type, x, y, modifiers}) {
|
_startDragSessionIfNeeded() {
|
||||||
const session = this._getCurrentDragSession();
|
const sess = dragService.getCurrentSession();
|
||||||
const dropEffect = session.dataTransfer.dropEffect;
|
if (sess) return;
|
||||||
|
dragService.startDragSessionForTests(
|
||||||
|
Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
|
||||||
|
Ci.nsIDragService.DRAGDROP_ACTION_COPY |
|
||||||
|
Ci.nsIDragService.DRAGDROP_ACTION_LINK
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ((type === 'drop' && dropEffect !== 'none') || type === 'dragover') {
|
_simulateDragEvent(type, x, y, modifiers) {
|
||||||
const win = this._frameTree.mainFrame().domWindow();
|
if (type !== 'drop' || dragService.dragAction) {
|
||||||
win.windowUtils.jugglerSendMouseEvent(
|
const window = this._frameTree.mainFrame().domWindow();
|
||||||
|
window.windowUtils.sendMouseEvent(
|
||||||
type,
|
type,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
@ -547,20 +630,75 @@ class PageAgent {
|
||||||
0, /*clickCount*/
|
0, /*clickCount*/
|
||||||
modifiers,
|
modifiers,
|
||||||
false /*aIgnoreRootScrollFrame*/,
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
0.0 /*pressure*/,
|
undefined /*pressure*/,
|
||||||
0 /*inputSource*/,
|
undefined /*inputSource*/,
|
||||||
|
undefined /*isDOMEventSynthesized*/,
|
||||||
|
undefined /*isWidgetEventSynthesized*/,
|
||||||
|
0, /*buttons*/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'drop')
|
||||||
|
this._cancelDragIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancelDragIfNeeded() {
|
||||||
|
this._dragging = false;
|
||||||
|
const sess = dragService.getCurrentSession();
|
||||||
|
if (sess)
|
||||||
|
dragService.endDragSession(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
|
||||||
|
this._startDragSessionIfNeeded();
|
||||||
|
const trapDrag = subject => {
|
||||||
|
this._dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't send mouse events if there is an active drag
|
||||||
|
if (!this._dragging) {
|
||||||
|
const frame = this._frameTree.mainFrame();
|
||||||
|
|
||||||
|
obs.addObserver(trapDrag, 'on-datatransfer-available');
|
||||||
|
frame.domWindow().windowUtils.sendMouseEvent(
|
||||||
|
type,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
button,
|
||||||
|
clickCount,
|
||||||
|
modifiers,
|
||||||
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
|
undefined /*pressure*/,
|
||||||
|
undefined /*inputSource*/,
|
||||||
true /*isDOMEventSynthesized*/,
|
true /*isDOMEventSynthesized*/,
|
||||||
false /*isWidgetEventSynthesized*/,
|
false /*isWidgetEventSynthesized*/,
|
||||||
0 /*buttons*/,
|
buttons);
|
||||||
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
obs.removeObserver(trapDrag, 'on-datatransfer-available');
|
||||||
false /*disablePointerEvent*/,
|
|
||||||
);
|
if (type === 'mousedown' && button === 2) {
|
||||||
return;
|
frame.domWindow().windowUtils.sendMouseEvent(
|
||||||
|
'contextmenu',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
button,
|
||||||
|
clickCount,
|
||||||
|
modifiers,
|
||||||
|
false /*aIgnoreRootScrollFrame*/,
|
||||||
|
undefined /*pressure*/,
|
||||||
|
undefined /*inputSource*/,
|
||||||
|
undefined /*isDOMEventSynthesized*/,
|
||||||
|
undefined /*isWidgetEventSynthesized*/,
|
||||||
|
buttons);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (type === 'dragend') {
|
|
||||||
const session = this._getCurrentDragSession();
|
// update drag state
|
||||||
session?.endDragSession(true);
|
if (this._dragging) {
|
||||||
return;
|
if (type === 'mousemove')
|
||||||
|
this._simulateDragEvent('dragover', x, y, modifiers);
|
||||||
|
else if (type === 'mouseup') // firefox will do drops when any mouse button is released
|
||||||
|
this._simulateDragEvent('drop', x, y, modifiers);
|
||||||
|
} else {
|
||||||
|
this._cancelDragIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class Runtime {
|
||||||
if (isWorker) {
|
if (isWorker) {
|
||||||
this._registerWorkerConsoleHandler();
|
this._registerWorkerConsoleHandler();
|
||||||
} else {
|
} else {
|
||||||
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
this._registerConsoleServiceListener(Services);
|
this._registerConsoleServiceListener(Services);
|
||||||
this._registerConsoleAPIListener(Services);
|
this._registerConsoleAPIListener(Services);
|
||||||
}
|
}
|
||||||
|
|
@ -184,13 +185,7 @@ class Runtime {
|
||||||
if (context._isIsolatedWorldContext())
|
if (context._isIsolatedWorldContext())
|
||||||
return false;
|
return false;
|
||||||
const domWindow = context._domWindow;
|
const domWindow = context._domWindow;
|
||||||
try {
|
return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID;
|
||||||
// `windowGlobalChild` might be dead already; accessing it will throw an error, message in a console,
|
|
||||||
// and infinite recursion.
|
|
||||||
return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (!executionContext)
|
if (!executionContext)
|
||||||
return;
|
return;
|
||||||
|
|
@ -239,8 +234,8 @@ class Runtime {
|
||||||
return {success: true, obj: obj.promiseValue};
|
return {success: true, obj: obj.promiseValue};
|
||||||
if (obj.promiseState === 'rejected') {
|
if (obj.promiseState === 'rejected') {
|
||||||
const debuggee = executionContext._debuggee;
|
const debuggee = executionContext._debuggee;
|
||||||
exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {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}, {useInnerBindings: true}).return;
|
exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
|
||||||
return {success: false, obj: null};
|
return {success: false, obj: null};
|
||||||
}
|
}
|
||||||
let resolve, reject;
|
let resolve, reject;
|
||||||
|
|
@ -267,8 +262,8 @@ class Runtime {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const debuggee = pendingPromise.executionContext._debuggee;
|
const debuggee = pendingPromise.executionContext._debuggee;
|
||||||
pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {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}, {useInnerBindings: true}).return;
|
pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
|
||||||
pendingPromise.resolve({success: false, obj: null});
|
pendingPromise.resolve({success: false, obj: null});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,9 +311,8 @@ class ExecutionContext {
|
||||||
this._id = generateId();
|
this._id = generateId();
|
||||||
this._auxData = auxData;
|
this._auxData = auxData;
|
||||||
this._jsonStringifyObject = this._debuggee.executeInGlobal(`((stringify, object) => {
|
this._jsonStringifyObject = this._debuggee.executeInGlobal(`((stringify, object) => {
|
||||||
const oldToJSON = Date.prototype?.toJSON;
|
const oldToJSON = Date.prototype.toJSON;
|
||||||
if (oldToJSON)
|
Date.prototype.toJSON = undefined;
|
||||||
Date.prototype.toJSON = undefined;
|
|
||||||
const oldArrayToJSON = Array.prototype.toJSON;
|
const oldArrayToJSON = Array.prototype.toJSON;
|
||||||
const oldArrayHadToJSON = Array.prototype.hasOwnProperty('toJSON');
|
const oldArrayHadToJSON = Array.prototype.hasOwnProperty('toJSON');
|
||||||
if (oldArrayHadToJSON)
|
if (oldArrayHadToJSON)
|
||||||
|
|
@ -331,8 +325,7 @@ class ExecutionContext {
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldToJSON)
|
Date.prototype.toJSON = oldToJSON;
|
||||||
Date.prototype.toJSON = oldToJSON;
|
|
||||||
if (oldArrayHadToJSON)
|
if (oldArrayHadToJSON)
|
||||||
Array.prototype.toJSON = oldArrayToJSON;
|
Array.prototype.toJSON = oldArrayToJSON;
|
||||||
|
|
||||||
|
|
@ -441,7 +434,7 @@ class ExecutionContext {
|
||||||
_instanceOf(debuggerObj, rawObj, className) {
|
_instanceOf(debuggerObj, rawObj, className) {
|
||||||
if (this._domWindow)
|
if (this._domWindow)
|
||||||
return rawObj instanceof this._domWindow[className];
|
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) {
|
_createRemoteObject(debuggerObj) {
|
||||||
|
|
@ -531,7 +524,7 @@ class ExecutionContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
_serialize(obj) {
|
_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)
|
if (result.throw)
|
||||||
throw new Error('Object is not serializable');
|
throw new Error('Object is not serializable');
|
||||||
return result.return === undefined ? undefined : JSON.parse(result.return);
|
return result.return === undefined ? undefined : JSON.parse(result.return);
|
||||||
|
|
@ -560,12 +553,15 @@ class ExecutionContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getResult(completionValue, exceptionDetails = {}) {
|
_getResult(completionValue, exceptionDetails = {}) {
|
||||||
if (!completionValue)
|
if (!completionValue) {
|
||||||
throw new Error('evaluation terminated');
|
exceptionDetails.text = 'Evaluation terminated!';
|
||||||
|
exceptionDetails.stack = '';
|
||||||
|
return {success: false, obj: null};
|
||||||
|
}
|
||||||
if (completionValue.throw) {
|
if (completionValue.throw) {
|
||||||
if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {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}, {useInnerBindings: true}).return;
|
exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
|
||||||
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}, {useInnerBindings: true}).return;
|
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
|
||||||
} else {
|
} else {
|
||||||
exceptionDetails.value = this._serialize(completionValue.throw);
|
exceptionDetails.value = this._serialize(completionValue.throw);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@
|
||||||
loadSubScript('chrome://juggler/content/content/Runtime.js');
|
loadSubScript('chrome://juggler/content/content/Runtime.js');
|
||||||
loadSubScript('chrome://juggler/content/SimpleChannel.js');
|
loadSubScript('chrome://juggler/content/SimpleChannel.js');
|
||||||
|
|
||||||
// SimpleChannel in worker is never replaced: its lifetime matches the lifetime
|
const channel = new SimpleChannel('worker::worker');
|
||||||
// of the worker itself, so anything would work as a unique identifier.
|
|
||||||
const channel = new SimpleChannel('worker::content', 'unique_identifier');
|
|
||||||
const eventListener = event => channel._onMessage(JSON.parse(event.data));
|
const eventListener = event => channel._onMessage(JSON.parse(event.data));
|
||||||
this.addEventListener('message', eventListener);
|
this.addEventListener('message', eventListener);
|
||||||
channel.setTransport({
|
channel.setTransport({
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,30 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* 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 {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||||
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
|
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
|
||||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
||||||
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
|
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
|
||||||
|
|
||||||
|
const browsingContextToAgents = new Map();
|
||||||
const helper = new Helper();
|
const helper = new Helper();
|
||||||
|
|
||||||
function initialize(browsingContext, docShell) {
|
function initialize(browsingContext, docShell, actor) {
|
||||||
const data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
|
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 = {
|
const applySetting = {
|
||||||
geolocation: (geolocation) => {
|
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) => {
|
bypassCSP: (bypassCSP) => {
|
||||||
docShell.bypassCSPEnabled = bypassCSP;
|
docShell.bypassCSPEnabled = bypassCSP;
|
||||||
},
|
},
|
||||||
|
|
@ -45,6 +69,10 @@ function initialize(browsingContext, docShell) {
|
||||||
docShell.languageOverride = locale;
|
docShell.languageOverride = locale;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
scrollbarsHidden: (hidden) => {
|
||||||
|
data.frameTree.setScrollbarsHidden(hidden);
|
||||||
|
},
|
||||||
|
|
||||||
javaScriptDisabled: (javaScriptDisabled) => {
|
javaScriptDisabled: (javaScriptDisabled) => {
|
||||||
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
|
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
|
||||||
},
|
},
|
||||||
|
|
@ -57,7 +85,7 @@ function initialize(browsingContext, docShell) {
|
||||||
docShell.overrideHasFocus = true;
|
docShell.overrideHasFocus = true;
|
||||||
docShell.forceActiveState = true;
|
docShell.forceActiveState = true;
|
||||||
docShell.disallowBFCache = true;
|
docShell.disallowBFCache = true;
|
||||||
data.frameTree = new FrameTree(browsingContext);
|
data.frameTree = new FrameTree(docShell);
|
||||||
for (const [name, value] of Object.entries(contextCrossProcessCookie.settings)) {
|
for (const [name, value] of Object.entries(contextCrossProcessCookie.settings)) {
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
applySetting[name](value);
|
applySetting[name](value);
|
||||||
|
|
@ -65,7 +93,7 @@ function initialize(browsingContext, docShell) {
|
||||||
for (const { worldName, name, script } of [...contextCrossProcessCookie.bindings, ...pageCrossProcessCookie.bindings])
|
for (const { worldName, name, script } of [...contextCrossProcessCookie.bindings, ...pageCrossProcessCookie.bindings])
|
||||||
data.frameTree.addBinding(worldName, name, script);
|
data.frameTree.addBinding(worldName, name, script);
|
||||||
data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]);
|
data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]);
|
||||||
data.channel = new SimpleChannel('', 'process-' + Services.appinfo.processID);
|
data.channel = SimpleChannel.createForActor(actor);
|
||||||
data.pageAgent = new PageAgent(data.channel, data.frameTree);
|
data.pageAgent = new PageAgent(data.channel, data.frameTree);
|
||||||
docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog;
|
docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog;
|
||||||
|
|
||||||
|
|
@ -94,7 +122,8 @@ function initialize(browsingContext, docShell) {
|
||||||
return data.failedToOverrideTimezone;
|
return data.failedToOverrideTimezone;
|
||||||
},
|
},
|
||||||
|
|
||||||
async awaitViewportDimensions({width, height}) {
|
async awaitViewportDimensions({width, height, deviceSizeIsPageSize}) {
|
||||||
|
docShell.deviceSizeIsPageSize = deviceSizeIsPageSize;
|
||||||
const win = docShell.domWindow;
|
const win = docShell.domWindow;
|
||||||
if (win.innerWidth === width && win.innerHeight === height)
|
if (win.innerWidth === width && win.innerHeight === height)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
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 {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
||||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||||
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
|
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
|
||||||
|
|
@ -13,7 +14,7 @@ const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.j
|
||||||
const helper = new Helper();
|
const helper = new Helper();
|
||||||
|
|
||||||
class BrowserHandler {
|
class BrowserHandler {
|
||||||
constructor(session, dispatcher, targetRegistry, startCompletePromise, onclose) {
|
constructor(session, dispatcher, targetRegistry, onclose, onstart) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
this._dispatcher = dispatcher;
|
this._dispatcher = dispatcher;
|
||||||
this._targetRegistry = targetRegistry;
|
this._targetRegistry = targetRegistry;
|
||||||
|
|
@ -23,27 +24,16 @@ class BrowserHandler {
|
||||||
this._createdBrowserContextIds = new Set();
|
this._createdBrowserContextIds = new Set();
|
||||||
this._attachedSessions = new Map();
|
this._attachedSessions = new Map();
|
||||||
this._onclose = onclose;
|
this._onclose = onclose;
|
||||||
this._startCompletePromise = startCompletePromise;
|
this._onstart = onstart;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.enable']({attachToDefaultContext, userPrefs = []}) {
|
async ['Browser.enable']({attachToDefaultContext}) {
|
||||||
if (this._enabled)
|
if (this._enabled)
|
||||||
return;
|
return;
|
||||||
await this._startCompletePromise;
|
await this._onstart();
|
||||||
this._enabled = true;
|
this._enabled = true;
|
||||||
this._attachToDefaultContext = attachToDefaultContext;
|
this._attachToDefaultContext = attachToDefaultContext;
|
||||||
|
|
||||||
for (const { name, value } of userPrefs) {
|
|
||||||
if (value === true || value === false)
|
|
||||||
Services.prefs.setBoolPref(name, value);
|
|
||||||
else if (typeof value === 'string')
|
|
||||||
Services.prefs.setStringPref(name, value);
|
|
||||||
else if (typeof value === 'number')
|
|
||||||
Services.prefs.setIntPref(name, value);
|
|
||||||
else
|
|
||||||
throw new Error(`Preference "${name}" has unsupported value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
|
helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
|
||||||
helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
||||||
|
|
@ -146,7 +136,6 @@ class BrowserHandler {
|
||||||
waitForWindowClosed(browserWindow),
|
waitForWindowClosed(browserWindow),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await this._startCompletePromise;
|
|
||||||
this._onclose();
|
this._onclose();
|
||||||
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
|
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
|
||||||
}
|
}
|
||||||
|
|
@ -163,12 +152,6 @@ class BrowserHandler {
|
||||||
this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers;
|
this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
['Browser.clearCache']() {
|
|
||||||
// Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147
|
|
||||||
Services.cache2.clear();
|
|
||||||
ChromeUtils.clearStyleSheetCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
['Browser.setHTTPCredentials']({browserContextId, credentials}) {
|
['Browser.setHTTPCredentials']({browserContextId, credentials}) {
|
||||||
this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials);
|
this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials);
|
||||||
}
|
}
|
||||||
|
|
@ -186,10 +169,6 @@ class BrowserHandler {
|
||||||
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
|
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
['Browser.setCacheDisabled']({browserContextId, cacheDisabled}) {
|
|
||||||
this._targetRegistry.browserContextForId(browserContextId).setCacheDisabled(cacheDisabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
|
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
|
||||||
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
|
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
|
||||||
}
|
}
|
||||||
|
|
@ -203,8 +182,7 @@ class BrowserHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.setOnlineOverride']({browserContextId, override}) {
|
async ['Browser.setOnlineOverride']({browserContextId, override}) {
|
||||||
const forceOffline = override === 'offline';
|
await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override));
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).setForceOffline(forceOffline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.setColorScheme']({browserContextId, colorScheme}) {
|
async ['Browser.setColorScheme']({browserContextId, colorScheme}) {
|
||||||
|
|
@ -255,6 +233,10 @@ class BrowserHandler {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
|
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}) {
|
async ['Browser.setInitScripts']({browserContextId, scripts}) {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,6 @@ class Dispatcher {
|
||||||
|
|
||||||
this._connection.send(JSON.stringify({id, sessionId, result}));
|
this._connection.send(JSON.stringify({id, sessionId, result}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dump(`
|
|
||||||
ERROR: ${e.message} ${e.stack}
|
|
||||||
`);
|
|
||||||
this._connection.send(JSON.stringify({id, sessionId, error: {
|
this._connection.send(JSON.stringify({id, sessionId, error: {
|
||||||
message: e.message,
|
message: e.message,
|
||||||
data: e.stack
|
data: e.stack
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const {Helper, EventWatcher} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
|
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
|
||||||
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
||||||
const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
||||||
|
|
@ -79,9 +79,6 @@ class PageHandler {
|
||||||
return (...args) => this._session.emitEvent(eventName, ...args);
|
return (...args) => this._session.emitEvent(eventName, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isDragging = false;
|
|
||||||
this._lastMousePosition = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
this._reportedFrameIds = new Set();
|
this._reportedFrameIds = new Set();
|
||||||
this._networkEventsForUnreportedFrameIds = new Map();
|
this._networkEventsForUnreportedFrameIds = new Map();
|
||||||
|
|
||||||
|
|
@ -92,13 +89,14 @@ class PageHandler {
|
||||||
// to be ignored by the protocol clients.
|
// to be ignored by the protocol clients.
|
||||||
this._isPageReady = false;
|
this._isPageReady = false;
|
||||||
|
|
||||||
|
// Whether the page is about to go cross-process after navigation.
|
||||||
|
this._isTransferringCrossProcessNavigation = false;
|
||||||
|
this._mainFrameId = undefined;
|
||||||
|
this._lastMainFrameNavigationId = undefined;
|
||||||
|
|
||||||
if (this._pageTarget.videoRecordingInfo())
|
if (this._pageTarget.videoRecordingInfo())
|
||||||
this._onVideoRecordingStarted();
|
this._onVideoRecordingStarted();
|
||||||
|
|
||||||
this._pageEventSink = {};
|
|
||||||
helper.decorateAsEventEmitter(this._pageEventSink);
|
|
||||||
|
|
||||||
this._pendingEventWatchers = new Set();
|
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
|
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
|
||||||
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
|
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
|
||||||
|
|
@ -107,6 +105,7 @@ class PageHandler {
|
||||||
}),
|
}),
|
||||||
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onVideoRecordingStarted.bind(this)),
|
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onVideoRecordingStarted.bind(this)),
|
||||||
helper.on(this._pageTarget, PageTarget.Events.ScreencastFrame, this._onScreencastFrame.bind(this)),
|
helper.on(this._pageTarget, PageTarget.Events.ScreencastFrame, this._onScreencastFrame.bind(this)),
|
||||||
|
helper.on(this._pageTarget, PageTarget.Events.TopBrowsingContextReplaced, this._onTopBrowsingContextReplaced.bind(this)),
|
||||||
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
|
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
|
||||||
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
|
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
|
||||||
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
|
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
|
||||||
|
|
@ -120,11 +119,10 @@ class PageHandler {
|
||||||
pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
|
pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
|
||||||
pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
|
pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
|
||||||
pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
|
pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
|
||||||
pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
|
pageNavigationAborted: params => this._handleNavigationEvent('Page.navigationAborted', params),
|
||||||
pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
|
pageNavigationCommitted: params => this._handleNavigationEvent('Page.navigationCommitted', params),
|
||||||
pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
|
pageNavigationStarted: params => this._handleNavigationEvent('Page.navigationStarted', params),
|
||||||
pageReady: this._onPageReady.bind(this),
|
pageReady: this._onPageReady.bind(this),
|
||||||
pageInputEvent: (event) => this._pageEventSink.emit(event.type, event),
|
|
||||||
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
|
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
|
||||||
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
|
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
|
||||||
pageWorkerCreated: this._onWorkerCreated.bind(this),
|
pageWorkerCreated: this._onWorkerCreated.bind(this),
|
||||||
|
|
@ -154,8 +152,6 @@ class PageHandler {
|
||||||
|
|
||||||
async dispose() {
|
async dispose() {
|
||||||
this._contentPage.dispose();
|
this._contentPage.dispose();
|
||||||
for (const watcher of this._pendingEventWatchers)
|
|
||||||
watcher.dispose();
|
|
||||||
helper.removeListeners(this._eventListeners);
|
helper.removeListeners(this._eventListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +164,28 @@ class PageHandler {
|
||||||
this._session.emitEvent('Page.screencastFrame', params);
|
this._session.emitEvent('Page.screencastFrame', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onTopBrowsingContextReplaced() {
|
||||||
|
this._isTransferringCrossProcessNavigation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleNavigationEvent(event, params) {
|
||||||
|
if (this._isTransferringCrossProcessNavigation && params.frameId === this._mainFrameId) {
|
||||||
|
// During a cross-process navigation, http channel in the new process might not be
|
||||||
|
// the same as the original one in the old process, for example after a redirect/interception.
|
||||||
|
// Therefore, the new proces has a new navigationId.
|
||||||
|
//
|
||||||
|
// To preserve protocol consistency, we replace the new navigationId with
|
||||||
|
// the old navigationId.
|
||||||
|
params.navigationId = this._lastMainFrameNavigationId || params.navigationId;
|
||||||
|
if (event === 'Page.navigationCommitted' || event === 'Page.navigationAborted')
|
||||||
|
this._isTransferringCrossProcessNavigation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'Page.navigationStarted' && params.frameId === this._mainFrameId)
|
||||||
|
this._lastMainFrameNavigationId = params.navigationId;
|
||||||
|
this._session.emitEvent(event, params);
|
||||||
|
}
|
||||||
|
|
||||||
_onPageReady(event) {
|
_onPageReady(event) {
|
||||||
this._isPageReady = true;
|
this._isPageReady = true;
|
||||||
this._session.emitEvent('Page.ready');
|
this._session.emitEvent('Page.ready');
|
||||||
|
|
@ -221,6 +239,8 @@ class PageHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFrameAttached({frameId, parentFrameId}) {
|
_onFrameAttached({frameId, parentFrameId}) {
|
||||||
|
if (!parentFrameId)
|
||||||
|
this._mainFrameId = frameId;
|
||||||
this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
|
this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
|
||||||
this._reportedFrameIds.add(frameId);
|
this._reportedFrameIds.add(frameId);
|
||||||
const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
|
const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
|
||||||
|
|
@ -256,13 +276,6 @@ class PageHandler {
|
||||||
return await this._contentPage.send('disposeObject', options);
|
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}) {
|
async ['Network.getResponseBody']({requestId}) {
|
||||||
return this._pageNetwork.getResponseBody(requestId);
|
return this._pageNetwork.getResponseBody(requestId);
|
||||||
}
|
}
|
||||||
|
|
@ -306,11 +319,11 @@ class PageHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.bringToFront'](options) {
|
async ['Page.bringToFront'](options) {
|
||||||
await this._pageTarget.activateAndRun(() => {});
|
this._pageTarget._window.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.setCacheDisabled']({cacheDisabled}) {
|
async ['Page.setCacheDisabled'](options) {
|
||||||
return await this._pageTarget.setCacheDisabled(cacheDisabled);
|
return await this._contentPage.send('setCacheDisabled', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.addBinding']({ worldName, name, script }) {
|
async ['Page.addBinding']({ worldName, name, script }) {
|
||||||
|
|
@ -321,7 +334,7 @@ class PageHandler {
|
||||||
return await this._contentPage.send('adoptNode', options);
|
return await this._contentPage.send('adoptNode', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.screenshot']({ mimeType, clip, omitDeviceScaleFactor, quality = 80}) {
|
async ['Page.screenshot']({ mimeType, clip, omitDeviceScaleFactor }) {
|
||||||
const rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
|
const rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
|
||||||
|
|
||||||
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
||||||
|
|
@ -364,15 +377,7 @@ class PageHandler {
|
||||||
let ctx = canvas.getContext('2d');
|
let ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(snapshot, 0, 0);
|
ctx.drawImage(snapshot, 0, 0);
|
||||||
snapshot.close();
|
snapshot.close();
|
||||||
|
const dataURL = canvas.toDataURL(mimeType);
|
||||||
if (mimeType === 'image/jpeg') {
|
|
||||||
if (quality < 0 || quality > 100)
|
|
||||||
throw new Error('Quality must be an integer value between 0 and 100; received ' + quality);
|
|
||||||
quality /= 100;
|
|
||||||
} else {
|
|
||||||
quality = undefined;
|
|
||||||
}
|
|
||||||
const dataURL = canvas.toDataURL(mimeType, quality);
|
|
||||||
return { data: dataURL.substring(dataURL.indexOf(',') + 1) };
|
return { data: dataURL.substring(dataURL.indexOf(',') + 1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,51 +385,8 @@ class PageHandler {
|
||||||
return await this._contentPage.send('getContentQuads', options);
|
return await this._contentPage.send('getContentQuads', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.navigate']({frameId, url, referer}) {
|
async ['Page.navigate'](options) {
|
||||||
const browsingContext = this._pageTarget.frameIdToBrowsingContext(frameId);
|
return await this._contentPage.send('navigate', options);
|
||||||
let sameDocumentNavigation = false;
|
|
||||||
try {
|
|
||||||
const uri = NetUtil.newURI(url);
|
|
||||||
// This is the same check that verifes browser-side if this is the same-document navigation.
|
|
||||||
// See CanonicalBrowsingContext::SupportsLoadingInParent.
|
|
||||||
sameDocumentNavigation = browsingContext.currentURI && uri.hasRef && uri.equalsExceptRef(browsingContext.currentURI);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Invalid url: "${url}"`);
|
|
||||||
}
|
|
||||||
let referrerURI = null;
|
|
||||||
let referrerInfo = null;
|
|
||||||
if (referer) {
|
|
||||||
try {
|
|
||||||
referrerURI = NetUtil.newURI(referer);
|
|
||||||
const ReferrerInfo = Components.Constructor(
|
|
||||||
'@mozilla.org/referrer-info;1',
|
|
||||||
'nsIReferrerInfo',
|
|
||||||
'init'
|
|
||||||
);
|
|
||||||
referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.UNSAFE_URL, true, referrerURI);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Invalid referer: "${referer}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigationId;
|
|
||||||
const unsubscribe = helper.addObserver((browsingContext, topic, loadIdentifier) => {
|
|
||||||
navigationId = helper.toProtocolNavigationId(loadIdentifier);
|
|
||||||
}, 'juggler-navigation-started-browser');
|
|
||||||
browsingContext.loadURI(Services.io.newURI(url), {
|
|
||||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
||||||
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
|
|
||||||
referrerInfo,
|
|
||||||
// postData: null,
|
|
||||||
// headers: null,
|
|
||||||
// Fake user activation.
|
|
||||||
hasValidUserGestureActivation: true,
|
|
||||||
});
|
|
||||||
unsubscribe();
|
|
||||||
|
|
||||||
return {
|
|
||||||
navigationId: sameDocumentNavigation ? null : navigationId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.goBack']({}) {
|
async ['Page.goBack']({}) {
|
||||||
|
|
@ -443,11 +405,9 @@ class PageHandler {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.reload']() {
|
async ['Page.reload']({}) {
|
||||||
await this._pageTarget.activateAndRun(() => {
|
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
||||||
const doc = this._pageTarget._tab.linkedBrowser.ownerDocument;
|
browsingContext.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
|
||||||
doc.getElementById('Browser:Reload').doCommand();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.describeNode'](options) {
|
async ['Page.describeNode'](options) {
|
||||||
|
|
@ -462,22 +422,8 @@ class PageHandler {
|
||||||
return await this._pageTarget.setInitScripts(scripts);
|
return await this._pageTarget.setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchKeyEvent']({type, keyCode, code, key, repeat, location, text}) {
|
async ['Page.dispatchKeyEvent'](options) {
|
||||||
// key events don't fire if we are dragging.
|
return await this._contentPage.send('dispatchKeyEvent', options);
|
||||||
if (this._isDragging) {
|
|
||||||
if (type === 'keydown' && key === 'Escape') {
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {
|
|
||||||
type: 'dragover',
|
|
||||||
x: this._lastMousePosition.x,
|
|
||||||
y: this._lastMousePosition.y,
|
|
||||||
modifiers: 0
|
|
||||||
});
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {type: 'dragend'});
|
|
||||||
this._isDragging = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await this._contentPage.send('dispatchKeyEvent', {type, keyCode, code, key, repeat, location, text});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchTouchEvent'](options) {
|
async ['Page.dispatchTouchEvent'](options) {
|
||||||
|
|
@ -488,161 +434,30 @@ class PageHandler {
|
||||||
return await this._contentPage.send('dispatchTapEvent', options);
|
return await this._contentPage.send('dispatchTapEvent', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchMouseEvent']({type, x, y, button, clickCount, modifiers, buttons}) {
|
async ['Page.dispatchMouseEvent'](options) {
|
||||||
const win = this._pageTarget._window;
|
return await this._contentPage.send('dispatchMouseEvent', options);
|
||||||
const sendEvents = async (types) => {
|
|
||||||
// 1. Scroll element to the desired location first; the coordinates are relative to the element.
|
|
||||||
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 = [];
|
|
||||||
for (const type of types) {
|
|
||||||
// This dispatches to the renderer synchronously.
|
|
||||||
const jugglerEventId = win.windowUtils.jugglerSendMouseEvent(
|
|
||||||
type,
|
|
||||||
x + boundingBox.left,
|
|
||||||
y + boundingBox.top,
|
|
||||||
button,
|
|
||||||
clickCount,
|
|
||||||
modifiers,
|
|
||||||
false /* aIgnoreRootScrollFrame */,
|
|
||||||
0.0 /* pressure */,
|
|
||||||
0 /* inputSource */,
|
|
||||||
true /* isDOMEventSynthesized */,
|
|
||||||
false /* isWidgetEventSynthesized */,
|
|
||||||
buttons,
|
|
||||||
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
|
||||||
false /* disablePointerEvent */
|
|
||||||
);
|
|
||||||
promises.push(watcher.ensureEvent(type, eventObject => eventObject.jugglerEventId === jugglerEventId));
|
|
||||||
}
|
|
||||||
await Promise.all(promises);
|
|
||||||
await watcher.dispose();
|
|
||||||
};
|
|
||||||
|
|
||||||
// We must switch to proper tab in the tabbed browser so that
|
|
||||||
// 1. Event is dispatched to a proper renderer.
|
|
||||||
// 2. We receive an ack from the renderer for the dispatched event.
|
|
||||||
await this._pageTarget.activateAndRun(async () => {
|
|
||||||
this._pageTarget.ensureContextMenuClosed();
|
|
||||||
// If someone asks us to dispatch mouse event outside of viewport, then we normally would drop it.
|
|
||||||
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
|
||||||
if (x < 0 || y < 0 || x > boundingBox.width || y > boundingBox.height) {
|
|
||||||
if (type !== 'mousemove')
|
|
||||||
return;
|
|
||||||
|
|
||||||
// A special hack: if someone tries to do `mousemove` outside of
|
|
||||||
// viewport coordinates, then move the mouse off from the Web Content.
|
|
||||||
// This way we can eliminate all the hover effects.
|
|
||||||
// NOTE: since this won't go inside the renderer, there's no need to wait for ACK.
|
|
||||||
win.windowUtils.sendMouseEvent(
|
|
||||||
'mousemove',
|
|
||||||
0 /* x */,
|
|
||||||
0 /* y */,
|
|
||||||
button,
|
|
||||||
clickCount,
|
|
||||||
modifiers,
|
|
||||||
false /* aIgnoreRootScrollFrame */,
|
|
||||||
0.0 /* pressure */,
|
|
||||||
0 /* inputSource */,
|
|
||||||
true /* isDOMEventSynthesized */,
|
|
||||||
false /* isWidgetEventSynthesized */,
|
|
||||||
buttons,
|
|
||||||
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
|
||||||
false /* disablePointerEvent */
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'mousedown') {
|
|
||||||
if (this._isDragging)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const eventNames = button === 2 ? ['mousedown', 'contextmenu'] : ['mousedown'];
|
|
||||||
await sendEvents(eventNames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'mousemove') {
|
|
||||||
this._lastMousePosition = { x, y };
|
|
||||||
if (this._isDragging) {
|
|
||||||
const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers);
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {type:'dragover', x, y, modifiers});
|
|
||||||
await watcher.ensureEventsAndDispose(['dragover']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers);
|
|
||||||
await sendEvents(['mousemove']);
|
|
||||||
|
|
||||||
// The order of events after 'mousemove' is sent:
|
|
||||||
// 1. [dragstart] - might or might NOT be emitted
|
|
||||||
// 2. [mousemove] - always emitted. This was awaited as part of `sendEvents` call.
|
|
||||||
// 3. [juggler-drag-finalized] - only emitted if dragstart was emitted.
|
|
||||||
|
|
||||||
if (watcher.hasEvent('dragstart')) {
|
|
||||||
const eventObject = await watcher.ensureEvent('juggler-drag-finalized');
|
|
||||||
this._isDragging = eventObject.dragSessionStarted;
|
|
||||||
}
|
|
||||||
watcher.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'mouseup') {
|
|
||||||
if (this._isDragging) {
|
|
||||||
const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers);
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {type: 'dragover', x, y, modifiers});
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {type: 'drop', x, y, modifiers});
|
|
||||||
await this._contentPage.send('dispatchDragEvent', {type: 'dragend', x, y, modifiers});
|
|
||||||
// NOTE:
|
|
||||||
// - 'drop' event might not be dispatched at all, depending on dropAction.
|
|
||||||
// - 'dragend' event might not be dispatched at all, if the source element was removed
|
|
||||||
// during drag. However, it'll be dispatched synchronously in the renderer.
|
|
||||||
await watcher.ensureEventsAndDispose(['dragover']);
|
|
||||||
this._isDragging = false;
|
|
||||||
} else {
|
|
||||||
await sendEvents(['mouseup']);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, { muteNotificationsPopup: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchWheelEvent']({x, y, button, deltaX, deltaY, deltaZ, modifiers }) {
|
async ['Page.dispatchWheelEvent']({x, y, button, deltaX, deltaY, deltaZ, modifiers }) {
|
||||||
|
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
||||||
|
x += boundingBox.left;
|
||||||
|
y += boundingBox.top;
|
||||||
const deltaMode = 0; // WheelEvent.DOM_DELTA_PIXEL
|
const deltaMode = 0; // WheelEvent.DOM_DELTA_PIXEL
|
||||||
const lineOrPageDeltaX = deltaX > 0 ? Math.floor(deltaX) : Math.ceil(deltaX);
|
const lineOrPageDeltaX = deltaX > 0 ? Math.floor(deltaX) : Math.ceil(deltaX);
|
||||||
const lineOrPageDeltaY = deltaY > 0 ? Math.floor(deltaY) : Math.ceil(deltaY);
|
const lineOrPageDeltaY = deltaY > 0 ? Math.floor(deltaY) : Math.ceil(deltaY);
|
||||||
|
|
||||||
await this._pageTarget.activateAndRun(async () => {
|
const win = this._pageTarget._window;
|
||||||
this._pageTarget.ensureContextMenuClosed();
|
win.windowUtils.sendWheelEvent(
|
||||||
|
x,
|
||||||
// 1. Scroll element to the desired location first; the coordinates are relative to the element.
|
y,
|
||||||
this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0);
|
deltaX,
|
||||||
// 2. Get element's bounding box in the browser after the scroll is completed.
|
deltaY,
|
||||||
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
deltaZ,
|
||||||
|
deltaMode,
|
||||||
const win = this._pageTarget._window;
|
modifiers,
|
||||||
// 3. Make sure compositor is flushed after scrolling.
|
lineOrPageDeltaX,
|
||||||
if (win.windowUtils.flushApzRepaints())
|
lineOrPageDeltaY,
|
||||||
await helper.awaitTopic('apz-repaints-flushed');
|
0 /* options */);
|
||||||
|
|
||||||
win.windowUtils.sendWheelEvent(
|
|
||||||
x + boundingBox.left,
|
|
||||||
y + boundingBox.top,
|
|
||||||
deltaX,
|
|
||||||
deltaY,
|
|
||||||
deltaZ,
|
|
||||||
deltaMode,
|
|
||||||
modifiers,
|
|
||||||
lineOrPageDeltaX,
|
|
||||||
lineOrPageDeltaY,
|
|
||||||
0 /* options */);
|
|
||||||
}, { muteNotificationsPopup: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.insertText'](options) {
|
async ['Page.insertText'](options) {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ browserTypes.TargetInfo = {
|
||||||
openerId: t.Optional(t.String),
|
openerId: t.Optional(t.String),
|
||||||
};
|
};
|
||||||
|
|
||||||
browserTypes.UserPreference = {
|
|
||||||
name: t.String,
|
|
||||||
value: t.Any,
|
|
||||||
};
|
|
||||||
|
|
||||||
browserTypes.CookieOptions = {
|
browserTypes.CookieOptions = {
|
||||||
name: t.String,
|
name: t.String,
|
||||||
value: t.String,
|
value: t.String,
|
||||||
|
|
@ -193,7 +188,6 @@ networkTypes.HTTPHeader = {
|
||||||
networkTypes.HTTPCredentials = {
|
networkTypes.HTTPCredentials = {
|
||||||
username: t.String,
|
username: t.String,
|
||||||
password: t.String,
|
password: t.String,
|
||||||
origin: t.Optional(t.String),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networkTypes.SecurityDetails = {
|
networkTypes.SecurityDetails = {
|
||||||
|
|
@ -251,7 +245,6 @@ const Browser = {
|
||||||
'enable': {
|
'enable': {
|
||||||
params: {
|
params: {
|
||||||
attachToDefaultContext: t.Boolean,
|
attachToDefaultContext: t.Boolean,
|
||||||
userPrefs: t.Optional(t.Array(browserTypes.UserPreference)),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'createBrowserContext': {
|
'createBrowserContext': {
|
||||||
|
|
@ -288,7 +281,6 @@ const Browser = {
|
||||||
headers: t.Array(networkTypes.HTTPHeader),
|
headers: t.Array(networkTypes.HTTPHeader),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'clearCache': {},
|
|
||||||
'setBrowserProxy': {
|
'setBrowserProxy': {
|
||||||
params: {
|
params: {
|
||||||
type: t.Enum(['http', 'https', 'socks', 'socks4']),
|
type: t.Enum(['http', 'https', 'socks', 'socks4']),
|
||||||
|
|
@ -322,12 +314,6 @@ const Browser = {
|
||||||
enabled: t.Boolean,
|
enabled: t.Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'setCacheDisabled': {
|
|
||||||
params: {
|
|
||||||
browserContextId: t.Optional(t.String),
|
|
||||||
cacheDisabled: t.Boolean,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'setGeolocationOverride': {
|
'setGeolocationOverride': {
|
||||||
params: {
|
params: {
|
||||||
browserContextId: t.Optional(t.String),
|
browserContextId: t.Optional(t.String),
|
||||||
|
|
@ -394,6 +380,12 @@ const Browser = {
|
||||||
viewport: t.Nullable(pageTypes.Viewport),
|
viewport: t.Nullable(pageTypes.Viewport),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'setScrollbarsHidden': {
|
||||||
|
params: {
|
||||||
|
browserContextId: t.Optional(t.String),
|
||||||
|
hidden: t.Boolean,
|
||||||
|
}
|
||||||
|
},
|
||||||
'setInitScripts': {
|
'setInitScripts': {
|
||||||
params: {
|
params: {
|
||||||
browserContextId: t.Optional(t.String),
|
browserContextId: t.Optional(t.String),
|
||||||
|
|
@ -481,17 +473,6 @@ const Browser = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Heap = {
|
|
||||||
targets: ['page'],
|
|
||||||
types: {},
|
|
||||||
events: {},
|
|
||||||
methods: {
|
|
||||||
'collectGarbage': {
|
|
||||||
params: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Network = {
|
const Network = {
|
||||||
targets: ['page'],
|
targets: ['page'],
|
||||||
types: networkTypes,
|
types: networkTypes,
|
||||||
|
|
@ -678,6 +659,7 @@ const Page = {
|
||||||
'navigationStarted': {
|
'navigationStarted': {
|
||||||
frameId: t.String,
|
frameId: t.String,
|
||||||
navigationId: t.String,
|
navigationId: t.String,
|
||||||
|
url: t.String,
|
||||||
},
|
},
|
||||||
'navigationCommitted': {
|
'navigationCommitted': {
|
||||||
frameId: t.String,
|
frameId: t.String,
|
||||||
|
|
@ -841,6 +823,7 @@ const Page = {
|
||||||
},
|
},
|
||||||
returns: {
|
returns: {
|
||||||
navigationId: t.Nullable(t.String),
|
navigationId: t.Nullable(t.String),
|
||||||
|
navigationURL: t.Nullable(t.String),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'goBack': {
|
'goBack': {
|
||||||
|
|
@ -860,7 +843,9 @@ const Page = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'reload': {
|
'reload': {
|
||||||
params: { },
|
params: {
|
||||||
|
frameId: t.String,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'adoptNode': {
|
'adoptNode': {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -877,7 +862,6 @@ const Page = {
|
||||||
params: {
|
params: {
|
||||||
mimeType: t.Enum(['image/png', 'image/jpeg']),
|
mimeType: t.Enum(['image/png', 'image/jpeg']),
|
||||||
clip: pageTypes.Clip,
|
clip: pageTypes.Clip,
|
||||||
quality: t.Optional(t.Number),
|
|
||||||
omitDeviceScaleFactor: t.Optional(t.Boolean),
|
omitDeviceScaleFactor: t.Optional(t.Boolean),
|
||||||
},
|
},
|
||||||
returns: {
|
returns: {
|
||||||
|
|
@ -923,7 +907,7 @@ const Page = {
|
||||||
},
|
},
|
||||||
'dispatchMouseEvent': {
|
'dispatchMouseEvent': {
|
||||||
params: {
|
params: {
|
||||||
type: t.Enum(['mousedown', 'mousemove', 'mouseup']),
|
type: t.String,
|
||||||
button: t.Number,
|
button: t.Number,
|
||||||
x: t.Number,
|
x: t.Number,
|
||||||
y: t.Number,
|
y: t.Number,
|
||||||
|
|
@ -1007,7 +991,7 @@ const Accessibility = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocol = {
|
this.protocol = {
|
||||||
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
|
domains: {Browser, Page, Runtime, Network, Accessibility},
|
||||||
};
|
};
|
||||||
this.checkScheme = checkScheme;
|
this.checkScheme = checkScheme;
|
||||||
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,6 @@ void HeadlessWindowCapturer::RegisterCaptureDataCallback(rtc::VideoSinkInterface
|
||||||
rtc::CritScope lock2(&_callBackCs);
|
rtc::CritScope lock2(&_callBackCs);
|
||||||
_dataCallBacks.insert(dataCallback);
|
_dataCallBacks.insert(dataCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HeadlessWindowCapturer::RegisterCaptureDataCallback(webrtc::RawVideoSinkInterface* dataCallback) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) {
|
void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) {
|
||||||
rtc::CritScope lock2(&_callBackCs);
|
rtc::CritScope lock2(&_callBackCs);
|
||||||
auto it = _dataCallBacks.find(dataCallback);
|
auto it = _dataCallBacks.find(dataCallback);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class HeadlessWindowCapturer : public webrtc::VideoCaptureModuleEx {
|
||||||
int32_t StopCaptureIfAllClientsClose() override;
|
int32_t StopCaptureIfAllClientsClose() override;
|
||||||
|
|
||||||
void RegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override;
|
void RegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override;
|
||||||
void RegisterCaptureDataCallback(webrtc::RawVideoSinkInterface* dataCallback) override;
|
|
||||||
void DeRegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override;
|
void DeRegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override;
|
||||||
|
|
||||||
int32_t SetCaptureRotation(webrtc::VideoRotation) override { return -1; }
|
int32_t SetCaptureRotation(webrtc::VideoRotation) override { return -1; }
|
||||||
|
|
|
||||||
|
|
@ -145,33 +145,11 @@ public:
|
||||||
uint8_t* u_data = image->planes[VPX_PLANE_U];
|
uint8_t* u_data = image->planes[VPX_PLANE_U];
|
||||||
uint8_t* v_data = image->planes[VPX_PLANE_V];
|
uint8_t* v_data = image->planes[VPX_PLANE_V];
|
||||||
|
|
||||||
/**
|
double src_width = src->width() - m_margin.LeftRight();
|
||||||
* Let's say we have the following image of 6x3 pixels (same number = same pixel value):
|
double src_height = src->height() - m_margin.top;
|
||||||
* 112233
|
// YUV offsets must be even.
|
||||||
* 112233
|
int yuvTopOffset = m_margin.top & 1 ? m_margin.top + 1 : m_margin.top;
|
||||||
* 445566
|
int yuvLeftOffset = m_margin.left & 1 ? m_margin.left + 1 : m_margin.left;
|
||||||
* In I420 format (see https://en.wikipedia.org/wiki/YUV), the image will have the following data planes:
|
|
||||||
* Y [stride_Y = 6]:
|
|
||||||
* 112233
|
|
||||||
* 112233
|
|
||||||
* 445566
|
|
||||||
* U [stride_U = 3] - this plane has aggregate for each 2x2 pixels:
|
|
||||||
* 123
|
|
||||||
* 456
|
|
||||||
* V [stride_V = 3] - this plane has aggregate for each 2x2 pixels:
|
|
||||||
* 123
|
|
||||||
* 456
|
|
||||||
*
|
|
||||||
* To crop this image efficiently, we can move src_Y/U/V pointer and
|
|
||||||
* adjust the src_width and src_height. However, we must cut off only **even**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
double src_width = src->width() - yuvLeftOffset;
|
|
||||||
double src_height = src->height() - yuvTopOffset;
|
|
||||||
|
|
||||||
if (src_width > image->w || src_height > image->h) {
|
if (src_width > image->w || src_height > image->h) {
|
||||||
double scale = std::min(image->w / src_width, image->h / src_height);
|
double scale = std::min(image->w / src_width, image->h / src_height);
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@
|
||||||
#include "modules/video_capture/video_capture.h"
|
#include "modules/video_capture/video_capture.h"
|
||||||
#include "mozilla/widget/PlatformWidgetTypes.h"
|
#include "mozilla/widget/PlatformWidgetTypes.h"
|
||||||
#include "video_engine/desktop_capture_impl.h"
|
#include "video_engine/desktop_capture_impl.h"
|
||||||
#include "VideoEngine.h"
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "jpeglib.h"
|
#include "jpeglib.h"
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +55,7 @@ rtc::scoped_refptr<webrtc::VideoCaptureModuleEx> CreateWindowCapturer(nsIWidget*
|
||||||
windowId.AppendPrintf("%" PRIuPTR, rawWindowId);
|
windowId.AppendPrintf("%" PRIuPTR, rawWindowId);
|
||||||
bool captureCursor = false;
|
bool captureCursor = false;
|
||||||
static int moduleId = 0;
|
static int moduleId = 0;
|
||||||
return rtc::scoped_refptr<webrtc::VideoCaptureModuleEx>(webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), camera::CaptureDeviceType::Window, captureCursor));
|
return rtc::scoped_refptr<webrtc::VideoCaptureModuleEx>(webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), CaptureDeviceType::Window, captureCursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
nsresult generateUid(nsString& uid) {
|
nsresult generateUid(nsString& uid) {
|
||||||
|
|
@ -129,7 +127,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
||||||
capability.height = 960;
|
capability.height = 960;
|
||||||
capability.maxFPS = ScreencastEncoder::fps;
|
capability.maxFPS = ScreencastEncoder::fps;
|
||||||
capability.videoType = webrtc::VideoType::kI420;
|
capability.videoType = webrtc::VideoType::kI420;
|
||||||
int error = mCaptureModule->StartCaptureCounted(capability);
|
int error = mCaptureModule->StartCapture(capability);
|
||||||
if (error) {
|
if (error) {
|
||||||
fprintf(stderr, "StartCapture error %d\n", error);
|
fprintf(stderr, "StartCapture error %d\n", error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -152,7 +150,6 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
||||||
mCaptureModule->DeRegisterCaptureDataCallback(this);
|
mCaptureModule->DeRegisterCaptureDataCallback(this);
|
||||||
else
|
else
|
||||||
mCaptureModule->DeRegisterRawFrameCallback(this);
|
mCaptureModule->DeRegisterRawFrameCallback(this);
|
||||||
mCaptureModule->StopCaptureCounted();
|
|
||||||
if (mEncoder) {
|
if (mEncoder) {
|
||||||
mEncoder->finish([this, protect = RefPtr{this}] {
|
mEncoder->finish([this, protect = RefPtr{this}] {
|
||||||
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
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
|
// 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.dataSubmissionEnabled", false);
|
||||||
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
|
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
|
||||||
|
|
@ -15,9 +11,6 @@ pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
||||||
// Force pdfs into downloads.
|
// Force pdfs into downloads.
|
||||||
pref("pdfjs.disabled", true);
|
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.
|
// Disable cross-process iframes, but not cross-process navigations.
|
||||||
pref("fission.webContentIsolationStrategy", 0);
|
pref("fission.webContentIsolationStrategy", 0);
|
||||||
|
|
||||||
|
|
@ -25,6 +18,9 @@ pref("fission.webContentIsolationStrategy", 0);
|
||||||
// We also separately disable BFCache in content via docSchell property.
|
// We also separately disable BFCache in content via docSchell property.
|
||||||
pref("fission.bfcacheInParent", false);
|
pref("fission.bfcacheInParent", false);
|
||||||
|
|
||||||
|
// File url navigations behave differently from http, we are not ready.
|
||||||
|
pref("browser.tabs.remote.separateFileUriProcess", false);
|
||||||
|
|
||||||
// Disable first-party-based cookie partitioning.
|
// Disable first-party-based cookie partitioning.
|
||||||
// When it is enabled, we have to retain "thirdPartyCookie^" permissions
|
// When it is enabled, we have to retain "thirdPartyCookie^" permissions
|
||||||
// in the storageState.
|
// in the storageState.
|
||||||
|
|
@ -43,13 +39,14 @@ pref("dom.ipc.processPrelaunch.enabled", false);
|
||||||
// Isolate permissions by user context.
|
// Isolate permissions by user context.
|
||||||
pref("permissions.isolateBy.userContext", true);
|
pref("permissions.isolateBy.userContext", true);
|
||||||
|
|
||||||
|
// We need this to issue Page.navigate from inside the renderer
|
||||||
|
// to cross-process domains, for example file urls.
|
||||||
|
pref("security.sandbox.content.level", 2);
|
||||||
|
|
||||||
// Allow creating files in content process - required for
|
// Allow creating files in content process - required for
|
||||||
// |Page.setFileInputFiles| protocol method.
|
// |Page.setFileInputFiles| protocol method.
|
||||||
pref("dom.file.createInChild", true);
|
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
|
// Do not warn when closing all open tabs
|
||||||
pref("browser.tabs.warnOnClose", false);
|
pref("browser.tabs.warnOnClose", false);
|
||||||
|
|
||||||
|
|
@ -88,28 +85,14 @@ pref("geo.provider.testing", true);
|
||||||
// THESE ARE NICHE PROPERTIES THAT ARE NICE TO HAVE
|
// THESE ARE NICHE PROPERTIES THAT ARE NICE TO HAVE
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
// Enable software-backed webgl. See https://phabricator.services.mozilla.com/D164016
|
|
||||||
pref("webgl.forbid-software", false);
|
|
||||||
|
|
||||||
// Disable auto-fill for credit cards and addresses.
|
|
||||||
// See https://github.com/microsoft/playwright/issues/21393
|
|
||||||
pref("extensions.formautofill.creditCards.supported", "off");
|
|
||||||
pref("extensions.formautofill.addresses.supported", "off");
|
|
||||||
|
|
||||||
// Allow access to system-added self-signed certificates. This aligns
|
|
||||||
// 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.
|
// Avoid stalling on shutdown, after "xpcom-will-shutdown" phase.
|
||||||
// This at least happens when shutting down soon after launching.
|
// This at least happens when shutting down soon after launching.
|
||||||
// See AppShutdown.cpp for more details on shutdown phases.
|
// See AppShutdown.cpp for more details on shutdown phases.
|
||||||
pref("toolkit.shutdown.fastShutdownStage", 3);
|
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.
|
// Use light theme by default.
|
||||||
pref("ui.systemUsesDarkTheme", 0);
|
pref("ui.systemUsesDarkTheme", 0);
|
||||||
|
|
||||||
|
|
@ -120,20 +103,9 @@ pref("prompts.contentPromptSubDialog", false);
|
||||||
// Do not use system colors - they are affected by themes.
|
// Do not use system colors - they are affected by themes.
|
||||||
pref("ui.use_standins_for_native_colors", true);
|
pref("ui.use_standins_for_native_colors", true);
|
||||||
|
|
||||||
// Turn off the Push service.
|
|
||||||
pref("dom.push.serverURL", "");
|
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", "");
|
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.safebrowsing.provider.mozilla.updateURL", "");
|
||||||
pref("browser.library.activity-stream.enabled", false);
|
pref("browser.library.activity-stream.enabled", false);
|
||||||
pref("browser.search.geoSpecificDefaults", false);
|
pref("browser.search.geoSpecificDefaults", false);
|
||||||
|
|
@ -328,8 +300,3 @@ pref("extensions.blocklist.enabled", false);
|
||||||
// Force Firefox Devtools to open in a separate window.
|
// Force Firefox Devtools to open in a separate window.
|
||||||
pref("devtools.toolbox.host", "window");
|
pref("devtools.toolbox.host", "window");
|
||||||
|
|
||||||
// Disable auto translations
|
|
||||||
pref("browser.translations.enable", false);
|
|
||||||
|
|
||||||
// Disable spell check
|
|
||||||
pref("layout.spellcheckDefault", 0);
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# A script to roll browser patches from internal repository.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
set +x
|
|
||||||
|
|
||||||
trap "cd $(pwd -P)" EXIT
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SCRIPT_PATH=$(pwd -P)
|
|
||||||
|
|
||||||
if [[ "$#" -ne 1 ]]; then
|
|
||||||
echo "Usage: $0 <path to playwright-browsers checkout>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SOURCE_DIRECTORY="$1"
|
|
||||||
|
|
||||||
if [[ $(basename "${SOURCE_DIRECTORY}") != "playwright-browsers" ]]; then
|
|
||||||
echo "ERROR: the source directory must be named 'playwright-browsers'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [[ -d "${SOURCE_DIRECTORY}/browser_patches" ]]; then
|
|
||||||
echo "ERROR: the ${SOURCE_DIRECTORY}/browser_patches does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
files=(
|
|
||||||
"./firefox/juggler/"
|
|
||||||
"./firefox/patches/"
|
|
||||||
"./firefox/preferences/"
|
|
||||||
"./firefox/UPSTREAM_CONFIG.sh"
|
|
||||||
"./webkit/embedder/"
|
|
||||||
"./webkit/patches/"
|
|
||||||
"./webkit/pw_run.sh"
|
|
||||||
"./webkit/UPSTREAM_CONFIG.sh"
|
|
||||||
"./winldd/"
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${files[@]}"; do
|
|
||||||
rsync -av --delete "${SOURCE_DIRECTORY}/browser_patches/${file}" "${SCRIPT_PATH}/${file}"
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
||||||
BASE_BRANCH="main"
|
BASE_BRANCH="main"
|
||||||
BASE_REVISION="76c95d6131edd36775a5eac01e297926fc974be8"
|
BASE_REVISION="654646fe6187abcf9ced6a3ace80eaf04754fd39"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@
|
||||||
#import <WebKit/WKUserContentControllerPrivate.h>
|
#import <WebKit/WKUserContentControllerPrivate.h>
|
||||||
#import <WebKit/WKWebViewConfigurationPrivate.h>
|
#import <WebKit/WKWebViewConfigurationPrivate.h>
|
||||||
#import <WebKit/WKWebViewPrivate.h>
|
#import <WebKit/WKWebViewPrivate.h>
|
||||||
#import <WebKit/WKWebpagePreferencesPrivate.h>
|
|
||||||
#import <WebKit/WKWebsiteDataStorePrivate.h>
|
#import <WebKit/WKWebsiteDataStorePrivate.h>
|
||||||
#import <WebKit/WebNSURLExtras.h>
|
#import <WebKit/WebNSURLExtras.h>
|
||||||
#import <WebKit/WebKit.h>
|
#import <WebKit/WebKit.h>
|
||||||
|
|
@ -98,7 +97,7 @@ const NSActivityOptions ActivityOptions =
|
||||||
|
|
||||||
for (NSString *argument in subArray) {
|
for (NSString *argument in subArray) {
|
||||||
if (![argument hasPrefix:@"--"])
|
if (![argument hasPrefix:@"--"])
|
||||||
_initialURL = [argument copy];
|
_initialURL = argument;
|
||||||
if ([argument hasPrefix:@"--user-data-dir="]) {
|
if ([argument hasPrefix:@"--user-data-dir="]) {
|
||||||
NSRange range = NSMakeRange(16, [argument length] - 16);
|
NSRange range = NSMakeRange(16, [argument length] - 16);
|
||||||
_userDataDir = [[argument substringWithRange:range] copy];
|
_userDataDir = [[argument substringWithRange:range] copy];
|
||||||
|
|
@ -181,7 +180,7 @@ const NSActivityOptions ActivityOptions =
|
||||||
_WKWebsiteDataStoreConfiguration *configuration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
|
_WKWebsiteDataStoreConfiguration *configuration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
|
||||||
if (_userDataDir) {
|
if (_userDataDir) {
|
||||||
// Local storage state should be stored in separate dirs for persistent contexts.
|
// Local storage state should be stored in separate dirs for persistent contexts.
|
||||||
[configuration setUnifiedOriginStorageLevel:_WKUnifiedOriginStorageLevelNone];
|
[configuration setShouldUseCustomStoragePaths:YES];
|
||||||
|
|
||||||
NSURL *cookieFile = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/cookie.db", _userDataDir]];
|
NSURL *cookieFile = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/cookie.db", _userDataDir]];
|
||||||
[configuration _setCookieStorageFile:cookieFile];
|
[configuration _setCookieStorageFile:cookieFile];
|
||||||
|
|
@ -231,18 +230,14 @@ const NSActivityOptions ActivityOptions =
|
||||||
configuration = [[WKWebViewConfiguration alloc] init];
|
configuration = [[WKWebViewConfiguration alloc] init];
|
||||||
configuration.websiteDataStore = [self persistentDataStore];
|
configuration.websiteDataStore = [self persistentDataStore];
|
||||||
configuration._controlledByAutomation = true;
|
configuration._controlledByAutomation = true;
|
||||||
configuration.preferences.elementFullscreenEnabled = YES;
|
configuration.preferences._fullScreenEnabled = YES;
|
||||||
configuration.preferences._developerExtrasEnabled = YES;
|
configuration.preferences._developerExtrasEnabled = YES;
|
||||||
configuration.preferences._mediaDevicesEnabled = YES;
|
configuration.preferences._mediaDevicesEnabled = YES;
|
||||||
configuration.preferences._mockCaptureDevicesEnabled = YES;
|
configuration.preferences._mockCaptureDevicesEnabled = YES;
|
||||||
// Enable WebM support.
|
|
||||||
configuration.preferences._alternateWebMPlayerEnabled = YES;
|
|
||||||
configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO;
|
configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO;
|
||||||
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
|
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
|
||||||
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
|
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
|
||||||
configuration.preferences._domTimersThrottlingEnabled = NO;
|
configuration.preferences._domTimersThrottlingEnabled = NO;
|
||||||
// Do not auto play audio and video with sound.
|
|
||||||
configuration.defaultWebpagePreferences._autoplayPolicy = _WKWebsiteAutoplayPolicyAllowWithoutSound;
|
|
||||||
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
|
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
|
||||||
processConfiguration.forceOverlayScrollbars = YES;
|
processConfiguration.forceOverlayScrollbars = YES;
|
||||||
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];
|
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];
|
||||||
|
|
@ -482,20 +477,8 @@ const NSActivityOptions ActivityOptions =
|
||||||
decisionHandler(WKNavigationResponsePolicyAllow);
|
decisionHandler(WKNavigationResponsePolicyAllow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
|
||||||
|
|
||||||
NSString *contentType = [httpResponse valueForHTTPHeaderField:@"Content-Type"];
|
|
||||||
if (!navigationResponse.canShowMIMEType && (contentType && [contentType length] > 0)) {
|
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType && ([contentType isEqualToString:@"application/pdf"] || [contentType isEqualToString:@"text/pdf"])) {
|
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
|
NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
|
||||||
if (disposition && [disposition hasPrefix:@"attachment"]) {
|
if (disposition && [disposition hasPrefix:@"attachment"]) {
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
decisionHandler(WKNavigationResponsePolicyDownload);
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ static void* keyValueObservingContext = &keyValueObservingContext;
|
||||||
| _WKRenderingProgressEventFirstLayoutAfterSuppressedIncrementalRendering
|
| _WKRenderingProgressEventFirstLayoutAfterSuppressedIncrementalRendering
|
||||||
| _WKRenderingProgressEventFirstPaintAfterSuppressedIncrementalRendering;
|
| _WKRenderingProgressEventFirstPaintAfterSuppressedIncrementalRendering;
|
||||||
|
|
||||||
_webView.customUserAgent = @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15";
|
_webView.customUserAgent = @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15";
|
||||||
|
|
||||||
_webView._usePlatformFindUI = NO;
|
_webView._usePlatformFindUI = NO;
|
||||||
|
|
||||||
|
|
@ -724,7 +724,7 @@ static BOOL areEssentiallyEqual(double a, double b)
|
||||||
[_webView loadHTMLString:HTMLString baseURL:nil];
|
[_webView loadHTMLString:HTMLString baseURL:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSSet *dataTypes(void)
|
static NSSet *dataTypes()
|
||||||
{
|
{
|
||||||
return [WKWebsiteDataStore allWebsiteDataTypes];
|
return [WKWebsiteDataStore allWebsiteDataTypes];
|
||||||
}
|
}
|
||||||
|
|
@ -792,20 +792,8 @@ static NSSet *dataTypes(void)
|
||||||
decisionHandler(WKNavigationResponsePolicyAllow);
|
decisionHandler(WKNavigationResponsePolicyAllow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
|
||||||
|
|
||||||
NSString *contentType = [httpResponse valueForHTTPHeaderField:@"Content-Type"];
|
|
||||||
if (!navigationResponse.canShowMIMEType && (contentType && [contentType length] > 0)) {
|
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType && ([contentType isEqualToString:@"application/pdf"] || [contentType isEqualToString:@"text/pdf"])) {
|
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
|
NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
|
||||||
if (disposition && [disposition hasPrefix:@"attachment"]) {
|
if (disposition && [disposition hasPrefix:@"attachment"]) {
|
||||||
decisionHandler(WKNavigationResponsePolicyDownload);
|
decisionHandler(WKNavigationResponsePolicyDownload);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ list(APPEND Playwright_PRIVATE_LIBRARIES
|
||||||
)
|
)
|
||||||
|
|
||||||
WEBKIT_EXECUTABLE_DECLARE(Playwright)
|
WEBKIT_EXECUTABLE_DECLARE(Playwright)
|
||||||
|
WEBKIT_WRAP_EXECUTABLE(Playwright
|
||||||
|
SOURCES ${TOOLS_DIR}/win/DLLLauncher/DLLLauncherMain.cpp Playwright.rc
|
||||||
|
LIBRARIES shlwapi
|
||||||
|
)
|
||||||
WEBKIT_EXECUTABLE(Playwright)
|
WEBKIT_EXECUTABLE(Playwright)
|
||||||
|
|
||||||
set_target_properties(Playwright PROPERTIES WIN32_EXECUTABLE ON)
|
set_target_properties(Playwright PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ void computeFullDesktopFrame()
|
||||||
s_windowSize.cy = scaleFactor * (desktop.bottom - desktop.top);
|
s_windowSize.cy = scaleFactor * (desktop.bottom - desktop.top);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI DllMain(HINSTANCE dllInstance, DWORD reason, LPVOID)
|
||||||
|
{
|
||||||
|
if (reason == DLL_PROCESS_ATTACH)
|
||||||
|
hInst = dllInstance;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
bool getAppDataFolder(_bstr_t& directory)
|
bool getAppDataFolder(_bstr_t& directory)
|
||||||
{
|
{
|
||||||
wchar_t appDataDirectory[MAX_PATH];
|
wchar_t appDataDirectory[MAX_PATH];
|
||||||
|
|
|
||||||
76
browser_patches/webkit/embedder/Playwright/win/Playwright.rc
Normal file
76
browser_patches/webkit/embedder/Playwright/win/Playwright.rc
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#include "PlaywrightResource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#define APSTUDIO_HIDDEN_SYMBOLS
|
||||||
|
#include "windows.h"
|
||||||
|
#undef APSTUDIO_HIDDEN_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (U.S.) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
#ifdef _WIN32
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
#pragma code_page(1252)
|
||||||
|
#endif //_WIN32
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_PLAYWRIGHT ICON "Playwright.ico"
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"PlaywrightResource.\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||||
|
"#include ""windows.h""\r\n"
|
||||||
|
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
#endif // English (U.S.) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ WebKitBrowserWindow::WebKitBrowserWindow(BrowserWindowClient& client, HWND mainW
|
||||||
WKPagePolicyClientV1 policyClient = { };
|
WKPagePolicyClientV1 policyClient = { };
|
||||||
policyClient.base.version = 1;
|
policyClient.base.version = 1;
|
||||||
policyClient.base.clientInfo = this;
|
policyClient.base.clientInfo = this;
|
||||||
policyClient.decidePolicyForResponse = decidePolicyForResponse;
|
policyClient.decidePolicyForResponse_deprecatedForUseWithV0 = decidePolicyForResponse;
|
||||||
policyClient.decidePolicyForNavigationAction = decidePolicyForNavigationAction;
|
policyClient.decidePolicyForNavigationAction = decidePolicyForNavigationAction;
|
||||||
WKPageSetPagePolicyClient(page, &policyClient.base);
|
WKPageSetPagePolicyClient(page, &policyClient.base);
|
||||||
|
|
||||||
|
|
@ -402,10 +402,9 @@ void WebKitBrowserWindow::decidePolicyForNavigationAction(WKPageRef page, WKFram
|
||||||
WKFramePolicyListenerUse(listener);
|
WKFramePolicyListenerUse(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebKitBrowserWindow::decidePolicyForResponse(WKPageRef page, WKFrameRef frame, WKURLResponseRef response, WKURLRequestRef request, bool canShowMIMEType, WKFramePolicyListenerRef listener, WKTypeRef userData, const void* clientInfo)
|
void WebKitBrowserWindow::decidePolicyForResponse(WKPageRef page, WKFrameRef frame, WKURLResponseRef response, WKURLRequestRef request, WKFramePolicyListenerRef listener, WKTypeRef userData, const void* clientInfo)
|
||||||
{
|
{
|
||||||
// Safari renders resources without content-type as text.
|
if (WKURLResponseIsAttachment(response))
|
||||||
if (WKURLResponseIsAttachment(response) || (!WKStringIsEmpty(WKURLResponseCopyMIMEType(response)) && !canShowMIMEType))
|
|
||||||
WKFramePolicyListenerDownload(listener);
|
WKFramePolicyListenerDownload(listener);
|
||||||
else
|
else
|
||||||
WKFramePolicyListenerUse(listener);
|
WKFramePolicyListenerUse(listener);
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ private:
|
||||||
static WKRect getWindowFrame(WKPageRef page, const void *clientInfo);
|
static WKRect getWindowFrame(WKPageRef page, const void *clientInfo);
|
||||||
static void didNotHandleKeyEvent(WKPageRef, WKNativeEventPtr, const void*);
|
static void didNotHandleKeyEvent(WKPageRef, WKNativeEventPtr, const void*);
|
||||||
static void decidePolicyForNavigationAction(WKPageRef, WKFrameRef, WKFrameNavigationType, WKEventModifiers, WKEventMouseButton, WKFrameRef, WKURLRequestRef, WKFramePolicyListenerRef, WKTypeRef, const void* clientInfo);
|
static void decidePolicyForNavigationAction(WKPageRef, WKFrameRef, WKFrameNavigationType, WKEventModifiers, WKEventMouseButton, WKFrameRef, WKURLRequestRef, WKFramePolicyListenerRef, WKTypeRef, const void* clientInfo);
|
||||||
static void decidePolicyForResponse(WKPageRef, WKFrameRef, WKURLResponseRef, WKURLRequestRef, bool, WKFramePolicyListenerRef, WKTypeRef, const void*);
|
static void decidePolicyForResponse(WKPageRef, WKFrameRef, WKURLResponseRef, WKURLRequestRef, WKFramePolicyListenerRef, WKTypeRef, const void*);
|
||||||
|
|
||||||
BrowserWindowClient& m_client;
|
BrowserWindowClient& m_client;
|
||||||
WKRetainPtr<WKViewRef> m_view;
|
WKRetainPtr<WKViewRef> m_view;
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ static void configureDataStore(WKWebsiteDataStoreRef dataStore) {
|
||||||
|
|
||||||
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpstrCmdLine, _In_ int nCmdShow)
|
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpstrCmdLine, _In_ int nCmdShow)
|
||||||
{
|
{
|
||||||
hInst = hInstance;
|
|
||||||
#ifdef _CRTDBG_MAP_ALLOC
|
#ifdef _CRTDBG_MAP_ALLOC
|
||||||
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
|
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
|
||||||
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
|
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
|
||||||
|
|
@ -166,3 +165,8 @@ exit:
|
||||||
|
|
||||||
return static_cast<int>(msg.wParam);
|
return static_cast<int>(msg.wParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" __declspec(dllexport) int WINAPI dllLauncherEntryPoint(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpstrCmdLine, int nCmdShow)
|
||||||
|
{
|
||||||
|
return wWinMain(hInstance, hPrevInstance, lpstrCmdLine, nCmdShow);
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,11 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
function getWebkitCheckoutPath() {
|
|
||||||
echo ${WK_CHECKOUT_PATH:-"$HOME/webkit"}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runOSX() {
|
function runOSX() {
|
||||||
# if script is run as-is
|
# if script is run as-is
|
||||||
WK_CHECKOUT_PATH=$(getWebkitCheckoutPath)
|
if [[ -f "${SCRIPT_PATH}/EXPECTED_BUILDS" && -n "$WK_CHECKOUT_PATH" && -d "$WK_CHECKOUT_PATH/WebKitBuild/Release/Playwright.app" ]]; then
|
||||||
if [[ -f "${SCRIPT_PATH}/EXPECTED_BUILDS" && -d "$WK_CHECKOUT_PATH/WebKitBuild/Release/Playwright.app" ]]; then
|
|
||||||
DYLIB_PATH="$WK_CHECKOUT_PATH/WebKitBuild/Release"
|
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
|
elif [[ -d $SCRIPT_PATH/Playwright.app ]]; then
|
||||||
DYLIB_PATH="$SCRIPT_PATH"
|
DYLIB_PATH="$SCRIPT_PATH"
|
||||||
elif [[ -d $SCRIPT_PATH/WebKitBuild/Release/Playwright.app ]]; then
|
elif [[ -d $SCRIPT_PATH/WebKitBuild/Release/Playwright.app ]]; then
|
||||||
|
|
@ -26,30 +23,29 @@ function runLinux() {
|
||||||
GIO_DIR="";
|
GIO_DIR="";
|
||||||
LD_PATH="";
|
LD_PATH="";
|
||||||
BUNDLE_DIR="";
|
BUNDLE_DIR="";
|
||||||
DEPENDENCIES_FOLDER="WebKitBuild/DependenciesGTK";
|
DEPENDENCIES_FOLDER="DependenciesGTK";
|
||||||
MINIBROWSER_FOLDER="minibrowser-gtk";
|
MINIBROWSER_FOLDER="minibrowser-gtk";
|
||||||
BUILD_FOLDER="WebKitBuild/GTK";
|
BUILD_FOLDER="WebKitBuild/GTK";
|
||||||
if [[ "$*" == *--headless* ]]; then
|
if [[ "$*" == *--headless* ]]; then
|
||||||
DEPENDENCIES_FOLDER="WebKitBuild/DependenciesWPE";
|
DEPENDENCIES_FOLDER="DependenciesWPE";
|
||||||
MINIBROWSER_FOLDER="minibrowser-wpe";
|
MINIBROWSER_FOLDER="minibrowser-wpe";
|
||||||
BUILD_FOLDER="WebKitBuild/WPE";
|
BUILD_FOLDER="WebKitBuild/WPE";
|
||||||
fi
|
fi
|
||||||
# Setting extra environment variables like LD_LIBRARY_PATH or WEBKIT_INJECTED_BUNDLE_PATH
|
# 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
|
# is only needed when calling MiniBrowser from the build folder. The MiniBrowser from
|
||||||
# the zip bundle wrapper already sets itself the needed env variables.
|
# the zip bundle wrapper already sets itself the needed env variables.
|
||||||
WK_CHECKOUT_PATH=$(getWebkitCheckoutPath)
|
|
||||||
if [[ -d $SCRIPT_PATH/$MINIBROWSER_FOLDER ]]; then
|
if [[ -d $SCRIPT_PATH/$MINIBROWSER_FOLDER ]]; then
|
||||||
MINIBROWSER="$SCRIPT_PATH/$MINIBROWSER_FOLDER/MiniBrowser"
|
MINIBROWSER="$SCRIPT_PATH/$MINIBROWSER_FOLDER/MiniBrowser"
|
||||||
elif [[ -d $WK_CHECKOUT_PATH/$BUILD_FOLDER ]]; then
|
elif [[ -d $HOME/webkit/$BUILD_FOLDER ]]; then
|
||||||
LD_PATH="$WK_CHECKOUT_PATH/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/checkout/$BUILD_FOLDER/Release/bin"
|
LD_PATH="$HOME/webkit/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/checkout/$BUILD_FOLDER/Release/bin"
|
||||||
GIO_DIR="$WK_CHECKOUT_PATH/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
GIO_DIR="$HOME/webkit/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||||
BUNDLE_DIR="$WK_CHECKOUT_PATH/$BUILD_FOLDER/Release/lib"
|
BUNDLE_DIR="$HOME/webkit/$BUILD_FOLDER/Release/lib"
|
||||||
MINIBROWSER="$WK_CHECKOUT_PATH/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
MINIBROWSER="$HOME/webkit/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
||||||
elif [[ -f $SCRIPT_PATH/MiniBrowser ]]; then
|
elif [[ -f $SCRIPT_PATH/MiniBrowser ]]; then
|
||||||
MINIBROWSER="$SCRIPT_PATH/MiniBrowser"
|
MINIBROWSER="$SCRIPT_PATH/MiniBrowser"
|
||||||
elif [[ -d $SCRIPT_PATH/$BUILD_FOLDER ]]; then
|
elif [[ -d $SCRIPT_PATH/$BUILD_FOLDER ]]; then
|
||||||
LD_PATH="$SCRIPT_PATH/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/$BUILD_FOLDER/Release/bin"
|
LD_PATH="$SCRIPT_PATH/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib:$SCRIPT_PATH/$BUILD_FOLDER/Release/bin"
|
||||||
GIO_DIR="$SCRIPT_PATH/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
GIO_DIR="$SCRIPT_PATH/$BUILD_FOLDER/$DEPENDENCIES_FOLDER/Root/lib/gio/modules"
|
||||||
BUNDLE_DIR="$SCRIPT_PATH/$BUILD_FOLDER/Release/lib"
|
BUNDLE_DIR="$SCRIPT_PATH/$BUILD_FOLDER/Release/lib"
|
||||||
MINIBROWSER="$SCRIPT_PATH/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
MINIBROWSER="$SCRIPT_PATH/$BUILD_FOLDER/Release/bin/MiniBrowser"
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
|
|
@ -39,4 +39,5 @@ fi
|
||||||
|
|
||||||
# create a TMP directory to copy all necessary files
|
# create a TMP directory to copy all necessary files
|
||||||
cd ./x64/Release
|
cd ./x64/Release
|
||||||
7z a "$ZIP_PATH" ./PrintDeps.exe
|
zip $ZIP_PATH ./PrintDeps.exe
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ id: accessibility-testing
|
||||||
title: "Accessibility testing"
|
title: "Accessibility testing"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright can be used to test your application for many types of accessibility issues.
|
Playwright can be used to test your application for many types of accessibility issues.
|
||||||
|
|
||||||
A few examples of problems this can catch include:
|
A few examples of problems this can catch include:
|
||||||
|
|
@ -14,6 +12,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.
|
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
|
## 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.
|
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
|
||||||
|
|
@ -70,24 +70,22 @@ For example, you can use [`AxeBuilder.include()`](https://github.com/dequelabs/a
|
||||||
`AxeBuilder.analyze()` will scan the page *in its current state* when you call it. To scan parts of a page that are revealed based on UI interactions, use [Locators](./locators.md) to interact with the page before invoking `analyze()`:
|
`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
|
```java
|
||||||
public class HomepageTests {
|
@Test
|
||||||
@Test
|
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
|
||||||
void navigationMenuFlyoutShouldNotHaveAutomaticallyDetectableAccessibilityViolations() throws Exception {
|
page.navigate("https://your-site.com/");
|
||||||
page.navigate("https://your-site.com/");
|
|
||||||
|
|
||||||
page.locator("button[aria-label=\"Navigation Menu\"]").click();
|
page.locator("button[aria-label=\"Navigation Menu\"]").click();
|
||||||
|
|
||||||
// It is important to waitFor() the page to be in the desired
|
// It is important to waitFor() the page to be in the desired
|
||||||
// state *before* running analyze(). Otherwise, axe might not
|
// state *before* running analyze(). Otherwise, axe might not
|
||||||
// find all the elements your test expects it to scan.
|
// find all the elements your test expects it to scan.
|
||||||
page.locator("#navigation-menu-flyout").waitFor();
|
page.locator("#navigation-menu-flyout").waitFor();
|
||||||
|
|
||||||
AxeResults accessibilityScanResults = new AxeBuilder(page)
|
AxeResults accessibilityScanResults = new AxeBuilder(page)
|
||||||
.include(Arrays.asList("#navigation-menu-flyout"))
|
.include(Arrays.asList("#navigation-menu-flyout"))
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
|
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -135,7 +133,7 @@ If the element in question is used repeatedly in many pages, consider [using a t
|
||||||
|
|
||||||
### Disabling individual scan rules
|
### 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.
|
You can find the rule IDs to pass to `disableRules()` in the `id` property of the violations you want to suppress. A [complete list of axe's rules](https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md) can be found in `axe-core`'s documentation.
|
||||||
|
|
||||||
|
|
@ -160,40 +158,38 @@ This approach avoids the downsides of using `AxeBuilder.exclude()` at the cost o
|
||||||
Here is an example of using fingerprints based on only rule IDs and "target" selectors pointing to each violation:
|
Here is an example of using fingerprints based on only rule IDs and "target" selectors pointing to each violation:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class HomepageTests {
|
@Test
|
||||||
@Test
|
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
|
||||||
shouldOnlyHaveAccessibilityViolationsMatchingKnownFingerprints() throws Exception {
|
page.navigate("https://your-site.com/");
|
||||||
page.navigate("https://your-site.com/");
|
|
||||||
|
|
||||||
AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();
|
AxeResults accessibilityScanResults = new AxeBuilder(page).analyze();
|
||||||
|
|
||||||
List<ViolationFingerprint> violationFingerprints = fingerprintsFromScanResults(accessibilityScanResults);
|
List<ViolationFingerprint> violationFingerprints = fingerprintsFromScanResults(accessibilityScanResults);
|
||||||
|
|
||||||
assertEquals(Arrays.asList(
|
assertEquals(Arrays.asList(
|
||||||
new ViolationFingerprint("aria-roles", "[span[role=\"invalid\"]]"),
|
new ViolationFingerprint("aria-roles", "[span[role=\"invalid\"]]"),
|
||||||
new ViolationFingerprint("color-contrast", "[li:nth-child(2) > span]"),
|
new ViolationFingerprint("color-contrast", "[li:nth-child(2) > span]"),
|
||||||
new ViolationFingerprint("label", "[input]")
|
new ViolationFingerprint("label", "[input]")
|
||||||
), violationFingerprints);
|
), violationFingerprints);
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can make your "fingerprint" as specific as you like. This one considers a violation to be
|
// 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.
|
// "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
|
// Using a record type makes it easy to compare fingerprints with assertEquals
|
||||||
public record ViolationFingerprint(String ruleId, String target) { }
|
public record ViolationFingerprint(String ruleId, String target) { }
|
||||||
|
|
||||||
public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
|
public List<ViolationFingerprint> fingerprintsFromScanResults(AxeResults results) {
|
||||||
return results.getViolations().stream()
|
return results.getViolations().stream()
|
||||||
// Each violation refers to one rule and multiple "nodes" which violate it
|
// Each violation refers to one rule and multiple "nodes" which violate it
|
||||||
.flatMap(violation -> violation.getNodes().stream()
|
.flatMap(violation -> violation.getNodes().stream()
|
||||||
.map(node -> new ViolationFingerprint(
|
.map(node -> new ViolationFingerprint(
|
||||||
violation.getId(),
|
violation.getId(),
|
||||||
// Each node contains a "target", which is a CSS selector that uniquely identifies it
|
// Each node contains a "target", which is a CSS selector that uniquely identifies it
|
||||||
// If the page involves iframes or shadow DOMs, it may be a chain of CSS selectors
|
// If the page involves iframes or shadow DOMs, it may be a chain of CSS selectors
|
||||||
node.getTarget().toString()
|
node.getTarget().toString()
|
||||||
)))
|
)))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -212,11 +208,11 @@ This example fixture creates an `AxeBuilder` object which is pre-configured with
|
||||||
|
|
||||||
```java
|
```java
|
||||||
class AxeTestFixtures extends TestFixtures {
|
class AxeTestFixtures extends TestFixtures {
|
||||||
AxeBuilder makeAxeBuilder() {
|
AxeBuilder makeAxeBuilder() {
|
||||||
return new AxeBuilder(page)
|
return new AxeBuilder(page)
|
||||||
.withTags(new String[]{"wcag2a", "wcag2aa", "wcag21a", "wcag21aa"})
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
.exclude("#commonly-reused-element-with-known-issue");
|
.exclude('#commonly-reused-element-with-known-issue');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -233,12 +229,10 @@ public class HomepageTests extends AxeTestFixtures {
|
||||||
AxeResults accessibilityScanResults = makeAxeBuilder()
|
AxeResults accessibilityScanResults = makeAxeBuilder()
|
||||||
// Automatically uses the shared AxeBuilder configuration,
|
// Automatically uses the shared AxeBuilder configuration,
|
||||||
// but supports additional test-specific configuration too
|
// but supports additional test-specific configuration too
|
||||||
.include("#specific-element-under-test")
|
.include('#specific-element-under-test')
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
|
assertEquals(Collections.emptyList(), accessibilityScanResults.getViolations());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ id: accessibility-testing
|
||||||
title: "Accessibility testing"
|
title: "Accessibility testing"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright can be used to test your application for many types of accessibility issues.
|
Playwright can be used to test your application for many types of accessibility issues.
|
||||||
|
|
||||||
A few examples of problems this can catch include:
|
A few examples of problems this can catch include:
|
||||||
|
|
@ -14,7 +12,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.
|
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.
|
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.
|
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.
|
||||||
|
|
@ -73,9 +71,7 @@ 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()`:
|
`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()`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('navigation menu should not have automatically detectable accessibility violations', async ({
|
test('navigation menu flyout should not have automatically detectable accessibility violations', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('https://your-site.com/');
|
await page.goto('https://your-site.com/');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Navigation Menu' }).click();
|
await page.getByRole('button', { name: 'Navigation Menu' }).click();
|
||||||
|
|
@ -86,8 +82,8 @@ test('navigation menu should not have automatically detectable accessibility vio
|
||||||
await page.locator('#navigation-menu-flyout').waitFor();
|
await page.locator('#navigation-menu-flyout').waitFor();
|
||||||
|
|
||||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||||
.include('#navigation-menu-flyout')
|
.include('#navigation-menu-flyout')
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
expect(accessibilityScanResults.violations).toEqual([]);
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
@ -99,15 +95,15 @@ By default, axe checks against a wide variety of accessibility rules. Some of th
|
||||||
|
|
||||||
You can constrain an accessibility scan to only run those rules which are "tagged" as corresponding to specific WCAG success criteria by using [`AxeBuilder.withTags()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderwithtagstags-stringarray). For example, [Accessibility Insights for Web's Automated Checks](https://accessibilityinsights.io/docs/web/getstarted/fastpass/?referrer=playwright-accessibility-testing-js) only include axe rules that test for violations of WCAG A and AA success criteria; to match that behavior, you would use the tags `wcag2a`, `wcag2aa`, `wcag21a`, and `wcag21aa`.
|
You can constrain an accessibility scan to only run those rules which are "tagged" as corresponding to specific WCAG success criteria by using [`AxeBuilder.withTags()`](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md#axebuilderwithtagstags-stringarray). For example, [Accessibility Insights for Web's Automated Checks](https://accessibilityinsights.io/docs/web/getstarted/fastpass/?referrer=playwright-accessibility-testing-js) only include axe rules that test for violations of WCAG A and AA success criteria; to match that behavior, you would use the tags `wcag2a`, `wcag2aa`, `wcag21a`, and `wcag21aa`.
|
||||||
|
|
||||||
Note that automated testing cannot detect all types of WCAG violations.
|
Note that [automated testing cannot detect all types of WCAG violations](#disclaimer).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('should not have any automatically detectable WCAG A or AA violations', async ({ page }) => {
|
test('should not have any automatically detectable WCAG A or AA violations', async ({ page }) => {
|
||||||
await page.goto('https://your-site.com/');
|
await page.goto('https://your-site.com/');
|
||||||
|
|
||||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
expect(accessibilityScanResults.violations).toEqual([]);
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
@ -130,14 +126,12 @@ This is usually the simplest option, but it has some important downsides:
|
||||||
Here is an example of excluding one element from being scanned in one specific test:
|
Here is an example of excluding one element from being scanned in one specific test:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('should not have any accessibility violations outside of elements with known issues', async ({
|
test('should not have any accessibility violations outside of elements with known issues', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('https://your-site.com/page-with-known-issues');
|
await page.goto('https://your-site.com/page-with-known-issues');
|
||||||
|
|
||||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||||
.exclude('#element-with-known-issue')
|
.exclude('#element-with-known-issue')
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
expect(accessibilityScanResults.violations).toEqual([]);
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
@ -147,19 +141,17 @@ If the element in question is used repeatedly in many pages, consider [using a t
|
||||||
|
|
||||||
### Disabling individual scan rules
|
### 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.
|
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.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('should not have any accessibility violations outside of rules with known issues', async ({
|
test('should not have any accessibility violations outside of rules with known issues', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto('https://your-site.com/page-with-known-issues');
|
await page.goto('https://your-site.com/page-with-known-issues');
|
||||||
|
|
||||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||||
.disableRules(['duplicate-id'])
|
.disableRules(['duplicate-id'])
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
expect(accessibilityScanResults.violations).toEqual([]);
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
@ -167,7 +159,7 @@ test('should not have any accessibility violations outside of rules with known i
|
||||||
|
|
||||||
### Using snapshots to allow specific known issues
|
### 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:
|
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:
|
||||||
|
|
||||||
|
|
@ -184,14 +176,14 @@ expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();
|
||||||
|
|
||||||
// my-test-utils.js
|
// my-test-utils.js
|
||||||
function violationFingerprints(accessibilityScanResults) {
|
function violationFingerprints(accessibilityScanResults) {
|
||||||
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
|
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
|
||||||
rule: violation.id,
|
rule: violation.id,
|
||||||
// These are CSS selectors which uniquely identify each element with
|
// These are CSS selectors which uniquely identify each element with
|
||||||
// a violation of the rule in question.
|
// a violation of the rule in question.
|
||||||
targets: violation.nodes.map(node => node.target),
|
targets: violation.nodes.map(node => node.target),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return JSON.stringify(violationFingerprints, null, 2);
|
return JSON.stringify(violationFingerprints, null, 2);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -231,7 +223,8 @@ The following example demonstrates creating and using a test fixture that covers
|
||||||
|
|
||||||
This example fixture creates an `AxeBuilder` object which is pre-configured with shared `withTags()` and `exclude()` configuration.
|
This example fixture creates an `AxeBuilder` object which is pre-configured with shared `withTags()` and `exclude()` configuration.
|
||||||
|
|
||||||
```js tab=js-ts title="axe-test.ts"
|
```js tab=js-ts
|
||||||
|
// axe-test.ts
|
||||||
import { test as base } from '@playwright/test';
|
import { test as base } from '@playwright/test';
|
||||||
import AxeBuilder from '@axe-core/playwright';
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
|
|
||||||
|
|
@ -246,8 +239,8 @@ type AxeFixture = {
|
||||||
export const test = base.extend<AxeFixture>({
|
export const test = base.extend<AxeFixture>({
|
||||||
makeAxeBuilder: async ({ page }, use, testInfo) => {
|
makeAxeBuilder: async ({ page }, use, testInfo) => {
|
||||||
const makeAxeBuilder = () => new AxeBuilder({ page })
|
const makeAxeBuilder = () => new AxeBuilder({ page })
|
||||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
.exclude('#commonly-reused-element-with-known-issue');
|
.exclude('#commonly-reused-element-with-known-issue');
|
||||||
|
|
||||||
await use(makeAxeBuilder);
|
await use(makeAxeBuilder);
|
||||||
}
|
}
|
||||||
|
|
@ -267,8 +260,8 @@ const AxeBuilder = require('@axe-core/playwright').default;
|
||||||
exports.test = base.test.extend({
|
exports.test = base.test.extend({
|
||||||
makeAxeBuilder: async ({ page }, use, testInfo) => {
|
makeAxeBuilder: async ({ page }, use, testInfo) => {
|
||||||
const makeAxeBuilder = () => new AxeBuilder({ page })
|
const makeAxeBuilder = () => new AxeBuilder({ page })
|
||||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
.exclude('#commonly-reused-element-with-known-issue');
|
.exclude('#commonly-reused-element-with-known-issue');
|
||||||
|
|
||||||
await use(makeAxeBuilder);
|
await use(makeAxeBuilder);
|
||||||
}
|
}
|
||||||
|
|
@ -287,10 +280,10 @@ test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
|
||||||
await page.goto('https://your-site.com/');
|
await page.goto('https://your-site.com/');
|
||||||
|
|
||||||
const accessibilityScanResults = await makeAxeBuilder()
|
const accessibilityScanResults = await makeAxeBuilder()
|
||||||
// Automatically uses the shared AxeBuilder configuration,
|
// Automatically uses the shared AxeBuilder configuration,
|
||||||
// but supports additional test-specific configuration too
|
// but supports additional test-specific configuration too
|
||||||
.include('#specific-element-under-test')
|
.include('#specific-element-under-test')
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
expect(accessibilityScanResults.violations).toEqual([]);
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ id: actionability
|
||||||
title: "Auto-waiting"
|
title: "Auto-waiting"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright performs a range of actionability checks on the elements before making actions to ensure these actions
|
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`.
|
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:
|
For example, for [`method: Page.click`], Playwright will ensure that:
|
||||||
- locator resolves to exactly one element
|
- element is [Attached] to the DOM
|
||||||
- element is [Visible]
|
- element is [Visible]
|
||||||
- element is [Stable], as in not animating or completed animation
|
- element is [Stable], as in not animating or completed animation
|
||||||
- element [Receives Events], as in not obscured by other elements
|
- element [Receives Events], as in not obscured by other elements
|
||||||
|
|
@ -17,75 +15,72 @@ For example, for [`method: Locator.click`], Playwright will ensure that:
|
||||||
|
|
||||||
Here is the complete list of actionability checks performed for each action:
|
Here is the complete list of actionability checks performed for each action:
|
||||||
|
|
||||||
| Action | [Visible] | [Stable] | [Receives Events] | [Enabled] | [Editable] |
|
| Action | [Attached] | [Visible] | [Stable] | [Receives Events] | [Enabled] | [Editable] |
|
||||||
| :- | :-: | :-: | :-: | :-: | :-: |
|
| :- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||||
| [`method: Locator.check`] | Yes | Yes | Yes | Yes | - |
|
| check | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.click`] | Yes | Yes | Yes | Yes | - |
|
| click | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.dblclick`] | Yes | Yes | Yes | Yes | - |
|
| dblclick | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.setChecked`] | Yes | Yes | Yes | Yes | - |
|
| setChecked | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.tap`] | Yes | Yes | Yes | Yes | - |
|
| tap | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.uncheck`] | Yes | Yes | Yes | Yes | - |
|
| uncheck | Yes | Yes | Yes | Yes | Yes | - |
|
||||||
| [`method: Locator.hover`] | Yes | Yes | Yes | - | - |
|
| hover | Yes | Yes | Yes | Yes | - | - |
|
||||||
| [`method: Locator.dragTo`] | Yes | Yes | Yes | - | - |
|
| scrollIntoViewIfNeeded | Yes | - | Yes | - | - | - |
|
||||||
| [`method: Locator.screenshot`] | Yes | Yes | - | - | - |
|
| screenshot | Yes | Yes | Yes | - | - | - |
|
||||||
| [`method: Locator.fill`] | Yes | - | - | Yes | Yes |
|
| fill | Yes | Yes | - | - | Yes | Yes |
|
||||||
| [`method: Locator.clear`] | Yes | - | - | Yes | Yes |
|
| selectText | Yes | Yes | - | - | - | - |
|
||||||
| [`method: Locator.selectOption`] | Yes | - | - | Yes | - |
|
| dispatchEvent | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.selectText`] | Yes | - | - | - | - |
|
| focus | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.scrollIntoViewIfNeeded`] | - | Yes | - | - | - |
|
| getAttribute | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.blur`] | - | - | - | - | - |
|
| innerText | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.dispatchEvent`] | - | - | - | - | - |
|
| innerHTML | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.focus`] | - | - | - | - | - |
|
| press | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.press`] | - | - | - | - | - |
|
| setInputFiles | Yes | - | - | - | - | - |
|
||||||
| [`method: Locator.pressSequentially`] | - | - | - | - | - |
|
| selectOption | Yes | Yes | - | - | Yes | - |
|
||||||
| [`method: Locator.setInputFiles`] | - | - | - | - | - |
|
| textContent | Yes | - | - | - | - | - |
|
||||||
|
| type | Yes | - | - | - | - | - |
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
## Forcing actions
|
## Forcing actions
|
||||||
|
|
||||||
Some actions like [`method: Locator.click`] support `force` option that disables non-essential actionability checks,
|
Some actions like [`method: Page.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
|
for example passing truthy `force` to [`method: Page.click`] method will not check that the target element actually
|
||||||
receives click events.
|
receives click events.
|
||||||
|
|
||||||
## Assertions
|
## 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: ElementHandle.isChecked`]
|
||||||
| :- | :- |
|
- [`method: ElementHandle.isDisabled`]
|
||||||
| [`method: LocatorAssertions.toBeAttached`] | Element is attached |
|
- [`method: ElementHandle.isEditable`]
|
||||||
| [`method: LocatorAssertions.toBeChecked`] | Checkbox is checked |
|
- [`method: ElementHandle.isEnabled`]
|
||||||
| [`method: LocatorAssertions.toBeDisabled`] | Element is disabled |
|
- [`method: ElementHandle.isHidden`]
|
||||||
| [`method: LocatorAssertions.toBeEditable`] | Element is editable |
|
- [`method: ElementHandle.isVisible`]
|
||||||
| [`method: LocatorAssertions.toBeEmpty`] | Container is empty |
|
- [`method: Page.isChecked`]
|
||||||
| [`method: LocatorAssertions.toBeEnabled`] | Element is enabled |
|
- [`method: Page.isDisabled`]
|
||||||
| [`method: LocatorAssertions.toBeFocused`] | Element is focused |
|
- [`method: Page.isEditable`]
|
||||||
| [`method: LocatorAssertions.toBeHidden`] | Element is not visible |
|
- [`method: Page.isEnabled`]
|
||||||
| [`method: LocatorAssertions.toBeInViewport`] | Element intersects viewport |
|
- [`method: Page.isHidden`]
|
||||||
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
|
- [`method: Page.isVisible`]
|
||||||
| [`method: LocatorAssertions.toContainText`] | Element contains text |
|
- [`method: Locator.isChecked`]
|
||||||
| [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute |
|
- [`method: Locator.isDisabled`]
|
||||||
| [`method: LocatorAssertions.toHaveClass`] | Element has a class property |
|
- [`method: Locator.isEditable`]
|
||||||
| [`method: LocatorAssertions.toHaveCount`] | List has exact number of children |
|
- [`method: Locator.isEnabled`]
|
||||||
| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property |
|
- [`method: Locator.isHidden`]
|
||||||
| [`method: LocatorAssertions.toHaveId`] | Element has an ID |
|
- [`method: Locator.isVisible`]
|
||||||
| [`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 |
|
|
||||||
|
|
||||||
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
|
## Visible
|
||||||
|
|
||||||
Element is considered visible when it has non-empty bounding box and does not have `visibility:hidden` computed style.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Stable
|
## Stable
|
||||||
|
|
||||||
|
|
@ -93,27 +88,18 @@ Element is considered stable when it has maintained the same bounding box for at
|
||||||
|
|
||||||
## Enabled
|
## Enabled
|
||||||
|
|
||||||
Element is considered enabled when it is **not disabled**.
|
Element is considered enabled unless it is a `<button>`, `<select>`, `<input>` or `<textarea>` with a `disabled` property.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Editable
|
## Editable
|
||||||
|
|
||||||
Element is considered editable when it is [enabled] and is **not readonly**.
|
Element is considered editable when it is [enabled] and does not have `readonly` property set.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
## Receives Events
|
## 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)`.
|
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;
|
- 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.
|
- after checking with the server, the disabled `Sign Up` button is replaced with another one that is now enabled.
|
||||||
|
|
||||||
|
|
@ -122,3 +108,4 @@ For example, consider a scenario where Playwright will click `Sign Up` button re
|
||||||
[Enabled]: #enabled "Enabled"
|
[Enabled]: #enabled "Enabled"
|
||||||
[Editable]: #editable "Editable"
|
[Editable]: #editable "Editable"
|
||||||
[Receives Events]: #receives-events "Receives Events"
|
[Receives Events]: #receives-events "Receives Events"
|
||||||
|
[Attached]: #attached "Attached"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ id: api-testing
|
||||||
title: "API testing"
|
title: "API testing"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
||||||
your application.
|
your application.
|
||||||
|
|
||||||
|
|
@ -16,7 +14,9 @@ A few examples where it may come in handy:
|
||||||
|
|
||||||
All of that could be achieved via [APIRequestContext] methods.
|
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
|
## Writing API Test
|
||||||
|
|
||||||
|
|
@ -32,19 +32,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.
|
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
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
using Microsoft.Playwright.MSTest;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace PlaywrightTests;
|
namespace PlaywrightTests;
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
public class TestGitHubAPI : PlaywrightTest
|
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()
|
public async Task SetUpAPITesting()
|
||||||
{
|
{
|
||||||
await CreateAPIRequestContext();
|
await CreateAPIRequestContext();
|
||||||
|
|
@ -66,7 +69,7 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TearDown]
|
||||||
public async Task TearDownAPITesting()
|
public async Task TearDownAPITesting()
|
||||||
{
|
{
|
||||||
await Request.DisposeAsync();
|
await Request.DisposeAsync();
|
||||||
|
|
@ -78,34 +81,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.
|
Now that we initialized request object we can add a few tests that will create new issues in the repository.
|
||||||
```csharp
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
using Microsoft.Playwright.MSTest;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace PlaywrightTests;
|
namespace PlaywrightTests;
|
||||||
|
|
||||||
[TestClass]
|
[TestFixture]
|
||||||
public class TestGitHubAPI : PlaywrightTest
|
public class TestGitHubAPI : PlaywrightTest
|
||||||
{
|
{
|
||||||
static string REPO = "test";
|
static string REPO = "test-repo-2";
|
||||||
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
|
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()
|
public async Task ShouldCreateBugReport()
|
||||||
{
|
{
|
||||||
var data = new Dictionary<string, string>
|
var data = new Dictionary<string, string>();
|
||||||
{
|
data.Add("title", "[Bug] report 1");
|
||||||
{ "title", "[Bug] report 1" },
|
data.Add("body", "Bug description");
|
||||||
{ "body", "Bug description" }
|
|
||||||
};
|
|
||||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
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");
|
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||||
await Expect(newIssue).ToBeOKAsync();
|
Assert.True(issues.Ok);
|
||||||
var issuesJsonResponse = await issues.JsonAsync();
|
var issuesJsonResponse = await issues.JsonAsync();
|
||||||
JsonElement? issue = null;
|
JsonElement? issue = null;
|
||||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||||
|
|
@ -118,24 +123,23 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.IsNotNull(issue);
|
Assert.NotNull(issue);
|
||||||
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[Test]
|
||||||
public async Task ShouldCreateFeatureRequests()
|
public async Task ShouldCreateFeatureRequests()
|
||||||
{
|
{
|
||||||
var data = new Dictionary<string, string>
|
var data = new Dictionary<string, string>();
|
||||||
{
|
data.Add("title", "[Feature] request 1");
|
||||||
{ "title", "[Feature] request 1" },
|
data.Add("body", "Feature description");
|
||||||
{ "body", "Feature description" }
|
|
||||||
};
|
|
||||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
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");
|
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||||
await Expect(newIssue).ToBeOKAsync();
|
Assert.True(issues.Ok);
|
||||||
var issuesJsonResponse = await issues.JsonAsync();
|
var issuesJsonResponse = await issues.JsonAsync();
|
||||||
|
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
|
||||||
|
|
||||||
JsonElement? issue = null;
|
JsonElement? issue = null;
|
||||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||||
|
|
@ -148,7 +152,7 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.IsNotNull(issue);
|
Assert.NotNull(issue);
|
||||||
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,48 +165,39 @@ 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.
|
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
|
```csharp
|
||||||
using System.Text.Json;
|
// ...
|
||||||
using Microsoft.Playwright;
|
|
||||||
using Microsoft.Playwright.MSTest;
|
|
||||||
|
|
||||||
namespace PlaywrightTests;
|
[SetUp]
|
||||||
|
public async Task SetUpAPITesting()
|
||||||
|
{
|
||||||
|
await CreateAPIRequestContext();
|
||||||
|
await CreateTestRepository();
|
||||||
|
}
|
||||||
|
|
||||||
[TestClass]
|
private async Task CreateTestRepository()
|
||||||
public class TestGitHubAPI : PlaywrightTest
|
{
|
||||||
{
|
var resp = await Request.PostAsync("/user/repos", new()
|
||||||
// ...
|
{
|
||||||
[TestInitialize]
|
DataObject = new Dictionary<string, string>()
|
||||||
public async Task SetUpAPITesting()
|
{
|
||||||
{
|
["name"] = REPO,
|
||||||
await CreateAPIRequestContext();
|
},
|
||||||
await CreateTestRepository();
|
});
|
||||||
}
|
Assert.True(resp.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CreateTestRepository()
|
[TearDown]
|
||||||
{
|
public async Task TearDownAPITesting()
|
||||||
var resp = await Request.PostAsync("/user/repos", new()
|
{
|
||||||
{
|
await DeleteTestRepository();
|
||||||
DataObject = new Dictionary<string, string>()
|
await Request.DisposeAsync();
|
||||||
{
|
}
|
||||||
["name"] = REPO,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await Expect(resp).ToBeOKAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCleanup]
|
private async Task DeleteTestRepository()
|
||||||
public async Task TearDownAPITesting()
|
{
|
||||||
{
|
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
||||||
await DeleteTestRepository();
|
Assert.True(resp.Ok);
|
||||||
await Request.DisposeAsync();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteTestRepository()
|
|
||||||
{
|
|
||||||
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
|
||||||
await Expect(resp).ToBeOKAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complete test example
|
### Complete test example
|
||||||
|
|
@ -210,34 +205,36 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
Here is the complete example of an API test:
|
Here is the complete example of an API test:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
using Microsoft.Playwright.MSTest;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace PlaywrightTests;
|
namespace PlaywrightTests;
|
||||||
|
|
||||||
[TestClass]
|
[TestFixture]
|
||||||
public class TestGitHubAPI : PlaywrightTest
|
public class TestGitHubAPI : PlaywrightTest
|
||||||
{
|
{
|
||||||
static string REPO = "test-repo-2";
|
static string REPO = "test-repo-2";
|
||||||
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
|
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()
|
public async Task ShouldCreateBugReport()
|
||||||
{
|
{
|
||||||
var data = new Dictionary<string, string>
|
var data = new Dictionary<string, string>();
|
||||||
{
|
data.Add("title", "[Bug] report 1");
|
||||||
{ "title", "[Bug] report 1" },
|
data.Add("body", "Bug description");
|
||||||
{ "body", "Bug description" }
|
|
||||||
};
|
|
||||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
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");
|
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||||
await Expect(newIssue).ToBeOKAsync();
|
Assert.True(issues.Ok);
|
||||||
var issuesJsonResponse = await issues.JsonAsync();
|
var issuesJsonResponse = await issues.JsonAsync();
|
||||||
JsonElement? issue = null;
|
JsonElement? issue = null;
|
||||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||||
|
|
@ -250,24 +247,23 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.IsNotNull(issue);
|
Assert.NotNull(issue);
|
||||||
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[Test]
|
||||||
public async Task ShouldCreateFeatureRequests()
|
public async Task ShouldCreateFeatureRequests()
|
||||||
{
|
{
|
||||||
var data = new Dictionary<string, string>
|
var data = new Dictionary<string, string>();
|
||||||
{
|
data.Add("title", "[Feature] request 1");
|
||||||
{ "title", "[Feature] request 1" },
|
data.Add("body", "Feature description");
|
||||||
{ "body", "Feature description" }
|
|
||||||
};
|
|
||||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
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");
|
var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
|
||||||
await Expect(newIssue).ToBeOKAsync();
|
Assert.True(issues.Ok);
|
||||||
var issuesJsonResponse = await issues.JsonAsync();
|
var issuesJsonResponse = await issues.JsonAsync();
|
||||||
|
var issuesJson = (await issues.JsonAsync())?.EnumerateArray();
|
||||||
|
|
||||||
JsonElement? issue = null;
|
JsonElement? issue = null;
|
||||||
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
|
||||||
|
|
@ -280,11 +276,11 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.IsNotNull(issue);
|
Assert.NotNull(issue);
|
||||||
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestInitialize]
|
[SetUp]
|
||||||
public async Task SetUpAPITesting()
|
public async Task SetUpAPITesting()
|
||||||
{
|
{
|
||||||
await CreateAPIRequestContext();
|
await CreateAPIRequestContext();
|
||||||
|
|
@ -293,16 +289,14 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
|
|
||||||
private async Task CreateAPIRequestContext()
|
private async Task CreateAPIRequestContext()
|
||||||
{
|
{
|
||||||
var headers = new Dictionary<string, string>
|
var headers = new Dictionary<string, string>();
|
||||||
{
|
// We set this header per GitHub guidelines.
|
||||||
// We set this header per GitHub guidelines.
|
headers.Add("Accept", "application/vnd.github.v3+json");
|
||||||
{ "Accept", "application/vnd.github.v3+json" },
|
// Add authorization token to all requests.
|
||||||
// Add authorization token to all requests.
|
// Assuming personal access token available in the environment.
|
||||||
// Assuming personal access token available in the environment.
|
headers.Add("Authorization", "token " + API_TOKEN);
|
||||||
{ "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.
|
// All requests we send go to this API endpoint.
|
||||||
BaseURL = "https://api.github.com",
|
BaseURL = "https://api.github.com",
|
||||||
|
|
@ -319,10 +313,10 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
["name"] = REPO,
|
["name"] = REPO,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await Expect(resp).ToBeOKAsync();
|
Assert.True(resp.Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TearDown]
|
||||||
public async Task TearDownAPITesting()
|
public async Task TearDownAPITesting()
|
||||||
{
|
{
|
||||||
await DeleteTestRepository();
|
await DeleteTestRepository();
|
||||||
|
|
@ -332,7 +326,7 @@ public class TestGitHubAPI : PlaywrightTest
|
||||||
private async Task DeleteTestRepository()
|
private async Task DeleteTestRepository()
|
||||||
{
|
{
|
||||||
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
|
||||||
await Expect(resp).ToBeOKAsync();
|
Assert.True(resp.Ok);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -343,26 +337,21 @@ The following test creates a new issue via API and then navigates to the list of
|
||||||
project to check that it appears at the top of the list. The check is performed using [LocatorAssertions].
|
project to check that it appears at the top of the list. The check is performed using [LocatorAssertions].
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
class TestGitHubAPI : PageTest
|
[Test]
|
||||||
{
|
public async Task LastCreatedIssueShouldBeFirstInTheList()
|
||||||
[TestMethod]
|
{
|
||||||
public async Task LastCreatedIssueShouldBeFirstInTheList()
|
var data = new Dictionary<string, string>();
|
||||||
{
|
data.Add("title", "[Feature] request 1");
|
||||||
var data = new Dictionary<string, string>
|
data.Add("body", "Feature description");
|
||||||
{
|
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
||||||
{ "title", "[Feature] request 1" },
|
Assert.True(newIssue.Ok);
|
||||||
{ "body", "Feature description" }
|
|
||||||
};
|
|
||||||
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
|
|
||||||
await Expect(newIssue).ToBeOKAsync();
|
|
||||||
|
|
||||||
// When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start
|
// 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.
|
// a browser, context, and page manually or inherit from 'PageTest' which will launch it for you.
|
||||||
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
|
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
|
||||||
var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First;
|
var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First;
|
||||||
await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1");
|
await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Check the server state after running user actions
|
## Check the server state after running user actions
|
||||||
|
|
@ -371,24 +360,20 @@ The following test creates a new issue via user interface in the browser and the
|
||||||
it was created:
|
it was created:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Make sure to extend from PageTest if you want to use the Page class.
|
[Test]
|
||||||
class GitHubTests : PageTest
|
public async Task LastCreatedIssueShouldBeOnTheServer()
|
||||||
{
|
{
|
||||||
[TestMethod]
|
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
|
||||||
public async Task LastCreatedIssueShouldBeOnTheServer()
|
await Page.Locator("text=New Issue").ClickAsync();
|
||||||
{
|
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
|
||||||
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
|
await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description");
|
||||||
await Page.Locator("text=New Issue").ClickAsync();
|
await Page.Locator("text=Submit new issue").ClickAsync();
|
||||||
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
|
String issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));
|
||||||
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('/'));
|
|
||||||
|
|
||||||
var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
|
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");
|
StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reuse authentication state
|
## Reuse authentication state
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ id: api-testing
|
||||||
title: "API testing"
|
title: "API testing"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
||||||
your application.
|
your application.
|
||||||
|
|
||||||
|
|
@ -16,6 +14,8 @@ A few examples where it may come in handy:
|
||||||
|
|
||||||
All of that could be achieved via [APIRequestContext] methods.
|
All of that could be achieved via [APIRequestContext] methods.
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
## Writing API Test
|
## Writing API Test
|
||||||
|
|
||||||
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||||
|
|
@ -194,7 +194,6 @@ public class TestGitHubAPI {
|
||||||
These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `@BeforeAll` and `@AfterAll` hooks for that.
|
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
|
```java
|
||||||
public class TestGitHubAPI {
|
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
void createTestRepository() {
|
void createTestRepository() {
|
||||||
|
|
@ -224,7 +223,6 @@ public class TestGitHubAPI {
|
||||||
disposeAPIRequestContext();
|
disposeAPIRequestContext();
|
||||||
closePlaywright();
|
closePlaywright();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complete test example
|
### Complete test example
|
||||||
|
|
@ -375,28 +373,24 @@ public class TestGitHubAPI {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
|
|
||||||
|
|
||||||
## Prepare server state via API calls
|
## 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
|
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].
|
project to check that it appears at the top of the list. The check is performed using [LocatorAssertions].
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class TestGitHubAPI {
|
@Test
|
||||||
@Test
|
void lastCreatedIssueShouldBeFirstInTheList() {
|
||||||
void lastCreatedIssueShouldBeFirstInTheList() {
|
Map<String, String> data = new HashMap<>();
|
||||||
Map<String, String> data = new HashMap<>();
|
data.put("title", "[Feature] request 1");
|
||||||
data.put("title", "[Feature] request 1");
|
data.put("body", "Feature description");
|
||||||
data.put("body", "Feature description");
|
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
|
||||||
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
|
RequestOptions.create().setData(data));
|
||||||
RequestOptions.create().setData(data));
|
assertTrue(newIssue.ok());
|
||||||
assertTrue(newIssue.ok());
|
|
||||||
|
|
||||||
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
|
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
|
||||||
Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
|
Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
|
||||||
assertThat(firstIssue).hasText("[Feature] request 1");
|
assertThat(firstIssue).hasText("[Feature] request 1");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -406,20 +400,18 @@ The following test creates a new issue via user interface in the browser and the
|
||||||
it was created:
|
it was created:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class TestGitHubAPI {
|
@Test
|
||||||
@Test
|
void lastCreatedIssueShouldBeOnTheServer() {
|
||||||
void lastCreatedIssueShouldBeOnTheServer() {
|
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
|
||||||
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
|
page.locator("text=New Issue").click();
|
||||||
page.locator("text=New Issue").click();
|
page.locator("[aria-label='Title']").fill("Bug report 1");
|
||||||
page.locator("[aria-label='Title']").fill("Bug report 1");
|
page.locator("[aria-label='Comment body']").fill("Bug description");
|
||||||
page.locator("[aria-label='Comment body']").fill("Bug description");
|
page.locator("text=Submit new issue").click();
|
||||||
page.locator("text=Submit new issue").click();
|
String issueId = page.url().substring(page.url().lastIndexOf('/'));
|
||||||
String issueId = page.url().substring(page.url().lastIndexOf('/'));
|
|
||||||
|
|
||||||
APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
|
APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
|
||||||
assertThat(newIssue).isOK();
|
assertThat(newIssue).isOK();
|
||||||
assertTrue(newIssue.text().contains("Bug report 1"));
|
assertTrue(newIssue.text().contains("Bug report 1"));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ id: api-testing
|
||||||
title: "API testing"
|
title: "API testing"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of
|
||||||
your application.
|
your application.
|
||||||
|
|
||||||
|
|
@ -18,6 +16,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.
|
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
|
## Writing API Test
|
||||||
|
|
||||||
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||||
|
|
|
||||||
|
|
@ -117,11 +117,11 @@ String snapshot = page.accessibility().snapshot();
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
def find_focused_node(node):
|
def find_focused_node(node):
|
||||||
if node.get("focused"):
|
if (node.get("focused"))
|
||||||
return node
|
return node
|
||||||
for child in (node.get("children") or []):
|
for child in (node.get("children") or []):
|
||||||
found_node = find_focused_node(child)
|
found_node = find_focused_node(child)
|
||||||
if found_node:
|
if (found_node)
|
||||||
return found_node
|
return found_node
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -133,11 +133,11 @@ if node:
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
def find_focused_node(node):
|
def find_focused_node(node):
|
||||||
if node.get("focused"):
|
if (node.get("focused"))
|
||||||
return node
|
return node
|
||||||
for child in (node.get("children") or []):
|
for child in (node.get("children") or []):
|
||||||
found_node = find_focused_node(child)
|
found_node = find_focused_node(child)
|
||||||
if found_node:
|
if (found_node)
|
||||||
return found_node
|
return found_node
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue