Compare commits
31 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3a035560a | ||
|
|
77d9a8e1f6 | ||
|
|
d07e05fa7b | ||
|
|
0a1c1dad67 | ||
|
|
2e4167027b | ||
|
|
5b17ca9d56 | ||
|
|
0037acffc6 | ||
|
|
d22bde13c4 | ||
|
|
e57b4b5073 | ||
|
|
46aeb8fe3d | ||
|
|
d8bc6dbeea | ||
|
|
03501cfdb2 | ||
|
|
241add240c | ||
|
|
92aa600af2 | ||
|
|
c098cafb7a | ||
|
|
aafaa2b9ed | ||
|
|
b9d665caf0 | ||
|
|
1d4521a12e | ||
|
|
7a683a9331 | ||
|
|
ca116db2cb | ||
|
|
8a52c3ca76 | ||
|
|
d73d188ae7 | ||
|
|
73d78f5988 | ||
|
|
55be85284c | ||
|
|
35f921e7aa | ||
|
|
786bb337f0 | ||
|
|
8f1f97f508 | ||
|
|
b651920bd5 | ||
|
|
4959558527 | ||
|
|
eaeb7de95a | ||
|
|
72a767fd4d |
|
|
@ -221,7 +221,7 @@ File path to respond with. The content type will be inferred from file extension
|
|||
is resolved relative to the current working directory.
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
36
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>];
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)});`);
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)._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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,39 +149,44 @@ 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 = [
|
||||
[require.resolve('@babel/plugin-proposal-class-properties')],
|
||||
[require.resolve('@babel/plugin-proposal-numeric-separator')],
|
||||
[require.resolve('@babel/plugin-proposal-logical-assignment-operators')],
|
||||
[require.resolve('@babel/plugin-proposal-nullish-coalescing-operator')],
|
||||
[require.resolve('@babel/plugin-proposal-optional-chaining')],
|
||||
[require.resolve('@babel/plugin-syntax-json-strings')],
|
||||
[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;
|
||||
const plugins = [];
|
||||
|
||||
if (tsconfigData) {
|
||||
plugins.push([require.resolve('babel-plugin-module-resolver'), {
|
||||
root: ['./'],
|
||||
alias: tsconfigData.alias,
|
||||
// Silences warning 'Could not resovle ...' that we trigger because we resolve
|
||||
// into 'foo/bar', and not 'foo/bar.ts'.
|
||||
loglevel: 'silent',
|
||||
}]);
|
||||
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')],
|
||||
[require.resolve('@babel/plugin-proposal-nullish-coalescing-operator')],
|
||||
[require.resolve('@babel/plugin-proposal-optional-chaining')],
|
||||
[require.resolve('@babel/plugin-syntax-json-strings')],
|
||||
[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')]
|
||||
);
|
||||
|
||||
if (tsconfigData) {
|
||||
plugins.push([require.resolve('babel-plugin-module-resolver'), {
|
||||
root: ['./'],
|
||||
alias: tsconfigData.alias,
|
||||
// Silences warning 'Could not resovle ...' that we trigger because we resolve
|
||||
// into 'foo/bar', and not 'foo/bar.ts'.
|
||||
loglevel: 'silent',
|
||||
}]);
|
||||
}
|
||||
|
||||
if (!isModule) {
|
||||
plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]);
|
||||
plugins.push([require.resolve('@babel/plugin-proposal-dynamic-import')]);
|
||||
}
|
||||
}
|
||||
|
||||
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 (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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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': `
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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': `
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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': `
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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': `
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,12 +123,12 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Class} clazz
|
||||
* @param {Documentation.Class} clazz
|
||||
*/
|
||||
function renderClass(clazz) {
|
||||
const name = classNameMap.get(clazz.name);
|
||||
|
|
@ -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,10 +189,10 @@ function renderEnum(name, literals) {
|
|||
const body = [];
|
||||
for (let literal of literals) {
|
||||
// strip out the quotes
|
||||
literal = literal.replace(/[\"]/g, ``)
|
||||
let escapedName = literal.replace(/[-]/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
|
||||
literal = literal.replace(/[\"]/g, ``);
|
||||
const escapedName = literal.replace(/[-]/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
|
||||
|
||||
body.push(`[EnumMember(Value = "${literal}")]`);
|
||||
body.push(`${escapedName},`);
|
||||
|
|
@ -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}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -238,9 +239,9 @@ function toArgumentName(name) {
|
|||
return name === 'event' ? `@${name}` : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} member
|
||||
*/
|
||||
/**
|
||||
* @param {Documentation.Member} member
|
||||
*/
|
||||
function toMemberName(member, makeAsync = false) {
|
||||
const assumedName = toTitleCase(member.alias || member.name);
|
||||
if (member.kind === 'interface')
|
||||
|
|
@ -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,18 +437,18 @@ function generateNameDefault(member, name, t, parent) {
|
|||
return attemptedName;
|
||||
}
|
||||
|
||||
if (member.kind === 'event') {
|
||||
if (member.kind === 'event')
|
||||
return `${name}Payload`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return enumName || t.name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.Type} type
|
||||
* @returns
|
||||
*
|
||||
* @param {Documentation.Type} type
|
||||
* @returns
|
||||
*/
|
||||
function generateEnumNameIfApplicable(type) {
|
||||
if (!type.union)
|
||||
|
|
@ -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);
|
||||
|
|
@ -630,9 +633,9 @@ function renderMethod(member, parent, name, options, out) {
|
|||
modifiers = 'public ';
|
||||
|
||||
member.argsArray
|
||||
.sort((a, b) => b.alias === 'options' ? -1 : 0) //move options to the back to the arguments list
|
||||
.forEach(processArg);
|
||||
|
||||
.sort((a, b) => b.alias === 'options' ? -1 : 0) // move options to the back to the arguments list
|
||||
.forEach(processArg);
|
||||
|
||||
let body = ';';
|
||||
if (options.mode === 'base') {
|
||||
// Generate options -> named transition.
|
||||
|
|
@ -669,7 +672,7 @@ function renderMethod(member, parent, name, options, out) {
|
|||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||
paramDocs.forEach((value, i) => printArgDoc(i, value, out));
|
||||
}
|
||||
if(member.deprecated)
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')})${body}`);
|
||||
} else {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue