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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.fetch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.get
|
||||
* since: v1.16
|
||||
- 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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.head
|
||||
* since: v1.16
|
||||
- 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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.patch
|
||||
* since: v1.16
|
||||
- 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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.patch.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.post
|
||||
* since: v1.16
|
||||
- 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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.post.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.put
|
||||
* since: v1.16
|
||||
- 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-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.put.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||
* since: v1.46
|
||||
|
||||
## async method: APIRequestContext.storageState
|
||||
* since: v1.16
|
||||
- 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.
|
||||
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
|
||||
* since: v1.18
|
||||
- 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.
|
||||
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
|
||||
- `expression` <[string]>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: "Dialogs"
|
|||
|
||||
## 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
|
||||
|
||||
|
|
@ -126,3 +126,55 @@ Page.Dialog += async (_, dialog) =>
|
|||
};
|
||||
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
|
||||
|
||||
```js title="example.spec.ts"
|
||||
const people = ['Alice', 'Bob'];
|
||||
for (const name of people) {
|
||||
test(`testing with ${name}`, async () => {
|
||||
// ...
|
||||
});
|
||||
[
|
||||
{ name: 'Alice', expected: 'Hello, Alice!' },
|
||||
{ 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.
|
||||
}
|
||||
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
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -62,7 +62,7 @@
|
|||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.13",
|
||||
"ws": "^8.5.0",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
|
|
@ -8028,9 +8028,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.13",
|
||||
"ws": "^8.5.0",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"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)
|
||||
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
%% 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) 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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 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
|
||||
=========================================
|
||||
|
|
|
|||
|
|
@ -15,19 +15,19 @@
|
|||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1454",
|
||||
"revision": "1456",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "127.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1454",
|
||||
"revision": "1456",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "128.0b3"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2035",
|
||||
"revision": "2036",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.4.2"
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
@ -399,15 +399,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
|
@ -702,9 +702,9 @@
|
|||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
|
||||
"integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.4.2"
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export type FetchOptions = {
|
|||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
maxRedirects?: number,
|
||||
maxRetries?: number,
|
||||
};
|
||||
|
||||
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState' | 'tracesDir'> & {
|
||||
|
|
@ -168,11 +169,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
throw new TargetClosedError(this._closeReason);
|
||||
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.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 params = objectToArray(options.params);
|
||||
const method = options.method || options.request?.method();
|
||||
const maxRedirects = options.maxRedirects;
|
||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
||||
const headersObj = options.headers || options.request?.headers() ;
|
||||
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
||||
|
|
@ -234,7 +235,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
timeout: options.timeout,
|
||||
failOnStatusCode: options.failOnStatusCode,
|
||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
||||
maxRedirects: maxRedirects,
|
||||
maxRedirects: options.maxRedirects,
|
||||
maxRetries: options.maxRetries,
|
||||
...fixtures
|
||||
});
|
||||
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._context.request._innerFetch({ request: this.request(), data: options.postData, ...options });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ scheme.APIRequestContextFetchParams = tObject({
|
|||
failOnStatusCode: tOptional(tBoolean),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
maxRedirects: tOptional(tNumber),
|
||||
maxRetries: tOptional(tNumber),
|
||||
});
|
||||
scheme.APIRequestContextFetchResult = tObject({
|
||||
response: tType('APIResponse'),
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
setHeader(headers, 'content-length', String(postData.byteLength));
|
||||
const controller = new ProgressController(metadata, this);
|
||||
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);
|
||||
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 }>{
|
||||
await this._updateRequestCookieHeader(url, options.headers);
|
||||
|
||||
|
|
|
|||
|
|
@ -434,12 +434,6 @@ export class HarTracer {
|
|||
const pageEntry = this._createPageEntryIfNeeded(page);
|
||||
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 = {
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
|
|
|
|||
|
|
@ -2122,6 +2122,10 @@ export module Protocol {
|
|||
* Array of <code>DOMNode</code> ids of any children marked as selected.
|
||||
*/
|
||||
selectedChildNodeIds?: NodeId[];
|
||||
/**
|
||||
* On / off state of switch form controls.
|
||||
*/
|
||||
switchState?: "off"|"on";
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 type { FSWatcher } from 'chokidar';
|
||||
import path from 'path';
|
||||
|
||||
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
||||
|
||||
|
|
@ -52,7 +51,7 @@ export class Watcher {
|
|||
if (!this._watchedFiles.length)
|
||||
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) => {
|
||||
if (this._throttleTimer)
|
||||
clearTimeout(this._throttleTimer);
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ export type APIRequestContextFetchParams = {
|
|||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
maxRedirects?: number,
|
||||
maxRetries?: number,
|
||||
};
|
||||
export type APIRequestContextFetchOptions = {
|
||||
params?: NameValue[],
|
||||
|
|
@ -337,6 +338,7 @@ export type APIRequestContextFetchOptions = {
|
|||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
maxRedirects?: number,
|
||||
maxRetries?: number,
|
||||
};
|
||||
export type APIRequestContextFetchResult = {
|
||||
response: APIResponse,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ APIRequestContext:
|
|||
failOnStatusCode: boolean?
|
||||
ignoreHTTPSErrors: boolean?
|
||||
maxRedirects: number?
|
||||
maxRetries: number?
|
||||
returns:
|
||||
response: APIResponse
|
||||
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ function adjustMonotonicTime(contexts: ContextEntry[], monotonicTimeDelta: numbe
|
|||
for (const frame of page.screencastFrames)
|
||||
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.' });
|
||||
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();
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
@ -483,3 +483,21 @@ it('should throw after dispose', async ({ playwright, server }) => {
|
|||
await request.dispose();
|
||||
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: {
|
||||
platform: process.platform,
|
||||
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,
|
||||
channel,
|
||||
mode,
|
||||
|
|
|
|||
|
|
@ -1236,3 +1236,38 @@ test('should open snapshot in new browser context', async ({ browser, page, runA
|
|||
await expect(newPage.getByText('hello')).toBeVisible();
|
||||
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);
|
||||
});
|
||||
|
||||
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.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).
|
||||
const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 };
|
||||
const context = await browser.newContext({
|
||||
|
|
|
|||
|
|
@ -124,9 +124,8 @@ it('should emulate reduced motion', async ({ page }) => {
|
|||
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.fixme(browserName === 'firefox');
|
||||
|
||||
// Pre-conditions
|
||||
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toEqual(false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue