Merge branch 'microsoft:main' into main
This commit is contained in:
commit
251028ff10
|
|
@ -344,6 +344,9 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
|
||||||
### option: APIRequestContext.fetch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.fetch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.fetch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.get
|
## async method: APIRequestContext.get
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[APIResponse]>
|
- returns: <[APIResponse]>
|
||||||
|
|
@ -433,6 +436,9 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
|
||||||
### option: APIRequestContext.get.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.get.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.get.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.head
|
## async method: APIRequestContext.head
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[APIResponse]>
|
- returns: <[APIResponse]>
|
||||||
|
|
@ -486,6 +492,9 @@ context cookies from the response. The method will automatically follow redirect
|
||||||
### option: APIRequestContext.head.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.head.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.head.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.patch
|
## async method: APIRequestContext.patch
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[APIResponse]>
|
- returns: <[APIResponse]>
|
||||||
|
|
@ -539,6 +548,9 @@ context cookies from the response. The method will automatically follow redirect
|
||||||
### option: APIRequestContext.patch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.patch.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.patch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.post
|
## async method: APIRequestContext.post
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[APIResponse]>
|
- returns: <[APIResponse]>
|
||||||
|
|
@ -713,6 +725,9 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
|
||||||
### option: APIRequestContext.post.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.post.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.post.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.put
|
## async method: APIRequestContext.put
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[APIResponse]>
|
- returns: <[APIResponse]>
|
||||||
|
|
@ -766,6 +781,9 @@ context cookies from the response. The method will automatically follow redirect
|
||||||
### option: APIRequestContext.put.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.put.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.put.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.storageState
|
## async method: APIRequestContext.storageState
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
- returns: <[Object]>
|
- returns: <[Object]>
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,16 @@ Whether to ignore HTTPS errors when sending network requests.
|
||||||
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
|
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.
|
Defaults to `20`. Pass `0` to not follow redirects.
|
||||||
|
|
||||||
|
## method: RequestOptions.setMaxRetries
|
||||||
|
* since: v1.46
|
||||||
|
- returns: <[RequestOptions]>
|
||||||
|
|
||||||
|
### param: RequestOptions.setMaxRetries.maxRetries
|
||||||
|
* since: v1.46
|
||||||
|
- `maxRetries` <[int]>
|
||||||
|
|
||||||
|
Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
|
||||||
## method: RequestOptions.setMethod
|
## method: RequestOptions.setMethod
|
||||||
* since: v1.18
|
* since: v1.18
|
||||||
- returns: <[RequestOptions]>
|
- returns: <[RequestOptions]>
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,12 @@ Whether to ignore HTTPS errors when sending network requests. Defaults to `false
|
||||||
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
|
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.
|
Defaults to `20`. Pass `0` to not follow redirects.
|
||||||
|
|
||||||
|
## js-python-csharp-fetch-option-maxretries
|
||||||
|
* langs: js, python, csharp
|
||||||
|
- `maxRetries` <[int]>
|
||||||
|
|
||||||
|
Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
|
||||||
## evaluate-expression
|
## evaluate-expression
|
||||||
- `expression` <[string]>
|
- `expression` <[string]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ title: "Dialogs"
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Playwright can interact with the web page dialogs such as [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) as well as [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) confirmation.
|
Playwright can interact with the web page dialogs such as [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) as well as [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) confirmation. For print dialogs, see [Print](#print-dialogs).
|
||||||
|
|
||||||
## alert(), confirm(), prompt() dialogs
|
## alert(), confirm(), prompt() dialogs
|
||||||
|
|
||||||
|
|
@ -126,3 +126,55 @@ Page.Dialog += async (_, dialog) =>
|
||||||
};
|
};
|
||||||
await Page.CloseAsync(new() { RunBeforeUnload = true });
|
await Page.CloseAsync(new() { RunBeforeUnload = true });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Print dialogs
|
||||||
|
|
||||||
|
In order to assert that a print dialog via [`window.print`](https://developer.mozilla.org/en-US/docs/Web/API/Window/print) was triggered, you can use the following snippet:
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.goto('<url>');
|
||||||
|
|
||||||
|
await page.evaluate('(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()');
|
||||||
|
await page.getByText('Print it!').click();
|
||||||
|
|
||||||
|
await page.waitForFunction('window.waitForPrintDialog');
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
page.navigate("<url>");
|
||||||
|
|
||||||
|
page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()");
|
||||||
|
page.getByText("Print it!").click();
|
||||||
|
|
||||||
|
page.waitForFunction("window.waitForPrintDialog");
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
await page.goto("<url>")
|
||||||
|
|
||||||
|
await page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()")
|
||||||
|
await page.get_by_text("Print it!").click()
|
||||||
|
|
||||||
|
await page.wait_for_function("window.waitForPrintDialog")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
page.goto("<url>")
|
||||||
|
|
||||||
|
page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()")
|
||||||
|
page.get_by_text("Print it!").click()
|
||||||
|
|
||||||
|
page.wait_for_function("window.waitForPrintDialog")
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await Page.GotoAsync("<url>");
|
||||||
|
|
||||||
|
await Page.EvaluateAsync("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()");
|
||||||
|
await Page.GetByText("Print it!").ClickAsync();
|
||||||
|
|
||||||
|
await Page.WaitForFunctionAsync("window.waitForPrintDialog");
|
||||||
|
```
|
||||||
|
|
||||||
|
This will wait for the print dialog to be opened after the button is clicked.
|
||||||
|
Make sure to evaluate the script before clicking the button / after the page is loaded.
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,61 @@ You can either parameterize tests on a test level or on a project level.
|
||||||
## Parameterized Tests
|
## Parameterized Tests
|
||||||
|
|
||||||
```js title="example.spec.ts"
|
```js title="example.spec.ts"
|
||||||
const people = ['Alice', 'Bob'];
|
[
|
||||||
for (const name of people) {
|
{ name: 'Alice', expected: 'Hello, Alice!' },
|
||||||
test(`testing with ${name}`, async () => {
|
{ name: 'Bob', expected: 'Hello, Bob!' },
|
||||||
// ...
|
{ name: 'Charlie', expected: 'Hello, Charlie!' },
|
||||||
});
|
].forEach(({ name, expected }) => {
|
||||||
// You can also do it with test.describe() or with multiple tests as long the test name is unique.
|
// You can also do it with test.describe() or with multiple tests as long the test name is unique.
|
||||||
}
|
test(`testing with ${name}`, async ({ page }) => {
|
||||||
|
await page.goto(`https://example.com/greet?name=${name}`);
|
||||||
|
await expect(page.getByRole('heading')).toHaveText(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Before and after hooks
|
||||||
|
|
||||||
|
Most of the time you should put `beforeEach`, `beforeAll`, `afterEach` and `afterAll` hooks outside of `forEach`, so that hooks are executed just once:
|
||||||
|
|
||||||
|
```js title="example.spec.ts"
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ name: 'Alice', expected: 'Hello, Alice!' },
|
||||||
|
{ name: 'Bob', expected: 'Hello, Bob!' },
|
||||||
|
{ name: 'Charlie', expected: 'Hello, Charlie!' },
|
||||||
|
].forEach(({ name, expected }) => {
|
||||||
|
test(`testing with ${name}`, async ({ page }) => {
|
||||||
|
await page.goto(`https://example.com/greet?name=${name}`);
|
||||||
|
await expect(page.getByRole('heading')).toHaveText(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to have hooks for each test, you can put them inside a `describe()` - so they are executed for each iteration / each invidual test:
|
||||||
|
|
||||||
|
```js title="example.spec.ts"
|
||||||
|
[
|
||||||
|
{ name: 'Alice', expected: 'Hello, Alice!' },
|
||||||
|
{ name: 'Bob', expected: 'Hello, Bob!' },
|
||||||
|
{ name: 'Charlie', expected: 'Hello, Charlie!' },
|
||||||
|
].forEach(({ name, expected }) => {
|
||||||
|
test.describe(() => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto(`https://example.com/greet?name=${name}`);
|
||||||
|
});
|
||||||
|
test(`testing with ${expected}`, async ({ page }) => {
|
||||||
|
await expect(page.getByRole('heading')).toHaveText(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameterized Projects
|
## Parameterized Projects
|
||||||
|
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -62,7 +62,7 @@
|
||||||
"ssim.js": "^3.5.0",
|
"ssim.js": "^3.5.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^5.0.13",
|
"vite": "^5.0.13",
|
||||||
"ws": "^8.5.0",
|
"ws": "^8.17.1",
|
||||||
"xml2js": "^0.5.0",
|
"xml2js": "^0.5.0",
|
||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
|
|
@ -8028,9 +8028,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.16.0",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
"ssim.js": "^3.5.0",
|
"ssim.js": "^3.5.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^5.0.13",
|
"vite": "^5.0.13",
|
||||||
"ws": "^8.5.0",
|
"ws": "^8.17.1",
|
||||||
"xml2js": "^0.5.0",
|
"xml2js": "^0.5.0",
|
||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ This project incorporates components from the projects listed below. The origina
|
||||||
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
||||||
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
||||||
- wrappy@1.0.2 (https://github.com/npm/wrappy)
|
- wrappy@1.0.2 (https://github.com/npm/wrappy)
|
||||||
- ws@8.4.2 (https://github.com/websockets/ws)
|
- ws@8.17.1 (https://github.com/websockets/ws)
|
||||||
- yauzl@2.10.0 (https://github.com/thejoshwolfe/yauzl)
|
- yauzl@2.10.0 (https://github.com/thejoshwolfe/yauzl)
|
||||||
- yazl@2.5.1 (https://github.com/thejoshwolfe/yazl)
|
- yazl@2.5.1 (https://github.com/thejoshwolfe/yazl)
|
||||||
|
|
||||||
|
|
@ -1435,29 +1435,30 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
=========================================
|
=========================================
|
||||||
END OF wrappy@1.0.2 AND INFORMATION
|
END OF wrappy@1.0.2 AND INFORMATION
|
||||||
|
|
||||||
%% ws@8.4.2 NOTICES AND INFORMATION BEGIN HERE
|
%% ws@8.17.1 NOTICES AND INFORMATION BEGIN HERE
|
||||||
=========================================
|
=========================================
|
||||||
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||||
|
Copyright (c) 2013 Arnout Kazemier and contributors
|
||||||
|
Copyright (c) 2016 Luigi Pinca and contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
in the Software without restriction, including without limitation the rights
|
the Software without restriction, including without limitation the rights to
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
furnished to do so, subject to the following conditions:
|
subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in all
|
||||||
copies or substantial portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
SOFTWARE.
|
|
||||||
=========================================
|
=========================================
|
||||||
END OF ws@8.4.2 AND INFORMATION
|
END OF ws@8.17.1 AND INFORMATION
|
||||||
|
|
||||||
%% yauzl@2.10.0 NOTICES AND INFORMATION BEGIN HERE
|
%% yauzl@2.10.0 NOTICES AND INFORMATION BEGIN HERE
|
||||||
=========================================
|
=========================================
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,19 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
"revision": "1454",
|
"revision": "1456",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "127.0"
|
"browserVersion": "127.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox-beta",
|
"name": "firefox-beta",
|
||||||
"revision": "1454",
|
"revision": "1456",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "128.0b3"
|
"browserVersion": "128.0b3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2035",
|
"revision": "2036",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"mac10.14": "1446",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"signal-exit": "3.0.7",
|
"signal-exit": "3.0.7",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"stack-utils": "2.0.5",
|
"stack-utils": "2.0.5",
|
||||||
"ws": "8.4.2"
|
"ws": "8.17.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
|
|
@ -399,15 +399,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.4.2",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"bufferutil": "^4.0.1",
|
"bufferutil": "^4.0.1",
|
||||||
"utf-8-validate": "^5.0.2"
|
"utf-8-validate": ">=5.0.2"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"bufferutil": {
|
"bufferutil": {
|
||||||
|
|
@ -702,9 +702,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.4.2",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
"signal-exit": "3.0.7",
|
"signal-exit": "3.0.7",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"stack-utils": "2.0.5",
|
"stack-utils": "2.0.5",
|
||||||
"ws": "8.4.2"
|
"ws": "8.17.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export type FetchOptions = {
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
maxRedirects?: number,
|
maxRedirects?: number,
|
||||||
|
maxRetries?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState' | 'tracesDir'> & {
|
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState' | 'tracesDir'> & {
|
||||||
|
|
@ -168,11 +169,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
throw new TargetClosedError(this._closeReason);
|
throw new TargetClosedError(this._closeReason);
|
||||||
assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
|
assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
|
||||||
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||||
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' should be greater than or equal to '0'`);
|
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
|
||||||
|
assert(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
|
||||||
const url = options.url !== undefined ? options.url : options.request!.url();
|
const url = options.url !== undefined ? options.url : options.request!.url();
|
||||||
const params = objectToArray(options.params);
|
const params = objectToArray(options.params);
|
||||||
const method = options.method || options.request?.method();
|
const method = options.method || options.request?.method();
|
||||||
const maxRedirects = options.maxRedirects;
|
|
||||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
||||||
const headersObj = options.headers || options.request?.headers() ;
|
const headersObj = options.headers || options.request?.headers() ;
|
||||||
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
||||||
|
|
@ -234,7 +235,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
failOnStatusCode: options.failOnStatusCode,
|
failOnStatusCode: options.failOnStatusCode,
|
||||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
||||||
maxRedirects: maxRedirects,
|
maxRedirects: options.maxRedirects,
|
||||||
|
maxRetries: options.maxRetries,
|
||||||
...fixtures
|
...fixtures
|
||||||
});
|
});
|
||||||
return new APIResponse(this, result.response);
|
return new APIResponse(this, result.response);
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(options: FallbackOverrides & { maxRedirects?: number, timeout?: number } = {}): Promise<APIResponse> {
|
async fetch(options: FallbackOverrides & { maxRedirects?: number, maxRetries?: number, timeout?: number } = {}): Promise<APIResponse> {
|
||||||
return await this._wrapApiCall(async () => {
|
return await this._wrapApiCall(async () => {
|
||||||
return await this._context.request._innerFetch({ request: this.request(), data: options.postData, ...options });
|
return await this._context.request._innerFetch({ request: this.request(), data: options.postData, ...options });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ scheme.APIRequestContextFetchParams = tObject({
|
||||||
failOnStatusCode: tOptional(tBoolean),
|
failOnStatusCode: tOptional(tBoolean),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
maxRedirects: tOptional(tNumber),
|
maxRedirects: tOptional(tNumber),
|
||||||
|
maxRetries: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
scheme.APIRequestContextFetchResult = tObject({
|
scheme.APIRequestContextFetchResult = tObject({
|
||||||
response: tType('APIResponse'),
|
response: tType('APIResponse'),
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
setHeader(headers, 'content-length', String(postData.byteLength));
|
setHeader(headers, 'content-length', String(postData.byteLength));
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const fetchResponse = await controller.run(progress => {
|
const fetchResponse = await controller.run(progress => {
|
||||||
return this._sendRequest(progress, requestUrl, options, postData);
|
return this._sendRequestWithRetries(progress, requestUrl, options, postData, params.maxRetries);
|
||||||
});
|
});
|
||||||
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
||||||
this.fetchLog.set(fetchUid, controller.metadata.log);
|
this.fetchLog.set(fetchUid, controller.metadata.log);
|
||||||
|
|
@ -247,6 +247,28 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _sendRequestWithRetries(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer, maxRetries?: number): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
|
||||||
|
maxRetries ??= 0;
|
||||||
|
let backoff = 250;
|
||||||
|
for (let i = 0; i <= maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
return await this._sendRequest(progress, url, options, postData);
|
||||||
|
} catch (e) {
|
||||||
|
if (maxRetries === 0)
|
||||||
|
throw e;
|
||||||
|
if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline))
|
||||||
|
throw new Error(`Failed after ${i + 1} attempt(s): ${e}`);
|
||||||
|
// Retry on connection reset only.
|
||||||
|
if (e.code !== 'ECONNRESET')
|
||||||
|
throw e;
|
||||||
|
progress.log(` Received ECONNRESET, will retry after ${backoff}ms.`);
|
||||||
|
await new Promise(f => setTimeout(f, backoff));
|
||||||
|
backoff *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
}
|
||||||
|
|
||||||
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
|
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
|
||||||
await this._updateRequestCookieHeader(url, options.headers);
|
await this._updateRequestCookieHeader(url, options.headers);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -434,12 +434,6 @@ export class HarTracer {
|
||||||
const pageEntry = this._createPageEntryIfNeeded(page);
|
const pageEntry = this._createPageEntryIfNeeded(page);
|
||||||
const request = response.request();
|
const request = response.request();
|
||||||
|
|
||||||
// Prefer "response received" time over "request sent" time
|
|
||||||
// for the purpose of matching requests that were used in a particular snapshot.
|
|
||||||
// Note that both snapshot time and request time are taken here in the Node process.
|
|
||||||
if (this._options.includeTraceInfo)
|
|
||||||
harEntry._monotonicTime = monotonicTime();
|
|
||||||
|
|
||||||
harEntry.response = {
|
harEntry.response = {
|
||||||
status: response.status(),
|
status: response.status(),
|
||||||
statusText: response.statusText(),
|
statusText: response.statusText(),
|
||||||
|
|
|
||||||
|
|
@ -2122,6 +2122,10 @@ export module Protocol {
|
||||||
* Array of <code>DOMNode</code> ids of any children marked as selected.
|
* Array of <code>DOMNode</code> ids of any children marked as selected.
|
||||||
*/
|
*/
|
||||||
selectedChildNodeIds?: NodeId[];
|
selectedChildNodeIds?: NodeId[];
|
||||||
|
/**
|
||||||
|
* On / off state of switch form controls.
|
||||||
|
*/
|
||||||
|
switchState?: "off"|"on";
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* A structure holding an RGBA color.
|
* A structure holding an RGBA color.
|
||||||
|
|
|
||||||
36
packages/playwright-core/types/types.d.ts
vendored
36
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -15963,6 +15963,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or
|
* If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or
|
||||||
* [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, GET method is used.
|
* [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, GET method is used.
|
||||||
|
|
@ -16063,6 +16069,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
@ -16143,6 +16155,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
@ -16223,6 +16241,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
@ -16345,6 +16369,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
@ -16425,6 +16455,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||||
|
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
import { chokidar } from './utilsBundle';
|
import { chokidar } from './utilsBundle';
|
||||||
import type { FSWatcher } from 'chokidar';
|
import type { FSWatcher } from 'chokidar';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
||||||
|
|
||||||
|
|
@ -52,7 +51,7 @@ export class Watcher {
|
||||||
if (!this._watchedFiles.length)
|
if (!this._watchedFiles.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const ignored = [...this._ignoredFolders, (name: string) => name.includes(path.sep + 'node_modules' + path.sep)];
|
const ignored = [...this._ignoredFolders, '**/node_modules/**'];
|
||||||
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored }).on('all', async (event, file) => {
|
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored }).on('all', async (event, file) => {
|
||||||
if (this._throttleTimer)
|
if (this._throttleTimer)
|
||||||
clearTimeout(this._throttleTimer);
|
clearTimeout(this._throttleTimer);
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,7 @@ export type APIRequestContextFetchParams = {
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
maxRedirects?: number,
|
maxRedirects?: number,
|
||||||
|
maxRetries?: number,
|
||||||
};
|
};
|
||||||
export type APIRequestContextFetchOptions = {
|
export type APIRequestContextFetchOptions = {
|
||||||
params?: NameValue[],
|
params?: NameValue[],
|
||||||
|
|
@ -337,6 +338,7 @@ export type APIRequestContextFetchOptions = {
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
maxRedirects?: number,
|
maxRedirects?: number,
|
||||||
|
maxRetries?: number,
|
||||||
};
|
};
|
||||||
export type APIRequestContextFetchResult = {
|
export type APIRequestContextFetchResult = {
|
||||||
response: APIResponse,
|
response: APIResponse,
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ APIRequestContext:
|
||||||
failOnStatusCode: boolean?
|
failOnStatusCode: boolean?
|
||||||
ignoreHTTPSErrors: boolean?
|
ignoreHTTPSErrors: boolean?
|
||||||
maxRedirects: number?
|
maxRedirects: number?
|
||||||
|
maxRetries: number?
|
||||||
returns:
|
returns:
|
||||||
response: APIResponse
|
response: APIResponse
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@ function adjustMonotonicTime(contexts: ContextEntry[], monotonicTimeDelta: numbe
|
||||||
for (const frame of page.screencastFrames)
|
for (const frame of page.screencastFrames)
|
||||||
frame.timestamp += monotonicTimeDelta;
|
frame.timestamp += monotonicTimeDelta;
|
||||||
}
|
}
|
||||||
|
for (const resource of context.resources) {
|
||||||
|
if (resource._monotonicTime)
|
||||||
|
resource._monotonicTime += monotonicTimeDelta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1287,3 +1287,21 @@ it('should not work after context dispose', async ({ context, server }) => {
|
||||||
await context.close({ reason: 'Test ended.' });
|
await context.close({ reason: 'Test ended.' });
|
||||||
expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain('Test ended.');
|
expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain('Test ended.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retrty ECONNRESET', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
||||||
|
}, async ({ context, server }) => {
|
||||||
|
let requestCount = 0;
|
||||||
|
server.setRoute('/test', (req, res) => {
|
||||||
|
if (requestCount++ < 3) {
|
||||||
|
req.socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||||
|
res.end('Hello!');
|
||||||
|
});
|
||||||
|
const response = await context.request.get(server.PREFIX + '/test', { maxRetries: 3 });
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toBe('Hello!');
|
||||||
|
expect(requestCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,7 @@ it('should throw an error when maxRedirects is less than 0', async ({ playwright
|
||||||
|
|
||||||
const request = await playwright.request.newContext();
|
const request = await playwright.request.newContext();
|
||||||
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH'])
|
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH'])
|
||||||
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' should be greater than or equal to '0'`);
|
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' must be greater than or equal to '0'`);
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -483,3 +483,21 @@ it('should throw after dispose', async ({ playwright, server }) => {
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
await expect(request.get(server.EMPTY_PAGE)).rejects.toThrow('Target page, context or browser has been closed');
|
await expect(request.get(server.EMPTY_PAGE)).rejects.toThrow('Target page, context or browser has been closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retry ECONNRESET', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
||||||
|
}, async ({ context, server }) => {
|
||||||
|
let requestCount = 0;
|
||||||
|
server.setRoute('/test', (req, res) => {
|
||||||
|
if (requestCount++ < 3) {
|
||||||
|
req.socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||||
|
res.end('Hello!');
|
||||||
|
});
|
||||||
|
const response = await context.request.fetch(server.PREFIX + '/test', { maxRetries: 3 });
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toBe('Hello!');
|
||||||
|
expect(requestCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,13 @@ for (const browserName of browserNames) {
|
||||||
metadata: {
|
metadata: {
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
docker: !!process.env.INSIDE_DOCKER,
|
docker: !!process.env.INSIDE_DOCKER,
|
||||||
headful: !!headed,
|
headless: (() => {
|
||||||
|
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||||
|
return 'headless-new';
|
||||||
|
if (headed)
|
||||||
|
return 'headed';
|
||||||
|
return 'headless';
|
||||||
|
})(),
|
||||||
browserName,
|
browserName,
|
||||||
channel,
|
channel,
|
||||||
mode,
|
mode,
|
||||||
|
|
|
||||||
|
|
@ -1236,3 +1236,38 @@ test('should open snapshot in new browser context', async ({ browser, page, runA
|
||||||
await expect(newPage.getByText('hello')).toBeVisible();
|
await expect(newPage.getByText('hello')).toBeVisible();
|
||||||
await newPage.close();
|
await newPage.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function parseMillis(s: string): number {
|
||||||
|
const matchMs = s.match(/(\d+)ms/);
|
||||||
|
if (matchMs)
|
||||||
|
return +matchMs[1];
|
||||||
|
const matchSeconds = s.match(/([\d.]+)s/);
|
||||||
|
if (!matchSeconds)
|
||||||
|
throw new Error('Failed to parse to millis: ' + s);
|
||||||
|
return (+matchSeconds[1]) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should show correct request start time', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31133' },
|
||||||
|
}, async ({ page, runAndTrace, server }) => {
|
||||||
|
server.setRoute('/api', (req, res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('done');
|
||||||
|
}, 1100);
|
||||||
|
});
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
return fetch('/api').then(r => r.text());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await traceViewer.selectAction('page.evaluate');
|
||||||
|
await traceViewer.showNetworkTab();
|
||||||
|
await expect(traceViewer.networkRequests).toContainText([/apiGET200text/]);
|
||||||
|
const line = traceViewer.networkRequests.getByText(/apiGET200text/);
|
||||||
|
const start = await line.locator('.grid-view-column-start').textContent();
|
||||||
|
const duration = await line.locator('.grid-view-column-duration').textContent();
|
||||||
|
expect(parseMillis(duration)).toBeGreaterThan(1000);
|
||||||
|
expect(parseMillis(start)).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -206,9 +206,8 @@ it.describe('screencast', () => {
|
||||||
expectRedFrames(videoFile, size);
|
expectRedFrames(videoFile, size);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should continue recording main page after popup closes', async ({ browser, browserName, trace, headless, isWindows }, testInfo) => {
|
it('should continue recording main page after popup closes', async ({ browser, browserName }, testInfo) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30837' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30837' });
|
||||||
it.fixme(browserName === 'firefox', 'https://github.com/microsoft/playwright/issues/30837');
|
|
||||||
// Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
|
// Firefox does not have a mobile variant and has a large minimum size (500 on windows and 450 elsewhere).
|
||||||
const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 };
|
const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 };
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,8 @@ it('should emulate reduced motion', async ({ page }) => {
|
||||||
await page.emulateMedia({ reducedMotion: null });
|
await page.emulateMedia({ reducedMotion: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep reduced motion and color emulation after reload', async ({ page, server, browserName }) => {
|
it('should keep reduced motion and color emulation after reload', async ({ page, server }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31328' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31328' });
|
||||||
it.fixme(browserName === 'firefox');
|
|
||||||
|
|
||||||
// Pre-conditions
|
// Pre-conditions
|
||||||
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toEqual(false);
|
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toEqual(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue