Merge branch 'main' into mod_each
Signed-off-by: Pavel Feldman <pavel.feldman@gmail.com>
This commit is contained in:
commit
c704916e03
|
|
@ -748,83 +748,6 @@ await page.SetContentAsync("<script>\n" +
|
|||
await page.GetByRole(AriaRole.Button).ClickAsync();
|
||||
```
|
||||
|
||||
An example of passing an element handle:
|
||||
|
||||
```js
|
||||
await context.exposeBinding('clicked', async (source, element) => {
|
||||
console.log(await element.textContent());
|
||||
}, { handle: true });
|
||||
await page.setContent(`
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
`);
|
||||
```
|
||||
|
||||
```java
|
||||
context.exposeBinding("clicked", (source, args) -> {
|
||||
ElementHandle element = (ElementHandle) args[0];
|
||||
System.out.println(element.textContent());
|
||||
return null;
|
||||
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
|
||||
page.setContent("" +
|
||||
"<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
```
|
||||
|
||||
```python async
|
||||
async def print(source, element):
|
||||
print(await element.text_content())
|
||||
|
||||
await context.expose_binding("clicked", print, handle=true)
|
||||
await page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```python sync
|
||||
def print(source, element):
|
||||
print(element.text_content())
|
||||
|
||||
context.expose_binding("clicked", print, handle=true)
|
||||
page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var result = new TaskCompletionSource<string>();
|
||||
var page = await Context.NewPageAsync();
|
||||
await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
|
||||
{
|
||||
return result.TrySetResult(await t.AsElement().TextContentAsync());
|
||||
});
|
||||
|
||||
await page.SetContentAsync("<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
// Note: it makes sense to await the result here, because otherwise, the context
|
||||
// gets closed and the binding function will throw an exception.
|
||||
Assert.AreEqual("Click me", await result.Task);
|
||||
```
|
||||
|
||||
### param: BrowserContext.exposeBinding.name
|
||||
* since: v1.8
|
||||
- `name` <[string]>
|
||||
|
|
@ -839,6 +762,7 @@ Callback function that will be called in the Playwright's context.
|
|||
|
||||
### option: BrowserContext.exposeBinding.handle
|
||||
* since: v1.8
|
||||
* deprecated: This option will be removed in the future.
|
||||
- `handle` <[boolean]>
|
||||
|
||||
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
|
||||
|
|
|
|||
|
|
@ -136,7 +136,8 @@ page.clock.pause_at("2020-02-02")
|
|||
```
|
||||
|
||||
```java
|
||||
page.clock().pauseAt(Instant.parse("2020-02-02"));
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
|
||||
page.clock().pauseAt(format.parse("2020-02-02"));
|
||||
page.clock().pauseAt("2020-02-02");
|
||||
```
|
||||
|
||||
|
|
@ -182,8 +183,8 @@ page.clock.set_fixed_time("2020-02-02")
|
|||
```
|
||||
|
||||
```java
|
||||
page.clock().setFixedTime(Instant.now());
|
||||
page.clock().setFixedTime(Instant.parse("2020-02-02"));
|
||||
page.clock().setFixedTime(new Date());
|
||||
page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
|
||||
page.clock().setFixedTime("2020-02-02");
|
||||
```
|
||||
|
||||
|
|
@ -225,8 +226,8 @@ page.clock.set_system_time("2020-02-02")
|
|||
```
|
||||
|
||||
```java
|
||||
page.clock().setSystemTime(Instant.now());
|
||||
page.clock().setSystemTime(Instant.parse("2020-02-02"));
|
||||
page.clock().setSystemTime(new Date());
|
||||
page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
|
||||
page.clock().setSystemTime("2020-02-02");
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1817,80 +1817,6 @@ class PageExamples
|
|||
}
|
||||
```
|
||||
|
||||
An example of passing an element handle:
|
||||
|
||||
```js
|
||||
await page.exposeBinding('clicked', async (source, element) => {
|
||||
console.log(await element.textContent());
|
||||
}, { handle: true });
|
||||
await page.setContent(`
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
`);
|
||||
```
|
||||
|
||||
```java
|
||||
page.exposeBinding("clicked", (source, args) -> {
|
||||
ElementHandle element = (ElementHandle) args[0];
|
||||
System.out.println(element.textContent());
|
||||
return null;
|
||||
}, new Page.ExposeBindingOptions().setHandle(true));
|
||||
page.setContent("" +
|
||||
"<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
```
|
||||
|
||||
```python async
|
||||
async def print(source, element):
|
||||
print(await element.text_content())
|
||||
|
||||
await page.expose_binding("clicked", print, handle=true)
|
||||
await page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```python sync
|
||||
def print(source, element):
|
||||
print(element.text_content())
|
||||
|
||||
page.expose_binding("clicked", print, handle=true)
|
||||
page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var result = new TaskCompletionSource<string>();
|
||||
await page.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
|
||||
{
|
||||
return result.TrySetResult(await t.AsElement().TextContentAsync());
|
||||
});
|
||||
|
||||
await page.SetContentAsync("<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
Console.WriteLine(await result.Task);
|
||||
```
|
||||
|
||||
### param: Page.exposeBinding.name
|
||||
* since: v1.8
|
||||
- `name` <[string]>
|
||||
|
|
@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context.
|
|||
|
||||
### option: Page.exposeBinding.handle
|
||||
* since: v1.8
|
||||
* deprecated: This option will be removed in the future.
|
||||
- `handle` <[boolean]>
|
||||
|
||||
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
|
||||
|
|
|
|||
|
|
@ -118,13 +118,14 @@ expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM"
|
|||
```java
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00")));
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
|
||||
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-02-02T08:00:00")));
|
||||
page.navigate("http://localhost:3333");
|
||||
Locator locator = page.getByTestId("current-time");
|
||||
|
||||
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
// Pause the time once reached that point.
|
||||
page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
|
||||
page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
|
||||
|
||||
// Assert the page state.
|
||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||
|
|
@ -315,15 +316,16 @@ expect(locator).to_have_text("2/2/2024, 10:00:02 AM")
|
|||
```
|
||||
|
||||
```java
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
|
||||
// Initialize clock with a specific time, let the page load naturally.
|
||||
page.clock().install(new Clock.InstallOptions()
|
||||
.setTime(Instant.parse("2024-02-02T08:00:00")));
|
||||
.setTime(format.parse("2024-02-02T08:00:00")));
|
||||
page.navigate("http://localhost:3333");
|
||||
Locator locator = page.getByTestId("current-time");
|
||||
|
||||
// Pause the time flow, stop the timers, you now have manual control
|
||||
// over the page time.
|
||||
page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
|
||||
page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
|
||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Tick through time manually, firing all timers in the process.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,69 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```csharp
|
||||
// Initialize clock with some time before the test time and let the page load naturally.
|
||||
// `Date.now` will progress as the timers fire.
|
||||
await Page.Clock.InstallAsync(new
|
||||
{
|
||||
Time = new DateTime(2024, 2, 2, 8, 0, 0)
|
||||
});
|
||||
await Page.GotoAsync("http://localhost:3333");
|
||||
|
||||
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
// Pause the time once reached that point.
|
||||
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
|
||||
|
||||
// Assert the page state.
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Close the laptop lid again and open it at 10:30am.
|
||||
await Page.Clock.FastForwardAsync("30:00");
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM");
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```csharp
|
||||
await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```csharp
|
||||
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
await page.Keyboard.PressAsync("ControlOrMeta+S");
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
@ -129,7 +192,7 @@ This version was also tested against the following stable channels:
|
|||
### New Locator Handler
|
||||
|
||||
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
|
||||
|
||||
|
||||
```csharp
|
||||
// Setup the handler.
|
||||
await Page.AddLocatorHandlerAsync(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,67 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```java
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock().install(new Clock.InstallOptions().setTime("2024-02-02T08:00:00"));
|
||||
page.navigate("http://localhost:3333");
|
||||
Locator locator = page.getByTestId("current-time");
|
||||
|
||||
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
// Pause the time once reached that point.
|
||||
page.clock().pauseAt("2024-02-02T10:00:00");
|
||||
|
||||
// Assert the page state.
|
||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Close the laptop lid again and open it at 10:30am.
|
||||
page.clock().fastForward("30:00");
|
||||
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```java
|
||||
page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```java
|
||||
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
page.keyboard.press("ControlOrMeta+S");
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
@ -201,7 +262,7 @@ Learn more about the fixtures in our [JUnit guide](./junit.md).
|
|||
### New Locator Handler
|
||||
|
||||
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
|
||||
|
||||
|
||||
```java
|
||||
// Setup the handler.
|
||||
page.addLocatorHandler(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,66 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```python
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
|
||||
page.goto("http://localhost:3333")
|
||||
|
||||
# Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
# Pause the time once reached that point.
|
||||
page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||
|
||||
# Assert the page state.
|
||||
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
|
||||
|
||||
# Close the laptop lid again and open it at 10:30am.
|
||||
page.clock.fast_forward("30:00")
|
||||
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```python
|
||||
page.get_by_label("Upload directory").set_input_files('mydir')
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```python
|
||||
# Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
page.keyboard.press("ControlOrMeta+S")
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
@ -109,7 +169,7 @@ This version was also tested against the following stable channels:
|
|||
### New Locator Handler
|
||||
|
||||
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
|
||||
|
||||
|
||||
```python
|
||||
# Setup the handler.
|
||||
page.add_locator_handler(
|
||||
|
|
|
|||
|
|
@ -43,48 +43,7 @@ Here is how typical test environment setup differs between traditional test styl
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```js tab=js-js title="todo-page.js"
|
||||
export class TodoPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.inputBox = this.page.locator('input.new-todo');
|
||||
this.todoItems = this.page.getByTestId('todo-item');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('https://demo.playwright.dev/todomvc/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async addToDo(text) {
|
||||
await this.inputBox.fill(text);
|
||||
await this.inputBox.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async remove(text) {
|
||||
const todo = this.todoItems.filter({ hasText: text });
|
||||
await todo.hover();
|
||||
await todo.getByLabel('Delete').click();
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
while ((await this.todoItems.count()) > 0) {
|
||||
await this.todoItems.first().hover();
|
||||
await this.todoItems.getByLabel('Delete').first().click();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="todo-page.ts"
|
||||
```js title="todo-page.ts"
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TodoPage {
|
||||
|
|
@ -167,48 +126,7 @@ Fixtures have a number of advantages over before/after hooks:
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```js tab=js-js title="todo-page.js"
|
||||
export class TodoPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.inputBox = this.page.locator('input.new-todo');
|
||||
this.todoItems = this.page.getByTestId('todo-item');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('https://demo.playwright.dev/todomvc/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async addToDo(text) {
|
||||
await this.inputBox.fill(text);
|
||||
await this.inputBox.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async remove(text) {
|
||||
const todo = this.todoItems.filter({ hasText: text });
|
||||
await todo.hover();
|
||||
await todo.getByLabel('Delete').click();
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
while ((await this.todoItems.count()) > 0) {
|
||||
await this.todoItems.first().hover();
|
||||
await this.todoItems.getByLabel('Delete').first().click();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="todo-page.ts"
|
||||
```js title="todo-page.ts"
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TodoPage {
|
||||
|
|
@ -246,34 +164,7 @@ export class TodoPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```js tab=js-js title="todo.spec.js"
|
||||
const base = require('@playwright/test');
|
||||
const { TodoPage } = require('./todo-page');
|
||||
|
||||
// Extend basic test by providing a "todoPage" fixture.
|
||||
const test = base.test.extend({
|
||||
todoPage: async ({ page }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo('item1');
|
||||
await todoPage.addToDo('item2');
|
||||
await use(todoPage);
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
});
|
||||
|
||||
test('should add an item', async ({ todoPage }) => {
|
||||
await todoPage.addToDo('my item');
|
||||
// ...
|
||||
});
|
||||
|
||||
test('should remove an item', async ({ todoPage }) => {
|
||||
await todoPage.remove('item1');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts title="example.spec.ts"
|
||||
```js title="example.spec.ts"
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
|
||||
|
|
@ -309,48 +200,8 @@ Below we create two fixtures `todoPage` and `settingsPage` that follow the [Page
|
|||
<details>
|
||||
<summary>Click to expand the code for the <code>TodoPage</code> and <code>SettingsPage</code></summary>
|
||||
<div>
|
||||
```js tab=js-js title="todo-page.js"
|
||||
export class TodoPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.inputBox = this.page.locator('input.new-todo');
|
||||
this.todoItems = this.page.getByTestId('todo-item');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('https://demo.playwright.dev/todomvc/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async addToDo(text) {
|
||||
await this.inputBox.fill(text);
|
||||
await this.inputBox.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async remove(text) {
|
||||
const todo = this.todoItems.filter({ hasText: text });
|
||||
await todo.hover();
|
||||
await todo.getByLabel('Delete').click();
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
while ((await this.todoItems.count()) > 0) {
|
||||
await this.todoItems.first().hover();
|
||||
await this.todoItems.getByLabel('Delete').first().click();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="todo-page.ts"
|
||||
```js title="todo-page.ts"
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TodoPage {
|
||||
|
|
@ -388,22 +239,7 @@ export class TodoPage {
|
|||
|
||||
SettingsPage is similar:
|
||||
|
||||
```js tab=js-js title="settings-page.js"
|
||||
export class SettingsPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async switchToDarkMode() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="settings-page.ts"
|
||||
```js title="settings-page.ts"
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export class SettingsPage {
|
||||
|
|
@ -419,36 +255,7 @@ export class SettingsPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```js tab=js-js title="my-test.js"
|
||||
const base = require('@playwright/test');
|
||||
const { TodoPage } = require('./todo-page');
|
||||
const { SettingsPage } = require('./settings-page');
|
||||
|
||||
// Extend base test by providing "todoPage" and "settingsPage".
|
||||
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
|
||||
exports.test = base.test.extend({
|
||||
todoPage: async ({ page }, use) => {
|
||||
// Set up the fixture.
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo('item1');
|
||||
await todoPage.addToDo('item2');
|
||||
|
||||
// Use the fixture value in the test.
|
||||
await use(todoPage);
|
||||
|
||||
// Clean up the fixture.
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
|
||||
settingsPage: async ({ page }, use) => {
|
||||
await use(new SettingsPage(page));
|
||||
},
|
||||
});
|
||||
exports.expect = base.expect;
|
||||
```
|
||||
|
||||
```js tab=js-ts title="my-test.ts"
|
||||
```js title="my-test.ts"
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
import { SettingsPage } from './settings-page';
|
||||
|
|
@ -493,20 +300,7 @@ Just mention fixture in your test function argument, and test runner will take c
|
|||
|
||||
Below we use the `todoPage` and `settingsPage` fixtures defined above.
|
||||
|
||||
```js tab=js-js
|
||||
const { test, expect } = require('./my-test');
|
||||
|
||||
test.beforeEach(async ({ settingsPage }) => {
|
||||
await settingsPage.switchToDarkMode();
|
||||
});
|
||||
|
||||
test('basic test', async ({ todoPage, page }) => {
|
||||
await todoPage.addToDo('something nice');
|
||||
await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts
|
||||
```js
|
||||
import { test, expect } from './my-test';
|
||||
|
||||
test.beforeEach(async ({ settingsPage }) => {
|
||||
|
|
@ -560,47 +354,7 @@ Playwright Test uses [worker processes](./test-parallel.md) to run test files. S
|
|||
|
||||
Below we'll create an `account` fixture that will be shared by all tests in the same worker, and override the `page` fixture to login into this account for each test. To generate unique accounts, we'll use the [`property: WorkerInfo.workerIndex`] that is available to any test or fixture. Note the tuple-like syntax for the worker fixture - we have to pass `{scope: 'worker'}` so that test runner sets up this fixture once per worker.
|
||||
|
||||
```js tab=js-js title="my-test.js"
|
||||
const base = require('@playwright/test');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
account: [async ({ browser }, use, workerInfo) => {
|
||||
// Unique username.
|
||||
const username = 'user' + workerInfo.workerIndex;
|
||||
const password = 'verysecure';
|
||||
|
||||
// Create the account with Playwright.
|
||||
const page = await browser.newPage();
|
||||
await page.goto('/signup');
|
||||
await page.getByLabel('User Name').fill(username);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByText('Sign up').click();
|
||||
// Make sure everything is ok.
|
||||
await expect(page.locator('#result')).toHaveText('Success');
|
||||
// Do not forget to cleanup.
|
||||
await page.close();
|
||||
|
||||
// Use the account value.
|
||||
await use({ username, password });
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
page: async ({ page, account }, use) => {
|
||||
// Sign in with our account.
|
||||
const { username, password } = account;
|
||||
await page.goto('/signin');
|
||||
await page.getByLabel('User Name').fill(username);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByText('Sign in').click();
|
||||
await expect(page.getByTestId('userinfo')).toHaveText(username);
|
||||
|
||||
// Use signed-in page in the test.
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
exports.expect = base.expect;
|
||||
```
|
||||
|
||||
```js tab=js-ts title="my-test.ts"
|
||||
```js title="my-test.ts"
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
type Account = {
|
||||
|
|
@ -652,32 +406,7 @@ Automatic fixtures are set up for each test/worker, even when the test does not
|
|||
|
||||
Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses [TestInfo] object that is available in each test/fixture to retrieve metadata about the test being run.
|
||||
|
||||
```js tab=js-js title="my-test.js"
|
||||
const debug = require('debug');
|
||||
const fs = require('fs');
|
||||
const base = require('@playwright/test');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
saveLogs: [async ({}, use, testInfo) => {
|
||||
// Collecting logs during the test.
|
||||
const logs = [];
|
||||
debug.log = (...args) => logs.push(args.map(String).join(''));
|
||||
debug.enable('myserver');
|
||||
|
||||
await use();
|
||||
|
||||
// After the test we can check whether the test passed or failed.
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
// outputPath() API guarantees a unique file name.
|
||||
const logFile = testInfo.outputPath('logs.txt');
|
||||
await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
|
||||
testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
|
||||
}
|
||||
}, { auto: true }],
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts title="my-test.ts"
|
||||
```js title="my-test.ts"
|
||||
import * as debug from 'debug';
|
||||
import * as fs from 'fs';
|
||||
import { test as base } from '@playwright/test';
|
||||
|
|
@ -707,22 +436,7 @@ export { expect } from '@playwright/test';
|
|||
|
||||
By default, fixture shares timeout with the test. However, for slow fixtures, especially [worker-scoped](#worker-scoped-fixtures) ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time.
|
||||
|
||||
```js tab=js-js
|
||||
const { test: base, expect } = require('@playwright/test');
|
||||
|
||||
const test = base.extend({
|
||||
slowFixture: [async ({}, use) => {
|
||||
// ... perform a slow operation ...
|
||||
await use('hello');
|
||||
}, { timeout: 60000 }]
|
||||
});
|
||||
|
||||
test('example test', async ({ slowFixture }) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts
|
||||
```js
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
|
||||
const test = base.extend<{ slowFixture: string }>({
|
||||
|
|
@ -752,48 +466,7 @@ Below we'll create a `defaultItem` option in addition to the `todoPage` fixture
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```js tab=js-js title="todo-page.js"
|
||||
export class TodoPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.inputBox = this.page.locator('input.new-todo');
|
||||
this.todoItems = this.page.getByTestId('todo-item');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('https://demo.playwright.dev/todomvc/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async addToDo(text) {
|
||||
await this.inputBox.fill(text);
|
||||
await this.inputBox.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
async remove(text) {
|
||||
const todo = this.todoItems.filter({ hasText: text });
|
||||
await todo.hover();
|
||||
await todo.getByLabel('Delete').click();
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
while ((await this.todoItems.count()) > 0) {
|
||||
await this.todoItems.first().hover();
|
||||
await this.todoItems.getByLabel('Delete').first().click();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="todo-page.ts"
|
||||
```js title="todo-page.ts"
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TodoPage {
|
||||
|
|
@ -832,28 +505,7 @@ export class TodoPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```js tab=js-js title="my-test.js"
|
||||
const base = require('@playwright/test');
|
||||
const { TodoPage } = require('./todo-page');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
defaultItem: ['Something nice', { option: true }],
|
||||
|
||||
// Our "todoPage" fixture depends on the option.
|
||||
todoPage: async ({ page, defaultItem }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo(defaultItem);
|
||||
await use(todoPage);
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
});
|
||||
exports.expect = base.expect;
|
||||
```
|
||||
|
||||
```js tab=js-ts title="my-test.ts"
|
||||
```js title="my-test.ts"
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
|
||||
|
|
@ -885,25 +537,7 @@ export { expect } from '@playwright/test';
|
|||
|
||||
We can now use `todoPage` fixture as usual, and set the `defaultItem` option in the config file.
|
||||
|
||||
```js tab=js-js title="playwright.config.ts"
|
||||
// @ts-check
|
||||
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
module.exports = defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'shopping',
|
||||
use: { defaultItem: 'Buy milk' },
|
||||
},
|
||||
{
|
||||
name: 'wellbeing',
|
||||
use: { defaultItem: 'Exercise!' },
|
||||
},
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts title="playwright.config.ts"
|
||||
```js title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
import type { MyOptions } from './my-test';
|
||||
|
||||
|
|
@ -932,50 +566,7 @@ Fixtures follow these rules to determine the execution order:
|
|||
|
||||
Consider the following example:
|
||||
|
||||
```js tab=js-js
|
||||
const { test: base } = require('@playwright/test');
|
||||
|
||||
const test = base.extend({
|
||||
workerFixture: [async ({ browser }) => {
|
||||
// workerFixture setup...
|
||||
await use('workerFixture');
|
||||
// workerFixture teardown...
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
autoWorkerFixture: [async ({ browser }) => {
|
||||
// autoWorkerFixture setup...
|
||||
await use('autoWorkerFixture');
|
||||
// autoWorkerFixture teardown...
|
||||
}, { scope: 'worker', auto: true }],
|
||||
|
||||
testFixture: [async ({ page, workerFixture }) => {
|
||||
// testFixture setup...
|
||||
await use('testFixture');
|
||||
// testFixture teardown...
|
||||
}, { scope: 'test' }],
|
||||
|
||||
autoTestFixture: [async () => {
|
||||
// autoTestFixture setup...
|
||||
await use('autoTestFixture');
|
||||
// autoTestFixture teardown...
|
||||
}, { scope: 'test', auto: true }],
|
||||
|
||||
unusedFixture: [async ({ page }) => {
|
||||
// unusedFixture setup...
|
||||
await use('unusedFixture');
|
||||
// unusedFixture teardown...
|
||||
}, { scope: 'test' }],
|
||||
});
|
||||
|
||||
test.beforeAll(async () => { /* ... */ });
|
||||
test.beforeEach(async ({ page }) => { /* ... */ });
|
||||
test('first test', async ({ page }) => { /* ... */ });
|
||||
test('second test', async ({ testFixture }) => { /* ... */ });
|
||||
test.afterEach(async () => { /* ... */ });
|
||||
test.afterAll(async () => { /* ... */ });
|
||||
```
|
||||
|
||||
```js tab=js-ts
|
||||
```js
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
const test = base.extend<{
|
||||
|
|
@ -1081,3 +672,30 @@ test('passes', async ({ database, page, a11y }) => {
|
|||
// use database and a11y fixtures.
|
||||
});
|
||||
```
|
||||
|
||||
## Box fixtures
|
||||
|
||||
You can minimize the fixture exposure to the reporters UI and error messages via boxing it:
|
||||
|
||||
```js
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export const test = base.extend({
|
||||
_helperFixture: [async ({}, use, testInfo) => {
|
||||
}, { box: true }],
|
||||
});
|
||||
```
|
||||
|
||||
## Custom fixture title
|
||||
|
||||
You can assign a custom title to a fixture to be used in error messages and in the
|
||||
reporters UI:
|
||||
|
||||
```js
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export const test = base.extend({
|
||||
_innerFixture: [async ({}, use, testInfo) => {
|
||||
}, { title: 'my fixture' }],
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -359,11 +359,11 @@ export class CRNetworkManager {
|
|||
});
|
||||
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request);
|
||||
|
||||
if (requestPausedEvent) {
|
||||
// We will not receive extra info when intercepting the request.
|
||||
if (route) {
|
||||
// We may not receive extra info when intercepting the request.
|
||||
// Use the headers from the Fetch.requestPausedPayload and release the allHeaders()
|
||||
// right away, so that client can call it from the route handler.
|
||||
request.request.setRawRequestHeaders(headersOverride ?? headersObjectToArray(requestPausedEvent.request.headers, '\n'));
|
||||
request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent!.request.headers, '\n'));
|
||||
}
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestStarted(request.request, route || undefined);
|
||||
}
|
||||
|
|
|
|||
60
packages/playwright-core/types/types.d.ts
vendored
60
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -814,21 +814,6 @@ export interface Page {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await page.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -875,21 +860,6 @@ export interface Page {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await page.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -7637,21 +7607,6 @@ export interface BrowserContext {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await context.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -7693,21 +7648,6 @@ export interface BrowserContext {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await context.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { formatLocation } from '../util';
|
||||
import { filterStackFile, formatLocation } from '../util';
|
||||
import * as crypto from 'crypto';
|
||||
import type { Fixtures } from '../../types/test';
|
||||
import type { Location } from '../../types/testReporter';
|
||||
|
|
@ -23,7 +23,7 @@ import type { FixturesWithLocation } from './config';
|
|||
export type FixtureScope = 'test' | 'worker';
|
||||
type FixtureAuto = boolean | 'all-hooks-included';
|
||||
const kScopeOrder: FixtureScope[] = ['test', 'worker'];
|
||||
type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined };
|
||||
type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean };
|
||||
type FixtureTuple = [ value: any, options: FixtureOptions ];
|
||||
export type FixtureRegistration = {
|
||||
// Fixture registration location.
|
||||
|
|
@ -49,8 +49,8 @@ export type FixtureRegistration = {
|
|||
super?: FixtureRegistration;
|
||||
// Whether this fixture is an option override value set from the config.
|
||||
optionOverride?: boolean;
|
||||
// Do not generate the step for this fixture.
|
||||
hideStep?: boolean;
|
||||
// Do not generate the step for this fixture, consider it internal.
|
||||
box?: boolean;
|
||||
};
|
||||
export type LoadError = {
|
||||
message: string;
|
||||
|
|
@ -63,7 +63,7 @@ type OptionOverrides = {
|
|||
};
|
||||
|
||||
function isFixtureTuple(value: any): value is FixtureTuple {
|
||||
return Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1] || 'option' in value[1] || 'timeout' in value[1]);
|
||||
return Array.isArray(value) && typeof value[1] === 'object';
|
||||
}
|
||||
|
||||
function isFixtureOption(value: any): value is FixtureTuple {
|
||||
|
|
@ -103,15 +103,15 @@ export class FixturePool {
|
|||
for (const entry of Object.entries(fixtures)) {
|
||||
const name = entry[0];
|
||||
let value = entry[1];
|
||||
let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle: string | undefined, hideStep: boolean | undefined } | undefined;
|
||||
let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle?: string, box?: boolean } | undefined;
|
||||
if (isFixtureTuple(value)) {
|
||||
options = {
|
||||
auto: value[1].auto ?? false,
|
||||
scope: value[1].scope || 'test',
|
||||
option: !!value[1].option,
|
||||
timeout: value[1].timeout,
|
||||
customTitle: (value[1] as any)._title,
|
||||
hideStep: (value[1] as any)._hideStep,
|
||||
customTitle: value[1].title,
|
||||
box: value[1].box,
|
||||
};
|
||||
value = value[0];
|
||||
}
|
||||
|
|
@ -128,9 +128,9 @@ export class FixturePool {
|
|||
continue;
|
||||
}
|
||||
} else if (previous) {
|
||||
options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, hideStep: undefined };
|
||||
options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, box: previous.box };
|
||||
} else if (!options) {
|
||||
options = { auto: false, scope: 'test', option: false, timeout: undefined, customTitle: undefined, hideStep: undefined };
|
||||
options = { auto: false, scope: 'test', option: false, timeout: undefined };
|
||||
}
|
||||
|
||||
if (!kScopeOrder.includes(options.scope)) {
|
||||
|
|
@ -152,7 +152,7 @@ export class FixturePool {
|
|||
}
|
||||
|
||||
const deps = fixtureParameterNames(fn, location, e => this._onLoadError(e));
|
||||
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, hideStep: options.hideStep, deps, super: previous, optionOverride: isOptionsOverride };
|
||||
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, box: options.box, deps, super: previous, optionOverride: isOptionsOverride };
|
||||
registrationId(registration);
|
||||
this._registrations.set(name, registration);
|
||||
}
|
||||
|
|
@ -161,29 +161,36 @@ export class FixturePool {
|
|||
private validate() {
|
||||
const markers = new Map<FixtureRegistration, 'visiting' | 'visited'>();
|
||||
const stack: FixtureRegistration[] = [];
|
||||
const visit = (registration: FixtureRegistration) => {
|
||||
let hasDependencyErrors = false;
|
||||
const addDependencyError = (message: string, location: Location) => {
|
||||
hasDependencyErrors = true;
|
||||
this._addLoadError(message, location);
|
||||
};
|
||||
const visit = (registration: FixtureRegistration, boxedOnly: boolean) => {
|
||||
markers.set(registration, 'visiting');
|
||||
stack.push(registration);
|
||||
for (const name of registration.deps) {
|
||||
const dep = this.resolve(name, registration);
|
||||
if (!dep) {
|
||||
if (name === registration.name)
|
||||
this._addLoadError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
|
||||
addDependencyError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
|
||||
else
|
||||
this._addLoadError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
|
||||
addDependencyError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
|
||||
continue;
|
||||
}
|
||||
if (kScopeOrder.indexOf(registration.scope) > kScopeOrder.indexOf(dep.scope)) {
|
||||
this._addLoadError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatLocation(dep.location)}.`, registration.location);
|
||||
addDependencyError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatPotentiallyInternalLocation(dep.location)}.`, registration.location);
|
||||
continue;
|
||||
}
|
||||
if (!markers.has(dep)) {
|
||||
visit(dep);
|
||||
visit(dep, boxedOnly);
|
||||
} else if (markers.get(dep) === 'visiting') {
|
||||
const index = stack.indexOf(dep);
|
||||
const regs = stack.slice(index, stack.length);
|
||||
const allRegs = stack.slice(index, stack.length);
|
||||
const filteredRegs = allRegs.filter(r => !r.box);
|
||||
const regs = boxedOnly ? filteredRegs : allRegs;
|
||||
const names = regs.map(r => `"${r.name}"`);
|
||||
this._addLoadError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location);
|
||||
addDependencyError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatPotentiallyInternalLocation(r.location)).join(' -> ')} -> ${formatPotentiallyInternalLocation(dep.location)}`, dep.location);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -191,11 +198,27 @@ export class FixturePool {
|
|||
stack.pop();
|
||||
};
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
const names = Array.from(this._registrations.keys()).sort();
|
||||
|
||||
// First iterate over non-boxed fixtures to provide clear error messages.
|
||||
for (const name of names) {
|
||||
const registration = this._registrations.get(name)!;
|
||||
if (!registration.box)
|
||||
visit(registration, true);
|
||||
}
|
||||
|
||||
// If no errors found, iterate over boxed fixtures
|
||||
if (!hasDependencyErrors) {
|
||||
for (const name of names) {
|
||||
const registration = this._registrations.get(name)!;
|
||||
if (registration.box)
|
||||
visit(registration, false);
|
||||
}
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
for (const name of names) {
|
||||
const registration = this._registrations.get(name)!;
|
||||
visit(registration);
|
||||
if (registration.scope === 'worker')
|
||||
hash.update(registration.id + ';');
|
||||
}
|
||||
|
|
@ -227,6 +250,11 @@ export class FixturePool {
|
|||
|
||||
const signatureSymbol = Symbol('signature');
|
||||
|
||||
export function formatPotentiallyInternalLocation(location: Location): string {
|
||||
const isUserFixture = location && filterStackFile(location.file);
|
||||
return isUserFixture ? formatLocation(location) : '<builtin>';
|
||||
}
|
||||
|
||||
export function fixtureParameterNames(fn: Function | any, location: Location, onError: LoadErrorSink): string[] {
|
||||
if (typeof fn !== 'function')
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
|||
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||
defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
|
||||
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
|
||||
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }],
|
||||
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker', box: true }],
|
||||
|
||||
playwright: [async ({ _playwrightImpl, screenshot }, use) => {
|
||||
await connector.setPlaywright(_playwrightImpl, screenshot);
|
||||
await use(_playwrightImpl);
|
||||
await connector.setPlaywright(undefined, screenshot);
|
||||
}, { scope: 'worker', _hideStep: true } as any],
|
||||
}, { scope: 'worker', box: true }],
|
||||
|
||||
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
|
||||
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
|
||||
|
|
@ -93,7 +93,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
await use(options);
|
||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
||||
(browserType as any)._defaultLaunchOptions = undefined;
|
||||
}, { scope: 'worker', auto: true }],
|
||||
}, { scope: 'worker', auto: true, box: true }],
|
||||
|
||||
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
|
||||
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
||||
|
|
@ -152,7 +152,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? 'allow'), { option: true }],
|
||||
contextOptions: [{}, { option: true }],
|
||||
|
||||
_combinedContextOptions: async ({
|
||||
_combinedContextOptions: [async ({
|
||||
acceptDownloads,
|
||||
bypassCSP,
|
||||
colorScheme,
|
||||
|
|
@ -223,7 +223,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
...contextOptions,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
}, { box: true }],
|
||||
|
||||
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
|
||||
if (testIdAttribute)
|
||||
|
|
@ -246,9 +246,9 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
(browserType as any)._defaultContextTimeout = undefined;
|
||||
(browserType as any)._defaultContextNavigationTimeout = undefined;
|
||||
}
|
||||
}, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
|
||||
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
||||
|
||||
_contextFactory: [async ({ browser, video, _reuseContext }, use, testInfo) => {
|
||||
_contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => {
|
||||
const testInfoImpl = testInfo as TestInfoImpl;
|
||||
const videoMode = normalizeVideoMode(video);
|
||||
const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
|
||||
|
|
@ -301,7 +301,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
}
|
||||
}));
|
||||
|
||||
}, { scope: 'test', _title: 'context' } as any],
|
||||
}, { scope: 'test', title: 'context', box: true }],
|
||||
|
||||
_optionContextReuseMode: ['none', { scope: 'worker', option: true }],
|
||||
_optionConnectOptions: [undefined, { scope: 'worker', option: true }],
|
||||
|
|
@ -312,7 +312,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
mode = 'when-possible';
|
||||
const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off';
|
||||
await use(reuse);
|
||||
}, { scope: 'worker', _title: 'context' } as any],
|
||||
}, { scope: 'worker', title: 'context', box: true }],
|
||||
|
||||
context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => {
|
||||
attachConnectedHeaderIfNeeded(testInfo, browser);
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ class Fixture {
|
|||
this.runner = runner;
|
||||
this.registration = registration;
|
||||
this.value = null;
|
||||
const shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option;
|
||||
const isInternalFixture = this.registration.location && filterStackFile(this.registration.location.file);
|
||||
const shouldGenerateStep = !this.registration.box && !this.registration.option;
|
||||
const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file);
|
||||
const title = this.registration.customTitle || this.registration.name;
|
||||
const location = isInternalFixture ? this.registration.location : undefined;
|
||||
const location = isUserFixture ? this.registration.location : undefined;
|
||||
this._stepInfo = shouldGenerateStep ? { category: 'fixture', location } : undefined;
|
||||
this._setupDescription = {
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
testInfo.expectedStatus = 'failed';
|
||||
break;
|
||||
case 'slow':
|
||||
testInfo.slow();
|
||||
testInfo._timeoutManager.slow();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
8
packages/playwright/types/test.d.ts
vendored
8
packages/playwright/types/test.d.ts
vendored
|
|
@ -4811,13 +4811,13 @@ export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) =
|
|||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined }];
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined }];
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
|
|||
|
|
@ -256,6 +256,41 @@ test('should detect fixture dependency cycle', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should hide boxed fixtures in dependency cycle', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'x.spec.ts': `
|
||||
import { test as base } from '@playwright/test';
|
||||
const test = base.extend({
|
||||
storageState: async ({ context, storageState }, use) => {
|
||||
await use(storageState);
|
||||
}
|
||||
});
|
||||
test('failed', async ({ page }) => {});
|
||||
`,
|
||||
});
|
||||
expect(result.output).toContain('Fixtures "context" -> "storageState" -> "context" form a dependency cycle: <builtin> -> x.spec.ts:3:25 -> <builtin>');
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should show boxed fixtures in dependency cycle if there are no public fixtures', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'x.spec.ts': `
|
||||
import { test as base } from '@playwright/test';
|
||||
const test = base.extend({
|
||||
f1: [async ({ f2 }, use) => {
|
||||
await use(f2);
|
||||
}, { box: true }],
|
||||
f2: [async ({ f1 }, use) => {
|
||||
await use(f1);
|
||||
}, { box: true }],
|
||||
});
|
||||
test('failed', async ({ f1, f2 }) => {});
|
||||
`,
|
||||
});
|
||||
expect(result.output).toContain('Fixtures "f1" -> "f2" -> "f1" form a dependency cycle: x.spec.ts:3:25 -> x.spec.ts:3:25 -> x.spec.ts:3:25');
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should not reuse fixtures from one file in another one', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
|
|
|
|||
|
|
@ -691,6 +691,31 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest }
|
|||
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]);
|
||||
});
|
||||
|
||||
test('should contain only one slow modifier', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'slow.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test.slow();
|
||||
test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
|
||||
`,
|
||||
'skip.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test.skip();
|
||||
test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
|
||||
`,
|
||||
'fixme.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test.fixme();
|
||||
test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'fixme' }, { type: 'issue', description: 'my-value' }]);
|
||||
expect(result.report.suites[1].specs[0].tests[0].annotations).toEqual([{ type: 'skip' }, { type: 'issue', description: 'my-value' }]);
|
||||
expect(result.report.suites[2].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }, { type: 'issue', description: 'my-value' }]);
|
||||
});
|
||||
|
||||
test('should skip beforeEach hooks upon modifiers', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ test('should respect fixture timeout', async ({ runInlineTest }) => {
|
|||
slowSetup: [async ({}, use) => {
|
||||
await new Promise(f => setTimeout(f, 2000));
|
||||
await use('hey');
|
||||
}, { timeout: 500, _title: 'custom title' }],
|
||||
}, { timeout: 500, title: 'custom title' }],
|
||||
slowTeardown: [async ({}, use) => {
|
||||
await use('hey');
|
||||
await new Promise(f => setTimeout(f, 2000));
|
||||
|
|
@ -227,7 +227,7 @@ test('should respect test.setTimeout in the worker fixture', async ({ runInlineT
|
|||
slowTeardown: [async ({}, use) => {
|
||||
await use('hey');
|
||||
await new Promise(f => setTimeout(f, 2000));
|
||||
}, { scope: 'worker', timeout: 400, _title: 'custom title' }],
|
||||
}, { scope: 'worker', timeout: 400, title: 'custom title' }],
|
||||
});
|
||||
test('test ok', async ({ fixture, noTimeout }) => {
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
|
|
|
|||
8
utils/generate_types/overrides-test.d.ts
vendored
8
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -140,13 +140,13 @@ export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) =
|
|||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined }];
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined }];
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
|
|||
Loading…
Reference in a new issue