Compare commits
31 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3a035560a | ||
|
|
77d9a8e1f6 | ||
|
|
d07e05fa7b | ||
|
|
0a1c1dad67 | ||
|
|
2e4167027b | ||
|
|
5b17ca9d56 | ||
|
|
0037acffc6 | ||
|
|
d22bde13c4 | ||
|
|
e57b4b5073 | ||
|
|
46aeb8fe3d | ||
|
|
d8bc6dbeea | ||
|
|
03501cfdb2 | ||
|
|
241add240c | ||
|
|
92aa600af2 | ||
|
|
c098cafb7a | ||
|
|
aafaa2b9ed | ||
|
|
b9d665caf0 | ||
|
|
1d4521a12e | ||
|
|
7a683a9331 | ||
|
|
ca116db2cb | ||
|
|
8a52c3ca76 | ||
|
|
d73d188ae7 | ||
|
|
73d78f5988 | ||
|
|
55be85284c | ||
|
|
35f921e7aa | ||
|
|
786bb337f0 | ||
|
|
8f1f97f508 | ||
|
|
b651920bd5 | ||
|
|
4959558527 | ||
|
|
eaeb7de95a | ||
|
|
72a767fd4d |
|
|
@ -221,7 +221,7 @@ File path to respond with. The content type will be inferred from file extension
|
||||||
is resolved relative to the current working directory.
|
is resolved relative to the current working directory.
|
||||||
|
|
||||||
### option: Route.fulfill.response
|
### option: Route.fulfill.response
|
||||||
* langs: js, java
|
* langs: js, java, python
|
||||||
- `response` <[APIResponse]>
|
- `response` <[APIResponse]>
|
||||||
|
|
||||||
[APIResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
|
[APIResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
|
||||||
|
|
|
||||||
|
|
@ -138,30 +138,30 @@ you can still opt into stable channels on the bots that are typically free of su
|
||||||
### Prerequisites for .NET
|
### Prerequisites for .NET
|
||||||
* langs: csharp
|
* langs: csharp
|
||||||
|
|
||||||
All examples require the `Microsoft.Playwright.CLI` to be installed. You only have to do this once:
|
To invoke Playwright CLI commands, you need to invoke a PowerShell script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet tool install -g Microsoft.Playwright.CLI
|
pwsh bin\Debug\netX\playwright.ps1 --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Playwright can install supported browsers by means of the CLI tool.
|
Playwright can install supported browsers by means of the CLI tool.
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Running without arguments will install all browsers
|
# Running without arguments will install all browsers
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install specific browsers by providing an argument:
|
You can also install specific browsers by providing an argument:
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Install WebKit
|
# Install WebKit
|
||||||
playwright install webkit
|
pwsh bin\Debug\netX\playwright.ps1 install webkit
|
||||||
```
|
```
|
||||||
|
|
||||||
See all supported browsers:
|
See all supported browsers:
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright install --help
|
pwsh bin\Debug\netX\playwright.ps1 install --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Managing browser binaries
|
## Managing browser binaries
|
||||||
|
|
@ -230,17 +230,17 @@ mvn test
|
||||||
|
|
||||||
```bash bash-flavor=bash lang=csharp
|
```bash bash-flavor=bash lang=csharp
|
||||||
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers
|
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=batch lang=csharp
|
```bash bash-flavor=batch lang=csharp
|
||||||
set PLAYWRIGHT_BROWSERS_PATH=%USERPROFILE%\pw-browsers
|
set PLAYWRIGHT_BROWSERS_PATH=%USERPROFILE%\pw-browsers
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=powershell lang=csharp
|
```bash bash-flavor=powershell lang=csharp
|
||||||
$env:PLAYWRIGHT_BROWSERS_PATH="$env:USERPROFILE\pw-browsers"
|
$env:PLAYWRIGHT_BROWSERS_PATH="$env:USERPROFILE\pw-browsers"
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
When running Playwright scripts, ask it to search for browsers in a shared location.
|
When running Playwright scripts, ask it to search for browsers in a shared location.
|
||||||
|
|
@ -398,17 +398,17 @@ mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=bash lang=csharp
|
```bash bash-flavor=bash lang=csharp
|
||||||
HTTPS_PROXY=https://192.0.2.1 playwright install
|
HTTPS_PROXY=https://192.0.2.1 pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=batch lang=csharp
|
```bash bash-flavor=batch lang=csharp
|
||||||
set HTTPS_PROXY=https://192.0.2.1
|
set HTTPS_PROXY=https://192.0.2.1
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=powershell lang=csharp
|
```bash bash-flavor=powershell lang=csharp
|
||||||
$env:HTTPS_PROXY="https://192.0.2.1"
|
$env:HTTPS_PROXY="https://192.0.2.1"
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Download from artifact repository
|
## Download from artifact repository
|
||||||
|
|
@ -479,17 +479,17 @@ mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=bash lang=csharp
|
```bash bash-flavor=bash lang=csharp
|
||||||
PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1 playwright install
|
PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1 pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=batch lang=csharp
|
```bash bash-flavor=batch lang=csharp
|
||||||
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
|
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=powershell lang=csharp
|
```bash bash-flavor=powershell lang=csharp
|
||||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="192.0.2.1"
|
$env:PLAYWRIGHT_DOWNLOAD_HOST="192.0.2.1"
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
It is also possible to use a per-browser download hosts using `PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST`, `PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST` and `PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST` env variables that
|
It is also possible to use a per-browser download hosts using `PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST`, `PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST` and `PLAYWRIGHT_WEBKIT_DOWNLOAD_HOST` env variables that
|
||||||
|
|
@ -563,19 +563,19 @@ mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=bash lang=csharp
|
```bash bash-flavor=bash lang=csharp
|
||||||
PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=203.0.113.3 PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1 playwright install
|
PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=203.0.113.3 PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1 pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=batch lang=csharp
|
```bash bash-flavor=batch lang=csharp
|
||||||
set PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=203.0.113.3
|
set PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=203.0.113.3
|
||||||
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
|
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=powershell lang=csharp
|
```bash bash-flavor=powershell lang=csharp
|
||||||
$env:PLAYWRIGHT_DOWNLOAD_HOST="192.0.2.1"
|
$env:PLAYWRIGHT_DOWNLOAD_HOST="192.0.2.1"
|
||||||
$env:PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST="203.0.113.3"
|
$env:PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST="203.0.113.3"
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skip browser downloads
|
## Skip browser downloads
|
||||||
|
|
@ -617,17 +617,17 @@ mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=bash lang=csharp
|
```bash bash-flavor=bash lang=csharp
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 playwright install
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=batch lang=csharp
|
```bash bash-flavor=batch lang=csharp
|
||||||
set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash bash-flavor=powershell lang=csharp
|
```bash bash-flavor=powershell lang=csharp
|
||||||
$env:PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
$env:PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Download single browser binary
|
## Download single browser binary
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,8 @@ playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Install the CLI once.
|
|
||||||
dotnet tool install --global Microsoft.Playwright.CLI
|
|
||||||
# Use the tools.
|
# Use the tools.
|
||||||
playwright
|
pwsh bin\Debug\netX\playwright.ps1 --help
|
||||||
```
|
```
|
||||||
|
|
||||||
```json js
|
```json js
|
||||||
|
|
@ -58,7 +56,7 @@ playwright install
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Running without arguments will install default browsers
|
# Running without arguments will install default browsers
|
||||||
playwright install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install specific browsers by providing an argument:
|
You can also install specific browsers by providing an argument:
|
||||||
|
|
@ -80,7 +78,7 @@ playwright install webkit
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Install WebKit
|
# Install WebKit
|
||||||
playwright install webkit
|
pwsh bin\Debug\netX\playwright.ps1 install webkit
|
||||||
```
|
```
|
||||||
|
|
||||||
See all supported browsers:
|
See all supported browsers:
|
||||||
|
|
@ -174,7 +172,7 @@ playwright codegen --load-storage=auth.json my.web.app
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
pwsh bin\Debug\netX\playwright.ps1 open --load-storage=auth.json my.web.app
|
pwsh bin\Debug\netX\playwright.ps1 open --load-storage=auth.json my.web.app
|
||||||
playwright codegen --load-storage=auth.json my.web.app
|
pwsh bin\Debug\netX\playwright.ps1 codegen --load-storage=auth.json my.web.app
|
||||||
# Perform actions in authenticated state.
|
# Perform actions in authenticated state.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -300,7 +298,7 @@ playwright open example.com
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Open page in Chromium
|
# Open page in Chromium
|
||||||
playwright open example.com
|
pwsh bin\Debug\netX\playwright.ps1 open example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash js
|
```bash js
|
||||||
|
|
@ -320,7 +318,7 @@ playwright wk example.com
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Open page in WebKit
|
# Open page in WebKit
|
||||||
playwright wk example.com
|
pwsh bin\Debug\netX\playwright.ps1 wk example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
### Emulate devices
|
### Emulate devices
|
||||||
|
|
@ -343,7 +341,7 @@ playwright open --device="iPhone 11" wikipedia.org
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate iPhone 11.
|
# Emulate iPhone 11.
|
||||||
playwright open --device="iPhone 11" wikipedia.org
|
pwsh bin\Debug\netX\playwright.ps1 open --device="iPhone 11" wikipedia.org
|
||||||
```
|
```
|
||||||
|
|
||||||
### Emulate color scheme and viewport size
|
### Emulate color scheme and viewport size
|
||||||
|
|
@ -365,7 +363,7 @@ playwright open --viewport-size=800,600 --color-scheme=dark twitter.com
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate screen size and color scheme.
|
# Emulate screen size and color scheme.
|
||||||
playwright open --viewport-size=800,600 --color-scheme=dark twitter.com
|
pwsh bin\Debug\netX\playwright.ps1 open --viewport-size=800,600 --color-scheme=dark twitter.com
|
||||||
```
|
```
|
||||||
|
|
||||||
### Emulate geolocation, language and timezone
|
### Emulate geolocation, language and timezone
|
||||||
|
|
@ -391,7 +389,7 @@ playwright open --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --l
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate timezone, language & location
|
# Emulate timezone, language & location
|
||||||
# Once page opens, click the "my location" button to see geolocation in action
|
# Once page opens, click the "my location" button to see geolocation in action
|
||||||
playwright open --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" maps.google.com
|
pwsh bin\Debug\netX\playwright.ps1 open --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" maps.google.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inspect selectors
|
## Inspect selectors
|
||||||
|
|
@ -491,7 +489,7 @@ playwright screenshot \
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Wait 3 seconds before capturing a screenshot after page loads ('load' event fires)
|
# Wait 3 seconds before capturing a screenshot after page loads ('load' event fires)
|
||||||
playwright screenshot \
|
pwsh bin\Debug\netX\playwright.ps1 screenshot \
|
||||||
--device="iPhone 11" \
|
--device="iPhone 11" \
|
||||||
--color-scheme=dark \
|
--color-scheme=dark \
|
||||||
--wait-for-timeout=3000 \
|
--wait-for-timeout=3000 \
|
||||||
|
|
@ -515,7 +513,7 @@ playwright screenshot --full-page en.wikipedia.org wiki-full.png
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Capture a full page screenshot
|
# Capture a full page screenshot
|
||||||
playwright screenshot --full-page en.wikipedia.org wiki-full.png
|
pwsh bin\Debug\netX\playwright.ps1 screenshot --full-page en.wikipedia.org wiki-full.png
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate PDF
|
## Generate PDF
|
||||||
|
|
@ -539,7 +537,7 @@ playwright pdf https://en.wikipedia.org/wiki/PDF wiki.pdf
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# See command help
|
# See command help
|
||||||
playwright pdf https://en.wikipedia.org/wiki/PDF wiki.pdf
|
pwsh bin\Debug\netX\playwright.ps1 pdf https://en.wikipedia.org/wiki/PDF wiki.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install system dependencies
|
## Install system dependencies
|
||||||
|
|
@ -563,7 +561,7 @@ playwright install-deps
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# See command help
|
# See command help
|
||||||
playwright install-deps
|
pwsh bin\Debug\netX\playwright.ps1 install-deps
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install the dependencies for a single browser only by passing it as an argument:
|
You can also install the dependencies for a single browser only by passing it as an argument:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ playwright codegen wikipedia.org
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright codegen wikipedia.org
|
pwsh bin\Debug\netX\playwright.ps1 codegen wikipedia.org
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `codegen` will attempt to generate resilient text-based selectors.
|
Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `codegen` will attempt to generate resilient text-based selectors.
|
||||||
|
|
@ -52,7 +52,7 @@ playwright codegen --save-storage=auth.json
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright codegen --save-storage=auth.json
|
pwsh bin\Debug\netX\playwright.ps1 codegen --save-storage=auth.json
|
||||||
# Perform authentication and exit.
|
# Perform authentication and exit.
|
||||||
# auth.json will contain the storage state.
|
# auth.json will contain the storage state.
|
||||||
```
|
```
|
||||||
|
|
@ -79,8 +79,8 @@ playwright codegen --load-storage=auth.json my.web.app
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright open --load-storage=auth.json my.web.app
|
pwsh bin\Debug\netX\playwright.ps1 open --load-storage=auth.json my.web.app
|
||||||
playwright codegen --load-storage=auth.json my.web.app
|
pwsh bin\Debug\netX\playwright.ps1 codegen --load-storage=auth.json my.web.app
|
||||||
# Perform actions in authenticated state.
|
# Perform actions in authenticated state.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -206,7 +206,7 @@ playwright codegen --device="iPhone 11" wikipedia.org
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate iPhone 11.
|
# Emulate iPhone 11.
|
||||||
playwright codegen --device="iPhone 11" wikipedia.org
|
pwsh bin\Debug\netX\playwright.ps1 codegen --device="iPhone 11" wikipedia.org
|
||||||
```
|
```
|
||||||
|
|
||||||
## Emulate color scheme and viewport size
|
## Emulate color scheme and viewport size
|
||||||
|
|
@ -230,7 +230,7 @@ playwright codegen --viewport-size=800,600 --color-scheme=dark twitter.com
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate screen size and color scheme.
|
# Emulate screen size and color scheme.
|
||||||
playwright codegen --viewport-size=800,600 --color-scheme=dark twitter.com
|
pwsh bin\Debug\netX\playwright.ps1 codegen --viewport-size=800,600 --color-scheme=dark twitter.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## Emulate geolocation, language and timezone
|
## Emulate geolocation, language and timezone
|
||||||
|
|
@ -256,5 +256,5 @@ playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348"
|
||||||
```bash csharp
|
```bash csharp
|
||||||
# Emulate timezone, language & location
|
# Emulate timezone, language & location
|
||||||
# Once page opens, click the "my location" button to see geolocation in action
|
# Once page opens, click the "my location" button to see geolocation in action
|
||||||
playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" maps.google.com
|
pwsh bin\Debug\netX\playwright.ps1 codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --lang="it-IT" maps.google.com
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,20 @@ configures Playwright for debugging and opens the inspector.
|
||||||
pytest -s
|
pytest -s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash bash-flavor=bash lang=csharp
|
||||||
|
PWDEBUG=1 dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash bash-flavor=batch lang=csharp
|
||||||
|
set PWDEBUG=1
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash bash-flavor=powershell lang=csharp
|
||||||
|
$env:PWDEBUG=1
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
Additional useful defaults are configured when `PWDEBUG=1` is set:
|
Additional useful defaults are configured when `PWDEBUG=1` is set:
|
||||||
- Browsers launch in the headed mode
|
- Browsers launch in the headed mode
|
||||||
- Default timeout is set to 0 (= no timeout)
|
- Default timeout is set to 0 (= no timeout)
|
||||||
|
|
@ -105,6 +119,10 @@ configures Playwright for debugging and opens the inspector.
|
||||||
playwright codegen wikipedia.org
|
playwright codegen wikipedia.org
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash csharp
|
||||||
|
pwsh bin\Debug\netX\playwright.ps1 codegen wikipedia.org
|
||||||
|
```
|
||||||
|
|
||||||
## Stepping through the Playwright script
|
## Stepping through the Playwright script
|
||||||
|
|
||||||
When `PWDEBUG=1` is set, Playwright Inspector window will be opened and the script will be
|
When `PWDEBUG=1` is set, Playwright Inspector window will be opened and the script will be
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ cd PlaywrightDemo
|
||||||
dotnet add package Microsoft.Playwright
|
dotnet add package Microsoft.Playwright
|
||||||
# Build the project
|
# Build the project
|
||||||
dotnet build
|
dotnet build
|
||||||
# Install required browsers
|
# Install required browsers - replace netX with actual output folder name, f.ex. net6.0.
|
||||||
pwsh bin\Debug\netX\playwright.ps1 install
|
pwsh bin\Debug\netX\playwright.ps1 install
|
||||||
|
|
||||||
# If the pwsh command does not work (throws TypeNotFound), make sure to use an up-to-date version of PowerShell.
|
# If the pwsh command does not work (throws TypeNotFound), make sure to use an up-to-date version of PowerShell.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,34 @@ title: "Release notes"
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
|
## Version 1.19
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Locator now supports a `has` option that makes sure it contains another locator inside:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await Page.Locator("article", new () { Has = Page.Locator(".highlight") }).ClickAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [locator documentation](./api/class-locator#locator-locator-option-has)
|
||||||
|
|
||||||
|
- New [`method: Locator.page`]
|
||||||
|
- [`method: Page.screenshot`] and [`method: Locator.screenshot`] now automatically hide blinking caret
|
||||||
|
- Playwright Codegen now generates locators and frame locators
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 100.0.4863.0
|
||||||
|
- Mozilla Firefox 96.0.1
|
||||||
|
- WebKit 15.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 98
|
||||||
|
- Microsoft Edge 98
|
||||||
|
|
||||||
|
|
||||||
## Version 1.18
|
## Version 1.18
|
||||||
|
|
||||||
### Locator Improvements
|
### Locator Improvements
|
||||||
|
|
@ -78,7 +106,7 @@ Playwright Trace Viewer is now **available online** at https://trace.playwright.
|
||||||
- Playwright now supports **Ubuntu 20.04 ARM64**. You can now run Playwright tests inside Docker on Apple M1 and on Raspberry Pi.
|
- Playwright now supports **Ubuntu 20.04 ARM64**. You can now run Playwright tests inside Docker on Apple M1 and on Raspberry Pi.
|
||||||
- You can now use Playwright to install stable version of Edge on Linux:
|
- You can now use Playwright to install stable version of Edge on Linux:
|
||||||
```bash
|
```bash
|
||||||
npx playwright install msedge
|
pwsh bin\Debug\netX\playwright.ps1 install msedge
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -105,7 +133,7 @@ Read more about [`method: Locator.waitFor`].
|
||||||
|
|
||||||
### 🎭 Playwright Trace Viewer
|
### 🎭 Playwright Trace Viewer
|
||||||
|
|
||||||
- run trace viewer with `npx playwright show-trace` and drop trace files to the trace viewer PWA
|
- run trace viewer with `pwsh bin\Debug\netX\playwright.ps1 show-trace` and drop trace files to the trace viewer PWA
|
||||||
- better visual attribution of action targets
|
- better visual attribution of action targets
|
||||||
|
|
||||||
Read more about [Trace Viewer](./trace-viewer).
|
Read more about [Trace Viewer](./trace-viewer).
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,33 @@ title: "Release notes"
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
|
## Version 1.19
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Locator now supports a `has` option that makes sure it contains another locator inside:
|
||||||
|
|
||||||
|
```java
|
||||||
|
page.locator("article", new Page.LocatorOptions().setHas(page.locator(".highlight"))).click();
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [locator documentation](./api/class-locator#locator-locator-option-has)
|
||||||
|
|
||||||
|
- New [`method: Locator.page`]
|
||||||
|
- [`method: Page.screenshot`] and [`method: Locator.screenshot`] now automatically hide blinking caret
|
||||||
|
- Playwright Codegen now generates locators and frame locators
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 100.0.4863.0
|
||||||
|
- Mozilla Firefox 96.0.1
|
||||||
|
- WebKit 15.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 98
|
||||||
|
- Microsoft Edge 98
|
||||||
|
|
||||||
## Version 1.18
|
## Version 1.18
|
||||||
|
|
||||||
### API Testing
|
### API Testing
|
||||||
|
|
@ -131,7 +158,7 @@ Playwright Trace Viewer is now **available online** at https://trace.playwright.
|
||||||
- Playwright now supports **Ubuntu 20.04 ARM64**. You can now run Playwright tests inside Docker on Apple M1 and on Raspberry Pi.
|
- Playwright now supports **Ubuntu 20.04 ARM64**. You can now run Playwright tests inside Docker on Apple M1 and on Raspberry Pi.
|
||||||
- You can now use Playwright to install stable version of Edge on Linux:
|
- You can now use Playwright to install stable version of Edge on Linux:
|
||||||
```bash
|
```bash
|
||||||
npx playwright install msedge
|
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install msedge"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -158,7 +185,7 @@ Read more about [`method: Locator.waitFor`].
|
||||||
|
|
||||||
### 🎭 Playwright Trace Viewer
|
### 🎭 Playwright Trace Viewer
|
||||||
|
|
||||||
- run trace viewer with `npx playwright show-trace` and drop trace files to the trace viewer PWA
|
- run trace viewer with `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="show-trace"` and drop trace files to the trace viewer PWA
|
||||||
- better visual attribution of action targets
|
- better visual attribution of action targets
|
||||||
|
|
||||||
Read more about [Trace Viewer](./trace-viewer).
|
Read more about [Trace Viewer](./trace-viewer).
|
||||||
|
|
@ -447,10 +474,7 @@ This version of Playwright was also tested against the following stable channels
|
||||||
- [Selecting elements based on layout](./selectors.md#selecting-elements-based-on-layout) with `:left-of()`, `:right-of()`, `:above()` and `:below()`.
|
- [Selecting elements based on layout](./selectors.md#selecting-elements-based-on-layout) with `:left-of()`, `:right-of()`, `:above()` and `:below()`.
|
||||||
- Playwright now includes [command line interface](./cli.md), former playwright-cli.
|
- Playwright now includes [command line interface](./cli.md), former playwright-cli.
|
||||||
```bash js
|
```bash js
|
||||||
npx playwright --help
|
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="--help"
|
||||||
```
|
|
||||||
```bash python
|
|
||||||
playwright --help
|
|
||||||
```
|
```
|
||||||
- [`method: Page.selectOption`] now waits for the options to be present.
|
- [`method: Page.selectOption`] now waits for the options to be present.
|
||||||
- New methods to [assert element state](./actionability#assertions) like [`method: Page.isEditable`].
|
- New methods to [assert element state](./actionability#assertions) like [`method: Page.isEditable`].
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,90 @@ title: "Release notes"
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
|
## Version 1.19
|
||||||
|
|
||||||
|
### Playwright Test Update
|
||||||
|
|
||||||
|
- Playwright Test v1.19 now supports *soft assertions*. Failed soft assertions
|
||||||
|
**do not** terminate test execution, but mark the test as failed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Make a few checks that will not stop the test when failed...
|
||||||
|
await expect.soft(page.locator('#status')).toHaveText('Success');
|
||||||
|
await expect.soft(page.locator('#eta')).toHaveText('1 day');
|
||||||
|
|
||||||
|
// ... and continue the test to check more things.
|
||||||
|
await page.locator('#next-page').click();
|
||||||
|
await expect.soft(page.locator('#title')).toHaveText('Make another order');
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [our documentation](./test-assertions#soft-assertions)
|
||||||
|
|
||||||
|
- You can now specify a **custom error message** as a second argument to the `expect` and `expect.soft` functions, for example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
await expect(page.locator('text=Name'), 'should be logged in').toBeVisible();
|
||||||
|
```
|
||||||
|
|
||||||
|
The error would look like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Error: should be logged in
|
||||||
|
|
||||||
|
Call log:
|
||||||
|
- expect.toBeVisible with timeout 5000ms
|
||||||
|
- waiting for selector "text=Name"
|
||||||
|
|
||||||
|
|
||||||
|
2 |
|
||||||
|
3 | test('example test', async({ page }) => {
|
||||||
|
> 4 | await expect(page.locator('text=Name'), 'should be logged in').toBeVisible();
|
||||||
|
| ^
|
||||||
|
5 | });
|
||||||
|
6 |
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [our documentation](./test-assertions#custom-error-message)
|
||||||
|
- By default, tests in a single file are run in order. If you have many independent tests in a single file, you can now
|
||||||
|
run them in parallel with [`method: Test.describe.configure`].
|
||||||
|
|
||||||
|
### Other Updates
|
||||||
|
|
||||||
|
- Locator now supports a `has` option that makes sure it contains another locator inside:
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.locator('article', {
|
||||||
|
has: page.locator('.highlight'),
|
||||||
|
}).click();
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [locator documentation](./api/class-locator#locator-locator-option-has)
|
||||||
|
|
||||||
|
- New [`method: Locator.page`]
|
||||||
|
- [`method: Page.screenshot`] and [`method: Locator.screenshot`] now automatically hide blinking caret
|
||||||
|
- Playwright Codegen now generates locators and frame locators
|
||||||
|
- New option `url` in [`property: TestConfig.webServer`] to ensure your web server is ready before running the tests
|
||||||
|
- New [`property: TestInfo.errors`] and [`property: TestResult.errors`] that contain all failed assertions and soft assertions.
|
||||||
|
|
||||||
|
|
||||||
|
### Potentially breaking change in Playwright Test Global Setup
|
||||||
|
|
||||||
|
It is unlikely that this change will affect you, no action is required if your tests keep running as they did.
|
||||||
|
|
||||||
|
We've noticed that in rare cases, the set of tests to be executed was configured in the global setup by means of the environment variables. We also noticed some applications that were post processing the reporters' output in the global teardown. If you are doing one of the two, [learn more](https://github.com/microsoft/playwright/issues/12018)
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 100.0.4863.0
|
||||||
|
- Mozilla Firefox 96.0.1
|
||||||
|
- WebKit 15.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 98
|
||||||
|
- Microsoft Edge 98
|
||||||
|
|
||||||
|
|
||||||
## Version 1.18
|
## Version 1.18
|
||||||
|
|
||||||
### Locator Improvements
|
### Locator Improvements
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,38 @@ title: "Release notes"
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
|
## Version 1.19
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Locator now supports a `has` option that makes sure it contains another locator inside:
|
||||||
|
|
||||||
|
```python async
|
||||||
|
await page.locator("article", has=page.locator(".highlight")).click()
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
page.locator("article", has=page.locator(".highlight")).click()
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [locator documentation](./api/class-locator#locator-locator-option-has)
|
||||||
|
|
||||||
|
- New [`method: Locator.page`]
|
||||||
|
- [`method: Page.screenshot`] and [`method: Locator.screenshot`] now automatically hide blinking caret
|
||||||
|
- Playwright Codegen now generates locators and frame locators
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 100.0.4863.0
|
||||||
|
- Mozilla Firefox 96.0.1
|
||||||
|
- WebKit 15.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 98
|
||||||
|
- Microsoft Edge 98
|
||||||
|
|
||||||
|
|
||||||
## Version 1.18
|
## Version 1.18
|
||||||
|
|
||||||
### API Testing
|
### API Testing
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ playwright show-trace trace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright show-trace trace.zip
|
pwsh bin\Debug\netX\playwright.ps1 show-trace trace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
## Actions
|
## Actions
|
||||||
|
|
@ -218,5 +218,5 @@ playwright show-trace https://example.com/trace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
playwright show-trace https://example.com/trace.zip
|
pwsh bin\Debug\netX\playwright.ps1 show-trace https://example.com/trace.zip
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -561,11 +561,6 @@ function test_playwright_cli_codegen_should_work {
|
||||||
echo "ERROR: missing @playwright/test in the output"
|
echo "ERROR: missing @playwright/test in the output"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "${OUTPUT}" != *"page.close"* ]]; then
|
|
||||||
echo "ERROR: missing page.close in the output"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Running playwright codegen --target=python"
|
echo "Running playwright codegen --target=python"
|
||||||
OUTPUT=$(PWTEST_CLI_EXIT=1 xvfb-run --auto-servernum -- bash -c "npx playwright codegen --target=python")
|
OUTPUT=$(PWTEST_CLI_EXIT=1 xvfb-run --auto-servernum -- bash -c "npx playwright codegen --target=python")
|
||||||
if [[ "${OUTPUT}" != *"chromium.launch"* ]]; then
|
if [[ "${OUTPUT}" != *"chromium.launch"* ]]; then
|
||||||
|
|
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -7515,11 +7515,11 @@
|
||||||
},
|
},
|
||||||
"packages/html-reporter": {},
|
"packages/html-reporter": {},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7529,11 +7529,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7543,7 +7543,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "8.3.0",
|
"commander": "8.3.0",
|
||||||
|
|
@ -7579,11 +7579,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7594,7 +7594,7 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "7.16.7",
|
"@babel/code-frame": "7.16.7",
|
||||||
|
|
@ -7629,7 +7629,7 @@
|
||||||
"open": "8.4.0",
|
"open": "8.4.0",
|
||||||
"pirates": "4.0.4",
|
"pirates": "4.0.4",
|
||||||
"pixelmatch": "5.2.1",
|
"pixelmatch": "5.2.1",
|
||||||
"playwright-core": "1.19.0-next",
|
"playwright-core": "1.19.2",
|
||||||
"pngjs": "6.0.0",
|
"pngjs": "6.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"source-map-support": "0.4.18",
|
"source-map-support": "0.4.18",
|
||||||
|
|
@ -7673,11 +7673,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -8446,7 +8446,7 @@
|
||||||
"open": "8.4.0",
|
"open": "8.4.0",
|
||||||
"pirates": "4.0.4",
|
"pirates": "4.0.4",
|
||||||
"pixelmatch": "5.2.1",
|
"pixelmatch": "5.2.1",
|
||||||
"playwright-core": "1.19.0-next",
|
"playwright-core": "1.19.2",
|
||||||
"pngjs": "6.0.0",
|
"pngjs": "6.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"source-map-support": "0.4.18",
|
"source-map-support": "0.4.18",
|
||||||
|
|
@ -12120,13 +12120,13 @@
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"version": "file:packages/playwright",
|
"version": "file:packages/playwright",
|
||||||
"requires": {
|
"requires": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playwright-chromium": {
|
"playwright-chromium": {
|
||||||
"version": "file:packages/playwright-chromium",
|
"version": "file:packages/playwright-chromium",
|
||||||
"requires": {
|
"requires": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playwright-core": {
|
"playwright-core": {
|
||||||
|
|
@ -12160,13 +12160,13 @@
|
||||||
"playwright-firefox": {
|
"playwright-firefox": {
|
||||||
"version": "file:packages/playwright-firefox",
|
"version": "file:packages/playwright-firefox",
|
||||||
"requires": {
|
"requires": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playwright-webkit": {
|
"playwright-webkit": {
|
||||||
"version": "file:packages/playwright-webkit",
|
"version": "file:packages/playwright-webkit",
|
||||||
"requires": {
|
"requires": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pngjs": {
|
"pngjs": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,11 @@ export const ProjectLink: React.FunctionComponent<{
|
||||||
export const AttachmentLink: React.FunctionComponent<{
|
export const AttachmentLink: React.FunctionComponent<{
|
||||||
attachment: TestAttachment,
|
attachment: TestAttachment,
|
||||||
href?: string,
|
href?: string,
|
||||||
}> = ({ attachment, href }) => {
|
linkName?: string,
|
||||||
|
}> = ({ attachment, href, linkName }) => {
|
||||||
return <TreeItem title={<span>
|
return <TreeItem title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} target='_blank'>{attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} target='_blank'>{linkName || attachment.name}</a>}
|
||||||
{attachment.body && <span>{attachment.name}</span>}
|
{attachment.body && <span>{attachment.name}</span>}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
return [<div className='attachment-body'>{attachment.body}</div>];
|
return [<div className='attachment-body'>{attachment.body}</div>];
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,12 @@ export const TestResultView: React.FC<{
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
||||||
{!!traces.length && <AutoChip header='Traces'>
|
{!!traces.length && <AutoChip header='Traces'>
|
||||||
{traces.map((a, i) => <div key={`trace-${i}`}>
|
{<div>
|
||||||
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
|
<a href={`trace/index.html?${traces.map((a, i) => `trace=${new URL(a.path!, window.location.href)}`).join('&')}`}>
|
||||||
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||||
</a>
|
</a>
|
||||||
<AttachmentLink attachment={a}></AttachmentLink>
|
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
|
||||||
</div>)}
|
</div>}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
||||||
{!!videos.length && <AutoChip header='Videos'>
|
{!!videos.length && <AutoChip header='Videos'>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate Chromium",
|
"description": "A high-level API to automate Chromium",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
@ -26,6 +26,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ set -x
|
||||||
|
|
||||||
rm -rf "/Applications/Google Chrome Beta.app"
|
rm -rf "/Applications/Google Chrome Beta.app"
|
||||||
cd /tmp
|
cd /tmp
|
||||||
curl -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/beta/googlechromebeta.dmg
|
curl -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg
|
||||||
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg
|
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg
|
||||||
cp -rf "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
|
cp -rf "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
|
||||||
hdiutil detach /Volumes/googlechromebeta.dmg
|
hdiutil detach /Volumes/googlechromebeta.dmg
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ set -x
|
||||||
|
|
||||||
rm -rf "/Applications/Google Chrome.app"
|
rm -rf "/Applications/Google Chrome.app"
|
||||||
cd /tmp
|
cd /tmp
|
||||||
curl -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg
|
curl -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg
|
||||||
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg
|
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg
|
||||||
cp -rf "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
|
cp -rf "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
|
||||||
hdiutil detach /Volumes/googlechrome.dmg
|
hdiutil detach /Volumes/googlechrome.dmg
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,13 @@ import { fork } from 'child_process';
|
||||||
|
|
||||||
if (process.env.PW_EXPERIMENTAL_TS_ESM) {
|
if (process.env.PW_EXPERIMENTAL_TS_ESM) {
|
||||||
const NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ` --experimental-loader=${require.resolve('@playwright/test/lib/experimentalLoader')}`;
|
const NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ` --experimental-loader=${require.resolve('@playwright/test/lib/experimentalLoader')}`;
|
||||||
fork(require.resolve('./innerCli'), process.argv.slice(2), {
|
const innerProcess = fork(require.resolve('./innerCli'), process.argv.slice(2), {
|
||||||
env: { ...process.env, NODE_OPTIONS }
|
env: { ...process.env, NODE_OPTIONS }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
innerProcess.on('close', code => {
|
||||||
|
if (code !== 0 && code !== null) process.exit(code);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
require('./innerCli');
|
require('./innerCli');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -611,7 +611,7 @@ function buildBasePlaywrightCLICommand(cliTargetLang: string | undefined): strin
|
||||||
case 'java':
|
case 'java':
|
||||||
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="...options.."`;
|
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="...options.."`;
|
||||||
case 'csharp':
|
case 'csharp':
|
||||||
return `playwright`;
|
return `pwsh bin\\Debug\\netX\\playwright.ps1`;
|
||||||
default:
|
default:
|
||||||
return `npx playwright`;
|
return `npx playwright`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,14 @@ export class APIRequest implements api.APIRequest {
|
||||||
})).request);
|
})).request);
|
||||||
context._tracing._localUtils = this._playwright._utils;
|
context._tracing._localUtils = this._playwright._utils;
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
|
context._request = this;
|
||||||
await this._onDidCreateContext?.(context);
|
await this._onDidCreateContext?.(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
|
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
|
||||||
private _request?: APIRequest;
|
_request?: APIRequest;
|
||||||
readonly _tracing: Tracing;
|
readonly _tracing: Tracing;
|
||||||
|
|
||||||
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
|
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
|
||||||
|
|
@ -89,8 +90,6 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
||||||
super(parent, type, guid, initializer, createInstrumentation());
|
super(parent, type, guid, initializer, createInstrumentation());
|
||||||
if (parent instanceof APIRequest)
|
|
||||||
this._request = parent;
|
|
||||||
this._tracing = Tracing.from(initializer.tracing);
|
this._tracing = Tracing.from(initializer.tracing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,15 @@ export class Electron extends SdkObject {
|
||||||
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
||||||
|
|
||||||
const browserLogsCollector = new RecentLogsCollector();
|
const browserLogsCollector = new RecentLogsCollector();
|
||||||
|
const env = options.env ? envArrayToObject(options.env) : process.env;
|
||||||
|
// When debugging Playwright test that runs Electron, NODE_OPTIONS
|
||||||
|
// will make the debugger attach to Electron's Node. But Playwright
|
||||||
|
// also needs to attach to drive the automation. Disable external debugging.
|
||||||
|
delete env.NODE_OPTIONS;
|
||||||
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
|
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
|
||||||
command: options.executablePath || require('electron/index.js'),
|
command: options.executablePath || require('electron/index.js'),
|
||||||
args: electronArguments,
|
args: electronArguments,
|
||||||
env: options.env ? envArrayToObject(options.env) : process.env,
|
env,
|
||||||
log: (message: string) => {
|
log: (message: string) => {
|
||||||
progress.log(message);
|
progress.log(message);
|
||||||
browserLogsCollector.log(message);
|
browserLogsCollector.log(message);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import * as http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import { pipeline, Readable, Transform } from 'stream';
|
import { pipeline, Readable, Transform, TransformCallback } from 'stream';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
import { HTTPCredentials } from '../../types/types';
|
import { HTTPCredentials } from '../../types/types';
|
||||||
|
|
@ -341,13 +341,6 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// These requests don't have response body.
|
|
||||||
if (['HEAD', 'PUT', 'TRACE'].includes(options.method!)) {
|
|
||||||
notifyBodyFinished();
|
|
||||||
request.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: Readable = response;
|
let body: Readable = response;
|
||||||
let transform: Transform | undefined;
|
let transform: Transform | undefined;
|
||||||
const encoding = response.headers['content-encoding'];
|
const encoding = response.headers['content-encoding'];
|
||||||
|
|
@ -362,7 +355,9 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
transform = zlib.createInflate();
|
transform = zlib.createInflate();
|
||||||
}
|
}
|
||||||
if (transform) {
|
if (transform) {
|
||||||
body = pipeline(response, transform, e => {
|
// Brotli and deflate decompressors throw if the input stream is empty.
|
||||||
|
const emptyStreamTransform = new SafeEmptyStreamTransform(notifyBodyFinished);
|
||||||
|
body = pipeline(response, emptyStreamTransform, transform, e => {
|
||||||
if (e)
|
if (e)
|
||||||
reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`));
|
reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`));
|
||||||
});
|
});
|
||||||
|
|
@ -407,6 +402,26 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SafeEmptyStreamTransform extends Transform {
|
||||||
|
private _receivedSomeData: boolean = false;
|
||||||
|
private _onEmptyStreamCallback: () => void;
|
||||||
|
|
||||||
|
constructor(onEmptyStreamCallback: () => void) {
|
||||||
|
super();
|
||||||
|
this._onEmptyStreamCallback = onEmptyStreamCallback;
|
||||||
|
}
|
||||||
|
override _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
|
||||||
|
this._receivedSomeData = true;
|
||||||
|
callback(null, chunk);
|
||||||
|
}
|
||||||
|
override _flush(callback: TransformCallback): void {
|
||||||
|
if (this._receivedSomeData)
|
||||||
|
callback(null);
|
||||||
|
else
|
||||||
|
this._onEmptyStreamCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BrowserContextAPIRequestContext extends APIRequestContext {
|
export class BrowserContextAPIRequestContext extends APIRequestContext {
|
||||||
private readonly _context: BrowserContext;
|
private readonly _context: BrowserContext;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,15 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
|
|
||||||
generateAction(actionInContext: ActionInContext): string {
|
generateAction(actionInContext: ActionInContext): string {
|
||||||
const action = actionInContext.action;
|
const action = actionInContext.action;
|
||||||
|
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
|
||||||
|
return '';
|
||||||
|
|
||||||
const pageAlias = actionInContext.frame.pageAlias;
|
const pageAlias = actionInContext.frame.pageAlias;
|
||||||
const formatter = new JavaScriptFormatter(2);
|
const formatter = new JavaScriptFormatter(2);
|
||||||
formatter.newLine();
|
formatter.newLine();
|
||||||
formatter.add('// ' + actionTitle(action));
|
formatter.add('// ' + actionTitle(action));
|
||||||
|
|
||||||
if (action.name === 'openPage') {
|
if (action.name === 'openPage') {
|
||||||
if (this._isTest)
|
|
||||||
return '';
|
|
||||||
formatter.add(`const ${pageAlias} = await context.newPage();`);
|
formatter.add(`const ${pageAlias} = await context.newPage();`);
|
||||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
||||||
formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
|
formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,18 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecorderApp extends EventEmitter {
|
export interface IRecorderApp extends EventEmitter {
|
||||||
|
close(): Promise<void>;
|
||||||
|
setPaused(paused: boolean): Promise<void>;
|
||||||
|
setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void>;
|
||||||
|
setFile(file: string): Promise<void>;
|
||||||
|
setSelector(selector: string, focus?: boolean): Promise<void>;
|
||||||
|
updateCallLogs(callLogs: CallLog[]): Promise<void>;
|
||||||
|
bringToFront(): void;
|
||||||
|
setSources(sources: Source[]): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
readonly wsEndpoint: string | undefined;
|
readonly wsEndpoint: string | undefined;
|
||||||
|
|
||||||
|
|
@ -85,7 +96,9 @@ export class RecorderApp extends EventEmitter {
|
||||||
await mainFrame.goto(internalCallMetadata(), 'https://playwright/index.html');
|
await mainFrame.goto(internalCallMetadata(), 'https://playwright/index.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async open(sdkLanguage: string, headed: boolean): Promise<RecorderApp> {
|
static async open(sdkLanguage: string, headed: boolean): Promise<IRecorderApp> {
|
||||||
|
if (process.env.PW_CODEGEN_NO_INSPECTOR)
|
||||||
|
return new HeadlessRecorderApp();
|
||||||
const recorderPlaywright = (require('../../playwright').createPlaywright as typeof import('../../playwright').createPlaywright)('javascript', true);
|
const recorderPlaywright = (require('../../playwright').createPlaywright as typeof import('../../playwright').createPlaywright)('javascript', true);
|
||||||
const args = [
|
const args = [
|
||||||
'--app=data:text/html,',
|
'--app=data:text/html,',
|
||||||
|
|
@ -163,3 +176,14 @@ export class RecorderApp extends EventEmitter {
|
||||||
await this._page.bringToFront();
|
await this._page.bringToFront();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HeadlessRecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
async setPaused(paused: boolean): Promise<void> {}
|
||||||
|
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {}
|
||||||
|
async setFile(file: string): Promise<void> {}
|
||||||
|
async setSelector(selector: string, focus?: boolean): Promise<void> {}
|
||||||
|
async updateCallLogs(callLogs: CallLog[]): Promise<void> {}
|
||||||
|
bringToFront(): void {}
|
||||||
|
async setSources(sources: Source[]): Promise<void> {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { CSharpLanguageGenerator } from './recorder/csharp';
|
||||||
import { PythonLanguageGenerator } from './recorder/python';
|
import { PythonLanguageGenerator } from './recorder/python';
|
||||||
import * as recorderSource from '../../generated/recorderSource';
|
import * as recorderSource from '../../generated/recorderSource';
|
||||||
import * as consoleApiSource from '../../generated/consoleApiSource';
|
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||||
import { RecorderApp } from './recorder/recorderApp';
|
import { IRecorderApp, RecorderApp } from './recorder/recorderApp';
|
||||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
|
||||||
import { Point } from '../../common/types';
|
import { Point } from '../../common/types';
|
||||||
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||||
|
|
@ -46,7 +46,7 @@ export class RecorderSupplement implements InstrumentationListener {
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _mode: Mode;
|
private _mode: Mode;
|
||||||
private _highlightedSelector = '';
|
private _highlightedSelector = '';
|
||||||
private _recorderApp: RecorderApp | null = null;
|
private _recorderApp: IRecorderApp | null = null;
|
||||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||||
private _recorderSources: Source[] = [];
|
private _recorderSources: Source[] = [];
|
||||||
private _userSources = new Map<string, Source>();
|
private _userSources = new Map<string, Source>();
|
||||||
|
|
@ -317,7 +317,7 @@ class ContextRecorder extends EventEmitter {
|
||||||
|
|
||||||
this._recorderSources = [];
|
this._recorderSources = [];
|
||||||
const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
|
const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
|
||||||
let text = '';
|
const throttledOutputFile = params.outputFile ? new ThrottledFile(params.outputFile) : null;
|
||||||
generator.on('change', () => {
|
generator.on('change', () => {
|
||||||
this._recorderSources = [];
|
this._recorderSources = [];
|
||||||
for (const languageGenerator of orderedLanguages) {
|
for (const languageGenerator of orderedLanguages) {
|
||||||
|
|
@ -330,21 +330,19 @@ class ContextRecorder extends EventEmitter {
|
||||||
source.revealLine = source.text.split('\n').length - 1;
|
source.revealLine = source.text.split('\n').length - 1;
|
||||||
this._recorderSources.push(source);
|
this._recorderSources.push(source);
|
||||||
if (languageGenerator === orderedLanguages[0])
|
if (languageGenerator === orderedLanguages[0])
|
||||||
text = source.text;
|
throttledOutputFile?.setContent(source.text);
|
||||||
}
|
}
|
||||||
this.emit(ContextRecorder.Events.Change, {
|
this.emit(ContextRecorder.Events.Change, {
|
||||||
sources: this._recorderSources,
|
sources: this._recorderSources,
|
||||||
primaryFileName: primaryLanguage.fileName
|
primaryFileName: primaryLanguage.fileName
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (params.outputFile) {
|
if (throttledOutputFile) {
|
||||||
context.on(BrowserContext.Events.BeforeClose, () => {
|
context.on(BrowserContext.Events.BeforeClose, () => {
|
||||||
fs.writeFileSync(params.outputFile!, text);
|
throttledOutputFile.flush();
|
||||||
text = '';
|
|
||||||
});
|
});
|
||||||
process.on('exit', () => {
|
process.on('exit', () => {
|
||||||
if (text)
|
throttledOutputFile.flush();
|
||||||
fs.writeFileSync(params.outputFile!, text);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._generator = generator;
|
this._generator = generator;
|
||||||
|
|
@ -590,3 +588,29 @@ function languageForFile(file: string) {
|
||||||
return 'csharp';
|
return 'csharp';
|
||||||
return 'javascript';
|
return 'javascript';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ThrottledFile {
|
||||||
|
private _file: string;
|
||||||
|
private _timer: NodeJS.Timeout | undefined;
|
||||||
|
private _text: string | undefined;
|
||||||
|
|
||||||
|
constructor(file: string) {
|
||||||
|
this._file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent(text: string) {
|
||||||
|
this._text = text;
|
||||||
|
if (!this._timer)
|
||||||
|
this._timer = setTimeout(() => this.flush(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush(): void {
|
||||||
|
if (this._timer) {
|
||||||
|
clearTimeout(this._timer);
|
||||||
|
this._timer = undefined;
|
||||||
|
}
|
||||||
|
if (this._text)
|
||||||
|
fs.writeFileSync(this._file, this._text);
|
||||||
|
this._text = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ export const deps: any = {
|
||||||
'libxi6',
|
'libxi6',
|
||||||
'libxrender1',
|
'libxrender1',
|
||||||
'libxt6',
|
'libxt6',
|
||||||
|
'libxtst6'
|
||||||
],
|
],
|
||||||
webkit: [
|
webkit: [
|
||||||
'gstreamer1.0-libav',
|
'gstreamer1.0-libav',
|
||||||
|
|
@ -646,7 +647,6 @@ deps['ubuntu20.04-arm64'] = {
|
||||||
chromium: [...deps['ubuntu20.04'].chromium],
|
chromium: [...deps['ubuntu20.04'].chromium],
|
||||||
firefox: [
|
firefox: [
|
||||||
...deps['ubuntu20.04'].firefox,
|
...deps['ubuntu20.04'].firefox,
|
||||||
'libxtst6'
|
|
||||||
],
|
],
|
||||||
webkit: [
|
webkit: [
|
||||||
...deps['ubuntu20.04'].webkit,
|
...deps['ubuntu20.04'].webkit,
|
||||||
|
|
|
||||||
|
|
@ -716,7 +716,7 @@ export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: strin
|
||||||
case 'java':
|
case 'java':
|
||||||
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="${parameters}"`;
|
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="${parameters}"`;
|
||||||
case 'csharp':
|
case 'csharp':
|
||||||
return `playwright ${parameters}`;
|
return `pwsh bin\\Debug\\netX\\playwright.ps1 ${parameters}`;
|
||||||
default:
|
default:
|
||||||
return `npx playwright ${parameters}`;
|
return `npx playwright ${parameters}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,7 @@ function determineUserAgent(): string {
|
||||||
osIdentifier = 'windows';
|
osIdentifier = 'windows';
|
||||||
osVersion = `${version[0]}.${version[1]}`;
|
osVersion = `${version[0]}.${version[1]}`;
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === 'darwin') {
|
||||||
const version = execSync('sw_vers -productVersion').toString().trim().split('.');
|
const version = execSync('sw_vers -productVersion', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split('.');
|
||||||
osIdentifier = 'macOS';
|
osIdentifier = 'macOS';
|
||||||
osVersion = `${version[0]}.${version[1]}`;
|
osVersion = `${version[0]}.${version[1]}`;
|
||||||
} else if (process.platform === 'linux') {
|
} else if (process.platform === 'linux') {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-firefox",
|
"name": "playwright-firefox",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate Firefox",
|
"description": "A high-level API to automate Firefox",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
@ -26,6 +26,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
"open": "8.4.0",
|
"open": "8.4.0",
|
||||||
"pirates": "4.0.4",
|
"pirates": "4.0.4",
|
||||||
"pixelmatch": "5.2.1",
|
"pixelmatch": "5.2.1",
|
||||||
"playwright-core": "1.19.0-next",
|
"playwright-core": "1.19.2",
|
||||||
"pngjs": "6.0.0",
|
"pngjs": "6.0.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"source-map-support": "0.4.18",
|
"source-map-support": "0.4.18",
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ function forceRegExp(pattern: string): RegExp {
|
||||||
const match = pattern.match(/^\/(.*)\/([gi]*)$/);
|
const match = pattern.match(/^\/(.*)\/([gi]*)$/);
|
||||||
if (match)
|
if (match)
|
||||||
return new RegExp(match[1], match[2]);
|
return new RegExp(match[1], match[2]);
|
||||||
return new RegExp(pattern.replace(/\\/g, '\\\\'), 'gi');
|
return new RegExp(pattern, 'gi');
|
||||||
}
|
}
|
||||||
|
|
||||||
function overridesFromOptions(options: { [key: string]: any }): Config {
|
function overridesFromOptions(options: { [key: string]: any }): Config {
|
||||||
|
|
|
||||||
|
|
@ -478,7 +478,7 @@ class Worker extends EventEmitter {
|
||||||
detached: false,
|
detached: false,
|
||||||
env: {
|
env: {
|
||||||
FORCE_COLOR: '1',
|
FORCE_COLOR: '1',
|
||||||
DEBUG_COLORS: process.stdout.isTTY ? '1' : '0',
|
DEBUG_COLORS: '1',
|
||||||
TEST_WORKER_INDEX: String(this.workerIndex),
|
TEST_WORKER_INDEX: String(this.workerIndex),
|
||||||
TEST_PARALLEL_INDEX: String(this.parallelIndex),
|
TEST_PARALLEL_INDEX: String(this.parallelIndex),
|
||||||
...process.env
|
...process.env
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Video, APIRequestContext } from 'playwright-core';
|
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Video, APIRequestContext, Tracing } from 'playwright-core';
|
||||||
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
|
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
|
||||||
import { rootTestType } from './testType';
|
import { rootTestType } from './testType';
|
||||||
import { createGuid, removeFolders, debugMode } from 'playwright-core/lib/utils/utils';
|
import { createGuid, removeFolders, debugMode } from 'playwright-core/lib/utils/utils';
|
||||||
|
|
@ -87,7 +87,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
await removeFolders([dir]);
|
await removeFolders([dir]);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
_browserOptions: [async ({ headless, channel, launchOptions }, use) => {
|
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
||||||
const options: LaunchOptions = {
|
const options: LaunchOptions = {
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
|
|
@ -97,8 +97,13 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
options.headless = headless;
|
options.headless = headless;
|
||||||
if (channel !== undefined)
|
if (channel !== undefined)
|
||||||
options.channel = channel;
|
options.channel = channel;
|
||||||
|
|
||||||
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
||||||
|
(browserType as any)._defaultLaunchOptions = options;
|
||||||
await use(options);
|
await use(options);
|
||||||
}, { scope: 'worker' }],
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
||||||
|
(browserType as any)._defaultLaunchOptions = undefined;
|
||||||
|
}, { scope: 'worker', auto: true }],
|
||||||
|
|
||||||
browser: [async ({ playwright, browserName }, use) => {
|
browser: [async ({ playwright, browserName }, use) => {
|
||||||
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
||||||
|
|
@ -249,41 +254,51 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startTracing = async (tracing: Tracing) => {
|
||||||
|
if (captureTrace) {
|
||||||
|
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
||||||
|
if (!(tracing as any)[kTracingStarted]) {
|
||||||
|
await tracing.start({ ...traceOptions, title });
|
||||||
|
(tracing as any)[kTracingStarted] = true;
|
||||||
|
} else {
|
||||||
|
await tracing.startChunk({ title });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(tracing as any)[kTracingStarted] = false;
|
||||||
|
await tracing.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
||||||
createdContexts.add(context);
|
createdContexts.add(context);
|
||||||
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
|
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
|
||||||
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
|
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
|
||||||
if (captureTrace) {
|
await startTracing(context.tracing);
|
||||||
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
|
||||||
if (!(context.tracing as any)[kTracingStarted]) {
|
|
||||||
await context.tracing.start({ ...traceOptions, title });
|
|
||||||
(context.tracing as any)[kTracingStarted] = true;
|
|
||||||
} else {
|
|
||||||
await context.tracing.startChunk({ title });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(context.tracing as any)[kTracingStarted] = false;
|
|
||||||
await context.tracing.stop();
|
|
||||||
}
|
|
||||||
const listener = createInstrumentationListener(context);
|
const listener = createInstrumentationListener(context);
|
||||||
(context as any)._instrumentation.addListener(listener);
|
(context as any)._instrumentation.addListener(listener);
|
||||||
(context.request as any)._instrumentation.addListener(listener);
|
(context.request as any)._instrumentation.addListener(listener);
|
||||||
};
|
};
|
||||||
const onDidCreateRequestContext = async (context: APIRequestContext) => {
|
const onDidCreateRequestContext = async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await startTracing(tracing);
|
||||||
(context as any)._instrumentation.addListener(createInstrumentationListener());
|
(context as any)._instrumentation.addListener(createInstrumentationListener());
|
||||||
};
|
};
|
||||||
|
|
||||||
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||||
|
|
||||||
const onWillCloseContext = async (context: BrowserContext) => {
|
const stopTracing = async (tracing: Tracing) => {
|
||||||
(context as any)[startedCollectingArtifacts] = true;
|
(tracing as any)[startedCollectingArtifacts] = true;
|
||||||
if (captureTrace) {
|
if (captureTrace) {
|
||||||
// Export trace for now. We'll know whether we have to preserve it
|
// Export trace for now. We'll know whether we have to preserve it
|
||||||
// after the test finishes.
|
// after the test finishes.
|
||||||
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
||||||
temporaryTraceFiles.push(tracePath);
|
temporaryTraceFiles.push(tracePath);
|
||||||
await context.tracing.stopChunk({ path: tracePath });
|
await tracing.stopChunk({ path: tracePath });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWillCloseContext = async (context: BrowserContext) => {
|
||||||
|
await stopTracing(context.tracing);
|
||||||
if (screenshot === 'on' || screenshot === 'only-on-failure') {
|
if (screenshot === 'on' || screenshot === 'only-on-failure') {
|
||||||
// Capture screenshot for now. We'll know whether we have to preserve them
|
// Capture screenshot for now. We'll know whether we have to preserve them
|
||||||
// after the test finishes.
|
// after the test finishes.
|
||||||
|
|
@ -295,16 +310,25 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWillCloseRequestContext = async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await stopTracing(tracing);
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Setup instrumentation and process existing contexts.
|
// 1. Setup instrumentation and process existing contexts.
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||||
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
|
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
|
||||||
(browserType as any)._onWillCloseContext = onWillCloseContext;
|
(browserType as any)._onWillCloseContext = onWillCloseContext;
|
||||||
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
||||||
(browserType as any)._defaultLaunchOptions = _browserOptions;
|
|
||||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
||||||
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
|
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
|
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
|
||||||
|
(playwright.request as any)._onWillCloseContext = onWillCloseRequestContext;
|
||||||
|
const existingApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
await Promise.all(existingApiRequests.map(onDidCreateRequestContext));
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Run the test.
|
// 2. Run the test.
|
||||||
await use();
|
await use();
|
||||||
|
|
@ -337,28 +361,37 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
(browserType as any)._onDidCreateContext = undefined;
|
(browserType as any)._onDidCreateContext = undefined;
|
||||||
(browserType as any)._onWillCloseContext = undefined;
|
(browserType as any)._onWillCloseContext = undefined;
|
||||||
(browserType as any)._defaultContextOptions = undefined;
|
(browserType as any)._defaultContextOptions = undefined;
|
||||||
(browserType as any)._defaultLaunchOptions = undefined;
|
|
||||||
}
|
}
|
||||||
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
||||||
(playwright.request as any)._onDidCreateContext = undefined;
|
|
||||||
for (const context of (playwright.request as any)._contexts)
|
for (const context of (playwright.request as any)._contexts)
|
||||||
context._instrumentation.removeAllListeners();
|
context._instrumentation.removeAllListeners();
|
||||||
|
const leftoverApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
(playwright.request as any)._onDidCreateContext = undefined;
|
||||||
|
(playwright.request as any)._onWillCloseContext = undefined;
|
||||||
|
|
||||||
// 5. Collect artifacts from any non-closed contexts.
|
const stopTraceChunk = async (tracing: Tracing): Promise<boolean> => {
|
||||||
await Promise.all(leftoverContexts.map(async context => {
|
|
||||||
// When we timeout during context.close(), we might end up with context still alive
|
// When we timeout during context.close(), we might end up with context still alive
|
||||||
// but artifacts being already collected. In this case, do not collect artifacts
|
// but artifacts being already collected. In this case, do not collect artifacts
|
||||||
// for the second time.
|
// for the second time.
|
||||||
if ((context as any)[startedCollectingArtifacts])
|
if ((tracing as any)[startedCollectingArtifacts])
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
if (preserveTrace)
|
if (preserveTrace)
|
||||||
await context.tracing.stopChunk({ path: addTraceAttachment() });
|
await tracing.stopChunk({ path: addTraceAttachment() });
|
||||||
else if (captureTrace)
|
else if (captureTrace)
|
||||||
await context.tracing.stopChunk();
|
await tracing.stopChunk();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Collect artifacts from any non-closed contexts.
|
||||||
|
await Promise.all(leftoverContexts.map(async context => {
|
||||||
|
if (!await stopTraceChunk(context.tracing))
|
||||||
|
return;
|
||||||
if (captureScreenshots)
|
if (captureScreenshots)
|
||||||
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
|
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
|
||||||
}));
|
}).concat(leftoverApiRequests.map(async context => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await stopTraceChunk(tracing);
|
||||||
|
})));
|
||||||
|
|
||||||
// 6. Either remove or attach temporary traces and screenshots for contexts closed
|
// 6. Either remove or attach temporary traces and screenshots for contexts closed
|
||||||
// before the test has finished.
|
// before the test has finished.
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,13 @@ export class BaseReporter implements Reporter {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ttyWidth() {
|
protected fitToScreen(line: string, suffix?: string): string {
|
||||||
return this._ttyWidthForTest || (process.env.PWTEST_SKIP_TEST_OUTPUT ? 80 : process.stdout.columns || 0);
|
const ttyWidth = this._ttyWidthForTest || (process.env.PWTEST_SKIP_TEST_OUTPUT ? 80 : process.stdout.columns || 0);
|
||||||
|
if (!ttyWidth) {
|
||||||
|
// Guard against the case where we cannot determine available width.
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return fitToWidth(line, ttyWidth, suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected generateStartingMessage() {
|
protected generateStartingMessage() {
|
||||||
|
|
@ -431,7 +436,7 @@ export function stripAnsiEscapes(str: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaves enough space for the "suffix" to also fit.
|
// Leaves enough space for the "suffix" to also fit.
|
||||||
export function fitToScreen(line: string, width: number, suffix?: string): string {
|
function fitToWidth(line: string, width: number, suffix?: string): string {
|
||||||
const suffixLength = suffix ? stripAnsiEscapes(suffix).length : 0;
|
const suffixLength = suffix ? stripAnsiEscapes(suffix).length : 0;
|
||||||
width -= suffixLength;
|
width -= suffixLength;
|
||||||
if (line.length <= width)
|
if (line.length <= width)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import colors from 'colors/safe';
|
import colors from 'colors/safe';
|
||||||
import { BaseReporter, fitToScreen, formatFailure, formatTestTitle } from './base';
|
import { BaseReporter, formatFailure, formatTestTitle } from './base';
|
||||||
import { FullConfig, TestCase, Suite, TestResult, FullResult } from '../../types/testReporter';
|
import { FullConfig, TestCase, Suite, TestResult, FullResult } from '../../types/testReporter';
|
||||||
|
|
||||||
class LineReporter extends BaseReporter {
|
class LineReporter extends BaseReporter {
|
||||||
|
|
@ -51,7 +51,7 @@ class LineReporter extends BaseReporter {
|
||||||
if (test && this._lastTest !== test) {
|
if (test && this._lastTest !== test) {
|
||||||
// Write new header for the output.
|
// Write new header for the output.
|
||||||
const title = colors.gray(formatTestTitle(this.config, test));
|
const title = colors.gray(formatTestTitle(this.config, test));
|
||||||
stream.write(fitToScreen(title, this.ttyWidth()) + `\n`);
|
stream.write(this.fitToScreen(title) + `\n`);
|
||||||
this._lastTest = test;
|
this._lastTest = test;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ class LineReporter extends BaseReporter {
|
||||||
if (process.env.PWTEST_SKIP_TEST_OUTPUT)
|
if (process.env.PWTEST_SKIP_TEST_OUTPUT)
|
||||||
process.stdout.write(`${title + suffix}\n`);
|
process.stdout.write(`${title + suffix}\n`);
|
||||||
else
|
else
|
||||||
process.stdout.write(`\u001B[1A\u001B[2K${fitToScreen(title, this.ttyWidth(), suffix) + colors.yellow(suffix)}\n`);
|
process.stdout.write(`\u001B[1A\u001B[2K${this.fitToScreen(title, suffix) + colors.yellow(suffix)}\n`);
|
||||||
|
|
||||||
if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected')) {
|
if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected')) {
|
||||||
if (!process.env.PWTEST_SKIP_TEST_OUTPUT)
|
if (!process.env.PWTEST_SKIP_TEST_OUTPUT)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import colors from 'colors/safe';
|
import colors from 'colors/safe';
|
||||||
import milliseconds from 'ms';
|
import milliseconds from 'ms';
|
||||||
import { BaseReporter, fitToScreen, formatTestTitle } from './base';
|
import { BaseReporter, formatTestTitle } from './base';
|
||||||
import { FullConfig, FullResult, Suite, TestCase, TestResult, TestStep } from '../../types/testReporter';
|
import { FullConfig, FullResult, Suite, TestCase, TestResult, TestStep } from '../../types/testReporter';
|
||||||
|
|
||||||
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal
|
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal
|
||||||
|
|
@ -55,7 +55,7 @@ class ListReporter extends BaseReporter {
|
||||||
}
|
}
|
||||||
const line = ' ' + colors.gray(formatTestTitle(this.config, test));
|
const line = ' ' + colors.gray(formatTestTitle(this.config, test));
|
||||||
const suffix = this._retrySuffix(result);
|
const suffix = this._retrySuffix(result);
|
||||||
process.stdout.write(this._fitToScreen(line, suffix) + suffix + '\n');
|
process.stdout.write(this.fitToScreen(line, suffix) + suffix + '\n');
|
||||||
}
|
}
|
||||||
this._testRows.set(test, this._lastRow++);
|
this._testRows.set(test, this._lastRow++);
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ class ListReporter extends BaseReporter {
|
||||||
process.stdout.write(`\u001B[${this._lastRow - testRow}A`);
|
process.stdout.write(`\u001B[${this._lastRow - testRow}A`);
|
||||||
// Erase line, go to the start
|
// Erase line, go to the start
|
||||||
process.stdout.write('\u001B[2K\u001B[0G');
|
process.stdout.write('\u001B[2K\u001B[0G');
|
||||||
process.stdout.write(this._fitToScreen(line, suffix) + suffix);
|
process.stdout.write(this.fitToScreen(line, suffix) + suffix);
|
||||||
// Go down if needed.
|
// Go down if needed.
|
||||||
if (testRow !== this._lastRow)
|
if (testRow !== this._lastRow)
|
||||||
process.stdout.write(`\u001B[${this._lastRow - testRow}E`);
|
process.stdout.write(`\u001B[${this._lastRow - testRow}E`);
|
||||||
|
|
@ -152,13 +152,6 @@ class ListReporter extends BaseReporter {
|
||||||
return (result.retry ? colors.yellow(` (retry #${result.retry})`) : '');
|
return (result.retry ? colors.yellow(` (retry #${result.retry})`) : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fitToScreen(line: string, suffix?: string): string {
|
|
||||||
const ttyWidth = this.ttyWidth();
|
|
||||||
if (!ttyWidth)
|
|
||||||
return line;
|
|
||||||
return fitToScreen(line, ttyWidth, suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateTestLineForTest(test: TestCase, line: string, suffix: string) {
|
private _updateTestLineForTest(test: TestCase, line: string, suffix: string) {
|
||||||
const testRow = this._testRows.get(test)!;
|
const testRow = this._testRows.get(test)!;
|
||||||
process.stdout.write(testRow + ' : ' + line + suffix + '\n');
|
process.stdout.write(testRow + ' : ' + line + suffix + '\n');
|
||||||
|
|
|
||||||
|
|
@ -302,12 +302,7 @@ export class Runner {
|
||||||
if (!total)
|
if (!total)
|
||||||
fatalErrors.push(createNoTestsError());
|
fatalErrors.push(createNoTestsError());
|
||||||
|
|
||||||
// 8. Fail when output fails.
|
// 8. Compute shards.
|
||||||
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {
|
|
||||||
fatalErrors.push(serializeError(e));
|
|
||||||
})));
|
|
||||||
|
|
||||||
// 9. Compute shards.
|
|
||||||
let testGroups = createTestGroups(rootSuite);
|
let testGroups = createTestGroups(rootSuite);
|
||||||
|
|
||||||
const shard = config.shard;
|
const shard = config.shard;
|
||||||
|
|
@ -341,20 +336,37 @@ export class Runner {
|
||||||
}
|
}
|
||||||
(config as any).__testGroupsCount = testGroups.length;
|
(config as any).__testGroupsCount = testGroups.length;
|
||||||
|
|
||||||
// 10. Report begin
|
// 9. Report begin
|
||||||
this._reporter.onBegin?.(config, rootSuite);
|
this._reporter.onBegin?.(config, rootSuite);
|
||||||
|
|
||||||
// 11. Bail out on errors prior to running global setup.
|
// 10. Bail out on errors prior to running global setup.
|
||||||
if (fatalErrors.length) {
|
if (fatalErrors.length) {
|
||||||
for (const error of fatalErrors)
|
for (const error of fatalErrors)
|
||||||
this._reporter.onError?.(error);
|
this._reporter.onError?.(error);
|
||||||
return { status: 'failed' };
|
return { status: 'failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Bail out if list mode only, don't do any work.
|
// 11. Bail out if list mode only, don't do any work.
|
||||||
if (list)
|
if (list)
|
||||||
return { status: 'passed' };
|
return { status: 'passed' };
|
||||||
|
|
||||||
|
// 12. Remove output directores.
|
||||||
|
try {
|
||||||
|
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async error => {
|
||||||
|
if ((error as any).code === 'EBUSY') {
|
||||||
|
// We failed to remove folder, might be due to the whole folder being mounted inside a container:
|
||||||
|
// https://github.com/microsoft/playwright/issues/12106
|
||||||
|
// Do a best-effort to remove all files inside of it instead.
|
||||||
|
const entries = await readDirAsync(outputDir).catch(e => []);
|
||||||
|
await Promise.all(entries.map(entry => removeFolderAsync(path.join(outputDir, entry))));
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
} catch (e) {
|
||||||
|
this._reporter.onError?.(serializeError(e));
|
||||||
|
return { status: 'failed' };
|
||||||
|
}
|
||||||
|
|
||||||
// 13. Run Global setup.
|
// 13. Run Global setup.
|
||||||
let globalTearDown: (() => Promise<void>) | undefined;
|
let globalTearDown: (() => Promise<void>) | undefined;
|
||||||
|
|
@ -432,7 +444,7 @@ export class Runner {
|
||||||
}, result);
|
}, result);
|
||||||
|
|
||||||
if (result.status !== 'passed') {
|
if (result.status !== 'passed') {
|
||||||
tearDown();
|
await tearDown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,11 +120,25 @@ function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | unde
|
||||||
return cachedTSConfigs.get(cwd);
|
return cachedTSConfigs.get(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
|
const scriptPreprocessor = process.env.PW_TEST_SOURCE_TRANSFORM ?
|
||||||
|
require(process.env.PW_TEST_SOURCE_TRANSFORM) : undefined;
|
||||||
|
|
||||||
export function transformHook(code: string, filename: string, isModule = false): string {
|
export function transformHook(code: string, filename: string, isModule = false): string {
|
||||||
if (isComponentImport(filename))
|
if (isComponentImport(filename))
|
||||||
return componentStub();
|
return componentStub();
|
||||||
|
|
||||||
const tsconfigData = loadAndValidateTsconfigForFile(filename);
|
// If we are not TypeScript and there is no applicable preprocessor - bail out.
|
||||||
|
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
|
||||||
|
const hasPreprocessor =
|
||||||
|
process.env.PW_TEST_SOURCE_TRANSFORM &&
|
||||||
|
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE &&
|
||||||
|
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f));
|
||||||
|
|
||||||
|
if (!isTypeScript && !hasPreprocessor)
|
||||||
|
return code;
|
||||||
|
|
||||||
|
const tsconfigData = isTypeScript ? loadAndValidateTsconfigForFile(filename) : undefined;
|
||||||
const cachePath = calculateCachePath(tsconfigData, code, filename);
|
const cachePath = calculateCachePath(tsconfigData, code, filename);
|
||||||
const codePath = cachePath + '.js';
|
const codePath = cachePath + '.js';
|
||||||
const sourceMapPath = cachePath + '.map';
|
const sourceMapPath = cachePath + '.map';
|
||||||
|
|
@ -135,7 +149,10 @@ export function transformHook(code: string, filename: string, isModule = false):
|
||||||
// Silence the annoying warning.
|
// Silence the annoying warning.
|
||||||
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
|
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
|
||||||
const babel: typeof import('@babel/core') = require('@babel/core');
|
const babel: typeof import('@babel/core') = require('@babel/core');
|
||||||
const plugins = [
|
const plugins = [];
|
||||||
|
|
||||||
|
if (isTypeScript) {
|
||||||
|
plugins.push(
|
||||||
[require.resolve('@babel/plugin-proposal-class-properties')],
|
[require.resolve('@babel/plugin-proposal-class-properties')],
|
||||||
[require.resolve('@babel/plugin-proposal-numeric-separator')],
|
[require.resolve('@babel/plugin-proposal-numeric-separator')],
|
||||||
[require.resolve('@babel/plugin-proposal-logical-assignment-operators')],
|
[require.resolve('@babel/plugin-proposal-logical-assignment-operators')],
|
||||||
|
|
@ -145,8 +162,8 @@ export function transformHook(code: string, filename: string, isModule = false):
|
||||||
[require.resolve('@babel/plugin-syntax-optional-catch-binding')],
|
[require.resolve('@babel/plugin-syntax-optional-catch-binding')],
|
||||||
[require.resolve('@babel/plugin-syntax-async-generators')],
|
[require.resolve('@babel/plugin-syntax-async-generators')],
|
||||||
[require.resolve('@babel/plugin-syntax-object-rest-spread')],
|
[require.resolve('@babel/plugin-syntax-object-rest-spread')],
|
||||||
[require.resolve('@babel/plugin-proposal-export-namespace-from')],
|
[require.resolve('@babel/plugin-proposal-export-namespace-from')]
|
||||||
] as any;
|
);
|
||||||
|
|
||||||
if (tsconfigData) {
|
if (tsconfigData) {
|
||||||
plugins.push([require.resolve('babel-plugin-module-resolver'), {
|
plugins.push([require.resolve('babel-plugin-module-resolver'), {
|
||||||
|
|
@ -158,16 +175,18 @@ export function transformHook(code: string, filename: string, isModule = false):
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PW_COMPONENT_TESTING)
|
|
||||||
plugins.unshift([require.resolve('@babel/plugin-transform-react-jsx')]);
|
|
||||||
|
|
||||||
if (!isModule) {
|
if (!isModule) {
|
||||||
plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]);
|
plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]);
|
||||||
plugins.push([require.resolve('@babel/plugin-proposal-dynamic-import')]);
|
plugins.push([require.resolve('@babel/plugin-proposal-dynamic-import')]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.PW_TEST_SOURCE_TRANSFORM)
|
if (process.env.PW_COMPONENT_TESTING)
|
||||||
plugins.push([process.env.PW_TEST_SOURCE_TRANSFORM]);
|
plugins.unshift([require.resolve('@babel/plugin-transform-react-jsx')]);
|
||||||
|
|
||||||
|
|
||||||
|
if (hasPreprocessor)
|
||||||
|
plugins.push([scriptPreprocessor]);
|
||||||
|
|
||||||
const result = babel.transformFileSync(filename, {
|
const result = babel.transformFileSync(filename, {
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
|
|
@ -193,7 +212,12 @@ export function transformHook(code: string, filename: string, isModule = false):
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installTransform(): () => void {
|
export function installTransform(): () => void {
|
||||||
return pirates.addHook((code: string, filename: string) => transformHook(code, filename), { exts: ['.ts', '.tsx'] });
|
const exts = ['.ts', '.tsx'];
|
||||||
|
|
||||||
|
// When script preprocessor is engaged, we transpile JS as well.
|
||||||
|
if (scriptPreprocessor)
|
||||||
|
exts.push('.js', '.mjs');
|
||||||
|
return pirates.addHook((code: string, filename: string) => transformHook(code, filename), { exts });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapFunctionWithLocation<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {
|
export function wrapFunctionWithLocation<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate WebKit",
|
"description": "A high-level API to automate WebKit",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
@ -25,6 +25,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.19.0-next",
|
"version": "1.19.2",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": "github:Microsoft/playwright",
|
"repository": "github:Microsoft/playwright",
|
||||||
"homepage": "https://playwright.dev",
|
"homepage": "https://playwright.dev",
|
||||||
|
|
@ -26,6 +26,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.19.0-next"
|
"playwright-core": "1.19.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put']
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
|
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
|
||||||
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
|
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
|
||||||
expect(await response.text()).toBe(['head', 'put'].includes(method) ? '' : '{"foo": "bar"}\n');
|
expect(await response.text()).toBe('head' === method ? '' : '{"foo": "bar"}\n');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,3 +363,18 @@ it('should not fail on empty body with encoding', async ({ playwright, server })
|
||||||
}
|
}
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return body for failing requests', async ({ playwright, server }) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
for (const method of ['head', 'put', 'trace']) {
|
||||||
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
|
res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' });
|
||||||
|
res.end('Not found.');
|
||||||
|
});
|
||||||
|
const response = await request.fetch(server.EMPTY_PAGE, { method });
|
||||||
|
expect(response.status()).toBe(404);
|
||||||
|
// HEAD response returns empty body in node http module.
|
||||||
|
expect(await response.text()).toBe(method === 'head' ? '' : 'Not found.');
|
||||||
|
}
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ it('should provide a Response with a file URL', async ({ page, asset, isAndroid,
|
||||||
|
|
||||||
const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href;
|
const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href;
|
||||||
const response = await page.goto(fileurl);
|
const response = await page.goto(fileurl);
|
||||||
if (isElectron || (browserName === 'chromium' && browserMajorVersion >= 100) || (browserName === 'webkit' && isWindows))
|
if (isElectron || (browserName === 'chromium' && browserMajorVersion >= 99) || (browserName === 'webkit' && isWindows))
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
else
|
else
|
||||||
expect(response.status()).toBe(0);
|
expect(response.status()).toBe(0);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
test('should filter by file name', async ({ runInlineTest }) => {
|
test('should filter by file name', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
@ -94,14 +93,6 @@ test('should run nothing for missing line', async ({ runInlineTest }) => {
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should escape path on windows', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'foo/test.spec.ts': `pwt.test('fails', () => { expect(1).toBe(2); });`,
|
|
||||||
}, undefined, undefined, { additionalArgs: [path.join('foo', 'test.spec.ts')] });
|
|
||||||
expect(result.exitCode).toBe(1);
|
|
||||||
expect(result.failed).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should focus a single nested test spec', async ({ runInlineTest }) => {
|
test('should focus a single nested test spec', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'foo.test.ts': `
|
'foo.test.ts': `
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
test('should list tests', async ({ runInlineTest }) => {
|
test('should list tests', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
@ -105,3 +107,36 @@ test('globalSetup and globalTeardown should not run', async ({ runInlineTest })
|
||||||
`Total: 2 tests in 2 files`,
|
`Total: 2 tests in 2 files`,
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('outputDir should not be removed', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const outputDir = testInfo.outputPath('dummy-output-dir');
|
||||||
|
|
||||||
|
const result1 = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { outputDir: ${JSON.stringify(outputDir)} };
|
||||||
|
`,
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('my test', async ({}, testInfo) => {
|
||||||
|
console.log(testInfo.outputDir);
|
||||||
|
require('fs').writeFileSync(testInfo.outputPath('myfile.txt'), 'hello');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, {}, {}, { usesCustomOutputDir: true });
|
||||||
|
expect(result1.exitCode).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(outputDir, 'a-my-test', 'myfile.txt'))).toBe(true);
|
||||||
|
|
||||||
|
const result2 = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { outputDir: ${JSON.stringify(outputDir)} };
|
||||||
|
`,
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('my test', async ({}, testInfo) => {
|
||||||
|
console.log(testInfo.outputDir);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { list: true }, {}, { usesCustomOutputDir: true });
|
||||||
|
expect(result2.exitCode).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(outputDir, 'a-my-test', 'myfile.txt'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,24 @@ test('should import esm from ts when package.json has type module in experimenta
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should propagate subprocess exit code in experimental mode', async ({ runInlineTest }) => {
|
||||||
|
// We only support experimental esm mode on Node 16+
|
||||||
|
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'package.json': JSON.stringify({ type: 'module' }),
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('failing test', ({}, testInfo) => {
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, {}, {
|
||||||
|
PW_EXPERIMENTAL_TS_ESM: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should filter stack trace for simple expect', async ({ runInlineTest }) => {
|
test('should filter stack trace for simple expect', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'expect-test.spec.ts': `
|
'expect-test.spec.ts': `
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
|
||||||
PW_TEST_REPORTER: undefined,
|
PW_TEST_REPORTER: undefined,
|
||||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||||
|
PW_TEST_SOURCE_TRANSFORM_SCOPE: undefined,
|
||||||
PW_OUT_OF_PROCESS_DRIVER: undefined,
|
PW_OUT_OF_PROCESS_DRIVER: undefined,
|
||||||
NODE_OPTIONS: undefined,
|
NODE_OPTIONS: undefined,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,26 @@ test('should respect headless in launchPersistent', async ({ runInlineTest }) =>
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect headless in modifiers that run before tests', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { use: { headless: false } };
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test.skip(({ browser }) => false);
|
||||||
|
|
||||||
|
test('should work', async ({ page }) => {
|
||||||
|
expect(await page.evaluate(() => navigator.userAgent)).not.toContain('Headless');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should call logger from launchOptions config', async ({ runInlineTest }, testInfo) => {
|
test('should call logger from launchOptions config', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,47 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy();
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { use: { trace: 'on' } };
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass', async ({request, page}, testInfo) => {
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api pass', async ({playwright}, testInfo) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fail', async ({request, page}, testInfo) => {
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
// One trace file for request context and one for each APIRequestContext
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-pass', 'trace-1.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-api-pass', 'trace-1.zip'))).toBeFalsy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace-1.zip'))).toBeTruthy();
|
||||||
|
// One leftover global APIRequestContext from 'api pass' test.
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace-2.zip'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should not throw with trace: on-first-retry and two retries in the same worker', async ({ runInlineTest }, testInfo) => {
|
test('should not throw with trace: on-first-retry and two retries in the same worker', async ({ runInlineTest }, testInfo) => {
|
||||||
const files = {};
|
const files = {};
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,40 @@ test('should show trace title', async ({ runInlineTest, page, showReport }) => {
|
||||||
await expect(page.locator('.workbench .title')).toHaveText('a.test.js:6 › passes');
|
await expect(page.locator('.workbench .title')).toHaveText('a.test.js:6 › passes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show multi trace source', async ({ runInlineTest, page, server, showReport }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.js': `
|
||||||
|
module.exports = { use: { trace: 'on' } };
|
||||||
|
`,
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('passes', async ({ playwright, page }) => {
|
||||||
|
await page.evaluate('2 + 2');
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
|
||||||
|
await showReport();
|
||||||
|
await page.click('text=passes');
|
||||||
|
// Expect one image-link to trace viewer and 2 separate download links
|
||||||
|
await expect(page.locator('img')).toHaveCount(1);
|
||||||
|
await expect(page.locator('a', { hasText: 'trace' })).toHaveText(['trace-1', 'trace-2']);
|
||||||
|
|
||||||
|
await page.click('img');
|
||||||
|
await page.click('.action-title >> text=page.evaluate');
|
||||||
|
await page.click('text=Source');
|
||||||
|
await expect(page.locator('.source-line-running')).toContainText('page.evaluate');
|
||||||
|
|
||||||
|
await page.click('.action-title >> text=apiRequestContext.get');
|
||||||
|
await page.click('text=Source');
|
||||||
|
await expect(page.locator('.source-line-running')).toContainText('request.get');
|
||||||
|
});
|
||||||
|
|
||||||
test('should show timed out steps', async ({ runInlineTest, page, showReport }) => {
|
test('should show timed out steps', async ({ runInlineTest, page, showReport }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.js': `
|
'playwright.config.js': `
|
||||||
|
|
|
||||||
|
|
@ -66,3 +66,19 @@ test('should print flaky failures', async ({ runInlineTest }) => {
|
||||||
expect(result.flaky).toBe(1);
|
expect(result.flaky).toBe(1);
|
||||||
expect(stripAnsi(result.output)).toContain('expect(testInfo.retry).toBe(1)');
|
expect(stripAnsi(result.output)).toContain('expect(testInfo.retry).toBe(1)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work without tty', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('one', async ({}) => {
|
||||||
|
expect(1).toBe(0);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'line' }, { PWTEST_TTY_WIDTH: '0', PWTEST_SKIP_TEST_OUTPUT: undefined });
|
||||||
|
const text = stripAnsi(result.output);
|
||||||
|
expect(text).toContain('[1/1] a.test.js:6:7 › one');
|
||||||
|
expect(text).toContain('1 failed');
|
||||||
|
expect(text).toContain('1) a.test');
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ test('should match cli string argument', async ({ runInlineTest }) => {
|
||||||
const { test } = pwt;
|
const { test } = pwt;
|
||||||
test('pass', ({}) => {});
|
test('pass', ({}) => {});
|
||||||
`
|
`
|
||||||
}, {}, {}, { additionalArgs: [`dir${path.sep}a`] });
|
}, {}, {}, { additionalArgs: [`dir\\${path.sep}a`] });
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']);
|
expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']);
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Documentation = require('./documentation');
|
const Documentation = require('./documentation');
|
||||||
const XmlDoc = require('./xmlDocumentation')
|
const XmlDoc = require('./xmlDocumentation');
|
||||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { parseApi } = require('./api_parser');
|
const { parseApi } = require('./api_parser');
|
||||||
|
|
@ -100,10 +101,10 @@ function writeFile(kind, name, spec, body, folder, extendsName = null) {
|
||||||
const out = [];
|
const out = [];
|
||||||
// console.log(`Generating ${name}`);
|
// console.log(`Generating ${name}`);
|
||||||
|
|
||||||
if (spec)
|
if (spec) {
|
||||||
out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth));
|
out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth));
|
||||||
else {
|
} else {
|
||||||
let ownDocumentation = documentedResults.get(name);
|
const ownDocumentation = documentedResults.get(name);
|
||||||
if (ownDocumentation) {
|
if (ownDocumentation) {
|
||||||
out.push('/// <summary>');
|
out.push('/// <summary>');
|
||||||
out.push(`/// ${ownDocumentation}`);
|
out.push(`/// ${ownDocumentation}`);
|
||||||
|
|
@ -122,7 +123,7 @@ function writeFile(kind, name, spec, body, folder, extendsName = null) {
|
||||||
out.push(...body);
|
out.push(...body);
|
||||||
out.push('}');
|
out.push('}');
|
||||||
|
|
||||||
let content = template.replace('[CONTENT]', out.join(EOL));
|
const content = template.replace('[CONTENT]', out.join(EOL));
|
||||||
fs.writeFileSync(path.join(folder, name + '.cs'), content);
|
fs.writeFileSync(path.join(folder, name + '.cs'), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +139,7 @@ function renderClass(clazz) {
|
||||||
for (const member of clazz.membersArray) {
|
for (const member of clazz.membersArray) {
|
||||||
// Classes inherit it from IAsyncDisposable
|
// Classes inherit it from IAsyncDisposable
|
||||||
if (member.name === 'dispose')
|
if (member.name === 'dispose')
|
||||||
continue
|
continue;
|
||||||
if (member.alias.startsWith('RunAnd'))
|
if (member.alias.startsWith('RunAnd'))
|
||||||
renderMember(member, clazz, { trimRunAndPrefix: true }, body);
|
renderMember(member, clazz, { trimRunAndPrefix: true }, body);
|
||||||
renderMember(member, clazz, {}, body);
|
renderMember(member, clazz, {}, body);
|
||||||
|
|
@ -162,15 +163,15 @@ function renderModelType(name, type) {
|
||||||
// TODO: consider how this could be merged with the `translateType` check
|
// TODO: consider how this could be merged with the `translateType` check
|
||||||
if (type.union
|
if (type.union
|
||||||
&& type.union[0].name === 'null'
|
&& type.union[0].name === 'null'
|
||||||
&& type.union.length == 2) {
|
&& type.union.length === 2)
|
||||||
type = type.union[1];
|
type = type.union[1];
|
||||||
}
|
|
||||||
|
|
||||||
if (type.name === 'Array') {
|
if (type.name === 'Array') {
|
||||||
throw new Error('Array at this stage is unexpected.');
|
throw new Error('Array at this stage is unexpected.');
|
||||||
} else if (type.properties) {
|
} else if (type.properties) {
|
||||||
for (const member of type.properties) {
|
for (const member of type.properties) {
|
||||||
let fakeType = new Type(name, null);
|
const fakeType = new Type(name, null);
|
||||||
renderMember(member, fakeType, {}, body);
|
renderMember(member, fakeType, {}, body);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -188,8 +189,8 @@ function renderEnum(name, literals) {
|
||||||
const body = [];
|
const body = [];
|
||||||
for (let literal of literals) {
|
for (let literal of literals) {
|
||||||
// strip out the quotes
|
// strip out the quotes
|
||||||
literal = literal.replace(/[\"]/g, ``)
|
literal = literal.replace(/[\"]/g, ``);
|
||||||
let escapedName = literal.replace(/[-]/g, ' ')
|
const escapedName = literal.replace(/[-]/g, ' ')
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
|
.map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
|
||||||
|
|
||||||
|
|
@ -213,22 +214,22 @@ function renderOptionType(name, type) {
|
||||||
writeFile('public class', name, null, body, optionsDir);
|
writeFile('public class', name, null, body, optionsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of documentation.classesArray) {
|
for (const element of documentation.classesArray)
|
||||||
renderClass(element);
|
renderClass(element);
|
||||||
}
|
|
||||||
|
|
||||||
for (let [name, type] of optionTypes)
|
|
||||||
|
for (const [name, type] of optionTypes)
|
||||||
renderOptionType(name, type);
|
renderOptionType(name, type);
|
||||||
|
|
||||||
for (let [name, type] of modelTypes)
|
for (const [name, type] of modelTypes)
|
||||||
renderModelType(name, type);
|
renderModelType(name, type);
|
||||||
|
|
||||||
for (let [name, literals] of enumTypes)
|
for (const [name, literals] of enumTypes)
|
||||||
renderEnum(name, literals);
|
renderEnum(name, literals);
|
||||||
|
|
||||||
if (process.argv[3] !== "--skip-format") {
|
if (process.argv[3] !== '--skip-format') {
|
||||||
// run the formatting tool for .net, to ensure the files are prepped
|
// run the formatting tool for .NET, to ensure the files are prepped
|
||||||
execSync(`dotnet format -f "${outputDir}" --include-generated --fix-whitespace`);
|
execSync(`dotnet format "${outputDir}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -273,10 +274,10 @@ function renderConstructors(name, type, out) {
|
||||||
out.push(`if(clone == null) return;`);
|
out.push(`if(clone == null) return;`);
|
||||||
|
|
||||||
type.properties.forEach(p => {
|
type.properties.forEach(p => {
|
||||||
let propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
|
const propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
|
||||||
let propName = toMemberName(p);
|
const propName = toMemberName(p);
|
||||||
const overloads = getPropertyOverloads(propType, p, propName, p.type);
|
const overloads = getPropertyOverloads(propType, p, propName, p.type);
|
||||||
for (let { name } of overloads)
|
for (const { name } of overloads)
|
||||||
out.push(`${name} = clone.${name};`);
|
out.push(`${name} = clone.${name};`);
|
||||||
});
|
});
|
||||||
out.push(`}`);
|
out.push(`}`);
|
||||||
|
|
@ -290,7 +291,7 @@ function renderConstructors(name, type, out) {
|
||||||
* @param {string[]} out
|
* @param {string[]} out
|
||||||
*/
|
*/
|
||||||
function renderMember(member, parent, options, out) {
|
function renderMember(member, parent, options, out) {
|
||||||
let name = toMemberName(member);
|
const name = toMemberName(member);
|
||||||
if (member.kind === 'method') {
|
if (member.kind === 'method') {
|
||||||
renderMethod(member, parent, name, { mode: 'options', trimRunAndPrefix: options.trimRunAndPrefix }, out);
|
renderMethod(member, parent, name, { mode: 'options', trimRunAndPrefix: options.trimRunAndPrefix }, out);
|
||||||
return;
|
return;
|
||||||
|
|
@ -315,12 +316,14 @@ function renderMember(member, parent, options, out) {
|
||||||
type = `IEnumerable<${parent.name}>`;
|
type = `IEnumerable<${parent.name}>`;
|
||||||
}
|
}
|
||||||
const overloads = getPropertyOverloads(type, member, name, parent);
|
const overloads = getPropertyOverloads(type, member, name, parent);
|
||||||
for (let { type, name, jsonName } of overloads) {
|
for (const overload of overloads) {
|
||||||
|
const { name, jsonName } = overload;
|
||||||
|
let { type } = overload;
|
||||||
out.push('');
|
out.push('');
|
||||||
if (member.spec)
|
if (member.spec)
|
||||||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||||
if (!member.clazz)
|
if (!member.clazz)
|
||||||
out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`)
|
out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`);
|
||||||
if (member.deprecated)
|
if (member.deprecated)
|
||||||
out.push(`[System.Obsolete]`);
|
out.push(`[System.Obsolete]`);
|
||||||
if (!type.endsWith('?') && !member.required)
|
if (!type.endsWith('?') && !member.required)
|
||||||
|
|
@ -376,10 +379,10 @@ function generateNameDefault(member, name, t, parent) {
|
||||||
return 'object';
|
return 'object';
|
||||||
|
|
||||||
// we'd get this call for enums, primarily
|
// we'd get this call for enums, primarily
|
||||||
let enumName = generateEnumNameIfApplicable(t);
|
const enumName = generateEnumNameIfApplicable(t);
|
||||||
if (!enumName && member) {
|
if (!enumName && member) {
|
||||||
if (member.kind === 'method' || member.kind === 'property') {
|
if (member.kind === 'method' || member.kind === 'property') {
|
||||||
let names = [
|
const names = [
|
||||||
parent.alias || parent.name,
|
parent.alias || parent.name,
|
||||||
toTitleCase(member.alias || member.name),
|
toTitleCase(member.alias || member.name),
|
||||||
toTitleCase(name),
|
toTitleCase(name),
|
||||||
|
|
@ -387,15 +390,15 @@ function generateNameDefault(member, name, t, parent) {
|
||||||
if (names[2] === names[1])
|
if (names[2] === names[1])
|
||||||
names.pop(); // get rid of duplicates, cheaply
|
names.pop(); // get rid of duplicates, cheaply
|
||||||
let attemptedName = names.pop();
|
let attemptedName = names.pop();
|
||||||
let typesDiffer = function (left, right) {
|
const typesDiffer = function(left, right) {
|
||||||
if (left.expression && right.expression)
|
if (left.expression && right.expression)
|
||||||
return left.expression !== right.expression;
|
return left.expression !== right.expression;
|
||||||
return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
|
return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
|
||||||
}
|
};
|
||||||
while (true) {
|
while (true) {
|
||||||
// crude attempt at removing plurality
|
// crude attempt at removing plurality
|
||||||
if (attemptedName.endsWith('s')
|
if (attemptedName.endsWith('s')
|
||||||
&& !["properties", "httpcredentials"].includes(attemptedName.toLowerCase()))
|
&& !['properties', 'httpcredentials'].includes(attemptedName.toLowerCase()))
|
||||||
attemptedName = attemptedName.substring(0, attemptedName.length - 1);
|
attemptedName = attemptedName.substring(0, attemptedName.length - 1);
|
||||||
|
|
||||||
// For some of these we don't want to generate generic types.
|
// For some of these we don't want to generate generic types.
|
||||||
|
|
@ -419,9 +422,9 @@ function generateNameDefault(member, name, t, parent) {
|
||||||
if (attemptedName === 'HeadersArray')
|
if (attemptedName === 'HeadersArray')
|
||||||
attemptedName = 'Header';
|
attemptedName = 'Header';
|
||||||
|
|
||||||
let probableType = modelTypes.get(attemptedName);
|
const probableType = modelTypes.get(attemptedName);
|
||||||
if ((probableType && typesDiffer(t, probableType))
|
if ((probableType && typesDiffer(t, probableType))
|
||||||
|| (["Value"].includes(attemptedName))) {
|
|| (['Value'].includes(attemptedName))) {
|
||||||
if (!names.length)
|
if (!names.length)
|
||||||
throw new Error(`Ran out of possible names: ${attemptedName}`);
|
throw new Error(`Ran out of possible names: ${attemptedName}`);
|
||||||
attemptedName = `${names.pop()}${attemptedName}`;
|
attemptedName = `${names.pop()}${attemptedName}`;
|
||||||
|
|
@ -434,9 +437,9 @@ function generateNameDefault(member, name, t, parent) {
|
||||||
return attemptedName;
|
return attemptedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.kind === 'event') {
|
if (member.kind === 'event')
|
||||||
return `${name}Payload`;
|
return `${name}Payload`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return enumName || t.name;
|
return enumName || t.name;
|
||||||
|
|
@ -453,9 +456,9 @@ function generateEnumNameIfApplicable(type) {
|
||||||
|
|
||||||
const potentialValues = type.union.filter(u => u.name.startsWith('"'));
|
const potentialValues = type.union.filter(u => u.name.startsWith('"'));
|
||||||
if ((potentialValues.length !== type.union.length)
|
if ((potentialValues.length !== type.union.length)
|
||||||
&& !(type.union[0].name === 'null' && potentialValues.length === type.union.length - 1)) {
|
&& !(type.union[0].name === 'null' && potentialValues.length === type.union.length - 1))
|
||||||
return null; // this isn't an enum, so we don't care, we let the caller generate the name
|
return null; // this isn't an enum, so we don't care, we let the caller generate the name
|
||||||
}
|
|
||||||
return type.name;
|
return type.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,7 +498,7 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
// TODO: this is something that will probably go into the docs
|
// TODO: this is something that will probably go into the docs
|
||||||
// translate simple getters into read-only properties, and simple
|
// translate simple getters into read-only properties, and simple
|
||||||
// set-only methods to settable properties
|
// set-only methods to settable properties
|
||||||
if (member.args.size == 0
|
if (member.args.size === 0
|
||||||
&& type !== 'void'
|
&& type !== 'void'
|
||||||
&& !name.startsWith('Get')
|
&& !name.startsWith('Get')
|
||||||
&& !name.startsWith('PostDataJSON')
|
&& !name.startsWith('PostDataJSON')
|
||||||
|
|
@ -511,9 +514,9 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: special case for generics handling!
|
// HACK: special case for generics handling!
|
||||||
if (type === 'T') {
|
if (type === 'T')
|
||||||
name = `${name}<T>`;
|
name = `${name}<T>`;
|
||||||
}
|
|
||||||
|
|
||||||
// adjust the return type for async methods
|
// adjust the return type for async methods
|
||||||
if (member.async) {
|
if (member.async) {
|
||||||
|
|
@ -525,11 +528,11 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
|
|
||||||
// render args
|
// render args
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
let args = [];
|
const args = [];
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
let explodedArgs = [];
|
const explodedArgs = [];
|
||||||
/** @type {Map<string, string>} */
|
/** @type {Map<string, string>} */
|
||||||
let argTypeMap = new Map([]);
|
const argTypeMap = new Map([]);
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} innerArgType
|
* @param {string} innerArgType
|
||||||
|
|
@ -540,11 +543,11 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
function pushArg(innerArgType, innerArgName, argument, isExploded = false) {
|
function pushArg(innerArgType, innerArgName, argument, isExploded = false) {
|
||||||
if (innerArgType === 'null')
|
if (innerArgType === 'null')
|
||||||
return;
|
return;
|
||||||
const requiredPrefix = (argument.required || isExploded) ? "" : "?";
|
const requiredPrefix = (argument.required || isExploded) ? '' : '?';
|
||||||
const requiredSuffix = (argument.required || isExploded) ? "" : " = default";
|
const requiredSuffix = (argument.required || isExploded) ? '' : ' = default';
|
||||||
var push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
|
const push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
|
||||||
if (isExploded)
|
if (isExploded)
|
||||||
explodedArgs.push(push)
|
explodedArgs.push(push);
|
||||||
else
|
else
|
||||||
args.push(push);
|
args.push(push);
|
||||||
argTypeMap.set(push, innerArgName);
|
argTypeMap.set(push, innerArgName);
|
||||||
|
|
@ -571,9 +574,9 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.type.expression === '[string]|[path]') {
|
if (arg.type.expression === '[string]|[path]') {
|
||||||
let argName = toArgumentName(arg.name);
|
const argName = toArgumentName(arg.name);
|
||||||
pushArg("string?", `${argName} = default`, arg);
|
pushArg('string?', `${argName} = default`, arg);
|
||||||
pushArg("string?", `${argName}Path = default`, arg);
|
pushArg('string?', `${argName}Path = default`, arg);
|
||||||
if (arg.spec) {
|
if (arg.spec) {
|
||||||
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
|
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
|
||||||
addParamsDoc(`${argName}Path`, [`Instead of specifying <paramref name="${argName}"/>, gives the file name to load from.`]);
|
addParamsDoc(`${argName}Path`, [`Instead of specifying <paramref name="${argName}"/>, gives the file name to load from.`]);
|
||||||
|
|
@ -582,9 +585,9 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
} else if (arg.type.expression === '[boolean]|[Array]<[string]>') {
|
} else if (arg.type.expression === '[boolean]|[Array]<[string]>') {
|
||||||
// HACK: this hurts my brain too
|
// HACK: this hurts my brain too
|
||||||
// we split this into two args, one boolean, with the logical name
|
// we split this into two args, one boolean, with the logical name
|
||||||
let argName = toArgumentName(arg.name);
|
const argName = toArgumentName(arg.name);
|
||||||
let leftArgType = translateType(arg.type.union[0], parent, (t) => { throw new Error('Not supported'); });
|
const leftArgType = translateType(arg.type.union[0], parent, t => { throw new Error('Not supported'); });
|
||||||
let rightArgType = translateType(arg.type.union[1], parent, (t) => { throw new Error('Not supported'); });
|
const rightArgType = translateType(arg.type.union[1], parent, t => { throw new Error('Not supported'); });
|
||||||
|
|
||||||
pushArg(leftArgType, argName, arg);
|
pushArg(leftArgType, argName, arg);
|
||||||
pushArg(rightArgType, `${argName}Values`, arg);
|
pushArg(rightArgType, `${argName}Values`, arg);
|
||||||
|
|
@ -596,15 +599,15 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const argName = toArgumentName(arg.alias || arg.name);
|
const argName = toArgumentName(arg.alias || arg.name);
|
||||||
const argType = translateType(arg.type, parent, (t) => generateNameDefault(member, argName, t, parent));
|
const argType = translateType(arg.type, parent, t => generateNameDefault(member, argName, t, parent));
|
||||||
|
|
||||||
if (argType === null && arg.type.union) {
|
if (argType === null && arg.type.union) {
|
||||||
// we might have to split this into multiple arguments
|
// we might have to split this into multiple arguments
|
||||||
let translatedArguments = arg.type.union.map(t => translateType(t, parent, (x) => generateNameDefault(member, argName, x, parent)));
|
const translatedArguments = arg.type.union.map(t => translateType(t, parent, x => generateNameDefault(member, argName, x, parent)));
|
||||||
if (translatedArguments.includes(null))
|
if (translatedArguments.includes(null))
|
||||||
throw new Error('Unexpected null in translated argument types. Aborting.');
|
throw new Error('Unexpected null in translated argument types. Aborting.');
|
||||||
|
|
||||||
let argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth);
|
const argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth);
|
||||||
for (const newArg of translatedArguments) {
|
for (const newArg of translatedArguments) {
|
||||||
pushArg(newArg, argName, arg, true); // push the exploded arg
|
pushArg(newArg, argName, arg, true); // push the exploded arg
|
||||||
addParamsDoc(argName, argDocumentation);
|
addParamsDoc(argName, argDocumentation);
|
||||||
|
|
@ -677,17 +680,17 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
explodedArgs.forEach((explodedArg, argIndex) => {
|
explodedArgs.forEach((explodedArg, argIndex) => {
|
||||||
if (!options.nodocs)
|
if (!options.nodocs)
|
||||||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||||
let overloadedArgs = [];
|
const overloadedArgs = [];
|
||||||
for (var i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
let arg = args[i];
|
const arg = args[i];
|
||||||
if (arg === 'EXPLODED_ARG' || arg === 'OPTIONAL_EXPLODED_ARG') {
|
if (arg === 'EXPLODED_ARG' || arg === 'OPTIONAL_EXPLODED_ARG') {
|
||||||
containsOptionalExplodedArgs = arg === 'OPTIONAL_EXPLODED_ARG';
|
containsOptionalExplodedArgs = arg === 'OPTIONAL_EXPLODED_ARG';
|
||||||
let argType = argTypeMap.get(explodedArg);
|
const argType = argTypeMap.get(explodedArg);
|
||||||
if (!options.nodocs)
|
if (!options.nodocs)
|
||||||
printArgDoc(argType, paramDocs.get(argType), out);
|
printArgDoc(argType, paramDocs.get(argType), out);
|
||||||
overloadedArgs.push(explodedArg);
|
overloadedArgs.push(explodedArg);
|
||||||
} else {
|
} else {
|
||||||
let argType = argTypeMap.get(arg);
|
const argType = argTypeMap.get(arg);
|
||||||
if (!options.nodocs)
|
if (!options.nodocs)
|
||||||
printArgDoc(argType, paramDocs.get(argType), out);
|
printArgDoc(argType, paramDocs.get(argType), out);
|
||||||
overloadedArgs.push(arg);
|
overloadedArgs.push(arg);
|
||||||
|
|
@ -703,13 +706,13 @@ function renderMethod(member, parent, name, options, out) {
|
||||||
// That particular overload only contains the required arguments, or rather
|
// That particular overload only contains the required arguments, or rather
|
||||||
// contains all the arguments *except* the exploded ones.
|
// contains all the arguments *except* the exploded ones.
|
||||||
if (containsOptionalExplodedArgs) {
|
if (containsOptionalExplodedArgs) {
|
||||||
var filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
|
const filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
|
||||||
if (!options.nodocs)
|
if (!options.nodocs)
|
||||||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||||
filteredArgs.forEach((arg) => {
|
filteredArgs.forEach(arg => {
|
||||||
if (arg === 'EXPLODED_ARG')
|
if (arg === 'EXPLODED_ARG')
|
||||||
throw new Error(`Unsupported required union arg combined an optional union inside ${member.name}`);
|
throw new Error(`Unsupported required union arg combined an optional union inside ${member.name}`);
|
||||||
let argType = argTypeMap.get(arg);
|
const argType = argTypeMap.get(arg);
|
||||||
if (!options.nodocs)
|
if (!options.nodocs)
|
||||||
printArgDoc(argType, paramDocs.get(argType), out);
|
printArgDoc(argType, paramDocs.get(argType), out);
|
||||||
});
|
});
|
||||||
|
|
@ -741,7 +744,7 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
console.warn(`${type.name} should be a 'string', but was a ${type.expression}`);
|
console.warn(`${type.name} should be a 'string', but was a ${type.expression}`);
|
||||||
return `string`;
|
return `string`;
|
||||||
}
|
}
|
||||||
if (type.union.length == 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name)
|
if (type.union.length === 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name)
|
||||||
return `IEnumerable<${type.union[0].name}>`; // an example of this is [string]|[Array]<[string]>
|
return `IEnumerable<${type.union[0].name}>`; // an example of this is [string]|[Array]<[string]>
|
||||||
if (type.expression === '[float]|"raf"')
|
if (type.expression === '[float]|"raf"')
|
||||||
return `Polling`; // hardcoded because there's no other way to denote this
|
return `Polling`; // hardcoded because there's no other way to denote this
|
||||||
|
|
@ -755,20 +758,20 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.name === 'Array') {
|
if (type.name === 'Array') {
|
||||||
if (type.templates.length != 1)
|
if (type.templates.length !== 1)
|
||||||
throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`);
|
throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`);
|
||||||
|
|
||||||
let innerType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
const innerType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||||
return isReturnType ? `IReadOnlyList<${innerType}>` : `IEnumerable<${innerType}>`;
|
return isReturnType ? `IReadOnlyList<${innerType}>` : `IEnumerable<${innerType}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.name === 'Object') {
|
if (type.name === 'Object') {
|
||||||
// take care of some common cases
|
// take care of some common cases
|
||||||
// TODO: this can be genericized
|
// TODO: this can be genericized
|
||||||
if (type.templates && type.templates.length == 2) {
|
if (type.templates && type.templates.length === 2) {
|
||||||
// get the inner types of both templates, and if they're strings, it's a keyvaluepair string, string,
|
// get the inner types of both templates, and if they're strings, it's a keyvaluepair string, string,
|
||||||
let keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||||
let valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
||||||
if (parent.name === 'Request' || parent.name === 'Response')
|
if (parent.name === 'Request' || parent.name === 'Response')
|
||||||
return `Dictionary<${keyType}, ${valueType}>`;
|
return `Dictionary<${keyType}, ${valueType}>`;
|
||||||
return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`;
|
return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`;
|
||||||
|
|
@ -776,24 +779,24 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
|
|
||||||
if ((type.name === 'Object')
|
if ((type.name === 'Object')
|
||||||
&& !type.properties
|
&& !type.properties
|
||||||
&& !type.union) {
|
&& !type.union)
|
||||||
return 'object';
|
return 'object';
|
||||||
}
|
|
||||||
// this is an additional type that we need to generate
|
// this is an additional type that we need to generate
|
||||||
let objectName = generateNameCallback(type);
|
const objectName = generateNameCallback(type);
|
||||||
if (objectName === 'Object') {
|
if (objectName === 'Object')
|
||||||
throw new Error('Object unexpected');
|
throw new Error('Object unexpected');
|
||||||
} else if (type.name === 'Object') {
|
else if (type.name === 'Object')
|
||||||
registerModelType(objectName, type);
|
registerModelType(objectName, type);
|
||||||
}
|
|
||||||
return `${objectName}${optional ? '?' : ''}`;
|
return `${objectName}${optional ? '?' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.name === 'Map') {
|
if (type.name === 'Map') {
|
||||||
if (type.templates && type.templates.length == 2) {
|
if (type.templates && type.templates.length === 2) {
|
||||||
// we map to a dictionary
|
// we map to a dictionary
|
||||||
let keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||||
let valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
||||||
return `Dictionary<${keyType}, ${valueType}>`;
|
return `Dictionary<${keyType}, ${valueType}>`;
|
||||||
} else {
|
} else {
|
||||||
throw 'Map has invalid number of templates.';
|
throw 'Map has invalid number of templates.';
|
||||||
|
|
@ -806,7 +809,7 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
|
|
||||||
let argsList = '';
|
let argsList = '';
|
||||||
if (type.args) {
|
if (type.args) {
|
||||||
let translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback, false, isReturnType));
|
const translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback, false, isReturnType));
|
||||||
if (translatedCallbackArguments.includes(null))
|
if (translatedCallbackArguments.includes(null))
|
||||||
throw new Error('There was an argument we could not parse. Aborting.');
|
throw new Error('There was an argument we could not parse. Aborting.');
|
||||||
|
|
||||||
|
|
@ -817,8 +820,8 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
// this is an Action
|
// this is an Action
|
||||||
return `Action<${argsList}>`;
|
return `Action<${argsList}>`;
|
||||||
} else {
|
} else {
|
||||||
let returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
|
const returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
|
||||||
if (returnType == null)
|
if (returnType === null)
|
||||||
throw new Error('Unexpected null as return type.');
|
throw new Error('Unexpected null as return type.');
|
||||||
|
|
||||||
return `Func<${argsList}, ${returnType}>`;
|
return `Func<${argsList}, ${returnType}>`;
|
||||||
|
|
@ -828,13 +831,13 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
|
||||||
if (type.templates) {
|
if (type.templates) {
|
||||||
// this should mean we have a generic type and we can translate that
|
// this should mean we have a generic type and we can translate that
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
var types = type.templates.map(template => translateType(template, parent));
|
const types = type.templates.map(template => translateType(template, parent));
|
||||||
return `${type.name}<${types.join(', ')}>`
|
return `${type.name}<${types.join(', ')}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// there's a chance this is a name we've already seen before, so check
|
// there's a chance this is a name we've already seen before, so check
|
||||||
// this is also where we map known types, like boolean -> bool, etc.
|
// this is also where we map known types, like boolean -> bool, etc.
|
||||||
let name = classNameMap.get(type.name) || type.name;
|
const name = classNameMap.get(type.name) || type.name;
|
||||||
return `${name}${optional ? '?' : ''}`;
|
return `${name}${optional ? '?' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -848,7 +851,7 @@ function registerModelType(typeName, type) {
|
||||||
if (typeName.endsWith('Option'))
|
if (typeName.endsWith('Option'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let potentialType = modelTypes.get(typeName);
|
const potentialType = modelTypes.get(typeName);
|
||||||
if (potentialType) {
|
if (potentialType) {
|
||||||
// console.log(`Type ${typeName} already exists, so skipping...`);
|
// console.log(`Type ${typeName} already exists, so skipping...`);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue