From 9a6f03eb87f985735c766bcefa6f8927bdd85f8c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 7 Oct 2024 18:43:25 +0200 Subject: [PATCH 1/7] fix(fetch): listener leaks on Socket (#32956) Closes https://github.com/microsoft/playwright/issues/32951 `node:http` reuses TCP Sockets under the hood. We weren't cleaning up our listeners, leading to the `MaxListenersExceededWarning`. This PR adds cleanup logic. It also raises the warning threshhold, so that it doesn't trigger until there's 100 concurrent requests over the same socket. --- packages/playwright-core/src/server/fetch.ts | 50 +++++++++++--------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 4408d91133..243e89cf1c 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -24,7 +24,7 @@ import zlib from 'zlib'; import type { HTTPCredentials } from '../../types/types'; import { TimeoutSettings } from '../common/timeoutSettings'; import { getUserAgent } from '../utils/userAgent'; -import { assert, constructURLBasedOnBaseURL, createGuid, monotonicTime } from '../utils'; +import { assert, constructURLBasedOnBaseURL, createGuid, eventsHelper, monotonicTime, type RegisteredListener } from '../utils'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; import { BrowserContext, verifyClientCertificates } from './browserContext'; import { CookieStore, domainMatches, parseRawCookie } from './cookieStore'; @@ -311,8 +311,11 @@ export abstract class APIRequestContext extends SdkObject { let securityDetails: har.SecurityDetails | undefined; + const listeners: RegisteredListener[] = []; + const request = requestConstructor(url, requestOptions as any, async response => { const responseAt = monotonicTime(); + const notifyRequestFinished = (body?: Buffer) => { const endAt = monotonicTime(); // spec: http://www.softwareishard.com/blog/har-12-spec/#timings @@ -477,12 +480,13 @@ export abstract class APIRequestContext extends SdkObject { }); request.on('error', reject); - const disposeListener = () => { - reject(new Error('Request context disposed.')); - request.destroy(); - }; - this.on(APIRequestContext.Events.Dispose, disposeListener); - request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); + listeners.push( + eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => { + reject(new Error('Request context disposed.')); + request.destroy(); + }) + ); + request.on('close', () => eventsHelper.removeEventListeners(listeners)); request.on('socket', socket => { // happy eyeballs don't emit lookup and connect events, so we use our custom ones @@ -491,22 +495,24 @@ export abstract class APIRequestContext extends SdkObject { tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt; // non-happy-eyeballs sockets - socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); - socket.on('connect', () => { tcpConnectionAt ??= monotonicTime(); }); - socket.on('secureConnect', () => { - tlsHandshakeAt = monotonicTime(); + listeners.push( + eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }), + eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }), + eventsHelper.addEventListener(socket, 'secureConnect', () => { + tlsHandshakeAt = monotonicTime(); - if (socket instanceof TLSSocket) { - const peerCertificate = socket.getPeerCertificate(); - securityDetails = { - protocol: socket.getProtocol() ?? undefined, - subjectName: peerCertificate.subject.CN, - validFrom: new Date(peerCertificate.valid_from).getTime() / 1000, - validTo: new Date(peerCertificate.valid_to).getTime() / 1000, - issuer: peerCertificate.issuer.CN - }; - } - }); + if (socket instanceof TLSSocket) { + const peerCertificate = socket.getPeerCertificate(); + securityDetails = { + protocol: socket.getProtocol() ?? undefined, + subjectName: peerCertificate.subject.CN, + validFrom: new Date(peerCertificate.valid_from).getTime() / 1000, + validTo: new Date(peerCertificate.valid_to).getTime() / 1000, + issuer: peerCertificate.issuer.CN + }; + } + }), + ); // when using socks proxy, having the socket means the connection got established if (agent instanceof SocksProxyAgent) From 4fe33db392b075ee302ddb6309b8fd3be5aad2e7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 7 Oct 2024 13:52:55 -0700 Subject: [PATCH 2/7] docs(route): header override propagation (#32971) Fix https://github.com/microsoft/playwright/issues/32939 --- docs/src/api/class-route.md | 2 +- packages/playwright-core/types/types.d.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/api/class-route.md b/docs/src/api/class-route.md index 106fbf55e0..c4e5fcb2e4 100644 --- a/docs/src/api/class-route.md +++ b/docs/src/api/class-route.md @@ -102,7 +102,7 @@ await page.RouteAsync("**/*", async route => **Details** -Note that any overrides such as [`option: url`] or [`option: headers`] only apply to the request being routed. If this request results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header through redirects, use the combination of [`method: Route.fetch`] and [`method: Route.fulfill`] instead. +The [`option: headers`] option applies to both the routed request and any redirects it initiates. However, [`option: url`], [`option: method`], and [`option: postData`] only apply to the original request and are not carried over to redirected requests. [`method: Route.continue`] will immediately send the request to the network, other matching handlers won't be invoked. Use [`method: Route.fallback`] If you want next matching handler in the chain to be invoked. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index fe182dac4e..abf92c0141 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -20614,12 +20614,12 @@ export interface Route { * * **Details** * - * Note that any overrides such as [`url`](https://playwright.dev/docs/api/class-route#route-continue-option-url) or - * [`headers`](https://playwright.dev/docs/api/class-route#route-continue-option-headers) only apply to the request - * being routed. If this request results in a redirect, overrides will not be applied to the new redirected request. - * If you want to propagate a header through redirects, use the combination of - * [route.fetch([options])](https://playwright.dev/docs/api/class-route#route-fetch) and - * [route.fulfill([options])](https://playwright.dev/docs/api/class-route#route-fulfill) instead. + * The [`headers`](https://playwright.dev/docs/api/class-route#route-continue-option-headers) option applies to both + * the routed request and any redirects it initiates. However, + * [`url`](https://playwright.dev/docs/api/class-route#route-continue-option-url), + * [`method`](https://playwright.dev/docs/api/class-route#route-continue-option-method), and + * [`postData`](https://playwright.dev/docs/api/class-route#route-continue-option-post-data) only apply to the + * original request and are not carried over to redirected requests. * * [route.continue([options])](https://playwright.dev/docs/api/class-route#route-continue) will immediately send the * request to the network, other matching handlers won't be invoked. Use From 4ab857ce8e055c2adcc07f62c453816853f71451 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 7 Oct 2024 14:06:28 -0700 Subject: [PATCH 3/7] test: fetch header propagation on redirect (#32970) Documenting current behavior with and without interception. Reference https://github.com/microsoft/playwright/issues/32939 --- tests/page/page-request-continue.spec.ts | 191 +++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 7487b3e35b..2df84e133a 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -17,6 +17,7 @@ import { test as it, expect } from './pageTest'; import type { Route } from 'playwright-core'; +import type * as http from 'http'; it('should work', async ({ page, server }) => { await page.route('**/*', route => route.continue()); @@ -473,6 +474,196 @@ it('continue should delete headers on redirects', { expect(serverRequest.headers.foo).toBeFalsy(); }); +it('propagate headers same origin redirect', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' }, + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' }, + ] +}, async ({ page, server }) => { + await page.goto(server.PREFIX + '/empty.html'); + let resolve; + const serverRequestPromise = new Promise(f => resolve = f); + server.setRoute('/something', (request, response) => { + if (request.method === 'OPTIONS') { + response.writeHead(204, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Allow-Headers': 'authorization,custom', + }); + response.end(); + return; + } + resolve(request); + response.writeHead(200, { }); + response.end('done'); + }); + await server.setRedirect('/redirect', '/something'); + const text = await page.evaluate(async url => { + const data = await fetch(url, { + headers: { + authorization: 'credentials', + custom: 'foo' + }, + credentials: 'include', + }); + return data.text(); + }, server.PREFIX + '/redirect'); + expect(text).toBe('done'); + const serverRequest = await serverRequestPromise; + expect.soft(serverRequest.headers['authorization']).toBe('credentials'); + expect.soft(serverRequest.headers['custom']).toBe('foo'); +}); + +it('propagate headers cross origin', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' }, + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' }, + ] +}, async ({ page, server }) => { + await page.goto(server.PREFIX + '/empty.html'); + let resolve; + const serverRequestPromise = new Promise(f => resolve = f); + server.setRoute('/something', (request, response) => { + if (request.method === 'OPTIONS') { + response.writeHead(204, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Allow-Headers': 'authorization,custom', + }); + response.end(); + return; + } + resolve(request); + response.writeHead(200, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + }); + response.end('done'); + }); + const text = await page.evaluate(async url => { + const data = await fetch(url, { + headers: { + authorization: 'credentials', + custom: 'foo' + }, + credentials: 'include', + }); + return data.text(); + }, server.CROSS_PROCESS_PREFIX + '/something'); + expect(text).toBe('done'); + const serverRequest = await serverRequestPromise; + expect.soft(serverRequest.headers['authorization']).toBe('credentials'); + expect.soft(serverRequest.headers['custom']).toBe('foo'); +}); + +it('propagate headers cross origin redirect', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' }, + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' }, + ] +}, async ({ page, server }) => { + await page.goto(server.PREFIX + '/empty.html'); + let resolve; + const serverRequestPromise = new Promise(f => resolve = f); + server.setRoute('/something', (request, response) => { + if (request.method === 'OPTIONS') { + response.writeHead(204, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Allow-Headers': 'authorization,custom', + }); + response.end(); + return; + } + resolve(request); + response.writeHead(200, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + }); + response.end('done'); + }); + server.setRoute('/redirect', (request, response) => { + response.writeHead(301, { location: `${server.CROSS_PROCESS_PREFIX}/something` }); + response.end(); + }); + const text = await page.evaluate(async url => { + const data = await fetch(url, { + headers: { + authorization: 'credentials', + custom: 'foo' + }, + credentials: 'include', + }); + return data.text(); + }, server.PREFIX + '/redirect'); + expect(text).toBe('done'); + const serverRequest = await serverRequestPromise; + // Authorization header not propagated to cross-origin redirect. + expect.soft(serverRequest.headers['authorization']).toBeFalsy(); + expect.soft(serverRequest.headers['custom']).toBe('foo'); +}); + +it('propagate headers cross origin redirect after interception', { + annotation: [ + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' }, + { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' }, + ] +}, async ({ page, server, browserName }) => { + await page.goto(server.PREFIX + '/empty.html'); + let resolve; + const serverRequestPromise = new Promise(f => resolve = f); + server.setRoute('/something', (request, response) => { + if (request.method === 'OPTIONS') { + response.writeHead(204, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Allow-Headers': 'authorization,custom', + }); + response.end(); + return; + } + resolve(request); + response.writeHead(200, { + 'Access-Control-Allow-Origin': server.PREFIX, + 'Access-Control-Allow-Credentials': 'true', + }); + response.end('done'); + }); + server.setRoute('/redirect', (request, response) => { + response.writeHead(301, { location: `${server.CROSS_PROCESS_PREFIX}/something` }); + response.end(); + }); + await page.route('**/redirect', async route => { + await route.continue({ + headers: { + ...route.request().headers(), + authorization: 'credentials', + custom: 'foo' + } + }); + }); + const text = await page.evaluate(async url => { + const data = await fetch(url, { + headers: { + authorization: 'none', + }, + credentials: 'include', + }); + return data.text(); + }, server.PREFIX + '/redirect'); + expect(text).toBe('done'); + const serverRequest = await serverRequestPromise; + if (browserName === 'webkit') + expect.soft(serverRequest.headers['authorization']).toBeFalsy(); + else + expect.soft(serverRequest.headers['authorization']).toBe('credentials'); + expect.soft(serverRequest.headers['custom']).toBe('foo'); +}); + it('should intercept css variable with background url', async ({ page, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' }); From 4d13677ebd92d9358f691fe149595e6d86803cb1 Mon Sep 17 00:00:00 2001 From: Aaron Sherwood <69980784+asherwoodcnet@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:24:18 -0700 Subject: [PATCH 4/7] chore: add Devuan OS fallback to Debian (#32990) --- packages/playwright-core/src/utils/hostPlatform.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/utils/hostPlatform.ts b/packages/playwright-core/src/utils/hostPlatform.ts index 9664418e3d..8c20d65cce 100644 --- a/packages/playwright-core/src/utils/hostPlatform.ts +++ b/packages/playwright-core/src/utils/hostPlatform.ts @@ -86,15 +86,20 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; } - if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian') { + if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian' || distroInfo?.id === 'devuan') { const isOfficiallySupportedPlatform = distroInfo?.id === 'debian'; - if (distroInfo?.version === '11') + let debianVersion = distroInfo?.version; + if (distroInfo.id === 'devuan') { + // Devuan is debian-based but it's always 7 versions behind + debianVersion = String(parseInt(distroInfo.version, 10) + 7); + } + if (debianVersion === '11') return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - if (distroInfo?.version === '12') + if (debianVersion === '12') return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; // use most recent supported release for 'debian testing' and 'unstable'. // they never include a numeric version entry in /etc/os-release. - if (distroInfo?.version === '') + if (debianVersion === '') return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; } return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; From 6ba5ee3a834a0ba18f5596650bc03d30d94dc468 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 7 Oct 2024 15:42:12 -0700 Subject: [PATCH 5/7] chore(ui-mode): expand all button (#32994) image Reference https://github.com/microsoft/playwright/issues/32825 --- .../src/ui/uiModeTestListView.tsx | 16 ++++++++++-- packages/trace-viewer/src/ui/uiModeView.tsx | 5 ++++ .../playwright-test/ui-mode-test-tree.spec.ts | 26 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index 99a5f22dfa..ce1c0fef37 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -47,12 +47,14 @@ export const TestListView: React.FC<{ isLoading?: boolean, onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void, requestedCollapseAllCount: number, + requestedExpandAllCount: number, setFilterText: (text: string) => void, onRevealSource: () => void, -}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText, onRevealSource }) => { +}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, requestedExpandAllCount, setFilterText, onRevealSource }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount); + const [expandAllCount, setExpandAllCount] = React.useState(requestedExpandAllCount); // Look for a first failure within the run batch to select it. React.useEffect(() => { @@ -67,6 +69,16 @@ export const TestListView: React.FC<{ return; } + if (expandAllCount !== requestedExpandAllCount) { + treeState.expandedItems.clear(); + for (const item of testTree.flatTreeItems()) + treeState.expandedItems.set(item.id, true); + setExpandAllCount(requestedExpandAllCount); + setSelectedTreeItemId(undefined); + setTreeState({ ...treeState }); + return; + } + if (!runningState || runningState.itemSelectedByUser) return; let selectedTreeItem: TreeItem | undefined; @@ -85,7 +97,7 @@ export const TestListView: React.FC<{ if (selectedTreeItem) setSelectedTreeItemId(selectedTreeItem.id); - }, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, treeState, setTreeState]); + }, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, expandAllCount, setExpandAllCount, requestedExpandAllCount, treeState, setTreeState]); // Compute selected item. const { selectedTreeItem } = React.useMemo(() => { diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index a5f1af7d99..69a5988641 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -90,6 +90,7 @@ export const UIModeView: React.FC<{}> = ({ const commandQueue = React.useRef(Promise.resolve()); const runTestBacklog = React.useRef>(new Set()); const [collapseAllCount, setCollapseAllCount] = React.useState(0); + const [expandAllCount, setExpandAllCount] = React.useState(0); const [isDisconnected, setIsDisconnected] = React.useState(false); const [hasBrowsers, setHasBrowsers] = React.useState(true); const [testServerConnection, setTestServerConnection] = React.useState(); @@ -473,6 +474,9 @@ export const UIModeView: React.FC<{}> = ({ { setCollapseAllCount(collapseAllCount + 1); }} /> + { + setExpandAllCount(expandAllCount + 1); + }} /> = ({ setWatchedTreeIds={setWatchedTreeIds} isLoading={isLoading} requestedCollapseAllCount={collapseAllCount} + requestedExpandAllCount={expandAllCount} setFilterText={setFilterText} onRevealSource={onRevealSource} /> diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index 7346c204e4..2c19ea0a87 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -297,6 +297,32 @@ test('should collapse all', async ({ runUITest }) => { `); }); +test('should expand all', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32825' } +}, async ({ runUITest }) => { + const { page } = await runUITest(basicTestTree); + + await page.getByTestId('test-tree').getByText('suite').click(); + await page.getByTitle('Collapse all').click(); + await expect.poll(dumpTestTree(page)).toContain(` + ► ◯ a.test.ts + ► ◯ b.test.ts + `); + + await page.getByTitle('Expand all').click(); + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ▼ ◯ suite + ◯ inner passes + ◯ inner fails + ▼ ◯ b.test.ts + ◯ passes + ◯ fails + `); +}); + test('should resolve title conflicts', async ({ runUITest }) => { const { page } = await runUITest({ 'a.test.ts': ` From 7047c3a6c6fdd5738a979ad32f0995ff70196b15 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 7 Oct 2024 17:12:36 -0700 Subject: [PATCH 6/7] fix(codegen): do not codegen non-existing fixtures (#32993) Closes https://github.com/microsoft/playwright/issues/32981 --- .../playwright-core/src/server/codegen/javascript.ts | 10 +++++++--- tests/library/inspector/cli-codegen-test.spec.ts | 10 ++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index 17f627b601..558670cd47 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -138,7 +138,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { generateTestHeader(options: LanguageGeneratorOptions): string { const formatter = new JavaScriptFormatter(); - const useText = formatContextOptions(options.contextOptions, options.deviceName); + const useText = formatContextOptions(options.contextOptions, options.deviceName, this._isTest); formatter.add(` import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test'; ${useText ? '\ntest.use(' + useText + ');\n' : ''} @@ -157,7 +157,7 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''} (async () => { const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)}); - const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`); + const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`); return formatter.format(); } @@ -199,8 +199,12 @@ function formatObjectOrVoid(value: any, indent = ' '): string { return result === '{}' ? '' : result; } -function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string { +function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, isTest: boolean): string { const device = deviceName && deviceDescriptors[deviceName]; + if (isTest) { + // No recordHAR fixture in test. + options = { ...options, recordHar: undefined }; + } if (!device) return formatObjectOrVoid(options); // Filter out all the properties from the device descriptor. diff --git a/tests/library/inspector/cli-codegen-test.spec.ts b/tests/library/inspector/cli-codegen-test.spec.ts index a5d5cb6fce..18eba82634 100644 --- a/tests/library/inspector/cli-codegen-test.spec.ts +++ b/tests/library/inspector/cli-codegen-test.spec.ts @@ -85,13 +85,11 @@ test('test', async ({ page }) => {`; await cli.waitFor(expectedResult); }); -test('should work with --save-har', async ({ runCLI }, testInfo) => { +test('should not generate recordHAR with --save-har', async ({ runCLI }, testInfo) => { const harFileName = testInfo.outputPath('har.har'); - const expectedResult = ` - recordHar: { - mode: 'minimal', - path: '${harFileName.replace(/\\/g, '\\\\')}' - }`; + const expectedResult = `test.use({ + serviceWorkers: 'block' +});`; const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`], { autoExitWhen: expectedResult, }); From 04bf425268fb442f8861c4b897bdeb1f8f62c9fc Mon Sep 17 00:00:00 2001 From: Mark <1031496+mrkstwrt@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:14:46 +0100 Subject: [PATCH 7/7] feat(base-reporter): Add tags to test output (#32930) --- packages/playwright/src/reporters/base.ts | 3 ++- tests/playwright-test/reporter-base.spec.ts | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 6776ce606f..4249429a36 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -410,7 +410,8 @@ export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestS else location = `${relativeTestPath(config, test)}:${step?.location?.line ?? test.location.line}:${step?.location?.column ?? test.location.column}`; const projectTitle = projectName ? `[${projectName}] › ` : ''; - return `${projectTitle}${location} › ${titles.join(' › ')}${stepSuffix(step)}`; + const tags = test.tags.length > 0 ? ` ${test.tags.join(' ')}` : ''; + return `${projectTitle}${location} › ${titles.join(' › ')}${stepSuffix(step)}${tags}`; } function formatTestHeader(config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string { diff --git a/tests/playwright-test/reporter-base.spec.ts b/tests/playwright-test/reporter-base.spec.ts index 6d6e499605..85ba731bf5 100644 --- a/tests/playwright-test/reporter-base.spec.ts +++ b/tests/playwright-test/reporter-base.spec.ts @@ -418,5 +418,19 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(result.passed).toBe(1); expect(result.output).toMatch(/\d+ passed \(\d+(\.\d)?(ms|s)\)/); }); + + test('should output tags', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo', '@bar'] }, async ({}) => { + expect(0).toBe(0); + }); + `, + }); + const text = result.output; + + expect(text).toContain('passes @foo @bar'); + }); }); -} \ No newline at end of file +}