From 951040185d99ba940d11159a9c933d4773e6e76f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 19 Jun 2024 17:45:27 +0200 Subject: [PATCH 01/12] docs(test-parameterize): improve forEach example (#31331) --- docs/src/test-parameterize-js.md | 60 ++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/docs/src/test-parameterize-js.md b/docs/src/test-parameterize-js.md index e6cc791207..8be8009549 100644 --- a/docs/src/test-parameterize-js.md +++ b/docs/src/test-parameterize-js.md @@ -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 From 6d38525119b3542242dbf4037f9348906a4b9b46 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 19 Jun 2024 18:04:22 +0200 Subject: [PATCH 02/12] docs: add guide for print dialogs (#31340) https://github.com/microsoft/playwright-internal/issues/211 Relates https://github.com/microsoft/playwright/issues/6543 --- docs/src/dialogs.md | 54 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/src/dialogs.md b/docs/src/dialogs.md index 1d8938778c..20037245ee 100644 --- a/docs/src/dialogs.md +++ b/docs/src/dialogs.md @@ -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(''); + +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(""); + +page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()"); +page.getByText("Print it!").click(); + +page.waitForFunction("window.waitForPrintDialog"); +``` + +```python async +await page.goto("") + +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("") + +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(""); + +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. From ee63843f7d1bc17646616cdb374d7e40c383a704 Mon Sep 17 00:00:00 2001 From: ryanrosello-og <50574915+ryanrosello-og@users.noreply.github.com> Date: Thu, 20 Jun 2024 02:05:20 +1000 Subject: [PATCH 03/12] feat(trace-viewer): add request method/status to the network details tab (#31274) --- .../trace-viewer/src/ui/networkResourceDetails.css | 7 ++++++- .../trace-viewer/src/ui/networkResourceDetails.tsx | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css index fce4e8b2c3..ce0648b8c2 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.css +++ b/packages/trace-viewer/src/ui/networkResourceDetails.css @@ -39,6 +39,11 @@ font-weight: bold; } +.network-request-details-general { + white-space: pre; + margin-left: 10px; +} + .network-request-details-tab .cm-wrapper { overflow: hidden; } @@ -52,7 +57,7 @@ .tab-network .toolbar::after { box-shadow: none !important; } - + .tab-network .tabbed-pane-tab.selected { font-weight: bold; } diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index c5a974b69e..e7b1bad4b6 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -77,8 +77,14 @@ const RequestTab: React.FunctionComponent<{ }, [resource]); return
-
URL
-
{resource.request.url}
+
General
+
{`URL: ${resource.request.url}`}
+
{`Method: ${resource.request.method}`}
+
{`Status Code: ${ + resource.response.status >= 200 && resource.response.status < 400 + ? `🟢 ${resource.response.status} ${resource.response.statusText}` + : `🔴 ${resource.response.status} ${resource.response.statusText}` + }`}
Request Headers
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
{requestBody &&
Request Body
} From 8ba4cff10f2b62f851242e564855eecc6aef116d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 19 Jun 2024 18:08:22 +0200 Subject: [PATCH 04/12] devops: add headless new to flakiness dashboard (#31381) --- tests/library/playwright.config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts index a70c1f5e6d..8cb0347a1a 100644 --- a/tests/library/playwright.config.ts +++ b/tests/library/playwright.config.ts @@ -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, From a7958ff95f6577dfa891d29a302e0f52c9b82ac9 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:25:57 -0700 Subject: [PATCH 05/12] feat(firefox-beta): roll to r1456 (#31376) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index ce21ea45a0..6fad0fd03c 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -21,7 +21,7 @@ }, { "name": "firefox-beta", - "revision": "1454", + "revision": "1456", "installByDefault": false, "browserVersion": "128.0b3" }, From 45ee31867327dc8694e37ea643d29e3fa5099bb3 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:03:58 -0700 Subject: [PATCH 06/12] feat(firefox): roll to r1456 (#31375) Fixes https://github.com/microsoft/playwright/issues/31328 Fixes https://github.com/microsoft/playwright/issues/30837 --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Max Schmitt --- packages/playwright-core/browsers.json | 2 +- tests/library/video.spec.ts | 3 +-- tests/page/page-emulate-media.spec.ts | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6fad0fd03c..6dad66e8fc 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,7 +15,7 @@ }, { "name": "firefox", - "revision": "1454", + "revision": "1456", "installByDefault": true, "browserVersion": "127.0" }, diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 5ceae2f04e..39dcaecbc6 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -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({ diff --git a/tests/page/page-emulate-media.spec.ts b/tests/page/page-emulate-media.spec.ts index d1ba297ae4..c895d64eff 100644 --- a/tests/page/page-emulate-media.spec.ts +++ b/tests/page/page-emulate-media.spec.ts @@ -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); From 82a44f28e29754f452da2c9f2111d1216c803931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:42:43 -0700 Subject: [PATCH 07/12] chore(deps-dev): bump ws from 8.16.0 to 8.17.1 (#31377) Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a3e299839..acd9467df5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 6935a8a999..125abb0b7c 100644 --- a/package.json +++ b/package.json @@ -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" } From 94f0cadea3ddb3467f6e5a8628d8ecd1766b31b8 Mon Sep 17 00:00:00 2001 From: Luke Page <137174537+lukpsaxo@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:14:10 +0200 Subject: [PATCH 08/12] fix(fs-watcher) ignore node_modules on windows (#31341) Partially fixes https://github.com/microsoft/playwright/issues/31337 by supporting ignoring node_modules on windows. When I debug the function it gets a unix style path filename on windows, so the function never ignores node_modules. The ignore path globs are expected to use the unix path seperator and I've tested this fix works on windows and I assume that since mac uses unix style, it also works there (this is a pretty standard glob construct (chokidar points at any match https://github.com/micromatch/anymatch and anymatch has this exact example in their readme.md) Signed-off-by: Luke Page <137174537+lukpsaxo@users.noreply.github.com> --- packages/playwright/src/fsWatcher.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/playwright/src/fsWatcher.ts b/packages/playwright/src/fsWatcher.ts index c0d4e4d04e..a1c09c343d 100644 --- a/packages/playwright/src/fsWatcher.ts +++ b/packages/playwright/src/fsWatcher.ts @@ -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); From a2b116aa39c1b4980594834bdf6661c80a60410b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 19 Jun 2024 15:06:20 -0700 Subject: [PATCH 09/12] fix(trace): ensure har entry _monotonicTime is always start time (#31385) * Revert harTracer change from https://github.com/microsoft/playwright/commit/aeba083da0d8da9591e2a72c571477691a095d74 to make sure that har.Entry._monotonicTime always represents request start time. The issue from the corresponding report was due to HEAD and GET request sent for the same URL, that use case is still addressed as we match by url + method * Adjust resources monotonic time as well when several contexts are shown in the trace viewer. Fixes https://github.com/microsoft/playwright/issues/31133 --- .../src/server/har/harTracer.ts | 6 ---- packages/trace-viewer/src/ui/modelUtil.ts | 4 +++ tests/library/trace-viewer.spec.ts | 35 +++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 289a4f6d87..268da9e84d 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -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(), diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index ba866ad7ca..94d42ba4a8 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -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; + } } } diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index c2634df148..69864380d8 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -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); +}); From 2dfda0a16f0428822bce68b2d39b082740f7fa5b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 19 Jun 2024 15:11:54 -0700 Subject: [PATCH 10/12] chore: bump ws to 8.17.1 in the utils bundle (#31384) Follow-up to #31377. --- .../playwright-core/ThirdPartyNotices.txt | 31 ++++++++++--------- .../bundles/utils/package-lock.json | 16 +++++----- .../bundles/utils/package.json | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index f0b678af04..3c5a71e20f 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -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 +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 ========================================= diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index df01638322..66c4cdae12 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -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": {} } } diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 786b544a97..8ac0c112fe 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -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", From 95fc2b8a8b449560e53999c866e9602f4bca27f1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 19 Jun 2024 18:10:14 -0700 Subject: [PATCH 11/12] feat(fetch): maxRetries for fetch (#31386) Fixes https://github.com/microsoft/playwright/issues/30978 --- docs/src/api/class-apirequestcontext.md | 18 ++++++++++ docs/src/api/class-requestoptions.md | 10 ++++++ docs/src/api/params.md | 6 ++++ packages/playwright-core/src/client/fetch.ts | 8 +++-- .../playwright-core/src/client/network.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/server/fetch.ts | 24 ++++++++++++- packages/playwright-core/types/types.d.ts | 36 +++++++++++++++++++ packages/protocol/src/channels.ts | 2 ++ packages/protocol/src/protocol.yml | 1 + tests/library/browsercontext-fetch.spec.ts | 18 ++++++++++ tests/library/global-fetch.spec.ts | 20 ++++++++++- 12 files changed, 140 insertions(+), 6 deletions(-) diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 7101d1996d..dab99bec2b 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -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]> diff --git a/docs/src/api/class-requestoptions.md b/docs/src/api/class-requestoptions.md index b6eeb8970a..401e13d944 100644 --- a/docs/src/api/class-requestoptions.md +++ b/docs/src/api/class-requestoptions.md @@ -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]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index a1dd84c4ef..253ca3a1fd 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -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]> diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 4598ac2418..44314c6fa3 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -41,6 +41,7 @@ export type FetchOptions = { failOnStatusCode?: boolean, ignoreHTTPSErrors?: boolean, maxRedirects?: number, + maxRetries?: number, }; type NewContextOptions = Omit & { @@ -168,11 +169,11 @@ export class APIRequestContext extends ChannelOwner= 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 implements api.Ro }); } - async fetch(options: FallbackOverrides & { maxRedirects?: number, timeout?: number } = {}): Promise { + async fetch(options: FallbackOverrides & { maxRedirects?: number, maxRetries?: number, timeout?: number } = {}): Promise { return await this._wrapApiCall(async () => { return await this._context.request._innerFetch({ request: this.request(), data: options.postData, ...options }); }); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5727192b59..9187dc13d2 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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'), diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 0f87e0046b..c02781f748 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -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 & { 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 & { body: Buffer }>{ await this._updateRequestCookieHeader(url, options.headers); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 28250cb271..67e92a917a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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 diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 13d34f1f51..ffb591556c 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -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, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 2bef7c8762..14799ef17e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -299,6 +299,7 @@ APIRequestContext: failOnStatusCode: boolean? ignoreHTTPSErrors: boolean? maxRedirects: number? + maxRetries: number? returns: response: APIResponse diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index ba01ccc07c..a8aaeb389f 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -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); +}); diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index a2eb629dc5..264df5c619 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -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); +}); From 0694474fe1b3de087f633e204ec7c183969394ad Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:52:59 -0700 Subject: [PATCH 12/12] feat(webkit): roll to r2036 (#31387) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- packages/playwright-core/src/server/webkit/protocol.d.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6dad66e8fc..d21d1c67e7 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2035", + "revision": "2036", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 389ce6b7ac..ba9ba9e5d3 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -2122,6 +2122,10 @@ export module Protocol { * Array of DOMNode 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.