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.
### option: Route.fulfill.response
* langs: js, java
* langs: js, java, python
- `response` <[APIResponse]>
[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
* 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
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.
```bash csharp
# 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:
```bash csharp
# Install WebKit
playwright install webkit
pwsh bin\Debug\netX\playwright.ps1 install webkit
```
See all supported browsers:
```bash csharp
playwright install --help
pwsh bin\Debug\netX\playwright.ps1 install --help
```
## Managing browser binaries
@ -230,17 +230,17 @@ mvn test
```bash bash-flavor=bash lang=csharp
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=batch lang=csharp
set PLAYWRIGHT_BROWSERS_PATH=%USERPROFILE%\pw-browsers
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=powershell lang=csharp
$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.
@ -398,17 +398,17 @@ mvn test
```
```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
set HTTPS_PROXY=https://192.0.2.1
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=powershell lang=csharp
$env:HTTPS_PROXY="https://192.0.2.1"
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
## Download from artifact repository
@ -479,17 +479,17 @@ mvn test
```
```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
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=powershell lang=csharp
$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
@ -563,19 +563,19 @@ mvn test
```
```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
set PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=203.0.113.3
set PLAYWRIGHT_DOWNLOAD_HOST=192.0.2.1
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=powershell lang=csharp
$env:PLAYWRIGHT_DOWNLOAD_HOST="192.0.2.1"
$env:PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST="203.0.113.3"
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
## Skip browser downloads
@ -617,17 +617,17 @@ mvn test
```
```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
set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
```bash bash-flavor=powershell lang=csharp
$env:PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
playwright install
pwsh bin\Debug\netX\playwright.ps1 install
```
## Download single browser binary

View file

@ -22,10 +22,8 @@ playwright
```
```bash csharp
# Install the CLI once.
dotnet tool install --global Microsoft.Playwright.CLI
# Use the tools.
playwright
pwsh bin\Debug\netX\playwright.ps1 --help
```
```json js
@ -58,7 +56,7 @@ playwright install
```bash csharp
# 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:
@ -80,7 +78,7 @@ playwright install webkit
```bash csharp
# Install WebKit
playwright install webkit
pwsh bin\Debug\netX\playwright.ps1 install webkit
```
See all supported browsers:
@ -174,7 +172,7 @@ playwright codegen --load-storage=auth.json my.web.app
```bash csharp
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.
```
@ -300,7 +298,7 @@ playwright open example.com
```bash csharp
# Open page in Chromium
playwright open example.com
pwsh bin\Debug\netX\playwright.ps1 open example.com
```
```bash js
@ -320,7 +318,7 @@ playwright wk example.com
```bash csharp
# Open page in WebKit
playwright wk example.com
pwsh bin\Debug\netX\playwright.ps1 wk example.com
```
### Emulate devices
@ -343,7 +341,7 @@ playwright open --device="iPhone 11" wikipedia.org
```bash csharp
# 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
@ -365,7 +363,7 @@ playwright open --viewport-size=800,600 --color-scheme=dark twitter.com
```bash csharp
# 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
@ -391,7 +389,7 @@ playwright open --timezone="Europe/Rome" --geolocation="41.890221,12.492348" --l
```bash csharp
# Emulate timezone, language & location
# 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
@ -491,7 +489,7 @@ playwright screenshot \
```bash csharp
# 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" \
--color-scheme=dark \
--wait-for-timeout=3000 \
@ -515,7 +513,7 @@ playwright screenshot --full-page en.wikipedia.org wiki-full.png
```bash csharp
# 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
@ -539,7 +537,7 @@ playwright pdf https://en.wikipedia.org/wiki/PDF wiki.pdf
```bash csharp
# 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
@ -563,7 +561,7 @@ playwright install-deps
```bash csharp
# 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:

View file

@ -22,7 +22,7 @@ playwright codegen wikipedia.org
```
```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.
@ -52,7 +52,7 @@ playwright codegen --save-storage=auth.json
```
```bash csharp
playwright codegen --save-storage=auth.json
pwsh bin\Debug\netX\playwright.ps1 codegen --save-storage=auth.json
# Perform authentication and exit.
# auth.json will contain the storage state.
```
@ -79,8 +79,8 @@ playwright codegen --load-storage=auth.json my.web.app
```
```bash csharp
playwright open --load-storage=auth.json my.web.app
playwright codegen --load-storage=auth.json my.web.app
pwsh bin\Debug\netX\playwright.ps1 open --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.
```
@ -206,7 +206,7 @@ playwright codegen --device="iPhone 11" wikipedia.org
```bash csharp
# 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
@ -230,7 +230,7 @@ playwright codegen --viewport-size=800,600 --color-scheme=dark twitter.com
```bash csharp
# 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
@ -256,5 +256,5 @@ playwright codegen --timezone="Europe/Rome" --geolocation="41.890221,12.492348"
```bash csharp
# Emulate timezone, language & location
# 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
```
```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:
- Browsers launch in the headed mode
- Default timeout is set to 0 (= no timeout)
@ -105,6 +119,10 @@ configures Playwright for debugging and opens the inspector.
playwright codegen wikipedia.org
```
```bash csharp
pwsh bin\Debug\netX\playwright.ps1 codegen wikipedia.org
```
## Stepping through the Playwright script
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
# Build the project
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
# 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 -->
## 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
### 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.
- You can now use Playwright to install stable version of Edge on Linux:
```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
- 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
Read more about [Trace Viewer](./trace-viewer).

View file

@ -5,6 +5,33 @@ title: "Release notes"
<!-- 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
### 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.
- You can now use Playwright to install stable version of Edge on Linux:
```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
- 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
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()`.
- Playwright now includes [command line interface](./cli.md), former playwright-cli.
```bash js
npx playwright --help
```
```bash python
playwright --help
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="--help"
```
- [`method: Page.selectOption`] now waits for the options to be present.
- New methods to [assert element state](./actionability#assertions) like [`method: Page.isEditable`].

View file

@ -5,6 +5,90 @@ title: "Release notes"
<!-- 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
### Locator Improvements

View file

@ -5,6 +5,38 @@ title: "Release notes"
<!-- 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
### API Testing

View file

@ -151,7 +151,7 @@ playwright show-trace trace.zip
```
```bash csharp
playwright show-trace trace.zip
pwsh bin\Debug\netX\playwright.ps1 show-trace trace.zip
```
## Actions
@ -218,5 +218,5 @@ playwright show-trace https://example.com/trace.zip
```
```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"
exit 1
fi
if [[ "${OUTPUT}" != *"page.close"* ]]; then
echo "ERROR: missing page.close in the output"
exit 1
fi
echo "Running 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

36
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.19.0-next",
"version": "1.19.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.19.0-next",
"version": "1.19.2",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -7515,11 +7515,11 @@
},
"packages/html-reporter": {},
"packages/playwright": {
"version": "1.19.0-next",
"version": "1.19.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
},
"bin": {
"playwright": "cli.js"
@ -7529,11 +7529,11 @@
}
},
"packages/playwright-chromium": {
"version": "1.19.0-next",
"version": "1.19.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
},
"bin": {
"playwright": "cli.js"
@ -7543,7 +7543,7 @@
}
},
"packages/playwright-core": {
"version": "1.19.0-next",
"version": "1.19.2",
"license": "Apache-2.0",
"dependencies": {
"commander": "8.3.0",
@ -7579,11 +7579,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.19.0-next",
"version": "1.19.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
},
"bin": {
"playwright": "cli.js"
@ -7594,7 +7594,7 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.19.0-next",
"version": "1.19.2",
"license": "Apache-2.0",
"dependencies": {
"@babel/code-frame": "7.16.7",
@ -7629,7 +7629,7 @@
"open": "8.4.0",
"pirates": "4.0.4",
"pixelmatch": "5.2.1",
"playwright-core": "1.19.0-next",
"playwright-core": "1.19.2",
"pngjs": "6.0.0",
"rimraf": "3.0.2",
"source-map-support": "0.4.18",
@ -7673,11 +7673,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.19.0-next",
"version": "1.19.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
},
"bin": {
"playwright": "cli.js"
@ -8446,7 +8446,7 @@
"open": "8.4.0",
"pirates": "4.0.4",
"pixelmatch": "5.2.1",
"playwright-core": "1.19.0-next",
"playwright-core": "1.19.2",
"pngjs": "6.0.0",
"rimraf": "3.0.2",
"source-map-support": "0.4.18",
@ -12120,13 +12120,13 @@
"playwright": {
"version": "file:packages/playwright",
"requires": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
},
"playwright-chromium": {
"version": "file:packages/playwright-chromium",
"requires": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
},
"playwright-core": {
@ -12160,13 +12160,13 @@
"playwright-firefox": {
"version": "file:packages/playwright-firefox",
"requires": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
},
"playwright-webkit": {
"version": "file:packages/playwright-webkit",
"requires": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
},
"pngjs": {

View file

@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",

View file

@ -68,10 +68,11 @@ export const ProjectLink: React.FunctionComponent<{
export const AttachmentLink: React.FunctionComponent<{
attachment: TestAttachment,
href?: string,
}> = ({ attachment, href }) => {
linkName?: string,
}> = ({ attachment, href, linkName }) => {
return <TreeItem title={<span>
{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>}
</span>} loadChildren={attachment.body ? () => {
return [<div className='attachment-body'>{attachment.body}</div>];

View file

@ -75,12 +75,12 @@ export const TestResultView: React.FC<{
</AutoChip>}
{!!traces.length && <AutoChip header='Traces'>
{traces.map((a, i) => <div key={`trace-${i}`}>
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
{<div>
<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 }} />
</a>
<AttachmentLink attachment={a}></AttachmentLink>
</div>)}
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
</div>}
</AutoChip>}
{!!videos.length && <AutoChip header='Videos'>

View file

@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate Chromium",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
@ -26,6 +26,6 @@
"install": "node install.js"
},
"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"
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
cp -rf "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
hdiutil detach /Volumes/googlechromebeta.dmg

View file

@ -4,7 +4,7 @@ set -x
rm -rf "/Applications/Google Chrome.app"
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
cp -rf "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
hdiutil detach /Volumes/googlechrome.dmg

View file

@ -1,6 +1,6 @@
{
"name": "playwright-core",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",

View file

@ -19,9 +19,13 @@ import { fork } from 'child_process';
if (process.env.PW_EXPERIMENTAL_TS_ESM) {
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 }
});
innerProcess.on('close', code => {
if (code !== 0 && code !== null) process.exit(code);
});
} else {
require('./innerCli');
}

View file

@ -611,7 +611,7 @@ function buildBasePlaywrightCLICommand(cliTargetLang: string | undefined): strin
case 'java':
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="...options.."`;
case 'csharp':
return `playwright`;
return `pwsh bin\\Debug\\netX\\playwright.ps1`;
default:
return `npx playwright`;
}

View file

@ -74,13 +74,14 @@ export class APIRequest implements api.APIRequest {
})).request);
context._tracing._localUtils = this._playwright._utils;
this._contexts.add(context);
context._request = this;
await this._onDidCreateContext?.(context);
return context;
}
}
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
private _request?: APIRequest;
_request?: APIRequest;
readonly _tracing: Tracing;
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) {
super(parent, type, guid, initializer, createInstrumentation());
if (parent instanceof APIRequest)
this._request = parent;
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 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({
command: options.executablePath || require('electron/index.js'),
args: electronArguments,
env: options.env ? envArrayToObject(options.env) : process.env,
env,
log: (message: string) => {
progress.log(message);
browserLogsCollector.log(message);

View file

@ -18,7 +18,7 @@ import * as http from 'http';
import * as https from 'https';
import { HttpsProxyAgent } from 'https-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 zlib from 'zlib';
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 transform: Transform | undefined;
const encoding = response.headers['content-encoding'];
@ -362,7 +355,9 @@ export abstract class APIRequestContext extends SdkObject {
transform = zlib.createInflate();
}
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)
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 {
private readonly _context: BrowserContext;

View file

@ -36,14 +36,15 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
generateAction(actionInContext: ActionInContext): string {
const action = actionInContext.action;
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
return '';
const pageAlias = actionInContext.frame.pageAlias;
const formatter = new JavaScriptFormatter(2);
formatter.newLine();
formatter.add('// ' + actionTitle(action));
if (action.name === 'openPage') {
if (this._isTest)
return '';
formatter.add(`const ${pageAlias} = await context.newPage();`);
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
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;
readonly wsEndpoint: string | undefined;
@ -85,7 +96,9 @@ export class RecorderApp extends EventEmitter {
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 args = [
'--app=data:text/html,',
@ -163,3 +176,14 @@ export class RecorderApp extends EventEmitter {
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 * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { RecorderApp } from './recorder/recorderApp';
import { IRecorderApp, RecorderApp } from './recorder/recorderApp';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { Point } from '../../common/types';
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
@ -46,7 +46,7 @@ export class RecorderSupplement implements InstrumentationListener {
private _context: BrowserContext;
private _mode: Mode;
private _highlightedSelector = '';
private _recorderApp: RecorderApp | null = null;
private _recorderApp: IRecorderApp | null = null;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
private _recorderSources: Source[] = [];
private _userSources = new Map<string, Source>();
@ -317,7 +317,7 @@ class ContextRecorder extends EventEmitter {
this._recorderSources = [];
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', () => {
this._recorderSources = [];
for (const languageGenerator of orderedLanguages) {
@ -330,21 +330,19 @@ class ContextRecorder extends EventEmitter {
source.revealLine = source.text.split('\n').length - 1;
this._recorderSources.push(source);
if (languageGenerator === orderedLanguages[0])
text = source.text;
throttledOutputFile?.setContent(source.text);
}
this.emit(ContextRecorder.Events.Change, {
sources: this._recorderSources,
primaryFileName: primaryLanguage.fileName
});
});
if (params.outputFile) {
if (throttledOutputFile) {
context.on(BrowserContext.Events.BeforeClose, () => {
fs.writeFileSync(params.outputFile!, text);
text = '';
throttledOutputFile.flush();
});
process.on('exit', () => {
if (text)
fs.writeFileSync(params.outputFile!, text);
throttledOutputFile.flush();
});
}
this._generator = generator;
@ -590,3 +588,29 @@ function languageForFile(file: string) {
return 'csharp';
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',
'libxrender1',
'libxt6',
'libxtst6'
],
webkit: [
'gstreamer1.0-libav',
@ -646,7 +647,6 @@ deps['ubuntu20.04-arm64'] = {
chromium: [...deps['ubuntu20.04'].chromium],
firefox: [
...deps['ubuntu20.04'].firefox,
'libxtst6'
],
webkit: [
...deps['ubuntu20.04'].webkit,

View file

@ -716,7 +716,7 @@ export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: strin
case 'java':
return `mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="${parameters}"`;
case 'csharp':
return `playwright ${parameters}`;
return `pwsh bin\\Debug\\netX\\playwright.ps1 ${parameters}`;
default:
return `npx playwright ${parameters}`;
}

View file

@ -457,7 +457,7 @@ function determineUserAgent(): string {
osIdentifier = 'windows';
osVersion = `${version[0]}.${version[1]}`;
} 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';
osVersion = `${version[0]}.${version[1]}`;
} else if (process.platform === 'linux') {

View file

@ -1,6 +1,6 @@
{
"name": "playwright-firefox",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate Firefox",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
@ -26,6 +26,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/test",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
@ -59,7 +59,7 @@
"open": "8.4.0",
"pirates": "4.0.4",
"pixelmatch": "5.2.1",
"playwright-core": "1.19.0-next",
"playwright-core": "1.19.2",
"pngjs": "6.0.0",
"rimraf": "3.0.2",
"source-map-support": "0.4.18",

View file

@ -181,7 +181,7 @@ function forceRegExp(pattern: string): RegExp {
const match = pattern.match(/^\/(.*)\/([gi]*)$/);
if (match)
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 {

View file

@ -478,7 +478,7 @@ class Worker extends EventEmitter {
detached: false,
env: {
FORCE_COLOR: '1',
DEBUG_COLORS: process.stdout.isTTY ? '1' : '0',
DEBUG_COLORS: '1',
TEST_WORKER_INDEX: String(this.workerIndex),
TEST_PARALLEL_INDEX: String(this.parallelIndex),
...process.env

View file

@ -16,7 +16,7 @@
import * as fs from 'fs';
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 { rootTestType } from './testType';
import { createGuid, removeFolders, debugMode } from 'playwright-core/lib/utils/utils';
@ -87,7 +87,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
await removeFolders([dir]);
}, { scope: 'worker' }],
_browserOptions: [async ({ headless, channel, launchOptions }, use) => {
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
const options: LaunchOptions = {
handleSIGINT: false,
timeout: 0,
@ -97,8 +97,13 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
options.headless = headless;
if (channel !== undefined)
options.channel = channel;
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
(browserType as any)._defaultLaunchOptions = 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) => {
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) => {
createdContexts.add(context);
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
if (captureTrace) {
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();
}
await startTracing(context.tracing);
const listener = createInstrumentationListener(context);
(context as any)._instrumentation.addListener(listener);
(context.request as any)._instrumentation.addListener(listener);
};
const onDidCreateRequestContext = async (context: APIRequestContext) => {
const tracing = (context as any)._tracing as Tracing;
await startTracing(tracing);
(context as any)._instrumentation.addListener(createInstrumentationListener());
};
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
const onWillCloseContext = async (context: BrowserContext) => {
(context as any)[startedCollectingArtifacts] = true;
const stopTracing = async (tracing: Tracing) => {
(tracing as any)[startedCollectingArtifacts] = true;
if (captureTrace) {
// Export trace for now. We'll know whether we have to preserve it
// after the test finishes.
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
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') {
// Capture screenshot for now. We'll know whether we have to preserve them
// 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.
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
(browserType as any)._onWillCloseContext = onWillCloseContext;
(browserType as any)._defaultContextOptions = _combinedContextOptions;
(browserType as any)._defaultLaunchOptions = _browserOptions;
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
}
{
(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.
await use();
@ -337,28 +361,37 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
(browserType as any)._onDidCreateContext = undefined;
(browserType as any)._onWillCloseContext = undefined;
(browserType as any)._defaultContextOptions = undefined;
(browserType as any)._defaultLaunchOptions = undefined;
}
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
(playwright.request as any)._onDidCreateContext = undefined;
for (const context of (playwright.request as any)._contexts)
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.
await Promise.all(leftoverContexts.map(async context => {
const stopTraceChunk = async (tracing: Tracing): Promise<boolean> => {
// 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
// for the second time.
if ((context as any)[startedCollectingArtifacts])
return;
if ((tracing as any)[startedCollectingArtifacts])
return false;
if (preserveTrace)
await context.tracing.stopChunk({ path: addTraceAttachment() });
await tracing.stopChunk({ path: addTraceAttachment() });
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)
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
// before the test has finished.

View file

@ -107,8 +107,13 @@ export class BaseReporter implements Reporter {
this.result = result;
}
protected ttyWidth() {
return this._ttyWidthForTest || (process.env.PWTEST_SKIP_TEST_OUTPUT ? 80 : process.stdout.columns || 0);
protected fitToScreen(line: string, suffix?: string): string {
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() {
@ -431,7 +436,7 @@ export function stripAnsiEscapes(str: string): string {
}
// 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;
width -= suffixLength;
if (line.length <= width)

View file

@ -15,7 +15,7 @@
*/
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';
class LineReporter extends BaseReporter {
@ -51,7 +51,7 @@ class LineReporter extends BaseReporter {
if (test && this._lastTest !== test) {
// Write new header for the output.
const title = colors.gray(formatTestTitle(this.config, test));
stream.write(fitToScreen(title, this.ttyWidth()) + `\n`);
stream.write(this.fitToScreen(title) + `\n`);
this._lastTest = test;
}
@ -69,7 +69,7 @@ class LineReporter extends BaseReporter {
if (process.env.PWTEST_SKIP_TEST_OUTPUT)
process.stdout.write(`${title + suffix}\n`);
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 (!process.env.PWTEST_SKIP_TEST_OUTPUT)

View file

@ -17,7 +17,7 @@
/* eslint-disable no-console */
import colors from 'colors/safe';
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';
// 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 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++);
}
@ -142,7 +142,7 @@ class ListReporter extends BaseReporter {
process.stdout.write(`\u001B[${this._lastRow - testRow}A`);
// Erase line, go to the start
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.
if (testRow !== this._lastRow)
process.stdout.write(`\u001B[${this._lastRow - testRow}E`);
@ -152,13 +152,6 @@ class ListReporter extends BaseReporter {
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) {
const testRow = this._testRows.get(test)!;
process.stdout.write(testRow + ' : ' + line + suffix + '\n');

View file

@ -302,12 +302,7 @@ export class Runner {
if (!total)
fatalErrors.push(createNoTestsError());
// 8. Fail when output fails.
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {
fatalErrors.push(serializeError(e));
})));
// 9. Compute shards.
// 8. Compute shards.
let testGroups = createTestGroups(rootSuite);
const shard = config.shard;
@ -341,20 +336,37 @@ export class Runner {
}
(config as any).__testGroupsCount = testGroups.length;
// 10. Report begin
// 9. Report begin
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) {
for (const error of fatalErrors)
this._reporter.onError?.(error);
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)
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.
let globalTearDown: (() => Promise<void>) | undefined;
@ -432,7 +444,7 @@ export class Runner {
}, result);
if (result.status !== 'passed') {
tearDown();
await tearDown();
return;
}

View file

@ -120,11 +120,25 @@ function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | unde
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 {
if (isComponentImport(filename))
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 codePath = cachePath + '.js';
const sourceMapPath = cachePath + '.map';
@ -135,7 +149,10 @@ export function transformHook(code: string, filename: string, isModule = false):
// Silence the annoying warning.
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
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-numeric-separator')],
[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-async-generators')],
[require.resolve('@babel/plugin-syntax-object-rest-spread')],
[require.resolve('@babel/plugin-proposal-export-namespace-from')],
] as any;
[require.resolve('@babel/plugin-proposal-export-namespace-from')]
);
if (tsconfigData) {
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) {
plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]);
plugins.push([require.resolve('@babel/plugin-proposal-dynamic-import')]);
}
}
if (process.env.PW_TEST_SOURCE_TRANSFORM)
plugins.push([process.env.PW_TEST_SOURCE_TRANSFORM]);
if (process.env.PW_COMPONENT_TESTING)
plugins.unshift([require.resolve('@babel/plugin-transform-react-jsx')]);
if (hasPreprocessor)
plugins.push([scriptPreprocessor]);
const result = babel.transformFileSync(filename, {
babelrc: false,
@ -193,7 +212,12 @@ export function transformHook(code: string, filename: string, isModule = false):
}
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 {

View file

@ -1,6 +1,6 @@
{
"name": "playwright-webkit",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate WebKit",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
@ -25,6 +25,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.19.0-next"
"playwright-core": "1.19.2"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright",
"version": "1.19.0-next",
"version": "1.19.2",
"description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
@ -26,6 +26,6 @@
"install": "node install.js"
},
"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.headers()['content-type']).toBe('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();
});
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 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);
else
expect(response.status()).toBe(0);

View file

@ -15,7 +15,6 @@
*/
import { test, expect } from './playwright-test-fixtures';
import path from 'path';
test('should filter by file name', async ({ runInlineTest }) => {
const result = await runInlineTest({
@ -94,14 +93,6 @@ test('should run nothing for missing line', async ({ runInlineTest }) => {
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 }) => {
const result = await runInlineTest({
'foo.test.ts': `

View file

@ -15,6 +15,8 @@
*/
import { test, expect } from './playwright-test-fixtures';
import path from 'path';
import fs from 'fs';
test('should list tests', async ({ runInlineTest }) => {
const result = await runInlineTest({
@ -105,3 +107,36 @@ test('globalSetup and globalTeardown should not run', async ({ runInlineTest })
`Total: 2 tests in 2 files`,
].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);
});
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 }) => {
const result = await runInlineTest({
'expect-test.spec.ts': `

View file

@ -127,6 +127,7 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
PW_TEST_REPORTER: undefined,
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
PW_TEST_SOURCE_TRANSFORM: undefined,
PW_TEST_SOURCE_TRANSFORM_SCOPE: undefined,
PW_OUT_OF_PROCESS_DRIVER: undefined,
NODE_OPTIONS: undefined,
},

View file

@ -257,6 +257,26 @@ test('should respect headless in launchPersistent', async ({ runInlineTest }) =>
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) => {
const result = await runInlineTest({
'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();
});
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) => {
const files = {};
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');
});
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 }) => {
const result = await runInlineTest({
'playwright.config.js': `

View file

@ -66,3 +66,19 @@ test('should print flaky failures', async ({ runInlineTest }) => {
expect(result.flaky).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;
test('pass', ({}) => {});
`
}, {}, {}, { additionalArgs: [`dir${path.sep}a`] });
}, {}, {}, { additionalArgs: [`dir\\${path.sep}a`] });
expect(result.passed).toBe(1);
expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']);
expect(result.exitCode).toBe(0);

View file

@ -17,8 +17,9 @@
// @ts-check
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Documentation = require('./documentation');
const XmlDoc = require('./xmlDocumentation')
const XmlDoc = require('./xmlDocumentation');
const PROJECT_DIR = path.join(__dirname, '..', '..');
const fs = require('fs');
const { parseApi } = require('./api_parser');
@ -100,10 +101,10 @@ function writeFile(kind, name, spec, body, folder, extendsName = null) {
const out = [];
// console.log(`Generating ${name}`);
if (spec)
if (spec) {
out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth));
else {
let ownDocumentation = documentedResults.get(name);
} else {
const ownDocumentation = documentedResults.get(name);
if (ownDocumentation) {
out.push('/// <summary>');
out.push(`/// ${ownDocumentation}`);
@ -122,7 +123,7 @@ function writeFile(kind, name, spec, body, folder, extendsName = null) {
out.push(...body);
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);
}
@ -138,7 +139,7 @@ function renderClass(clazz) {
for (const member of clazz.membersArray) {
// Classes inherit it from IAsyncDisposable
if (member.name === 'dispose')
continue
continue;
if (member.alias.startsWith('RunAnd'))
renderMember(member, clazz, { trimRunAndPrefix: true }, body);
renderMember(member, clazz, {}, body);
@ -162,15 +163,15 @@ function renderModelType(name, type) {
// TODO: consider how this could be merged with the `translateType` check
if (type.union
&& type.union[0].name === 'null'
&& type.union.length == 2) {
&& type.union.length === 2)
type = type.union[1];
}
if (type.name === 'Array') {
throw new Error('Array at this stage is unexpected.');
} else if (type.properties) {
for (const member of type.properties) {
let fakeType = new Type(name, null);
const fakeType = new Type(name, null);
renderMember(member, fakeType, {}, body);
}
} else {
@ -188,8 +189,8 @@ function renderEnum(name, literals) {
const body = [];
for (let literal of literals) {
// strip out the quotes
literal = literal.replace(/[\"]/g, ``)
let escapedName = literal.replace(/[-]/g, ' ')
literal = literal.replace(/[\"]/g, ``);
const escapedName = literal.replace(/[-]/g, ' ')
.split(' ')
.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);
}
for (const element of documentation.classesArray) {
for (const element of documentation.classesArray)
renderClass(element);
}
for (let [name, type] of optionTypes)
for (const [name, type] of optionTypes)
renderOptionType(name, type);
for (let [name, type] of modelTypes)
for (const [name, type] of modelTypes)
renderModelType(name, type);
for (let [name, literals] of enumTypes)
for (const [name, literals] of enumTypes)
renderEnum(name, literals);
if (process.argv[3] !== "--skip-format") {
// run the formatting tool for .net, to ensure the files are prepped
execSync(`dotnet format -f "${outputDir}" --include-generated --fix-whitespace`);
if (process.argv[3] !== '--skip-format') {
// run the formatting tool for .NET, to ensure the files are prepped
execSync(`dotnet format "${outputDir}"`);
}
/**
@ -273,10 +274,10 @@ function renderConstructors(name, type, out) {
out.push(`if(clone == null) return;`);
type.properties.forEach(p => {
let propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
let propName = toMemberName(p);
const propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
const propName = toMemberName(p);
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(`}`);
@ -290,7 +291,7 @@ function renderConstructors(name, type, out) {
* @param {string[]} out
*/
function renderMember(member, parent, options, out) {
let name = toMemberName(member);
const name = toMemberName(member);
if (member.kind === 'method') {
renderMethod(member, parent, name, { mode: 'options', trimRunAndPrefix: options.trimRunAndPrefix }, out);
return;
@ -315,12 +316,14 @@ function renderMember(member, parent, options, out) {
type = `IEnumerable<${parent.name}>`;
}
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('');
if (member.spec)
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
if (!member.clazz)
out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`)
out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`);
if (member.deprecated)
out.push(`[System.Obsolete]`);
if (!type.endsWith('?') && !member.required)
@ -376,10 +379,10 @@ function generateNameDefault(member, name, t, parent) {
return 'object';
// we'd get this call for enums, primarily
let enumName = generateEnumNameIfApplicable(t);
const enumName = generateEnumNameIfApplicable(t);
if (!enumName && member) {
if (member.kind === 'method' || member.kind === 'property') {
let names = [
const names = [
parent.alias || parent.name,
toTitleCase(member.alias || member.name),
toTitleCase(name),
@ -387,15 +390,15 @@ function generateNameDefault(member, name, t, parent) {
if (names[2] === names[1])
names.pop(); // get rid of duplicates, cheaply
let attemptedName = names.pop();
let typesDiffer = function (left, right) {
const typesDiffer = function(left, right) {
if (left.expression && right.expression)
return left.expression !== right.expression;
return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
}
};
while (true) {
// crude attempt at removing plurality
if (attemptedName.endsWith('s')
&& !["properties", "httpcredentials"].includes(attemptedName.toLowerCase()))
&& !['properties', 'httpcredentials'].includes(attemptedName.toLowerCase()))
attemptedName = attemptedName.substring(0, attemptedName.length - 1);
// 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')
attemptedName = 'Header';
let probableType = modelTypes.get(attemptedName);
const probableType = modelTypes.get(attemptedName);
if ((probableType && typesDiffer(t, probableType))
|| (["Value"].includes(attemptedName))) {
|| (['Value'].includes(attemptedName))) {
if (!names.length)
throw new Error(`Ran out of possible names: ${attemptedName}`);
attemptedName = `${names.pop()}${attemptedName}`;
@ -434,9 +437,9 @@ function generateNameDefault(member, name, t, parent) {
return attemptedName;
}
if (member.kind === 'event') {
if (member.kind === 'event')
return `${name}Payload`;
}
}
return enumName || t.name;
@ -453,9 +456,9 @@ function generateEnumNameIfApplicable(type) {
const potentialValues = type.union.filter(u => u.name.startsWith('"'));
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 type.name;
}
@ -495,7 +498,7 @@ function renderMethod(member, parent, name, options, out) {
// TODO: this is something that will probably go into the docs
// translate simple getters into read-only properties, and simple
// set-only methods to settable properties
if (member.args.size == 0
if (member.args.size === 0
&& type !== 'void'
&& !name.startsWith('Get')
&& !name.startsWith('PostDataJSON')
@ -511,9 +514,9 @@ function renderMethod(member, parent, name, options, out) {
}
// HACK: special case for generics handling!
if (type === 'T') {
if (type === 'T')
name = `${name}<T>`;
}
// adjust the return type for async methods
if (member.async) {
@ -525,11 +528,11 @@ function renderMethod(member, parent, name, options, out) {
// render args
/** @type {string[]} */
let args = [];
const args = [];
/** @type {string[]} */
let explodedArgs = [];
const explodedArgs = [];
/** @type {Map<string, string>} */
let argTypeMap = new Map([]);
const argTypeMap = new Map([]);
/**
*
* @param {string} innerArgType
@ -540,11 +543,11 @@ function renderMethod(member, parent, name, options, out) {
function pushArg(innerArgType, innerArgName, argument, isExploded = false) {
if (innerArgType === 'null')
return;
const requiredPrefix = (argument.required || isExploded) ? "" : "?";
const requiredSuffix = (argument.required || isExploded) ? "" : " = default";
var push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
const requiredPrefix = (argument.required || isExploded) ? '' : '?';
const requiredSuffix = (argument.required || isExploded) ? '' : ' = default';
const push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
if (isExploded)
explodedArgs.push(push)
explodedArgs.push(push);
else
args.push(push);
argTypeMap.set(push, innerArgName);
@ -571,9 +574,9 @@ function renderMethod(member, parent, name, options, out) {
}
if (arg.type.expression === '[string]|[path]') {
let argName = toArgumentName(arg.name);
pushArg("string?", `${argName} = default`, arg);
pushArg("string?", `${argName}Path = default`, arg);
const argName = toArgumentName(arg.name);
pushArg('string?', `${argName} = default`, arg);
pushArg('string?', `${argName}Path = default`, arg);
if (arg.spec) {
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
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]>') {
// HACK: this hurts my brain too
// we split this into two args, one boolean, with the logical name
let argName = toArgumentName(arg.name);
let 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 argName = toArgumentName(arg.name);
const leftArgType = translateType(arg.type.union[0], 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(rightArgType, `${argName}Values`, arg);
@ -596,15 +599,15 @@ function renderMethod(member, parent, name, options, out) {
}
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) {
// 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))
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) {
pushArg(newArg, argName, arg, true); // push the exploded arg
addParamsDoc(argName, argDocumentation);
@ -677,17 +680,17 @@ function renderMethod(member, parent, name, options, out) {
explodedArgs.forEach((explodedArg, argIndex) => {
if (!options.nodocs)
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
let overloadedArgs = [];
for (var i = 0; i < args.length; i++) {
let arg = args[i];
const overloadedArgs = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === 'EXPLODED_ARG' || arg === 'OPTIONAL_EXPLODED_ARG') {
containsOptionalExplodedArgs = arg === 'OPTIONAL_EXPLODED_ARG';
let argType = argTypeMap.get(explodedArg);
const argType = argTypeMap.get(explodedArg);
if (!options.nodocs)
printArgDoc(argType, paramDocs.get(argType), out);
overloadedArgs.push(explodedArg);
} else {
let argType = argTypeMap.get(arg);
const argType = argTypeMap.get(arg);
if (!options.nodocs)
printArgDoc(argType, paramDocs.get(argType), out);
overloadedArgs.push(arg);
@ -703,13 +706,13 @@ function renderMethod(member, parent, name, options, out) {
// That particular overload only contains the required arguments, or rather
// contains all the arguments *except* the exploded ones.
if (containsOptionalExplodedArgs) {
var filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
const filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
if (!options.nodocs)
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
filteredArgs.forEach((arg) => {
filteredArgs.forEach(arg => {
if (arg === 'EXPLODED_ARG')
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)
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}`);
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]>
if (type.expression === '[float]|"raf"')
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.templates.length != 1)
if (type.templates.length !== 1)
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}>`;
}
if (type.name === 'Object') {
// take care of some common cases
// 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,
let keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
let valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
if (parent.name === 'Request' || parent.name === 'Response')
return `Dictionary<${keyType}, ${valueType}>`;
return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`;
@ -776,24 +779,24 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
if ((type.name === 'Object')
&& !type.properties
&& !type.union) {
&& !type.union)
return 'object';
}
// this is an additional type that we need to generate
let objectName = generateNameCallback(type);
if (objectName === 'Object') {
const objectName = generateNameCallback(type);
if (objectName === 'Object')
throw new Error('Object unexpected');
} else if (type.name === 'Object') {
else if (type.name === 'Object')
registerModelType(objectName, type);
}
return `${objectName}${optional ? '?' : ''}`;
}
if (type.name === 'Map') {
if (type.templates && type.templates.length == 2) {
if (type.templates && type.templates.length === 2) {
// we map to a dictionary
let keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
let valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
return `Dictionary<${keyType}, ${valueType}>`;
} else {
throw 'Map has invalid number of templates.';
@ -806,7 +809,7 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
let argsList = '';
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))
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
return `Action<${argsList}>`;
} else {
let returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
if (returnType == null)
const returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
if (returnType === null)
throw new Error('Unexpected null as return type.');
return `Func<${argsList}, ${returnType}>`;
@ -828,13 +831,13 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
if (type.templates) {
// this should mean we have a generic type and we can translate that
/** @type {string[]} */
var types = type.templates.map(template => translateType(template, parent));
return `${type.name}<${types.join(', ')}>`
const types = type.templates.map(template => translateType(template, parent));
return `${type.name}<${types.join(', ')}>`;
}
// 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.
let name = classNameMap.get(type.name) || type.name;
const name = classNameMap.get(type.name) || type.name;
return `${name}${optional ? '?' : ''}`;
}
@ -848,7 +851,7 @@ function registerModelType(typeName, type) {
if (typeName.endsWith('Option'))
return;
let potentialType = modelTypes.get(typeName);
const potentialType = modelTypes.get(typeName);
if (potentialType) {
// console.log(`Type ${typeName} already exists, so skipping...`);
return;