From 414fa0b97e2807ab4136518e41c70f018f5442f9 Mon Sep 17 00:00:00 2001 From: Yugendra N Date: Thu, 28 May 2026 14:23:10 +0530 Subject: [PATCH 1/3] Remove unnecessary tabIndex from gantt chart SVG rect elements for improved accessibility (#41029) --- packages/html-reporter/src/gantt.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/html-reporter/src/gantt.tsx b/packages/html-reporter/src/gantt.tsx index de875e87101cb..0151773157beb 100644 --- a/packages/html-reporter/src/gantt.tsx +++ b/packages/html-reporter/src/gantt.tsx @@ -157,7 +157,6 @@ export const GanttChart = ({ height={barHeight} fill={color} rx='2' - tabIndex={0} > {entry.tooltip} From 7444bcaeb0823252ca1e8b942f515bc5616c5e57 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 13:46:54 +0200 Subject: [PATCH 2/3] chore: roll driver/Dockerfile to recent Node.js LTS version (#41033) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index 10acdd5af58f0..11c25f6f428b7 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="24.15.0" # autogenerated via ./update-playwright-node.mjs +NODE_VERSION="24.16.0" # autogenerated via ./update-playwright-node.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From dfe5ab506f863f50db56e9dc1fbe1a451cc9aa91 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 28 May 2026 08:21:50 -0700 Subject: [PATCH 3/3] fix(fetch): preserve error code in decompression pipeline for retry logic (#40946) --- packages/playwright-core/src/server/fetch.ts | 23 ++++++-- tests/library/browsercontext-fetch.spec.ts | 59 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 7299449c041ad..d4f1376588d52 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -482,8 +482,6 @@ export abstract class APIRequestContext extends SdkObject { return; } } - response.on('aborted', () => reject(new Error('aborted'))); - const chunks: Buffer[] = []; const notifyBodyFinished = () => { const body = Buffer.concat(chunks); @@ -522,11 +520,21 @@ export abstract class APIRequestContext extends SdkObject { // Brotli and deflate decompressors throw if the input stream is empty. const emptyStreamTransform = new SafeEmptyStreamTransform(notifyBodyFinished); body = pipeline(response, emptyStreamTransform, transform, e => { - if (e) - reject(new Error(`failed to decompress '${encoding}' encoding: ${e.message}`)); + if (e) { + if (isNetworkConnectionError(e)) + reject(e); + else + reject(new Error(`failed to decompress '${encoding}' encoding: ${e.message}`)); + } + }); + body.on('error', e => { + if (isNetworkConnectionError(e)) + reject(e); + else + reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`)); }); - body.on('error', e => reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`))); } else { + response.on('aborted', () => reject(new Error('aborted'))); body.on('error', reject); } @@ -804,6 +812,11 @@ function removeHeader(headers: { [name: string]: string }, name: string) { delete headers[existing[0]]; } +function isNetworkConnectionError(e: any): boolean { + const code = e?.code; + return code === 'ECONNRESET' || code === 'EPIPE' || code === 'ECONNABORTED'; +} + function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HTTPCredentials) { const { username, password } = credentials; const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64'); diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 871e435b6df70..0b755200a662f 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -1401,3 +1401,62 @@ it('should retry on ECONNRESET', { expect(await response.text()).toBe('Hello!'); expect(requestCount).toBe(4); }); + +it('should retry ECONNRESET on compressed response', async ({ context, server }) => { + let requestCount = 0; + server.setRoute('/test-gzip', (req, res) => { + if (requestCount++ < 2) { + req.socket.destroy(); + return; + } + res.writeHead(200, { + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/plain', + }); + const gzipStream = zlib.createGzip(); + pipeline(gzipStream, res, err => { + if (err) + console.log(`Server error: ${err}`); + }); + gzipStream.write('compressed-retry-ok'); + gzipStream.end(); + }); + const response = await context.request.get(server.PREFIX + '/test-gzip', { maxRetries: 3 }); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('compressed-retry-ok'); + expect(requestCount).toBe(3); +}); + +it('should retry ECONNRESET mid-stream during gzip decompression', async ({ context, server }) => { + let requestCount = 0; + server.setRoute('/test-gzip-midstream', (req, res) => { + requestCount++; + if (requestCount <= 2) { + // Send response headers to make client enter the decompression pipeline, + // then destroy the socket. This exercises the fix: without it, the + // pipeline error callback wraps the error, stripping .code for retry. + res.writeHead(200, { + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/plain', + }); + res.flushHeaders(); + req.socket.destroy(); + return; + } + res.writeHead(200, { + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/plain', + }); + const gzipStream = zlib.createGzip(); + pipeline(gzipStream, res, err => { + if (err) + console.log(`Server error: ${err}`); + }); + gzipStream.write('midstream-retry-ok'); + gzipStream.end(); + }); + const response = await context.request.get(server.PREFIX + '/test-gzip-midstream', { maxRetries: 3 }); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('midstream-retry-ok'); + expect(requestCount).toBe(3); +});