Compare commits
27 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20427b9b92 | ||
|
|
6155a50190 | ||
|
|
b9f4f2cdd3 | ||
|
|
d7ad0a0180 | ||
|
|
128b2ff1d5 | ||
|
|
e9fe663e89 | ||
|
|
249825f1ac | ||
|
|
db82ae0179 | ||
|
|
4b3932de9a | ||
|
|
81d23946e8 | ||
|
|
a4a3f722ba | ||
|
|
53ecdf7db6 | ||
|
|
4413b01e40 | ||
|
|
1cad99ccf5 | ||
|
|
725dd8b4b1 | ||
|
|
63642bd77c | ||
|
|
bb3b96e433 | ||
|
|
d32d466f2f | ||
|
|
31ace756d8 | ||
|
|
822227f925 | ||
|
|
f276edf8f1 | ||
|
|
b7d36117d1 | ||
|
|
07af3c1439 | ||
|
|
a8542d86cf | ||
|
|
8d3481ea22 | ||
|
|
24be5c2881 | ||
|
|
6d152e9b2f |
|
|
@ -1,7 +1,7 @@
|
|||
# class: APIResponseAssertions
|
||||
* since: v1.18
|
||||
|
||||
The [APIResponseAssertions] class provides assertion methods that can be used to make assertions about the [APIResponse] in the tests. A new instance of [APIResponseAssertions] is created by calling [`method: PlaywrightAssertions.expectAPIResponse`]:
|
||||
The [APIResponseAssertions] class provides assertion methods that can be used to make assertions about the [APIResponse] in the tests.
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -1398,9 +1398,6 @@ Will throw an error if the context closes before new [Page] is created.
|
|||
### param: BrowserContext.waitForPage.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: BrowserContext.waitForPage.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: BrowserContext.waitForPage.predicate
|
||||
* since: v1.9
|
||||
* langs: csharp, java, python
|
||||
|
|
@ -1411,6 +1408,9 @@ Receives the [Page] object and resolves to truthy value when the waiting should
|
|||
### option: BrowserContext.waitForPage.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: BrowserContext.waitForPage.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: BrowserContext.waitForEvent2
|
||||
* since: v1.8
|
||||
* langs: python
|
||||
|
|
|
|||
|
|
@ -2001,9 +2001,6 @@ a navigation.
|
|||
### param: Frame.waitForNavigation.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Frame.waitForNavigation.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Frame.waitForNavigation.url = %%-wait-for-navigation-url-%%
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -2013,6 +2010,9 @@ a navigation.
|
|||
### option: Frame.waitForNavigation.timeout = %%-navigation-timeout-%%
|
||||
* since: v1.8
|
||||
|
||||
### param: Frame.waitForNavigation.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Frame.waitForSelector
|
||||
* since: v1.8
|
||||
- returns: <[null]|[ElementHandle]>
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@ expect(value).toMatches(/Is \d+ enough/);
|
|||
|
||||
### param: GenericAssertions.toMatch.expected
|
||||
* since: v1.9
|
||||
- `expected` <[RegExp]>
|
||||
- `expected` <[RegExp]|[string]>
|
||||
|
||||
Regular expression to match against.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# class: LocatorAssertions
|
||||
* since: v1.17
|
||||
|
||||
The [LocatorAssertions] class provides assertion methods that can be used to make assertions about the [Locator] state in the tests. A new instance of [LocatorAssertions] is created by calling [`method: PlaywrightAssertions.expectLocator`]:
|
||||
The [LocatorAssertions] class provides assertion methods that can be used to make assertions about the [Locator] state in the tests.
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
@ -157,6 +157,21 @@ The opposite of [`method: LocatorAssertions.toBeHidden`].
|
|||
### option: LocatorAssertions.NotToBeHidden.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.18
|
||||
|
||||
## async method: LocatorAssertions.NotToBeInViewport
|
||||
* since: v1.31
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toBeInViewport`].
|
||||
|
||||
### option: LocatorAssertions.NotToBeInViewport.ratio
|
||||
* since: v1.31
|
||||
* langs: python
|
||||
- `ratio` <[float]>
|
||||
|
||||
### option: LocatorAssertions.NotToBeInViewport.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.31
|
||||
* langs: python
|
||||
|
||||
## async method: LocatorAssertions.NotToBeVisible
|
||||
* since: v1.20
|
||||
* langs: python
|
||||
|
|
@ -689,7 +704,7 @@ Ensures the [Locator] points to an element that intersects viewport, according t
|
|||
**Usage**
|
||||
|
||||
```js
|
||||
const locator = page.locator('button.submit');
|
||||
const locator = page.getByRole('button');
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
await expect(locator).toBeInViewport();
|
||||
// Make sure element is fully outside of viewport.
|
||||
|
|
@ -699,7 +714,7 @@ await expect(locator).toBeInViewport({ ratio: 0.5 });
|
|||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.locator("button.submit");
|
||||
Locator locator = page.getByRole(AriaRole.BUTTON);
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
assertThat(locator).isInViewport();
|
||||
// Make sure element is fully outside of viewport.
|
||||
|
|
@ -709,7 +724,7 @@ assertThat(locator).isInViewport(new LocatorAssertions.IsInViewportOptions().set
|
|||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("button.submit");
|
||||
var locator = Page.GetByRole(AriaRole.Button);
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
await Expect(locator).ToBeInViewportAsync();
|
||||
// Make sure element is fully outside of viewport.
|
||||
|
|
@ -721,25 +736,25 @@ await Expect(locator).ToBeInViewportAsync(new() { Ratio = 0.5 });
|
|||
```python async
|
||||
from playwright.async_api import expect
|
||||
|
||||
locator = page.locator("button.submit")
|
||||
locator = page.get_by_role("button")
|
||||
# Make sure at least some part of element intersects viewport.
|
||||
await expect(locator).to_be_in_viewport()
|
||||
# Make sure element is fully outside of viewport.
|
||||
await expect(locator).not_to_be_in_viewport()
|
||||
# Make sure that at least half of the element intersects viewport.
|
||||
await expect(locator).to_be_in_viewport(ratio=0.5);
|
||||
await expect(locator).to_be_in_viewport(ratio=0.5)
|
||||
```
|
||||
|
||||
```python sync
|
||||
from playwright.sync_api import expect
|
||||
|
||||
locator = page.locator("button.submit")
|
||||
locator = page.get_by_role("button")
|
||||
# Make sure at least some part of element intersects viewport.
|
||||
expect(locator).to_be_in_viewport()
|
||||
# Make sure element is fully outside of viewport.
|
||||
expect(locator).not_to_be_in_viewport()
|
||||
# Make sure that at least half of the element intersects viewport.
|
||||
expect(locator).to_be_in_viewport(ratio=0.5);
|
||||
expect(locator).to_be_in_viewport(ratio=0.5)
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3798,10 +3798,10 @@ Video object associated with this page.
|
|||
|
||||
Performs action and waits for the Page to close.
|
||||
|
||||
### param: Page.waitForClose.callback = %%-java-wait-for-event-callback-%%
|
||||
### option: Page.waitForClose.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForClose.timeout = %%-wait-for-event-timeout-%%
|
||||
### param: Page.waitForClose.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForConsoleMessage
|
||||
|
|
@ -3823,9 +3823,6 @@ Will throw an error if the page is closed before the [`event: Page.console`] eve
|
|||
### param: Page.waitForConsoleMessage.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForConsoleMessage.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForConsoleMessage.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([ConsoleMessage]\):[boolean]>
|
||||
|
|
@ -3835,6 +3832,9 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti
|
|||
### option: Page.waitForConsoleMessage.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForConsoleMessage.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForDownload
|
||||
* since: v1.9
|
||||
* langs: java, python, csharp
|
||||
|
|
@ -3854,9 +3854,6 @@ Will throw an error if the page is closed before the download event is fired.
|
|||
### param: Page.waitForDownload.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForDownload.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForDownload.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([Download]\):[boolean]>
|
||||
|
|
@ -3866,6 +3863,9 @@ Receives the [Download] object and resolves to truthy value when the waiting sho
|
|||
### option: Page.waitForDownload.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForDownload.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForEvent
|
||||
* since: v1.8
|
||||
* langs: js, python
|
||||
|
|
@ -3939,9 +3939,6 @@ Will throw an error if the page is closed before the file chooser is opened.
|
|||
### param: Page.waitForFileChooser.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForFileChooser.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForFileChooser.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([FileChooser]\):[boolean]>
|
||||
|
|
@ -3951,6 +3948,9 @@ Receives the [FileChooser] object and resolves to truthy value when the waiting
|
|||
### option: Page.waitForFileChooser.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForFileChooser.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForFunction
|
||||
* since: v1.8
|
||||
- returns: <[JSHandle]>
|
||||
|
|
@ -4245,9 +4245,6 @@ a navigation.
|
|||
### param: Page.waitForNavigation.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForNavigation.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForNavigation.url = %%-wait-for-navigation-url-%%
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -4257,6 +4254,9 @@ a navigation.
|
|||
### option: Page.waitForNavigation.timeout = %%-navigation-timeout-%%
|
||||
* since: v1.8
|
||||
|
||||
### param: Page.waitForNavigation.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForPopup
|
||||
* since: v1.9
|
||||
* langs: java, python, csharp
|
||||
|
|
@ -4276,9 +4276,6 @@ Will throw an error if the page is closed before the popup event is fired.
|
|||
### param: Page.waitForPopup.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForPopup.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForPopup.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([Page]\):[boolean]>
|
||||
|
|
@ -4288,6 +4285,9 @@ Receives the [Page] object and resolves to truthy value when the waiting should
|
|||
### option: Page.waitForPopup.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForPopup.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForRequest
|
||||
* since: v1.8
|
||||
* langs:
|
||||
|
|
@ -4369,9 +4369,6 @@ await page.RunAndWaitForRequestAsync(async () =>
|
|||
### param: Page.waitForRequest.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForRequest.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForRequest.urlOrPredicate
|
||||
* since: v1.8
|
||||
- `urlOrPredicate` <[string]|[RegExp]|[function]\([Request]\):[boolean]>
|
||||
|
|
@ -4394,6 +4391,9 @@ Request URL string, regex or predicate receiving [Request] object.
|
|||
Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be
|
||||
changed by using the [`method: Page.setDefaultTimeout`] method.
|
||||
|
||||
### param: Page.waitForRequest.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForRequestFinished
|
||||
* since: v1.12
|
||||
* langs: java, python, csharp
|
||||
|
|
@ -4413,9 +4413,6 @@ Will throw an error if the page is closed before the [`event: Page.requestFinish
|
|||
### param: Page.waitForRequestFinished.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForRequestFinished.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.12
|
||||
|
||||
### option: Page.waitForRequestFinished.predicate
|
||||
* since: v1.12
|
||||
- `predicate` <[function]\([Request]\):[boolean]>
|
||||
|
|
@ -4425,6 +4422,9 @@ Receives the [Request] object and resolves to truthy value when the waiting shou
|
|||
### option: Page.waitForRequestFinished.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForRequestFinished.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.12
|
||||
|
||||
## async method: Page.waitForResponse
|
||||
* since: v1.8
|
||||
* langs:
|
||||
|
|
@ -4510,9 +4510,6 @@ await page.RunAndWaitForResponseAsync(async () =>
|
|||
### param: Page.waitForResponse.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForResponse.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForResponse.urlOrPredicate
|
||||
* since: v1.8
|
||||
- `urlOrPredicate` <[string]|[RegExp]|[function]\([Response]\):[boolean]>
|
||||
|
|
@ -4537,6 +4534,9 @@ it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/We
|
|||
Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be
|
||||
changed by using the [`method: BrowserContext.setDefaultTimeout`] or [`method: Page.setDefaultTimeout`] methods.
|
||||
|
||||
### param: Page.waitForResponse.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForSelector
|
||||
* since: v1.8
|
||||
- returns: <[null]|[ElementHandle]>
|
||||
|
|
@ -4768,9 +4768,6 @@ Will throw an error if the page is closed before the WebSocket event is fired.
|
|||
### param: Page.waitForWebSocket.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForWebSocket.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForWebSocket.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([WebSocket]\):[boolean]>
|
||||
|
|
@ -4780,6 +4777,9 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh
|
|||
### option: Page.waitForWebSocket.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForWebSocket.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: Page.waitForWorker
|
||||
* since: v1.9
|
||||
* langs: java, python, csharp
|
||||
|
|
@ -4799,9 +4799,6 @@ Will throw an error if the page is closed before the worker event is fired.
|
|||
### param: Page.waitForWorker.action = %%-csharp-wait-for-event-action-%%
|
||||
* since: v1.12
|
||||
|
||||
### param: Page.waitForWorker.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Page.waitForWorker.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([Worker]\):[boolean]>
|
||||
|
|
@ -4811,6 +4808,9 @@ Receives the [Worker] object and resolves to truthy value when the waiting shoul
|
|||
### option: Page.waitForWorker.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Page.waitForWorker.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## method: Page.workers
|
||||
* since: v1.8
|
||||
- returns: <[Array]<[Worker]>>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# class: PageAssertions
|
||||
* since: v1.17
|
||||
|
||||
The [PageAssertions] class provides assertion methods that can be used to make assertions about the [Page] state in the tests. A new instance of [PageAssertions] is created by calling [`method: PlaywrightAssertions.expectPage`]:
|
||||
The [PageAssertions] class provides assertion methods that can be used to make assertions about the [Page] state in the tests.
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# class: PlaywrightAssertions
|
||||
* langs: js, java, csharp
|
||||
* since: v1.17
|
||||
|
||||
Playwright gives you Web-First Assertions with convenience methods for creating assertions that will wait and retry until the expected condition is met.
|
||||
|
|
@ -98,7 +99,7 @@ PlaywrightAssertions.assertThat(response).isOK();
|
|||
|
||||
## method: PlaywrightAssertions.expectGeneric
|
||||
* since: v1.9
|
||||
* langs:
|
||||
* langs: js
|
||||
- alias-js: expect
|
||||
- returns: <[GenericAssertions]>
|
||||
|
||||
|
|
@ -106,6 +107,7 @@ Creates a [GenericAssertions] object for the given value.
|
|||
|
||||
### param: PlaywrightAssertions.expectGeneric.value
|
||||
* since: v1.9
|
||||
* langs: js
|
||||
- `value` <[any]>
|
||||
|
||||
Value that will be asserted.
|
||||
|
|
|
|||
|
|
@ -496,8 +496,12 @@ Note that [`option: headers`] option will apply to the fetched request as well a
|
|||
|
||||
If set changes the request URL. New URL must have same protocol as original one.
|
||||
|
||||
### option: Route.fetch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||
### option: Route.fetch.maxRedirects
|
||||
* since: v1.31
|
||||
- `maxRedirects` <[int]>
|
||||
|
||||
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
|
||||
Defaults to `20`. Pass `0` to not follow redirects.
|
||||
|
||||
### option: Route.fetch.method
|
||||
* since: v1.29
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ Script that evaluates to a selector engine instance. The script is evaluated in
|
|||
### param: Selectors.register.script
|
||||
* since: v1.8
|
||||
* langs: python
|
||||
- `script` <[string]>
|
||||
- `script` ?<[string]>
|
||||
|
||||
Raw script content.
|
||||
|
||||
|
|
|
|||
|
|
@ -95,9 +95,6 @@ Performs action and waits for a frame to be sent. If predicate is provided, it p
|
|||
[WebSocketFrame] value into the `predicate` function and waits for `predicate(webSocketFrame)` to return a truthy value.
|
||||
Will throw an error if the WebSocket or Page is closed before the frame is received.
|
||||
|
||||
### param: WebSocket.waitForFrameReceived.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: WebSocket.waitForFrameReceived.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([WebSocketFrame]\):[boolean]>
|
||||
|
|
@ -107,6 +104,9 @@ Receives the [WebSocketFrame] object and resolves to truthy value when the waiti
|
|||
### option: WebSocket.waitForFrameReceived.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: WebSocket.waitForFrameReceived.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: WebSocket.waitForFrameSent
|
||||
* since: v1.10
|
||||
* langs: java
|
||||
|
|
@ -116,9 +116,6 @@ Performs action and waits for a frame to be sent. If predicate is provided, it p
|
|||
[WebSocketFrame] value into the `predicate` function and waits for `predicate(webSocketFrame)` to return a truthy value.
|
||||
Will throw an error if the WebSocket or Page is closed before the frame is sent.
|
||||
|
||||
### param: WebSocket.waitForFrameSent.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: WebSocket.waitForFrameSent.predicate
|
||||
* since: v1.9
|
||||
- `predicate` <[function]\([WebSocketFrame]\):[boolean]>
|
||||
|
|
@ -128,6 +125,9 @@ Receives the [WebSocketFrame] object and resolves to truthy value when the waiti
|
|||
### option: WebSocket.waitForFrameSent.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: WebSocket.waitForFrameSent.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
## async method: WebSocket.waitForEvent2
|
||||
* since: v1.8
|
||||
* langs: python
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@ Optional argument to pass to [`param: expression`].
|
|||
|
||||
Performs action and waits for the Worker to close.
|
||||
|
||||
### param: Worker.waitForClose.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
||||
### option: Worker.waitForClose.timeout = %%-wait-for-event-timeout-%%
|
||||
* since: v1.9
|
||||
|
||||
### param: Worker.waitForClose.callback = %%-java-wait-for-event-callback-%%
|
||||
* since: v1.9
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Playwright executes tests in isolated environments called [browser contexts](./b
|
|||
|
||||
Regardless of the authentication strategy you choose, you are likely to store authenticated browser state on the file system.
|
||||
|
||||
We recommend to create `playwright/.auth` directory and add it to your `.gitignore`. You authentication routine will produce authenticated browser state and save it to a file in this `playwright/.auth` directory. Later on, tests will reuse this state and start already authenticated.
|
||||
We recommend to create `playwright/.auth` directory and add it to your `.gitignore`. Your authentication routine will produce authenticated browser state and save it to a file in this `playwright/.auth` directory. Later on, tests will reuse this state and start already authenticated.
|
||||
|
||||
```bash tab=bash-bash
|
||||
mkdir -p playwright/.auth
|
||||
|
|
@ -27,21 +27,6 @@ New-Item -ItemType Directory -Force -Path playwright\.auth
|
|||
Add-Content -path .gitignore "`r`nplaywright/.auth"
|
||||
```
|
||||
|
||||
Usually you would want to reuse authenticated state between multiple test runs, especially when authoring tests. All the examples in this guide authenticate lazily, and reuse auth state when possible. However, your app may require to re-authenticate after some amount of time. In this case, just remove `playwright/.auth` directory, and re-run your tests.
|
||||
|
||||
```bash tab=bash-bash
|
||||
# Remove auth state
|
||||
rm -rf playwright/.auth
|
||||
```
|
||||
|
||||
```batch tab=bash-batch
|
||||
rd /s /q playwright/.auth
|
||||
```
|
||||
|
||||
```powershell tab=bash-powershell
|
||||
Remove-Item -Recurse -Force playwright/.auth
|
||||
```
|
||||
|
||||
## Basic: shared account in all tests
|
||||
* langs: js
|
||||
|
||||
|
|
@ -61,16 +46,10 @@ Create `auth.setup.ts` that will prepare authenticated browser state for all oth
|
|||
```js
|
||||
// auth.setup.ts
|
||||
import { test as setup } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const authFile = 'playwright/.auth/user.json';
|
||||
|
||||
setup('authenticate', async ({ page }) => {
|
||||
// Reuse authenticate from previous runs.
|
||||
if (fs.existsSync(authFile))
|
||||
return;
|
||||
|
||||
// Perform authentication steps. Replace these actions with your own.
|
||||
await page.goto('https://github.com/login');
|
||||
await page.getByLabel('Username or email address').fill('username');
|
||||
|
|
@ -160,7 +139,7 @@ export const test = baseTest.extend<{}, { workerStorageState: string }>({
|
|||
workerStorageState: [async ({ browser }, use) => {
|
||||
// Use parallelIndex as a unique identifier for each worker.
|
||||
const id = test.info().parallelIndex;
|
||||
const fileName = path.resolve(__dirname, `.auth/${id}.json`);
|
||||
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
|
||||
|
||||
if (fs.existsSync(fileName)) {
|
||||
// Reuse existing authentication state if any.
|
||||
|
|
@ -327,17 +306,11 @@ In the [setup project](#basic-shared-account-in-all-tests):
|
|||
|
||||
```js
|
||||
// auth.setup.ts
|
||||
import { test } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
const authFile = 'playwright/.auth/user.json';
|
||||
|
||||
test('authenticate', async ({ request }) => {
|
||||
// Reuse authenticate from previous runs.
|
||||
if (fs.existsSync(authFile))
|
||||
return;
|
||||
|
||||
setup('authenticate', async ({ request }) => {
|
||||
// Send authentication request. Replace with your own.
|
||||
await request.post('https://github.com/login', {
|
||||
form: {
|
||||
|
|
@ -366,7 +339,7 @@ export const test = baseTest.extend<{}, { workerStorageState: string }>({
|
|||
workerStorageState: [async ({}, use) => {
|
||||
// Use parallelIndex as a unique identifier for each worker.
|
||||
const id = test.info().parallelIndex;
|
||||
const fileName = path.resolve(__dirname, `.auth/${id}.json`);
|
||||
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
|
||||
|
||||
if (fs.existsSync(fileName)) {
|
||||
// Reuse existing authentication state if any.
|
||||
|
|
@ -410,17 +383,11 @@ We will authenticate multiple times in the setup project.
|
|||
|
||||
```js
|
||||
// auth.setup.ts
|
||||
import { test } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
const adminFile = 'playwright/.auth/admin.json';
|
||||
|
||||
test('authenticate as admin', async ({ page }) => {
|
||||
// Reuse authenticate from previous runs.
|
||||
if (fs.existsSync(adminFile))
|
||||
return;
|
||||
|
||||
setup('authenticate as admin', async ({ page }) => {
|
||||
// Perform authentication steps. Replace these actions with your own.
|
||||
await page.goto('https://github.com/login');
|
||||
await page.getByLabel('Username or email address').fill('admin');
|
||||
|
|
@ -433,11 +400,7 @@ test('authenticate as admin', async ({ page }) => {
|
|||
|
||||
const userFile = 'playwright/.auth/user.json';
|
||||
|
||||
test('authenticate as user', async ({ page }) => {
|
||||
// Reuse authenticate from previous runs.
|
||||
if (fs.existsSync(userFile))
|
||||
return;
|
||||
|
||||
setup('authenticate as user', async ({ page }) => {
|
||||
// Perform authentication steps. Replace these actions with your own.
|
||||
await page.goto('https://github.com/login');
|
||||
await page.getByLabel('Username or email address').fill('user');
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ jobs:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
|
@ -194,7 +194,7 @@ jobs:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
|
|
@ -218,7 +218,7 @@ jobs:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
|
|
@ -239,7 +239,7 @@ jobs:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup dotnet
|
||||
|
|
@ -264,7 +264,7 @@ jobs:
|
|||
name: 'Playwright Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -299,7 +299,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -325,7 +325,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -369,7 +369,7 @@ Running Playwright on CircleCI is very similar to running on GitHub Actions. In
|
|||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
```
|
||||
|
||||
Note: When using the docker agent definition, you are specifying the resource class of where playwright runs to the 'medium' tier [here](https://circleci.com/docs/configuration-reference?#docker-execution-environment). The default behavior of Playwright is to set the number of workers to the detected core count (2 in the case of the medium tier). Overriding the number of workers to greater than this number will cause unnecessary timeouts and failures.
|
||||
|
|
@ -403,7 +403,7 @@ to run tests on Jenkins.
|
|||
|
||||
```groovy
|
||||
pipeline {
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.31.0-focal' } }
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.31.2-focal' } }
|
||||
stages {
|
||||
stage('e2e-tests') {
|
||||
steps {
|
||||
|
|
@ -421,7 +421,7 @@ pipeline {
|
|||
Bitbucket Pipelines can use public [Docker images as build environments](https://confluence.atlassian.com/bitbucket/use-docker-images-as-build-environments-792298897.html). To run Playwright tests on Bitbucket, use our public Docker image ([see Dockerfile](./docker.md)).
|
||||
|
||||
```yml
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
|
@ -434,7 +434,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
script:
|
||||
...
|
||||
```
|
||||
|
|
@ -450,7 +450,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
parallel: 7
|
||||
script:
|
||||
- npm ci
|
||||
|
|
@ -465,7 +465,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
parallel:
|
||||
matrix:
|
||||
- PROJECT: ['chromium', 'webkit']
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@ This Docker image is intended to be used for testing and development purposes on
|
|||
### Pull the image
|
||||
|
||||
```bash js
|
||||
docker pull mcr.microsoft.com/playwright:v1.31.0-focal
|
||||
docker pull mcr.microsoft.com/playwright:v1.31.2-focal
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker pull mcr.microsoft.com/playwright/python:v1.31.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/python:v1.31.2-focal
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.31.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.31.2-focal
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker pull mcr.microsoft.com/playwright/java:v1.31.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/java:v1.31.2-focal
|
||||
```
|
||||
|
||||
### Run the image
|
||||
|
|
@ -42,19 +42,19 @@ By default, the Docker image will use the `root` user to run the browsers. This
|
|||
On trusted websites, you can avoid creating a separate user and use root for it since you trust the code which will run on the browsers.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
#### Crawling and scraping
|
||||
|
|
@ -62,19 +62,19 @@ docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.31.0-focal /
|
|||
On untrusted websites, it's recommended to use a separate user for launching the browsers in combination with the seccomp profile. Inside the container or if you are using the Docker image as a base image you have to use `adduser` for it.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.31.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.31.2-focal /bin/bash
|
||||
```
|
||||
|
||||
[`seccomp_profile.json`](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json) is needed to run Chromium with sandbox. This is a [default Docker seccomp profile](https://github.com/docker/engine/blob/d0d99b04cf6e00ed3fc27e81fc3d94e7eda70af3/profiles/seccomp/default.json) with extra user namespace cloning permissions:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,48 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.31
|
||||
|
||||
### New APIs
|
||||
|
||||
- New assertion [`method: LocatorAssertions.toBeInViewport`] ensures that locator points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
||||
|
||||
|
||||
```csharp
|
||||
var locator = Page.GetByRole(AriaRole.Button);
|
||||
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
await Expect(locator).ToBeInViewportAsync();
|
||||
|
||||
// Make sure element is fully outside of viewport.
|
||||
await Expect(locator).Not.ToBeInViewportAsync();
|
||||
|
||||
// Make sure that at least half of the element intersects viewport.
|
||||
await Expect(locator).ToBeInViewportAsync(new() { Ratio = 0.5 });
|
||||
```
|
||||
|
||||
- New methods [`method: BrowserContext.newCDPSession`] and [`method: Browser.newBrowserCDPSession`] create a [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) session for the page and browser respectively.
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- DOM snapshots in trace viewer can be now opened in a separate window.
|
||||
- New option [`option: Route.fetch.maxRedirects`] for method [`method: Route.fetch`].
|
||||
- Playwright now supports Debian 11 arm64.
|
||||
- Official [docker images](./docker.md) now include Node 18 instead of Node 16.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 111.0.5563.19
|
||||
* Mozilla Firefox 109.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 110
|
||||
* Microsoft Edge 110
|
||||
|
||||
|
||||
## Version 1.30
|
||||
|
||||
### Browser Versions
|
||||
|
|
|
|||
|
|
@ -4,6 +4,46 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.31
|
||||
|
||||
### New APIs
|
||||
|
||||
- New assertion [`method: LocatorAssertions.toBeInViewport`] ensures that locator points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
||||
|
||||
|
||||
```java
|
||||
Locator locator = page.getByRole(AriaRole.BUTTON);
|
||||
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
assertThat(locator).isInViewport();
|
||||
|
||||
// Make sure element is fully outside of viewport.
|
||||
assertThat(locator).not().isInViewport();
|
||||
|
||||
// Make sure that at least half of the element intersects viewport.
|
||||
assertThat(locator).isInViewport(new LocatorAssertions.IsInViewportOptions().setRatio(0.5));
|
||||
```
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- DOM snapshots in trace viewer can be now opened in a separate window.
|
||||
- New option [`option: Route.fetch.maxRedirects`] for method [`method: Route.fetch`].
|
||||
- Playwright now supports Debian 11 arm64.
|
||||
- Official [docker images](./docker.md) now include Node 18 instead of Node 16.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 111.0.5563.19
|
||||
* Mozilla Firefox 109.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 110
|
||||
* Microsoft Edge 110
|
||||
|
||||
|
||||
## Version 1.30
|
||||
|
||||
### Browser Versions
|
||||
|
|
|
|||
|
|
@ -6,6 +6,108 @@ toc_max_heading_level: 2
|
|||
|
||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||
|
||||
## Version 1.31
|
||||
|
||||
### New APIs
|
||||
|
||||
- New property [`property: TestProject.dependencies`] to configure dependencies between projects.
|
||||
|
||||
Using dependencies allows global setup to produce traces and other artifacts,
|
||||
see the setup steps in the test report and more.
|
||||
|
||||
```js
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: /global.setup\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: devices['Desktop Chrome'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: devices['Desktop Firefox'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: devices['Desktop Safari'],
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
- New assertion [`method: LocatorAssertions.toBeInViewport`] ensures that locator points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
||||
|
||||
```js
|
||||
const button = page.getByRole('button');
|
||||
|
||||
// Make sure at least some part of element intersects viewport.
|
||||
await expect(button).toBeInViewport();
|
||||
|
||||
// Make sure element is fully outside of viewport.
|
||||
await expect(button).not.toBeInViewport();
|
||||
|
||||
// Make sure that at least half of the element intersects viewport.
|
||||
await expect(button).toBeInViewport({ ratio: 0.5 });
|
||||
```
|
||||
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- DOM snapshots in trace viewer can be now opened in a separate window.
|
||||
- New method `defineConfig` to be used in `playwright.config`.
|
||||
- New option [`option: Route.fetch.maxRedirects`] for method [`method: Route.fetch`].
|
||||
- Playwright now supports Debian 11 arm64.
|
||||
- Official [docker images](./docker.md) now include Node 18 instead of Node 16.
|
||||
|
||||
|
||||
## ⚠️ Breaking change in component tests
|
||||
|
||||
Note: **component tests only**, does not affect end-to-end tests.
|
||||
|
||||
`playwright-ct.config` configuration file for [component testing](./test-components.md) now requires calling `defineConfig`.
|
||||
|
||||
```js
|
||||
// Before
|
||||
|
||||
import { type PlaywrightTestConfig, devices } from '@playwright/experimental-ct-react';
|
||||
const config: PlaywrightTestConfig = {
|
||||
// ... config goes here ...
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
Replace `config` variable definition with `defineConfig` call:
|
||||
|
||||
```js
|
||||
// After
|
||||
|
||||
import { defineConfig, devices } from '@playwright/experimental-ct-react';
|
||||
export default defineConfig({
|
||||
// ... config goes here ...
|
||||
});
|
||||
```
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 111.0.5563.19
|
||||
* Mozilla Firefox 109.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 110
|
||||
* Microsoft Edge 110
|
||||
|
||||
|
||||
## Version 1.30
|
||||
|
||||
### Browser Versions
|
||||
|
|
@ -326,7 +428,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Announcements
|
||||
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.31.0-jammy`.
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.31.2-jammy`.
|
||||
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
|
||||
* 🪦 This is the last release with Node.js 12 support, we recommend upgrading to Node.js LTS (16).
|
||||
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
|
||||
|
|
@ -577,7 +679,7 @@ Read more about [component testing with Playwright](./test-components).
|
|||
}
|
||||
});
|
||||
```
|
||||
* Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.31.0-jammy`.
|
||||
* Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.31.2-jammy`.
|
||||
|
||||
### ⚠️ Breaking Changes ⚠️
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,47 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.31
|
||||
|
||||
### New APIs
|
||||
|
||||
- New assertion [`method: LocatorAssertions.toBeInViewport`] ensures that locator points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
||||
|
||||
|
||||
```python
|
||||
from playwright.sync_api import expect
|
||||
|
||||
locator = page.get_by_role("button")
|
||||
|
||||
# Make sure at least some part of element intersects viewport.
|
||||
expect(locator).to_be_in_viewport()
|
||||
|
||||
# Make sure element is fully outside of viewport.
|
||||
expect(locator).not_to_be_in_viewport()
|
||||
|
||||
# Make sure that at least half of the element intersects viewport.
|
||||
expect(locator).to_be_in_viewport(ratio=0.5)
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- DOM snapshots in trace viewer can be now opened in a separate window.
|
||||
- New option [`option: Route.fetch.maxRedirects`] for method [`method: Route.fetch`].
|
||||
- Playwright now supports Debian 11 arm64.
|
||||
- Official [docker images](./docker.md) now include Node 18 instead of Node 16.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 111.0.5563.19
|
||||
* Mozilla Firefox 109.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 110
|
||||
* Microsoft Edge 110
|
||||
|
||||
|
||||
## Version 1.30
|
||||
|
||||
### Browser Versions
|
||||
|
|
@ -196,7 +237,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Announcements
|
||||
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.31.0-jammy`.
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.31.2-jammy`.
|
||||
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
|
||||
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
|
|||
If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots:
|
||||
|
||||
```bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.31.0-focal /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.31.2-focal /bin/bash
|
||||
npm install
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
|
|
|||
66
package-lock.json
generated
66
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -5893,11 +5893,11 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5907,11 +5907,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5921,7 +5921,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5932,10 +5932,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
@ -5945,10 +5945,10 @@
|
|||
},
|
||||
"packages/playwright-ct-solid": {
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-solid": "^2.5.0"
|
||||
},
|
||||
|
|
@ -5961,10 +5961,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
@ -5997,10 +5997,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
@ -6046,10 +6046,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue2": {
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
@ -6061,11 +6061,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6076,11 +6076,11 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6093,11 +6093,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6841,7 +6841,7 @@
|
|||
"@playwright/experimental-ct-react": {
|
||||
"version": "file:packages/playwright-ct-react",
|
||||
"requires": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.1.1"
|
||||
}
|
||||
|
|
@ -6849,7 +6849,7 @@
|
|||
"@playwright/experimental-ct-solid": {
|
||||
"version": "file:packages/playwright-ct-solid",
|
||||
"requires": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"solid-js": "^1.6.10",
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-solid": "^2.5.0"
|
||||
|
|
@ -6858,7 +6858,7 @@
|
|||
"@playwright/experimental-ct-svelte": {
|
||||
"version": "file:packages/playwright-ct-svelte",
|
||||
"requires": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"svelte": "^3.55.1",
|
||||
"vite": "^4.1.1"
|
||||
|
|
@ -6882,7 +6882,7 @@
|
|||
"@playwright/experimental-ct-vue": {
|
||||
"version": "file:packages/playwright-ct-vue",
|
||||
"requires": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
@ -6917,7 +6917,7 @@
|
|||
"@playwright/experimental-ct-vue2": {
|
||||
"version": "file:packages/playwright-ct-vue2",
|
||||
"requires": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"vite": "^4.1.1",
|
||||
"vue": "^2.7.14"
|
||||
|
|
@ -6928,7 +6928,7 @@
|
|||
"requires": {
|
||||
"@types/node": "*",
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
|
|
@ -9032,13 +9032,13 @@
|
|||
"playwright": {
|
||||
"version": "file:packages/playwright",
|
||||
"requires": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
},
|
||||
"playwright-chromium": {
|
||||
"version": "file:packages/playwright-chromium",
|
||||
"requires": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
|
|
@ -9047,13 +9047,13 @@
|
|||
"playwright-firefox": {
|
||||
"version": "file:packages/playwright-firefox",
|
||||
"requires": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
},
|
||||
"playwright-webkit": {
|
||||
"version": "file:packages/playwright-webkit",
|
||||
"requires": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
"name": "chromium",
|
||||
"revision": "1048",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"win64": "1050"
|
||||
},
|
||||
"browserVersion": "111.0.5563.19"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@ import { Artifact } from './artifact';
|
|||
import { APIRequestContext } from './fetch';
|
||||
import { createInstrumentation } from './clientInstrumentation';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { HarRouter } from './harRouter';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
_pages = new Set<Page>();
|
||||
private _router: network.NetworkRouter;
|
||||
private _routes: network.RouteHandler[] = [];
|
||||
readonly _browser: Browser | null = null;
|
||||
private _browserType: BrowserType | undefined;
|
||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||
|
|
@ -72,7 +73,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
if (parent instanceof Browser)
|
||||
this._browser = parent;
|
||||
this._isChromium = this._browser?._name === 'chromium';
|
||||
this._router = new network.NetworkRouter(this, this._options.baseURL);
|
||||
this.tracing = Tracing.from(initializer.tracing);
|
||||
this.request = APIRequestContext.from(initializer.requestContext);
|
||||
|
||||
|
|
@ -153,8 +153,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async _onRoute(route: network.Route) {
|
||||
if (await this._router.handleRoute(route))
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (!routeHandler.matches(route.request().url()))
|
||||
continue;
|
||||
if (routeHandler.willExpire())
|
||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
await route._innerContinue(true);
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +261,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||
await this._router.route(url, handler, options);
|
||||
this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
|
||||
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
||||
|
|
@ -272,11 +283,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
await this._recordIntoHAR(har, null, options);
|
||||
return;
|
||||
}
|
||||
await this._router.routeFromHAR(har, options);
|
||||
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
|
||||
harRouter.addContextRoute(this);
|
||||
}
|
||||
|
||||
async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise<void> {
|
||||
await this._router.unroute(url, handler);
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
|
||||
private async _updateInterceptionPatterns() {
|
||||
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._channel.setNetworkInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
*/
|
||||
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { Events } from './events';
|
||||
import type { LocalUtils } from './localUtils';
|
||||
import type { Route } from './network';
|
||||
import type { URLMatch } from './types';
|
||||
import type { Page } from './page';
|
||||
|
||||
type HarNotFoundAction = 'abort' | 'fallback';
|
||||
|
||||
|
|
@ -24,21 +28,23 @@ export class HarRouter {
|
|||
private _localUtils: LocalUtils;
|
||||
private _harId: string;
|
||||
private _notFoundAction: HarNotFoundAction;
|
||||
private _options: { urlMatch?: URLMatch; baseURL?: string; };
|
||||
|
||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction): Promise<HarRouter> {
|
||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
|
||||
const { harId, error } = await localUtils._channel.harOpen({ file });
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
return new HarRouter(localUtils, harId!, notFoundAction);
|
||||
return new HarRouter(localUtils, harId!, notFoundAction, options);
|
||||
}
|
||||
|
||||
private constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction) {
|
||||
private constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }) {
|
||||
this._localUtils = localUtils;
|
||||
this._harId = harId;
|
||||
this._options = options;
|
||||
this._notFoundAction = notFoundAction;
|
||||
}
|
||||
|
||||
async handleRoute(route: Route) {
|
||||
private async _handle(route: Route) {
|
||||
const request = route.request();
|
||||
|
||||
const response = await this._localUtils._channel.harLookup({
|
||||
|
|
@ -77,6 +83,16 @@ export class HarRouter {
|
|||
await route.fallback();
|
||||
}
|
||||
|
||||
async addContextRoute(context: BrowserContext) {
|
||||
await context.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
||||
context.once(Events.BrowserContext.Close, () => this.dispose());
|
||||
}
|
||||
|
||||
async addPageRoute(page: Page) {
|
||||
await page.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
||||
page.once(Events.Page.Close, () => this.dispose());
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ import { urlMatches } from '../utils/network';
|
|||
import { MultiMap } from '../utils/multimap';
|
||||
import { APIResponse } from './fetch';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { HarRouter } from './harRouter';
|
||||
import { kBrowserOrContextClosedError } from '../common/errors';
|
||||
|
||||
export type NetworkCookie = {
|
||||
|
|
@ -629,64 +627,7 @@ export function validateHeaders(headers: Headers) {
|
|||
}
|
||||
}
|
||||
|
||||
export class NetworkRouter {
|
||||
private _owner: Page | BrowserContext;
|
||||
private _baseURL: string | undefined;
|
||||
private _routes: RouteHandler[] = [];
|
||||
|
||||
constructor(owner: Page | BrowserContext, baseURL: string | undefined) {
|
||||
this._owner = owner;
|
||||
this._baseURL = baseURL;
|
||||
}
|
||||
|
||||
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||
this._routes.unshift(new RouteHandler(this._baseURL, url, handler, options.times));
|
||||
await this._updateInterception();
|
||||
}
|
||||
|
||||
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback' } = {}): Promise<void> {
|
||||
const harRouter = await HarRouter.create(this._owner._connection.localUtils(), har, options.notFound || 'abort');
|
||||
await this.route(options.url || '**/*', route => harRouter.handleRoute(route));
|
||||
this._owner.once('close', () => harRouter.dispose());
|
||||
}
|
||||
|
||||
async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
await this._updateInterception();
|
||||
}
|
||||
|
||||
async handleRoute(route: Route) {
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (!routeHandler.matches(route.request().url()))
|
||||
continue;
|
||||
if (routeHandler.willExpire())
|
||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length)
|
||||
this._owner._wrapApiCall(() => this._updateInterception(), true).catch(() => {});
|
||||
if (handled)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _updateInterception() {
|
||||
const patterns: channels.BrowserContextSetNetworkInterceptionPatternsParams['patterns'] = [];
|
||||
let all = false;
|
||||
for (const handler of this._routes) {
|
||||
if (isString(handler.url))
|
||||
patterns.push({ glob: handler.url });
|
||||
else if (isRegExp(handler.url))
|
||||
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||
else
|
||||
all = true;
|
||||
}
|
||||
await this._owner._channel.setNetworkInterceptionPatterns(all ? { patterns: [{ glob: '**/*' }] } : { patterns });
|
||||
}
|
||||
}
|
||||
|
||||
class RouteHandler {
|
||||
export class RouteHandler {
|
||||
private handledCount = 0;
|
||||
private readonly _baseURL: string | undefined;
|
||||
private readonly _times: number;
|
||||
|
|
@ -700,6 +641,22 @@ class RouteHandler {
|
|||
this.handler = handler;
|
||||
}
|
||||
|
||||
static prepareInterceptionPatterns(handlers: RouteHandler[]) {
|
||||
const patterns: channels.BrowserContextSetNetworkInterceptionPatternsParams['patterns'] = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if (isString(handler.url))
|
||||
patterns.push({ glob: handler.url });
|
||||
else if (isRegExp(handler.url))
|
||||
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||
else
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
return [{ glob: '**/*' }];
|
||||
return patterns;
|
||||
}
|
||||
|
||||
public matches(requestURL: string): boolean {
|
||||
return urlMatches(this._baseURL, requestURL, this.url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,13 +46,12 @@ import { Keyboard, Mouse, Touchscreen } from './input';
|
|||
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import { NetworkRouter, type RouteHandlerCallback } from './network';
|
||||
import { Response, Route, validateHeaders, WebSocket } from './network';
|
||||
import type { Request } from './network';
|
||||
import { type RouteHandlerCallback, type Request, Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
||||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, URLMatch, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||
import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { HarRouter } from './harRouter';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
width?: string | number,
|
||||
|
|
@ -84,7 +83,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
private _closed = false;
|
||||
_closedOrCrashedPromise: Promise<void>;
|
||||
private _viewportSize: Size | null;
|
||||
private _router: NetworkRouter;
|
||||
private _routes: RouteHandler[] = [];
|
||||
|
||||
readonly accessibility: Accessibility;
|
||||
readonly coverage: Coverage;
|
||||
|
|
@ -110,7 +109,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
super(parent, type, guid, initializer);
|
||||
this._browserContext = parent as unknown as BrowserContext;
|
||||
this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings);
|
||||
this._router = new NetworkRouter(this, this._browserContext._options.baseURL);
|
||||
|
||||
this.accessibility = new Accessibility(this._channel);
|
||||
this.keyboard = new Keyboard(this);
|
||||
|
|
@ -187,8 +185,18 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
private async _onRoute(route: Route) {
|
||||
if (await this._router.handleRoute(route))
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (!routeHandler.matches(route.request().url()))
|
||||
continue;
|
||||
if (routeHandler.willExpire())
|
||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
await this._browserContext._onRoute(route);
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +457,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||
await this._router.route(url, handler, options);
|
||||
this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
|
||||
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
||||
|
|
@ -457,11 +466,18 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
await this._browserContext._recordIntoHAR(har, this, options);
|
||||
return;
|
||||
}
|
||||
await this._router.routeFromHAR(har, options);
|
||||
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
|
||||
harRouter.addPageRoute(this);
|
||||
}
|
||||
|
||||
async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
|
||||
await this._router.unroute(url, handler);
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
|
||||
private async _updateInterceptionPatterns() {
|
||||
const patterns = RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._channel.setNetworkInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
|
|
|
|||
|
|
@ -1586,7 +1586,6 @@ scheme.FrameExpectParams = tObject({
|
|||
expectedText: tOptional(tArray(tType('ExpectedTextValue'))),
|
||||
expectedNumber: tOptional(tNumber),
|
||||
expectedValue: tOptional(tType('SerializedArgument')),
|
||||
viewportRatio: tOptional(tNumber),
|
||||
useInnerText: tOptional(tBoolean),
|
||||
isNot: tBoolean,
|
||||
timeout: tOptional(tNumber),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import { Browser } from '../browser';
|
|||
import type * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import { NET_DEFAULT_TIMEOUT } from '../../utils/network';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { wrapInASCIIBox } from '../../utils/ascii';
|
||||
|
|
@ -45,13 +44,11 @@ import { ProgressController } from '../progress';
|
|||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||
import { helper } from '../helper';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import type http from 'http';
|
||||
import { registry } from '../registry';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { chromiumSwitches } from './chromiumSwitches';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../happy-eyeballs';
|
||||
|
||||
const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
|
||||
|
||||
|
|
@ -338,21 +335,11 @@ async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
|
|||
return endpointURL;
|
||||
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
|
||||
const isHTTPS = endpointURL.startsWith('https://');
|
||||
const json = await new Promise<string>((resolve, reject) => {
|
||||
(isHTTPS ? https : http).get(httpURL, {
|
||||
timeout: NET_DEFAULT_TIMEOUT,
|
||||
agent: isHTTPS ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent,
|
||||
}, resp => {
|
||||
if (resp.statusCode! < 200 || resp.statusCode! >= 400) {
|
||||
reject(new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
|
||||
`This does not look like a DevTools server, try connecting via ws://.`));
|
||||
}
|
||||
let data = '';
|
||||
resp.on('data', chunk => data += chunk);
|
||||
resp.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
const json = await fetchData({
|
||||
url: httpURL,
|
||||
}, async (_, resp) => new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
|
||||
`This does not look like a DevTools server, try connecting via ws://.`)
|
||||
);
|
||||
return JSON.parse(json).webSocketDebuggerUrl;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
|||
import { BrowserContext } from './browserContext';
|
||||
import { CookieStore, domainMatches } from './cookieStore';
|
||||
import { MultipartFormData } from './formData';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import type { Playwright } from './playwright';
|
||||
|
|
@ -69,7 +69,7 @@ export type APIRequestFinishedEvent = {
|
|||
body?: Buffer;
|
||||
};
|
||||
|
||||
export type SendRequestOptions = https.RequestOptions & {
|
||||
type SendRequestOptions = https.RequestOptions & {
|
||||
maxRedirects: number,
|
||||
deadline: number,
|
||||
__testHookLookup?: (hostname: string) => LookupAddress[]
|
||||
|
|
|
|||
|
|
@ -393,8 +393,21 @@ export class FFPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
const mainFrame = this._page._frameManager.mainFrame();
|
||||
// This is a workaround for https://github.com/microsoft/playwright/issues/21145
|
||||
let hash = '';
|
||||
try {
|
||||
hash = (new URL(mainFrame.url())).hash;
|
||||
} catch (e) {
|
||||
// Ignore URL parsing error, if any.
|
||||
}
|
||||
if (hash.length) {
|
||||
const context = await mainFrame._utilityContext();
|
||||
await context.rawEvaluateJSON(`void window.location.reload();`);
|
||||
} else {
|
||||
await this._session.send('Page.reload');
|
||||
}
|
||||
}
|
||||
|
||||
async goBack(): Promise<boolean> {
|
||||
const { success } = await this._session.send('Page.goBack', { frameId: this._page.mainFrame()._id });
|
||||
|
|
|
|||
|
|
@ -1191,7 +1191,7 @@ export class InjectedScript {
|
|||
// Viewport intersection
|
||||
if (expression === 'to.be.in.viewport') {
|
||||
const ratio = await this.viewportRatio(element);
|
||||
return { received: `viewport ratio ${ratio}`, matches: ratio > 0 && ratio > (options.viewportRatio ?? 0) - 1e-9 };
|
||||
return { received: `viewport ratio ${ratio}`, matches: ratio > 0 && ratio > (options.expectedNumber ?? 0) - 1e-9 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ class Recorder {
|
|||
const activeElement = this._deepActiveElement(document);
|
||||
// Firefox dispatches "focus" event to body when clicking on a backgrounded headed browser window.
|
||||
// We'd like to ignore this stray event.
|
||||
if (activeElement === document.body)
|
||||
if (userGesture && activeElement === document.body)
|
||||
return;
|
||||
const result = activeElement ? generateSelector(this._injectedScript, activeElement, this._testIdAttributeName) : null;
|
||||
this._activeModel = result && result.selector ? result : null;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import type { WebSocket } from '../utilsBundle';
|
|||
import type { ClientRequest, IncomingMessage } from 'http';
|
||||
import type { Progress } from './progress';
|
||||
import { makeWaitForNextTask } from '../utils';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||
|
||||
export type ProtocolRequest = {
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ import * as http from 'http';
|
|||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import type { SendRequestOptions } from './fetch';
|
||||
import { ManualPromise } from './manualPromise';
|
||||
|
||||
// Implementation(partial) of Happy Eyeballs 2 algorithm described in
|
||||
// https://www.rfc-editor.org/rfc/rfc8305
|
||||
|
|
@ -50,7 +49,7 @@ export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
|
|||
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent();
|
||||
|
||||
async function createConnectionAsync(options: http.ClientRequestArgs, oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, useTLS: boolean) {
|
||||
const lookup = (options as SendRequestOptions).__testHookLookup || lookupAddresses;
|
||||
const lookup = (options as any).__testHookLookup || lookupAddresses;
|
||||
const hostname = clientRequestArgsToHostName(options);
|
||||
const addresses = await lookup(hostname);
|
||||
const sockets = new Set<net.Socket>();
|
||||
|
|
@ -24,6 +24,7 @@ import * as URL from 'url';
|
|||
import type { URLMatch } from '../common/types';
|
||||
import { isString, isRegExp } from './rtti';
|
||||
import { globToRegex } from './glob';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
|
||||
|
||||
export async function createSocket(host: string, port: number): Promise<net.Socket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -39,15 +40,22 @@ export type HTTPRequestParams = {
|
|||
headers?: http.OutgoingHttpHeaders,
|
||||
data?: string | Buffer,
|
||||
timeout?: number,
|
||||
rejectUnauthorized?: boolean,
|
||||
};
|
||||
|
||||
export const NET_DEFAULT_TIMEOUT = 30_000;
|
||||
|
||||
export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMessage) => void, onError: (error: Error) => void) {
|
||||
const parsedUrl = URL.parse(params.url);
|
||||
let options: https.RequestOptions = { ...parsedUrl };
|
||||
options.method = params.method || 'GET';
|
||||
options.headers = params.headers;
|
||||
let options: https.RequestOptions = {
|
||||
...parsedUrl,
|
||||
agent: parsedUrl.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent,
|
||||
method: params.method || 'GET',
|
||||
headers: params.headers,
|
||||
};
|
||||
if (params.rejectUnauthorized !== undefined)
|
||||
options.rejectUnauthorized = params.rejectUnauthorized;
|
||||
|
||||
const timeout = params.timeout ?? NET_DEFAULT_TIMEOUT;
|
||||
|
||||
const proxyURL = getProxyForUrl(params.url);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
|
|
|
|||
2
packages/playwright-ct-react/cli.js
Normal file → Executable file
2
packages/playwright-ct-react/cli.js
Normal file → Executable file
|
|
@ -14,4 +14,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
module.exports = require('playwright-core/cli');
|
||||
module.exports = require('@playwright/test/cli');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
|
|
|
|||
2
packages/playwright-ct-solid/cli.js
Normal file → Executable file
2
packages/playwright-ct-solid/cli.js
Normal file → Executable file
|
|
@ -14,4 +14,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
module.exports = require('playwright-core/cli');
|
||||
module.exports = require('@playwright/test/cli');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "Playwright Component Testing for Solid",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"dependencies": {
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-solid": "^2.5.0",
|
||||
"@playwright/test": "1.31.0-next"
|
||||
"@playwright/test": "1.31.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.6.10"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
|
|
|
|||
2
packages/playwright-ct-svelte/cli.js
Normal file → Executable file
2
packages/playwright-ct-svelte/cli.js
Normal file → Executable file
|
|
@ -14,4 +14,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
module.exports = require('playwright-core/cli');
|
||||
module.exports = require('@playwright/test/cli');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
|
|
|
|||
2
packages/playwright-ct-vue/cli.js
Normal file → Executable file
2
packages/playwright-ct-vue/cli.js
Normal file → Executable file
|
|
@ -14,4 +14,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
module.exports = require('playwright-core/cli');
|
||||
module.exports = require('@playwright/test/cli');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
|
|
|
|||
2
packages/playwright-ct-vue2/cli.js
Normal file → Executable file
2
packages/playwright-ct-vue2/cli.js
Normal file → Executable file
|
|
@ -14,4 +14,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
module.exports = require('playwright-core/cli');
|
||||
module.exports = require('@playwright/test/cli');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "Playwright Component Testing for Vue2",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.31.0-next",
|
||||
"@playwright/test": "1.31.2",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"vite": "^4.1.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
*/
|
||||
|
||||
const pwt = require('./lib/index');
|
||||
const { defineConfig } = require('./lib/common/configLoader');
|
||||
const playwright = require('playwright-core');
|
||||
const defineConfig = config => config;
|
||||
const combinedExports = {
|
||||
...playwright,
|
||||
...pwt,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
|
|||
|
||||
export const defaultTimeout = 30000;
|
||||
|
||||
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
|
||||
export const defineConfig = (config: any) => {
|
||||
config[kDefineConfigWasUsed] = true;
|
||||
return config;
|
||||
};
|
||||
|
||||
export class ConfigLoader {
|
||||
private _fullConfig: FullConfigInternal;
|
||||
|
||||
|
|
@ -122,6 +128,7 @@ export class ConfigLoader {
|
|||
this._fullConfig._internal.ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._internal.ignoreSnapshots);
|
||||
this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
|
||||
this._fullConfig._internal.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p }));
|
||||
this._fullConfig._internal.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed];
|
||||
|
||||
const workers = takeFirst(config.workers, '50%');
|
||||
if (typeof workers === 'string') {
|
||||
|
|
@ -454,6 +461,7 @@ export const baseFullConfig: FullConfigInternal = {
|
|||
cliGrep: undefined,
|
||||
cliGrepInvert: undefined,
|
||||
listOnly: false,
|
||||
defineConfigWasUsed: false,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type ConfigInternal = {
|
|||
cliProjectFilter?: string[];
|
||||
testIdMatcher?: Matcher;
|
||||
passWithNoTests?: boolean;
|
||||
defineConfigWasUsed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ async function resolve(specifier: string, context: { parentURL?: string }, defau
|
|||
specifier = url.pathToFileURL(resolved).toString();
|
||||
}
|
||||
const result = await defaultResolve(specifier, context, defaultResolve);
|
||||
if (result?.url)
|
||||
if (result?.url && result.url.startsWith('file://'))
|
||||
currentFileDepsCollector()?.add(url.fileURLToPath(result.url));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export function toBeInViewport(
|
|||
options?: { timeout?: number, ratio?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.in.viewport', { isNot, viewportRatio: options?.ratio, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.be.in.viewport', { isNot, expectedNumber: options?.ratio, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext, ContextReuseMode } from './common/types';
|
||||
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext, ContextReuseMode, FullConfigInternal } from './common/types';
|
||||
import type { Component, JsxComponent, MountOptions } from '../types/component';
|
||||
|
||||
let boundCallbacksForMount: Function[] = [];
|
||||
|
|
@ -37,7 +37,9 @@ export const fixtures: Fixtures<
|
|||
|
||||
_ctWorker: [{ context: undefined, hash: '' }, { scope: 'worker' }],
|
||||
|
||||
page: async ({ page }, use) => {
|
||||
page: async ({ page }, use, info) => {
|
||||
if (!(info.config as FullConfigInternal)._internal.defineConfigWasUsed)
|
||||
throw new Error('Component testing requires the use of the defineConfig() in your playwright-ct.config.{ts,js}: https://aka.ms/playwright/ct-define-config');
|
||||
await (page as any)._wrapApiCall(async () => {
|
||||
await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => {
|
||||
boundCallbacksForMount[ordinal](...args);
|
||||
|
|
|
|||
|
|
@ -13,13 +13,11 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
import net from 'net';
|
||||
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
import { raceAgainstTimeout, launchProcess } from 'playwright-core/lib/utils';
|
||||
import { raceAgainstTimeout, launchProcess, httpRequest } from 'playwright-core/lib/utils';
|
||||
|
||||
import type { FullConfig, Reporter } from '../../types/testReporter';
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
|
|
@ -159,20 +157,18 @@ async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Re
|
|||
}
|
||||
|
||||
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: Reporter['onStdErr']): Promise<number> {
|
||||
const commonRequestOptions = { headers: { Accept: '*/*' } };
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const requestOptions = isHttps ? {
|
||||
...commonRequestOptions,
|
||||
rejectUnauthorized: !ignoreHTTPSErrors,
|
||||
} : commonRequestOptions;
|
||||
return new Promise(resolve => {
|
||||
debugWebServer(`HTTP GET: ${url}`);
|
||||
(isHttps ? https : http).get(url, requestOptions, res => {
|
||||
httpRequest({
|
||||
url: url.toString(),
|
||||
headers: { Accept: '*/*' },
|
||||
rejectUnauthorized: !ignoreHTTPSErrors
|
||||
}, res => {
|
||||
res.resume();
|
||||
const statusCode = res.statusCode ?? 0;
|
||||
debugWebServer(`HTTP Status: ${statusCode}`);
|
||||
resolve(statusCode);
|
||||
}).on('error', error => {
|
||||
}, error => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
|
||||
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
|
||||
debugWebServer(`Error while checking if ${url} is available: ${error.message}`);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import type { FullConfig, FullResult, Reporter, Suite, TestCase } from '../../types/testReporter';
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { formatFailure, formatTestTitle, stripAnsiEscapes } from './base';
|
||||
import { formatFailure, stripAnsiEscapes } from './base';
|
||||
import { assert } from 'playwright-core/lib/utils';
|
||||
|
||||
class JUnitReporter implements Reporter {
|
||||
|
|
@ -60,7 +60,7 @@ class JUnitReporter implements Reporter {
|
|||
const children: XMLEntry[] = [];
|
||||
for (const projectSuite of this.suite.suites) {
|
||||
for (const fileSuite of projectSuite.suites)
|
||||
children.push(this._buildTestSuite(fileSuite));
|
||||
children.push(this._buildTestSuite(projectSuite.title, fileSuite));
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ class JUnitReporter implements Reporter {
|
|||
}
|
||||
}
|
||||
|
||||
private _buildTestSuite(suite: Suite): XMLEntry {
|
||||
private _buildTestSuite(projectName: string, suite: Suite): XMLEntry {
|
||||
let tests = 0;
|
||||
let skipped = 0;
|
||||
let failures = 0;
|
||||
|
|
@ -106,7 +106,7 @@ class JUnitReporter implements Reporter {
|
|||
++failures;
|
||||
for (const result of test.results)
|
||||
duration += result.duration;
|
||||
this._addTestCase(test, children);
|
||||
this._addTestCase(suite.title, test, children);
|
||||
});
|
||||
this.totalTests += tests;
|
||||
this.totalSkipped += skipped;
|
||||
|
|
@ -115,9 +115,9 @@ class JUnitReporter implements Reporter {
|
|||
const entry: XMLEntry = {
|
||||
name: 'testsuite',
|
||||
attributes: {
|
||||
name: suite.location ? path.relative(this.config.rootDir, suite.location.file) : '',
|
||||
name: suite.title,
|
||||
timestamp: this.timestamp,
|
||||
hostname: '',
|
||||
hostname: projectName,
|
||||
tests,
|
||||
failures,
|
||||
skipped,
|
||||
|
|
@ -130,14 +130,16 @@ class JUnitReporter implements Reporter {
|
|||
return entry;
|
||||
}
|
||||
|
||||
private _addTestCase(test: TestCase, entries: XMLEntry[]) {
|
||||
private _addTestCase(suiteName: string, test: TestCase, entries: XMLEntry[]) {
|
||||
const entry = {
|
||||
name: 'testcase',
|
||||
attributes: {
|
||||
// Skip root, project, file
|
||||
name: test.titlePath().slice(3).join(' '),
|
||||
classname: formatTestTitle(this.config, test, undefined, true),
|
||||
name: test.titlePath().slice(3).join(' › '),
|
||||
// filename
|
||||
classname: suiteName,
|
||||
time: (test.results.reduce((acc, value) => acc + value.duration, 0)) / 1000
|
||||
|
||||
},
|
||||
children: [] as XMLEntry[]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ export class Dispatcher {
|
|||
}
|
||||
this._queue.shift();
|
||||
}
|
||||
|
||||
// If all remaining tests were skipped, resolve finished state.
|
||||
this._checkFinished();
|
||||
}
|
||||
|
||||
private async _scheduleJob() {
|
||||
|
|
|
|||
13
packages/playwright-test/types/test.d.ts
vendored
13
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -4095,7 +4095,7 @@ interface GenericAssertions<R> {
|
|||
*
|
||||
* @param expected Regular expression to match against.
|
||||
*/
|
||||
toMatch(expected: RegExp): R;
|
||||
toMatch(expected: RegExp | string): R;
|
||||
/**
|
||||
* Compares contents of the value with contents of `expected`, performing "deep equality" check. Allows extra
|
||||
* properties to be present in the value, unlike
|
||||
|
|
@ -4268,8 +4268,7 @@ export {};
|
|||
|
||||
/**
|
||||
* The [APIResponseAssertions] class provides assertion methods that can be used to make assertions about the
|
||||
* [APIResponse] in the tests. A new instance of [APIResponseAssertions] is created by calling
|
||||
* [expect(response)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-api-response):
|
||||
* [APIResponse] in the tests.
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
|
|
@ -4309,8 +4308,7 @@ interface APIResponseAssertions {
|
|||
|
||||
/**
|
||||
* The [LocatorAssertions] class provides assertion methods that can be used to make assertions about the [Locator]
|
||||
* state in the tests. A new instance of [LocatorAssertions] is created by calling
|
||||
* [expect(locator)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-locator):
|
||||
* state in the tests.
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
|
|
@ -4475,7 +4473,7 @@ interface LocatorAssertions {
|
|||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('button.submit');
|
||||
* const locator = page.getByRole('button');
|
||||
* // Make sure at least some part of element intersects viewport.
|
||||
* await expect(locator).toBeInViewport();
|
||||
* // Make sure element is fully outside of viewport.
|
||||
|
|
@ -5009,8 +5007,7 @@ interface LocatorAssertions {
|
|||
|
||||
/**
|
||||
* The [PageAssertions] class provides assertion methods that can be used to make assertions about the [Page] state in
|
||||
* the tests. A new instance of [PageAssertions] is created by calling
|
||||
* [expect(page)](https://playwright.dev/docs/api/class-playwrightassertions#playwright-assertions-expect-page):
|
||||
* the tests.
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.31.0-next",
|
||||
"version": "1.31.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.31.0-next"
|
||||
"playwright-core": "1.31.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2844,7 +2844,6 @@ export type FrameExpectParams = {
|
|||
expectedText?: ExpectedTextValue[],
|
||||
expectedNumber?: number,
|
||||
expectedValue?: SerializedArgument,
|
||||
viewportRatio?: number,
|
||||
useInnerText?: boolean,
|
||||
isNot: boolean,
|
||||
timeout?: number,
|
||||
|
|
@ -2854,7 +2853,6 @@ export type FrameExpectOptions = {
|
|||
expectedText?: ExpectedTextValue[],
|
||||
expectedNumber?: number,
|
||||
expectedValue?: SerializedArgument,
|
||||
viewportRatio?: number,
|
||||
useInnerText?: boolean,
|
||||
timeout?: number,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2144,7 +2144,6 @@ Frame:
|
|||
items: ExpectedTextValue
|
||||
expectedNumber: number?
|
||||
expectedValue: SerializedArgument?
|
||||
viewportRatio: number?
|
||||
useInnerText: boolean?
|
||||
isNot: boolean
|
||||
timeout: number?
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
*/
|
||||
|
||||
import '@web/third_party/vscode/codicon.css';
|
||||
import { Workbench } from './ui/workbench';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { applyTheme } from '@web/theme';
|
||||
import '@web/common.css';
|
||||
import { WorkbenchLoader } from './ui/workbench';
|
||||
|
||||
(async () => {
|
||||
applyTheme();
|
||||
|
|
@ -37,5 +36,5 @@ import '@web/common.css';
|
|||
setInterval(function() { fetch('ping'); }, 10000);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Workbench/>, document.querySelector('#root'));
|
||||
ReactDOM.render(<WorkbenchLoader></WorkbenchLoader>, document.querySelector('#root'));
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -14,50 +14,6 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.action-list {
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.action-list-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-entry {
|
||||
display: flex;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.action-entry.highlighted,
|
||||
.action-entry.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.action-entry.highlighted {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.action-list-content:focus .action-entry.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.action-list-content:focus .action-entry.selected * {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.action-title {
|
||||
flex: auto;
|
||||
display: block;
|
||||
|
|
@ -124,10 +80,3 @@
|
|||
.action-entry .codicon-warning {
|
||||
color: darkorange;
|
||||
}
|
||||
|
||||
.no-actions-entry {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { ListView } from '@web/components/listView';
|
||||
import * as React from 'react';
|
||||
import './actionList.css';
|
||||
import * as modelUtil from './modelUtil';
|
||||
|
|
@ -26,7 +27,6 @@ import type { Language } from '@isomorphic/locatorGenerators';
|
|||
export interface ActionListProps {
|
||||
actions: ActionTraceEvent[],
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
sdkLanguage: Language | undefined;
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
|
|
@ -36,92 +36,33 @@ export interface ActionListProps {
|
|||
export const ActionList: React.FC<ActionListProps> = ({
|
||||
actions = [],
|
||||
selectedAction,
|
||||
highlightedAction,
|
||||
sdkLanguage,
|
||||
onSelected = () => {},
|
||||
onHighlighted = () => {},
|
||||
setSelectedTab = () => {},
|
||||
}) => {
|
||||
const actionListRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
actionListRef.current?.focus();
|
||||
}, [selectedAction, actionListRef]);
|
||||
|
||||
return <div className='action-list vbox'>
|
||||
<div
|
||||
className='action-list-content'
|
||||
tabIndex={0}
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||
return;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const index = selectedAction ? actions.indexOf(selectedAction) : -1;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (index === -1)
|
||||
newIndex = 0;
|
||||
else
|
||||
newIndex = Math.min(index + 1, actions.length - 1);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (index === -1)
|
||||
newIndex = actions.length - 1;
|
||||
else
|
||||
newIndex = Math.max(index - 1, 0);
|
||||
}
|
||||
const element = actionListRef.current?.children.item(newIndex);
|
||||
scrollIntoViewIfNeeded(element);
|
||||
onSelected(actions[newIndex]);
|
||||
}}
|
||||
ref={actionListRef}
|
||||
>
|
||||
{actions.length === 0 && <div className='no-actions-entry'>No actions recorded</div>}
|
||||
{actions.map(action => <ActionListItem
|
||||
action={action}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={onSelected}
|
||||
onHighlighted={onHighlighted}
|
||||
selectedAction={selectedAction}
|
||||
sdkLanguage={sdkLanguage}
|
||||
setSelectedTab={setSelectedTab}
|
||||
/>)}
|
||||
</div>
|
||||
</div>;
|
||||
return <ListView
|
||||
items={actions}
|
||||
selectedItem={selectedAction}
|
||||
onSelected={(action: ActionTraceEvent) => onSelected(action)}
|
||||
onHighlighted={(action: ActionTraceEvent) => onHighlighted(action)}
|
||||
itemKey={(action: ActionTraceEvent) => action.metadata.id}
|
||||
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, setSelectedTab)}
|
||||
showNoItemsMessage={true}
|
||||
></ListView>;
|
||||
};
|
||||
|
||||
const ActionListItem: React.FC<{
|
||||
const renderAction = (
|
||||
action: ActionTraceEvent,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
sdkLanguage: Language | undefined,
|
||||
setSelectedTab: (tab: string) => void,
|
||||
}> = ({ action, onSelected, onHighlighted, highlightedAction, selectedAction, sdkLanguage, setSelectedTab }) => {
|
||||
setSelectedTab: (tab: string) => void
|
||||
) => {
|
||||
const { metadata } = action;
|
||||
const selectedSuffix = action === selectedAction ? ' selected' : '';
|
||||
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
||||
const error = metadata.error?.error?.message;
|
||||
const { errors, warnings } = modelUtil.stats(action);
|
||||
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
|
||||
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (divRef.current && selectedAction === action)
|
||||
scrollIntoViewIfNeeded(divRef.current);
|
||||
}, [selectedAction, action]);
|
||||
|
||||
return <div
|
||||
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
||||
key={metadata.id}
|
||||
onClick={() => onSelected(action)}
|
||||
onMouseEnter={() => onHighlighted(action)}
|
||||
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
|
||||
ref={divRef}
|
||||
>
|
||||
return <>
|
||||
<div className='action-title'>
|
||||
<span>{metadata.apiName}</span>
|
||||
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
||||
|
|
@ -133,14 +74,5 @@ const ActionListItem: React.FC<{
|
|||
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
||||
</div>
|
||||
{error && <div className='codicon codicon-issues' title={error} />}
|
||||
</div>;
|
||||
</>;
|
||||
};
|
||||
|
||||
function scrollIntoViewIfNeeded(element?: Element | null) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,8 @@
|
|||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
color: var(--vscode-sideBarTitle-foreground);
|
||||
line-height: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.call-line {
|
||||
|
|
@ -68,6 +66,7 @@
|
|||
}
|
||||
|
||||
.call-value {
|
||||
margin-left: 2px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
|
@ -77,18 +76,18 @@
|
|||
content: '\00a0';
|
||||
}
|
||||
|
||||
.call-line .datetime,
|
||||
.call-line .string,
|
||||
.call-line .locator {
|
||||
.call-value.datetime,
|
||||
.call-value.string,
|
||||
.call-value.locator {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.call-line .number,
|
||||
.call-line .bigint,
|
||||
.call-line .boolean,
|
||||
.call-line .symbol,
|
||||
.call-line .undefined,
|
||||
.call-line .function,
|
||||
.call-line .object {
|
||||
.call-value.number,
|
||||
.call-value.bigint,
|
||||
.call-value.boolean,
|
||||
.call-value.symbol,
|
||||
.call-value.undefined,
|
||||
.call-value.function,
|
||||
.call-value.object {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export const CallTab: React.FunctionComponent<{
|
|||
<div className='call-line'>{action.metadata.apiName}</div>
|
||||
{<>
|
||||
<div className='call-section'>Time</div>
|
||||
{action.metadata.wallTime && <div className='call-line'>wall time: <span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||
<div className='call-line'>duration: <span className='call-value datetime' title={duration}>{duration}</span></div>
|
||||
{action.metadata.wallTime && <div className='call-line'>wall time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
|
||||
<div className='call-line'>duration:<span className='call-value datetime' title={duration}>{duration}</span></div>
|
||||
</>}
|
||||
{ !!paramKeys.length && <div className='call-section'>Parameters</div> }
|
||||
{
|
||||
|
|
@ -82,7 +82,7 @@ function renderProperty(property: Property, key: string) {
|
|||
text = `"${text}"`;
|
||||
return (
|
||||
<div key={key} className='call-line'>
|
||||
{property.name}: <span className={`call-value ${property.type}`} title={property.text}>{text}</span>
|
||||
{property.name}:<span className={`call-value ${property.type}`} title={property.text}>{text}</span>
|
||||
{ ['string', 'number', 'object', 'locator'].includes(property.type) &&
|
||||
<CopyToClipboard value={property.text} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.codicon-check {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.codicon-close {
|
||||
color: var(--red);
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import './copyToClipboard.css';
|
||||
|
||||
export const CopyToClipboard: React.FunctionComponent<{
|
||||
value: string,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
*/
|
||||
|
||||
.network-request {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
padding: 0 3px;
|
||||
width: 100%;
|
||||
flex: none;
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -58,6 +57,7 @@
|
|||
.network-request-details {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.network-request-details-url {
|
||||
|
|
@ -84,6 +84,7 @@
|
|||
background-color: var(--vscode-sideBar-background);
|
||||
border: black 1px solid;
|
||||
max-height: 500px;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.network-request-details-header {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
align-items: stretch;
|
||||
outline: none;
|
||||
--window-header-height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snapshot-controls {
|
||||
|
|
@ -85,18 +86,25 @@ iframe#snapshot {
|
|||
|
||||
.popout-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: var(--gray);
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: var(--vscode-sideBarTitle-foreground);
|
||||
font-size: 14px;
|
||||
z-index: 100;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.popout-icon:not(.popout-disabled):hover {
|
||||
color: var(--blue);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.popout-icon.popout-disabled {
|
||||
opacity: 0.7;
|
||||
opacity: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
.window-dot {
|
||||
|
|
@ -109,11 +117,11 @@ iframe#snapshot {
|
|||
}
|
||||
|
||||
.window-address-bar {
|
||||
background-color: white;
|
||||
background-color: var(--vscode-input-background);
|
||||
border-radius: 12.5px;
|
||||
color: #1c1e21;
|
||||
color: var(--vscode-input-foreground);
|
||||
flex: 1 0;
|
||||
font: 400 13px Arial,sans-serif;
|
||||
font: 400 16px Arial,sans-serif;
|
||||
margin: 0 16px 0 8px;
|
||||
padding: 5px 15px;
|
||||
overflow: hidden;
|
||||
|
|
@ -121,11 +129,6 @@ iframe#snapshot {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.dark-mode .window-address-bar {
|
||||
background-color: #1b1b1d;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.window-menu-bar {
|
||||
background-color: #aaa;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,12 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
if (!iframeRef.current)
|
||||
return;
|
||||
try {
|
||||
iframeRef.current.src = snapshotUrl + (pointX === undefined ? '' : `&pointX=${pointX}&pointY=${pointY}`);
|
||||
const newUrl = snapshotUrl + (pointX === undefined ? '' : `&pointX=${pointX}&pointY=${pointY}`);
|
||||
// Try preventing history entry from being created.
|
||||
if (iframeRef.current.contentWindow)
|
||||
iframeRef.current.contentWindow.location.replace(newUrl);
|
||||
else
|
||||
iframeRef.current.src = newUrl;
|
||||
} catch (e) {
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -41,10 +41,8 @@ export const Timeline: React.FunctionComponent<{
|
|||
context: MultiTraceModel,
|
||||
boundaries: Boundaries,
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
onSelected: (action: ActionTraceEvent) => void,
|
||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
||||
}> = ({ context, boundaries, selectedAction, onSelected }) => {
|
||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
const barsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
|
@ -92,7 +90,7 @@ export const Timeline: React.FunctionComponent<{
|
|||
}, [context, boundaries, measure.width]);
|
||||
|
||||
const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined;
|
||||
let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === (highlightedAction || selectedAction));
|
||||
let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === selectedAction);
|
||||
targetBar = hoveredBar || targetBar;
|
||||
|
||||
const findHoveredBarIndex = (x: number, y: number) => {
|
||||
|
|
@ -132,14 +130,11 @@ export const Timeline: React.FunctionComponent<{
|
|||
const index = findHoveredBarIndex(x, y);
|
||||
setPreviewPoint({ x, clientY: event.clientY });
|
||||
setHoveredBarIndex(index);
|
||||
if (typeof index === 'number')
|
||||
onHighlighted(bars[index].action);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setPreviewPoint(undefined);
|
||||
setHoveredBarIndex(undefined);
|
||||
onHighlighted(undefined);
|
||||
};
|
||||
|
||||
const onClick = (event: React.MouseEvent) => {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
contain: size;
|
||||
}
|
||||
|
||||
.workbench .header {
|
||||
.header {
|
||||
display: flex;
|
||||
background-color: #000;
|
||||
flex: none;
|
||||
|
|
|
|||
|
|
@ -33,15 +33,11 @@ import { Timeline } from './timeline';
|
|||
import './workbench.css';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
export const WorkbenchLoader: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
||||
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
|
||||
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
|
||||
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
||||
const [dragOver, setDragOver] = React.useState<boolean>(false);
|
||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
|
||||
|
|
@ -67,7 +63,6 @@ export const Workbench: React.FunctionComponent<{
|
|||
window.history.pushState({}, '', href);
|
||||
setTraceURLs(blobUrls);
|
||||
setUploadedTraceNames(fileNames);
|
||||
setSelectedAction(undefined);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
};
|
||||
|
|
@ -134,24 +129,6 @@ export const Workbench: React.FunctionComponent<{
|
|||
})();
|
||||
}, [traceURLs, uploadedTraceNames]);
|
||||
|
||||
const boundaries = { minimum: model.startTime, maximum: model.endTime };
|
||||
|
||||
|
||||
// Leave some nice free space on the right hand side.
|
||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||
const { errors, warnings } = selectedAction ? modelUtil.stats(selectedAction) : { errors: 0, warnings: 0 };
|
||||
const consoleCount = errors + warnings;
|
||||
const networkCount = selectedAction ? modelUtil.resourcesForAction(selectedAction).length : 0;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={selectedAction} sdkLanguage={model.sdkLanguage} /> },
|
||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={selectedAction} /> },
|
||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={selectedAction} /> },
|
||||
];
|
||||
|
||||
if (model.hasSource)
|
||||
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> });
|
||||
|
||||
return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
|
||||
<div className='hbox header'>
|
||||
<div className='logo'>🎭</div>
|
||||
|
|
@ -160,55 +137,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
<div className='spacer'></div>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '20px', flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
||||
<Timeline
|
||||
context={model}
|
||||
boundaries={boundaries}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => setSelectedAction(action)}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<SplitView sidebarSize={300} orientation='horizontal'>
|
||||
<SnapshotTab action={selectedAction} />
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||
</SplitView>
|
||||
<TabbedPane tabs={
|
||||
[
|
||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||
sdkLanguage={model.sdkLanguage}
|
||||
actions={model.actions}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
setSelectedTab={setSelectedPropertiesTab}
|
||||
/> },
|
||||
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
|
||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||
{model.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
||||
<div className='call-line'>duration: <span className='number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||
<div className='call-section'>Browser</div>
|
||||
<div className='call-line'>engine: <span className='string' title={model.browserName}>{model.browserName}</span></div>
|
||||
{model.platform && <div className='call-line'>platform: <span className='string' title={model.platform}>{model.platform}</span></div>}
|
||||
{model.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={model.options.userAgent}>{model.options.userAgent}</span></div>}
|
||||
<div className='call-section'>Viewport</div>
|
||||
{model.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</span></div>}
|
||||
{model.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!model.options.viewport?.height)}>{model.options.viewport.height}</span></div>}
|
||||
<div className='call-line'>is mobile: <span className='boolean' title={String(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</span></div>
|
||||
{model.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(model.options.deviceScaleFactor)}>{String(model.options.deviceScaleFactor)}</span></div>}
|
||||
<div className='call-section'>Counts</div>
|
||||
<div className='call-line'>pages: <span className='number'>{model.pages.length}</span></div>
|
||||
<div className='call-line'>actions: <span className='number'>{model.actions.length}</span></div>
|
||||
<div className='call-line'>events: <span className='number'>{model.events.length}</span></div>
|
||||
</div> },
|
||||
]
|
||||
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
||||
</SplitView>
|
||||
<Workbench model={model} view='standalone'></Workbench>
|
||||
{!!progress.total && <div className='progress'>
|
||||
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
|
||||
</div>}
|
||||
|
|
@ -241,4 +170,91 @@ export const Workbench: React.FunctionComponent<{
|
|||
</div>;
|
||||
};
|
||||
|
||||
const emptyModel = new MultiTraceModel([]);
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
model: MultiTraceModel,
|
||||
view: 'embedded' | 'standalone'
|
||||
}> = ({ model, view }) => {
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
|
||||
|
||||
const activeAction = highlightedAction || selectedAction;
|
||||
const boundaries = { minimum: model.startTime, maximum: model.endTime };
|
||||
|
||||
// Leave some nice free space on the right hand side.
|
||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
|
||||
const consoleCount = errors + warnings;
|
||||
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={activeAction} sdkLanguage={model.sdkLanguage} /> },
|
||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
|
||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
|
||||
];
|
||||
|
||||
if (model.hasSource)
|
||||
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> });
|
||||
|
||||
return <div className='vbox'>
|
||||
<div style={{ paddingLeft: '20px', flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
||||
<Timeline
|
||||
context={model}
|
||||
boundaries={boundaries}
|
||||
selectedAction={activeAction}
|
||||
onSelected={action => setSelectedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<SplitView sidebarSize={300} orientation={view === 'embedded' ? 'vertical' : 'horizontal'}>
|
||||
<SnapshotTab action={activeAction} />
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||
</SplitView>
|
||||
<TabbedPane tabs={
|
||||
[
|
||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||
sdkLanguage={model.sdkLanguage}
|
||||
actions={model.actions}
|
||||
selectedAction={selectedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
onHighlighted={action => {
|
||||
setHighlightedAction(action);
|
||||
}}
|
||||
setSelectedTab={setSelectedPropertiesTab}
|
||||
/> },
|
||||
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
|
||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||
{model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
||||
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||
<div className='call-section'>Browser</div>
|
||||
<div className='call-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div>
|
||||
{model.platform && <div className='call-line'>platform:<span className='call-value string' title={model.platform}>{model.platform}</span></div>}
|
||||
{model.options.userAgent && <div className='call-line'>user agent:<span className='call-value datetime' title={model.options.userAgent}>{model.options.userAgent}</span></div>}
|
||||
<div className='call-section'>Viewport</div>
|
||||
{model.options.viewport && <div className='call-line'>width:<span className='call-value number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</span></div>}
|
||||
{model.options.viewport && <div className='call-line'>height:<span className='call-value number' title={String(!!model.options.viewport?.height)}>{model.options.viewport.height}</span></div>}
|
||||
<div className='call-line'>is mobile:<span className='call-value boolean' title={String(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</span></div>
|
||||
{model.options.deviceScaleFactor && <div className='call-line'>device scale:<span className='call-value number' title={String(model.options.deviceScaleFactor)}>{String(model.options.deviceScaleFactor)}</span></div>}
|
||||
<div className='call-section'>Counts</div>
|
||||
<div className='call-line'>pages:<span className='call-value number'>{model.pages.length}</span></div>
|
||||
<div className='call-line'>actions:<span className='call-value number'>{model.actions.length}</span></div>
|
||||
<div className='call-line'>events:<span className='call-value number'>{model.events.length}</span></div>
|
||||
</div> },
|
||||
]
|
||||
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
||||
</SplitView>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const emptyModel = new MultiTraceModel([]);
|
||||
|
||||
export async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', url);
|
||||
const response = await fetch(`context?${params.toString()}`);
|
||||
const contextEntry = await response.json() as ContextEntry;
|
||||
return new MultiTraceModel([contextEntry]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,3 +96,12 @@ svg {
|
|||
flex: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codicon-check {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.codicon-close,
|
||||
.codicon-error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
|
|
|||
65
packages/web/src/components/listView.css
Normal file
65
packages/web/src/components/listView.css
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.list-view {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.list-view-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.list-view-entry {
|
||||
display: flex;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.list-view-entry.highlighted,
|
||||
.list-view-entry.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.list-view-entry.highlighted {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.selected * {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.list-view-empty {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
147
packages/web/src/components/listView.tsx
Normal file
147
packages/web/src/components/listView.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import './listView.css';
|
||||
|
||||
export type ListViewProps = {
|
||||
items: any[],
|
||||
itemKey: (item: any) => string,
|
||||
itemRender: (item: any) => React.ReactNode,
|
||||
itemIcon?: (item: any) => string | undefined,
|
||||
itemIndent?: (item: any) => number | undefined,
|
||||
selectedItem?: any,
|
||||
onAccepted?: (item: any) => void,
|
||||
onSelected?: (item: any) => void,
|
||||
onHighlighted?: (item: any | undefined) => void,
|
||||
showNoItemsMessage?: boolean,
|
||||
};
|
||||
|
||||
export const ListView: React.FC<ListViewProps> = ({
|
||||
items = [],
|
||||
itemKey,
|
||||
itemRender,
|
||||
itemIcon,
|
||||
itemIndent,
|
||||
selectedItem,
|
||||
onAccepted,
|
||||
onSelected,
|
||||
onHighlighted,
|
||||
showNoItemsMessage,
|
||||
}) => {
|
||||
const itemListRef = React.createRef<HTMLDivElement>();
|
||||
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
||||
|
||||
return <div className='list-view vbox'>
|
||||
<div
|
||||
className='list-view-content'
|
||||
tabIndex={0}
|
||||
onDoubleClick={() => onAccepted?.(selectedItem)}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onAccepted?.(selectedItem);
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||
return;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const index = selectedItem ? items.indexOf(selectedItem) : -1;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (index === -1)
|
||||
newIndex = 0;
|
||||
else
|
||||
newIndex = Math.min(index + 1, items.length - 1);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (index === -1)
|
||||
newIndex = items.length - 1;
|
||||
else
|
||||
newIndex = Math.max(index - 1, 0);
|
||||
}
|
||||
const element = itemListRef.current?.children.item(newIndex);
|
||||
scrollIntoViewIfNeeded(element);
|
||||
onHighlighted?.(undefined);
|
||||
onSelected?.(items[newIndex]);
|
||||
}}
|
||||
ref={itemListRef}
|
||||
>
|
||||
{showNoItemsMessage && items.length === 0 && <div className='list-view-empty'>No items</div>}
|
||||
{items.map(item => <ListItemView
|
||||
key={itemKey(item)}
|
||||
icon={itemIcon?.(item)}
|
||||
indent={itemIndent?.(item)}
|
||||
isHighlighted={item === highlightedItem}
|
||||
isSelected={item === selectedItem}
|
||||
onSelected={() => onSelected?.(item)}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedItem(item);
|
||||
onHighlighted?.(item);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHighlightedItem(undefined);
|
||||
onHighlighted?.(undefined);
|
||||
}}
|
||||
>
|
||||
{itemRender(item)}
|
||||
</ListItemView>)}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ListItemView: React.FC<{
|
||||
key: string,
|
||||
icon: string | undefined,
|
||||
indent: number | undefined,
|
||||
isHighlighted: boolean,
|
||||
isSelected: boolean,
|
||||
onSelected: () => void,
|
||||
onMouseEnter: () => void,
|
||||
onMouseLeave: () => void,
|
||||
children: React.ReactNode | React.ReactNode[],
|
||||
}> = ({ key, icon, indent, onSelected, onMouseEnter, onMouseLeave, isHighlighted, isSelected, children }) => {
|
||||
const selectedSuffix = isSelected ? ' selected' : '';
|
||||
const highlightedSuffix = isHighlighted ? ' highlighted' : '';
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (divRef.current && isSelected)
|
||||
scrollIntoViewIfNeeded(divRef.current);
|
||||
}, [isSelected]);
|
||||
|
||||
return <div
|
||||
key={key}
|
||||
className={'list-view-entry' + selectedSuffix + highlightedSuffix}
|
||||
onClick={onSelected}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={divRef}
|
||||
>
|
||||
{indent ? <div style={{ minWidth: indent * 16 }}></div> : undefined}
|
||||
<div className={'codicon ' + icon} style={{ minWidth: 16, marginRight: 4 }}></div>
|
||||
{typeof children === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{children}</div> : children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
function scrollIntoViewIfNeeded(element?: Element | null) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export function msToString(ms: number): string {
|
|||
return '-';
|
||||
|
||||
if (ms === 0)
|
||||
return '0ms';
|
||||
return '0';
|
||||
|
||||
if (ms < 1000)
|
||||
return ms.toFixed(0) + 'ms';
|
||||
|
|
|
|||
|
|
@ -55,13 +55,13 @@ class TraceViewerPage {
|
|||
}
|
||||
|
||||
async actionIconsText(action: string) {
|
||||
const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`);
|
||||
const entry = await this.page.waitForSelector(`.list-view-entry:has-text("${action}")`);
|
||||
await entry.waitForSelector('.action-icon-value:visible');
|
||||
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
||||
}
|
||||
|
||||
async actionIcons(action: string) {
|
||||
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
|
||||
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
||||
}
|
||||
|
||||
async selectAction(title: string, ordinal: number = 0) {
|
||||
|
|
|
|||
|
|
@ -143,29 +143,29 @@ test('should open console errors on click', async ({ showTraceViewer, browserNam
|
|||
expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show params and return value', async ({ showTraceViewer, browserName }) => {
|
||||
test('should show params and return value', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.evaluate');
|
||||
await expect(traceViewer.callLines).toHaveText([
|
||||
/page.evaluate/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
/expression: "\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
||||
'isFunction: true',
|
||||
'arg: {"a":"paramA","b":4}',
|
||||
'value: "return paramA"'
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
/expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
|
||||
'isFunction:true',
|
||||
'arg:{"a":"paramA","b":4}',
|
||||
'value:"return paramA"'
|
||||
]);
|
||||
|
||||
await traceViewer.selectAction(`locator('button')`);
|
||||
await expect(traceViewer.callLines).toContainText([
|
||||
/expect.toHaveText/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
/locator: locator\('button'\)/,
|
||||
/expression: "to.have.text"/,
|
||||
/timeout: 10000/,
|
||||
/matches: true/,
|
||||
/received: "Click"/,
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
/locator:locator\('button'\)/,
|
||||
/expression:"to.have.text"/,
|
||||
/timeout:10000/,
|
||||
/matches:true/,
|
||||
/received:"Click"/,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -174,12 +174,12 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) =>
|
|||
await traceViewer.selectAction('page.evaluate', 1);
|
||||
await expect(traceViewer.callLines).toHaveText([
|
||||
/page.evaluate/,
|
||||
/wall time: [0-9/:,APM ]+/,
|
||||
/duration: [\d]+ms/,
|
||||
'expression: "() => 1 + 1"',
|
||||
'isFunction: true',
|
||||
'arg: null',
|
||||
'value: 2'
|
||||
/wall time:[0-9/:,APM ]+/,
|
||||
/duration:[\d]+ms/,
|
||||
'expression:"() => 1 + 1"',
|
||||
'isFunction:true',
|
||||
'arg:null',
|
||||
'value:2'
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -604,15 +604,15 @@ test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
|||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.page.locator('text=Metadata').click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||
await expect(callLine.getByText('duration')).toHaveText(/duration: [\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages: 1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions: [\d]+/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/);
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform:[\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width:[\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height:[\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages:1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions:[\d]+/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/);
|
||||
});
|
||||
|
||||
test('should open two trace files', async ({ context, page, request, server, showTraceViewer }, testInfo) => {
|
||||
|
|
@ -655,16 +655,16 @@ test('should open two trace files', async ({ context, page, request, server, sho
|
|||
await traceViewer.page.locator('text=Metadata').click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
// Should get metadata from the context trace
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||
// duration in the metatadata section
|
||||
await expect(callLine.getByText('duration').first()).toHaveText(/duration: [\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine: [\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform: [\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width: [\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height: [\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages: 1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions: 6/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events: [\d]+/);
|
||||
await expect(callLine.getByText('duration').first()).toHaveText(/duration:[\dms]+/);
|
||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||
await expect(callLine.getByText('platform')).toHaveText(/platform:[\w]+/);
|
||||
await expect(callLine.getByText('width')).toHaveText(/width:[\d]+/);
|
||||
await expect(callLine.getByText('height')).toHaveText(/height:[\d]+/);
|
||||
await expect(callLine.getByText('pages')).toHaveText(/pages:1/);
|
||||
await expect(callLine.getByText('actions')).toHaveText(/actions:6/);
|
||||
await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/);
|
||||
});
|
||||
|
||||
test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, browserName }) => {
|
||||
|
|
@ -702,7 +702,7 @@ test('should include requestUrl in route.continue', async ({ page, runAndTrace,
|
|||
await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click();
|
||||
const callLine = traceViewer.page.locator('.call-line');
|
||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
||||
await expect(callLine.getByText(/^url: .*/)).toContainText(server.EMPTY_PAGE);
|
||||
await expect(callLine.getByText(/^url:.*/)).toContainText(server.EMPTY_PAGE);
|
||||
});
|
||||
|
||||
test('should include requestUrl in route.abort', async ({ page, runAndTrace, server }) => {
|
||||
|
|
|
|||
|
|
@ -185,6 +185,13 @@ it('page.reload should work with cross-origin redirect', async ({ page, server,
|
|||
await expect(page).toHaveURL(server.CROSS_PROCESS_PREFIX + '/title.html');
|
||||
});
|
||||
|
||||
it('page.reload should work on a page with a hash', async ({ page, server }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21145' });
|
||||
await page.goto(server.EMPTY_PAGE + '#hash');
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(server.EMPTY_PAGE + '#hash');
|
||||
});
|
||||
|
||||
it('page.goBack during renderer-initiated navigation', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ test('should work with generic matchers', async ({ runTSC }) => {
|
|||
expect({}).toEqual({});
|
||||
expect([1, 2]).toHaveLength(2);
|
||||
expect('abc').toMatch(/a.?c/);
|
||||
expect('abc').toMatch('abc');
|
||||
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
|
||||
expect({}).toStrictEqual({});
|
||||
expect(() => { throw new Error('Something bad'); }).toThrow('something');
|
||||
|
|
|
|||
|
|
@ -632,3 +632,32 @@ test('should import export assignment from ts', async ({ runInlineTest }) => {
|
|||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should support node imports', async ({ runInlineTest, nodeVersion }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(nodeVersion.major < 16);
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': 'export default {}',
|
||||
'package.json': JSON.stringify({
|
||||
type: 'module'
|
||||
}),
|
||||
'test.json': 'test data',
|
||||
'utils.mjs': `
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
export async function utilityModuleThatImportsNodeModule() {
|
||||
return await fs.readFile('test.json', 'utf8');
|
||||
}
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { utilityModuleThatImportsNodeModule } from './utils.mjs';
|
||||
|
||||
test('pass', async () => {
|
||||
expect(await utilityModuleThatImportsNodeModule()).toBe('test data');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ test('should not render projects if they dont exist', async ({ runInlineTest })
|
|||
expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['name']).toBe('one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('a.test.js › one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toBe('a.test.js');
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
|
|
@ -259,18 +259,20 @@ test('should render projects', async ({ runInlineTest }) => {
|
|||
expect(xml['testsuites']['testsuite'].length).toBe(2);
|
||||
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['name']).toBe('a.test.js');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['hostname']).toBe('project1');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['tests']).toBe('1');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['name']).toBe('one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('[project1] › a.test.js › one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toBe('a.test.js');
|
||||
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['name']).toBe('a.test.js');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['hostname']).toBe('project2');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['tests']).toBe('1');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['failures']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['skipped']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['name']).toBe('one');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['classname']).toContain('[project2] › a.test.js › one');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['classname']).toBe('a.test.js');
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -96,3 +96,26 @@ test('should respect shard=1/2 in config', async ({ runInlineTest }) => {
|
|||
expect(result.output).toContain('test2-done');
|
||||
expect(result.output).toContain('test3-done');
|
||||
});
|
||||
|
||||
test('should work with workers=1 and --fully-parallel', async ({ runInlineTest }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21226' });
|
||||
const tests = {
|
||||
'a1.spec.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('should pass', async ({ }) => {
|
||||
});
|
||||
test.skip('should skip', async ({ }) => {
|
||||
});
|
||||
`,
|
||||
'a2.spec.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('shoul pass', async ({ }) => {
|
||||
});
|
||||
`,
|
||||
};
|
||||
|
||||
const result = await runInlineTest(tests, { shard: '1/2', ['fully-parallel']: true, workers: 1 });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.skipped).toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -608,3 +608,27 @@ test('should treat 3XX as available server', async ({ runInlineTest }, { workerI
|
|||
expect(result.output).toContain('[WebServer] listening');
|
||||
expect(result.output).toContain('[WebServer] error from server');
|
||||
});
|
||||
|
||||
test('should check ipv4 and ipv6 with happy eyeballs when URL is passed', async ({ runInlineTest }, { workerIndex }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20784' });
|
||||
const port = workerIndex * 2 + 10500;
|
||||
const result = await runInlineTest({
|
||||
'test.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({}) => {});
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
webServer: {
|
||||
command: 'node -e "require(\\'http\\').createServer((req, res) => res.end()).listen(${port}, \\'127.0.0.1\\')"',
|
||||
url: 'http://localhost:${port}/',
|
||||
}
|
||||
};
|
||||
`,
|
||||
}, {}, { DEBUG: 'pw:webserver' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.output).toContain('Process started');
|
||||
expect(result.output).toContain(`HTTP GET: http://localhost:${port}/`);
|
||||
expect(result.output).toContain('WebServer available');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -329,15 +329,10 @@ class Documentation {
|
|||
return sortKey(m1).localeCompare(sortKey(m2), 'en', { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
// Options should be the last argument.
|
||||
this.membersArray.forEach(member => {
|
||||
const optionsIndex = member.argsArray.findIndex(a => a.name === 'options');
|
||||
if (optionsIndex !== -1) {
|
||||
const options = member.argsArray[optionsIndex];
|
||||
member.argsArray.splice(optionsIndex, 1);
|
||||
member.argsArray.push(options);
|
||||
}
|
||||
});
|
||||
// Ideally, we would automatically make options the last argument.
|
||||
// However, that breaks Java, since options are not always last in Java, for example
|
||||
// in page.waitForFileChooser(options, callback).
|
||||
// So, the order must be carefully setup in the md file!
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -297,7 +297,7 @@ interface GenericAssertions<R> {
|
|||
toEqual(expected: unknown): R;
|
||||
toHaveLength(expected: number): R;
|
||||
toHaveProperty(keyPath: string | Array<string>, value?: unknown): R;
|
||||
toMatch(expected: RegExp): R;
|
||||
toMatch(expected: RegExp | string): R;
|
||||
toMatchObject(expected: Record<string, unknown> | Array<unknown>): R;
|
||||
toStrictEqual(expected: unknown): R;
|
||||
toThrow(error?: unknown): R;
|
||||
|
|
|
|||
Loading…
Reference in a new issue