Compare commits

...

31 commits

Author SHA1 Message Date
Andrey Lushnikov c3a035560a
chore: mark v1.19.2 (#12320) 2022-02-23 15:00:31 -08:00
Andrey Lushnikov 77d9a8e1f6
cherry-pick(#12323): fix: tolerate EBUSY error when removing output directories (#12324)
SHA: 71edad327b

- Previous attempt: https://github.com/microsoft/playwright/pull/12300
- Revert with reasoning: ebc52d10e4

Fixes #12106
2022-02-23 14:53:47 -08:00
Andrey Lushnikov d07e05fa7b
Revert "cherry-pick(#12300): chore: best-effort cleanup for output folders that are mounted (#12318)" (#12322)
This reverts commit 0a1c1dad67.

Reason for revert: turns out this fix results in a 5-second delay
when starting tests in docker, with `test-results` folder being
a non-removable mount.

The reason for the delay is the `maxBusyTries` option that we
supply by default to rimraf when trying to remove the folder.

While this option might come handy when removing temporary
browser profile folder, it doesn't serve us well in this particular
usecase.

References #12106
2022-02-23 14:10:27 -08:00
Andrey Lushnikov 0a1c1dad67
cherry-pick(#12300): chore: best-effort cleanup for output folders that are mounted (#12318)
Fixes #12106
2022-02-23 13:15:35 -08:00
Dmitry Gozman 2e4167027b
cherry-pick(#12291): fix(list mode): keep outputDir intact (#12293) 2022-02-22 13:39:56 -08:00
Pavel Feldman 5b17ca9d56 cherry-pick(#12250): fix(electron): do not attach external debugger when running Electron tests 2022-02-19 19:00:41 -08:00
Yury Semikhatsky 0037acffc6
cherry-pick(1.19): open all test traces in one viewer (#12142) (#12163) 2022-02-16 09:56:13 -08:00
Andrey Lushnikov d22bde13c4
chore: mark v1.19.1 (#12132) 2022-02-15 13:25:32 -08:00
Andrey Lushnikov e57b4b5073
cherry-pick(#12070): fix: propagate exit code in experimental mode (#12133)
SHA: 5db7ce5964

In experimental ESM mode a child process is forked in order to run the tests. Currently the exit code of this child process is not propagated to the exit code of the parent process, which means that the process exits with a status code of `0` even if some of the tests failed.

This makes it difficult to use Playwright in CI in experimental mode, as the CI pipeline as a whole will pass despite the test failures.

This change addresses this by propagating the exit code in the case where it is non-zero.

Co-authored-by: pierscowburn <me@pierscowburn.com>
2022-02-15 13:25:14 -08:00
Andrey Lushnikov 46aeb8fe3d
cherry-pick(#12124):fix(docker): add missing dependency to the docker 1.19 (#12127)
SHA: e6d79a4f10

The `libxtst6` is required in both amd64 and arm64.

Fixes #12075
2022-02-15 12:49:33 -08:00
Yury Semikhatsky d8bc6dbeea
cherry-pick(1.19): always return non-empty body regardless of request method (#12102) (#12121) 2022-02-15 11:24:15 -08:00
Andrey Lushnikov 03501cfdb2
cherry-pick(#12048): tests: fix installation tests (#12050)
SHA: 94fc45a3db

Follow-up to da2cecbea0
2022-02-11 13:18:20 -08:00
Andrey Lushnikov 241add240c
cherry-pick(#12025): chore(dotnet): do not use global CLI and use ps1 instead (#12047)
SHA: 7e7996a7b7
Fixes https://github.com/microsoft/playwright-dotnet/issues/2005

Co-authored-by: Max Schmitt <max@schmitt.mx>
2022-02-11 11:29:04 -08:00
Andrey Lushnikov 92aa600af2
cherry-pick(#12045): docs: add release notes for all the languages (#12046)
SHA 619d1d8617
2022-02-11 11:25:47 -08:00
Max Schmitt c098cafb7a
cherry-pick(release-1.19): chore: fix .NET generation script for .NET 6 (#12041)
PR: #11965
2022-02-11 11:19:22 -08:00
Andrey Lushnikov aafaa2b9ed
chore: mark v1.19.0 (#11981) 2022-02-11 09:42:54 -08:00
Andrey Lushnikov b9d665caf0
cherry-pick(#12003): docs: avoid .net version ambiguity (#12040)
SHA 1df07aa2cf

Co-authored-by: Erik Ejlskov Jensen <ErikEJ@users.noreply.github.com>
2022-02-11 09:39:50 -08:00
Andrey Lushnikov 1d4521a12e
cherry-pick(#12036): docs: fix release notes headers (#12037)
SHA 46b89f109a
2022-02-11 09:15:06 -08:00
Andrey Lushnikov 7a683a9331
cherry-pick(#12019) docs: add release notes for 1.19 (#12034)
SHA: 979fa2b2f0
2022-02-11 09:05:40 -08:00
Dmitry Gozman ca116db2cb
cherry-pick(#12016): fix(reporters): correctly handle missing stdout.columns (#12033)
When columns are not available, do not trim the output.
2022-02-11 08:58:13 -08:00
Pavel Feldman 8a52c3ca76 cherry-pick(#12022): chore: don't close page in generated test 2022-02-11 08:12:54 -08:00
Max Schmitt d73d188ae7
cherry-pick(release-1.19): docs(python): enable Route.fulfill.response (#12030)
2815180162
2022-02-11 15:27:37 +01:00
Pavel Feldman 73d78f5988 cherry-pick(#12020): chore: headless mode for codegen 2022-02-10 21:24:22 -08:00
Pavel Feldman 55be85284c cherry-pick(#12012): fix(teardown): await teardown in failed test runs 2022-02-10 12:47:06 -08:00
Pavel Feldman 35f921e7aa cherry-pick(#12004): chore: revert "fix(test-runner): escape backslashes in win cli" 2022-02-10 08:55:56 -08:00
Andrey Lushnikov 786bb337f0
cherry-pick(#11991): fix(mac): avoid printing empty line to stderr on mac (#11993)
SHA: 1f6b84f445

It turns out, `sw_vers` prints an empty stderr line and we inherit it.

Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
2022-02-09 15:38:30 -08:00
Andrey Lushnikov 8f1f97f508
cherry-pick(#11984): fix(test-runner): fix browser initialization in test modifiers (#11992)
SHA: 6904b3294e

Fixes #11985
2022-02-09 15:35:36 -08:00
Andrey Lushnikov b651920bd5
cherry-pick(#11974): test: fix tests for chromium-based browser channels (#11979)
SHA: 439c8e9c40
2022-02-09 11:40:50 -08:00
Andrey Lushnikov 4959558527
cherry-pick(#11973): fix: proper chrome-beta channel installation on MacOS (#11978)
SHA: 1e1a6acaf7

chrome-beta installation on MacOS should download universal binaries.

The old download URL for chrome-beta was downloading Chrome Beta M96
2022-02-09 11:40:41 -08:00
Yury Semikhatsky eaeb7de95a
cherry-pick(#11954): respect tracing config for APIRequestContext (#11976)
706c897031

Fixes #10585
2022-02-09 11:31:10 -08:00
Pavel Feldman 72a767fd4d cherry-pick(#11953): feat(debug): allow preprocessing JS scripts as well 2022-02-09 10:23:00 -08:00
55 changed files with 817 additions and 324 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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`].

View file

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

View file

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

View file

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

View file

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

@ -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": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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');
} }

View file

@ -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`;
} }

View file

@ -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);
} }

View file

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

View file

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

View file

@ -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)});`);

View file

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

View file

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

View file

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

View file

@ -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}`;
} }

View file

@ -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') {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
});

View file

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

View file

@ -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': `

View file

@ -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);
});

View file

@ -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': `

View file

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

View file

@ -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': `

View file

@ -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++) {

View file

@ -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': `

View file

@ -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);
});

View file

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

View file

@ -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}"`);
} }
/** /**
@ -238,9 +239,9 @@ function toArgumentName(name) {
return name === 'event' ? `@${name}` : name; return name === 'event' ? `@${name}` : name;
} }
/** /**
* @param {Documentation.Member} member * @param {Documentation.Member} member
*/ */
function toMemberName(member, makeAsync = false) { function toMemberName(member, makeAsync = false) {
const assumedName = toTitleCase(member.alias || member.name); const assumedName = toTitleCase(member.alias || member.name);
if (member.kind === 'interface') if (member.kind === 'interface')
@ -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);
@ -630,7 +633,7 @@ function renderMethod(member, parent, name, options, out) {
modifiers = 'public '; modifiers = 'public ';
member.argsArray member.argsArray
.sort((a, b) => b.alias === 'options' ? -1 : 0) //move options to the back to the arguments list .sort((a, b) => b.alias === 'options' ? -1 : 0) // move options to the back to the arguments list
.forEach(processArg); .forEach(processArg);
let body = ';'; let body = ';';
@ -669,7 +672,7 @@ function renderMethod(member, parent, name, options, out) {
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
paramDocs.forEach((value, i) => printArgDoc(i, value, out)); paramDocs.forEach((value, i) => printArgDoc(i, value, out));
} }
if(member.deprecated) if (member.deprecated)
out.push(`[System.Obsolete]`); out.push(`[System.Obsolete]`);
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')})${body}`); out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')})${body}`);
} else { } else {
@ -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;