diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 85264a49dff62..464bfdd8eee77 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -301,13 +301,12 @@ class NetworkRequest { const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel); credentials = proxy ? {username: proxy.username, password: proxy.password} : null; } else { - credentials = pageNetwork._target.browserContext().httpCredentials; + const origin = (aChannel.URI.scheme + '://' + aChannel.URI.hostPort).toLowerCase(); + const httpCredentials = pageNetwork._target.browserContext().httpCredentials || []; + credentials = httpCredentials.find(c => !c.origin || c.origin.toLowerCase() === origin) || null; } if (!credentials) return false; - const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort; - if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase()) - return false; authInfo.username = credentials.username; authInfo.password = credentials.password; // This will produce a new request with respective auth header set. diff --git a/browser_patches/firefox/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js index db4f203fbaebc..f6a3f89f81ee2 100644 --- a/browser_patches/firefox/juggler/protocol/Protocol.js +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -273,7 +273,7 @@ const Browser = { 'setHTTPCredentials': { params: { browserContextId: t.Optional(t.String), - credentials: t.Nullable(networkTypes.HTTPCredentials), + credentials: t.Nullable(t.Array(networkTypes.HTTPCredentials)), }, }, 'setRequestInterception': { diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index 169ba7fae0192..26bfa4803d384 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -107,12 +107,23 @@ export default defineConfig({ }); ``` +You can omit test tags that are automatically appended to test titles: + +```js title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + reporter: [['list', { omitTags: true }]], +}); +``` + List report supports the following configuration options and environment variables: | Environment Variable Name | Reporter Config Option| Description | Default |---|---|---|---| | `PLAYWRIGHT_LIST_PRINT_STEPS` | `printSteps` | Whether to print each step on its own line. | `false` | `PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE` | `printFailuresInline` | Whether to print failure details immediately after a failed test instead of at the end. | `false` +| `PLAYWRIGHT_LIST_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false` | `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise. | `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. | `NO_COLOR` | | Whether to disable colored output ([no-color.org](https://no-color.org/)). Any non-empty value disables colors. | unset @@ -152,6 +163,7 @@ Line report supports the following configuration options and environment variabl | Environment Variable Name | Reporter Config Option| Description | Default |---|---|---|---| +| `PLAYWRIGHT_LINE_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false` | `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise. | `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. | `NO_COLOR` | | Whether to disable colored output ([no-color.org](https://no-color.org/)). Any non-empty value disables colors. | unset @@ -195,6 +207,7 @@ Dot report supports the following configuration options and environment variable | Environment Variable Name | Reporter Config Option| Description | Default |---|---|---|---| +| `PLAYWRIGHT_DOT_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false` | `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise. | `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise. | `NO_COLOR` | | Whether to disable colored output ([no-color.org](https://no-color.org/)). Any non-empty value disables colors. | unset @@ -414,6 +427,7 @@ JUnit report supports following configuration options and environment variables: | `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout. | `PLAYWRIGHT_JUNIT_STRIP_ANSI` | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is. | `PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME` | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included. +| `PLAYWRIGHT_JUNIT_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to failure details. | `false` | `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `` report entry. | Empty string. | `PLAYWRIGHT_JUNIT_SUITE_NAME` | | Value of the `name` attribute on the root `` report entry. | Empty string. @@ -435,6 +449,8 @@ export default defineConfig({ }); ``` +The `github` reporter accepts `omitTags` (or the `PLAYWRIGHT_GITHUB_OMIT_TAGS` environment variable) to suppress test tags in its annotations, for example `reporter: [['github', { omitTags: true }]]`. + ## Custom reporters You can create a custom reporter by implementing a class with some of the reporter methods. Learn more about the [Reporter] API. diff --git a/package-lock.json b/package-lock.json index 7990ae88323c2..6c2121a1261b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5625,9 +5625,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 25e42eae2b73a..6383abb91ccaf 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -31,7 +31,7 @@ }, { "name": "firefox", - "revision": "1538", + "revision": "1539", "installByDefault": true, "browserVersion": "153.0", "title": "Firefox" @@ -45,7 +45,7 @@ }, { "name": "webkit", - "revision": "2336", + "revision": "2337", "installByDefault": true, "revisionOverrides": { "mac14": "2251", diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index fa1e1b65768b7..c02dad2086a79 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -322,7 +322,7 @@ export class FFBrowserContext extends BrowserContext { let credentials = null; if (httpCredentials) { const { username, password, origin } = httpCredentials; - credentials = { username, password, origin }; + credentials = [{ username, password, origin }]; } await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials }); } diff --git a/packages/playwright-core/src/server/firefox/protocol.d.ts b/packages/playwright-core/src/server/firefox/protocol.d.ts index e55a72ce404b7..18da296d70272 100644 --- a/packages/playwright-core/src/server/firefox/protocol.d.ts +++ b/packages/playwright-core/src/server/firefox/protocol.d.ts @@ -136,7 +136,7 @@ export namespace Protocol { username: string; password: string; origin?: string; - }|null; + }[]|null; }; export type setHTTPCredentialsReturnValue = void; export type setRequestInterceptionParameters = { diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index cde57e4dc9b64..7a099c55b1a6a 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -4373,6 +4373,17 @@ might return multiple quads for inline nodes. } export namespace Emulation { + /** + * Credentials for HTTP authentication. + */ + export interface AuthCredentials { + username: string; + password: string; + /** + * When specified, the credentials are only used for challenges from the matching origin. + */ + origin?: string; + } /** @@ -4395,12 +4406,10 @@ might return multiple quads for inline nodes. export type setJavaScriptEnabledReturnValue = { } /** - * Credentials to use during HTTP authentication. + * Credentials to use during HTTP authentication. The first credentials with matching origin are used. When missing, automation handling of authentication challenges is disabled. */ export type setAuthCredentialsParameters = { - username?: string; - password?: string; - origin?: string; + credentials?: AuthCredentials[]; } export type setAuthCredentialsReturnValue = { } diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index a53085d50458a..0ef1768254d64 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -753,7 +753,7 @@ export class WKPage implements PageDelegate { async updateHttpCredentials() { const credentials = this._browserContext._options.httpCredentials || { username: '', password: '', origin: '' }; - await this._pageProxySession.send('Emulation.setAuthCredentials', { username: credentials.username, password: credentials.password, origin: credentials.origin }); + await this._pageProxySession.send('Emulation.setAuthCredentials', { credentials: [{ username: credentials.username, password: credentials.password, origin: credentials.origin }] }); } async updateFileChooserInterception() { diff --git a/packages/playwright-core/src/tools/backend/cookies.ts b/packages/playwright-core/src/tools/backend/cookies.ts index 1c52428d79ed0..fbd80f6797070 100644 --- a/packages/playwright-core/src/tools/backend/cookies.ts +++ b/packages/playwright-core/src/tools/backend/cookies.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTool } from './tool'; const cookieList = defineTool({ @@ -137,7 +138,7 @@ const cookieDelete = defineTool({ handle: async (context, params, response) => { const browserContext = await context.ensureBrowserContext(); await browserContext.clearCookies({ name: params.name }); - response.addCode(`await page.context().clearCookies({ name: '${params.name}' });`); + response.addCode(`await page.context().clearCookies({ name: ${escapeWithQuotes(params.name)} });`); }, }); diff --git a/packages/playwright-core/src/tools/backend/keyboard.ts b/packages/playwright-core/src/tools/backend/keyboard.ts index da2554cf73489..34de28a584deb 100644 --- a/packages/playwright-core/src/tools/backend/keyboard.ts +++ b/packages/playwright-core/src/tools/backend/keyboard.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTabTool } from './tool'; import { elementSchema } from './snapshot'; @@ -33,7 +34,7 @@ const press = defineTabTool({ handle: async (tab, params, response) => { response.addCode(`// Press ${params.key}`); - response.addCode(`await page.keyboard.press('${params.key}');`); + response.addCode(`await page.keyboard.press(${escapeWithQuotes(params.key)});`); if (params.key === 'Enter') { response.setIncludeSnapshot(); await tab.waitForCompletion(async () => { @@ -62,7 +63,7 @@ const pressSequentially = defineTabTool({ handle: async (tab, params, response) => { response.addCode(`// Press ${params.text}`); - response.addCode(`await page.keyboard.type('${params.text}');`); + response.addCode(`await page.keyboard.type(${escapeWithQuotes(params.text)});`); await tab.page.keyboard.type(params.text); if (params.submit) { response.addCode(`await page.keyboard.press('Enter');`); @@ -133,7 +134,7 @@ const keydown = defineTabTool({ }, handle: async (tab, params, response) => { - response.addCode(`await page.keyboard.down('${params.key}');`); + response.addCode(`await page.keyboard.down(${escapeWithQuotes(params.key)});`); await tab.page.keyboard.down(params.key); }, }); @@ -153,7 +154,7 @@ const keyup = defineTabTool({ }, handle: async (tab, params, response) => { - response.addCode(`await page.keyboard.up('${params.key}');`); + response.addCode(`await page.keyboard.up(${escapeWithQuotes(params.key)});`); await tab.page.keyboard.up(params.key); }, }); diff --git a/packages/playwright-core/src/tools/backend/navigate.ts b/packages/playwright-core/src/tools/backend/navigate.ts index 460642cd92068..b3de47e83d991 100644 --- a/packages/playwright-core/src/tools/backend/navigate.ts +++ b/packages/playwright-core/src/tools/backend/navigate.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTool, defineTabTool } from './tool'; const navigate = defineTool({ @@ -35,7 +36,7 @@ const navigate = defineTool({ const url = await tab.checkUrlAndNavigate(params.url); response.setIncludeSnapshot(); - response.addCode(`await page.goto('${url}');`); + response.addCode(`await page.goto(${escapeWithQuotes(url)});`); }, }); diff --git a/packages/playwright-core/src/tools/backend/route.ts b/packages/playwright-core/src/tools/backend/route.ts index 0080b4a9ec762..71a3ae014f93d 100644 --- a/packages/playwright-core/src/tools/backend/route.ts +++ b/packages/playwright-core/src/tools/backend/route.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTool } from './tool'; import type * as playwright from '../../..'; @@ -81,7 +82,7 @@ const route = defineTool({ await context.addRoute(entry); response.addTextResult(`Route added for pattern: ${params.pattern}`); - response.addCode(`await page.context().route('${params.pattern}', async route => { /* route handler */ });`); + response.addCode(`await page.context().route(${escapeWithQuotes(params.pattern)}, async route => { /* route handler */ });`); }, }); diff --git a/packages/playwright-core/src/tools/backend/storage.ts b/packages/playwright-core/src/tools/backend/storage.ts index 62bb898e8cb4e..0308496cc8bc1 100644 --- a/packages/playwright-core/src/tools/backend/storage.ts +++ b/packages/playwright-core/src/tools/backend/storage.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTool } from './tool'; const storageState = defineTool({ @@ -35,7 +36,7 @@ const storageState = defineTool({ const state = await browserContext.storageState(); const serializedState = JSON.stringify(state, null, 2); const resolvedFile = await response.resolveClientFile({ prefix: 'storage-state', ext: 'json', suggestedFilename: params.filename }, 'Storage state'); - response.addCode(`await page.context().storageState({ path: '${resolvedFile.relativeName}' });`); + response.addCode(`await page.context().storageState({ path: ${escapeWithQuotes(resolvedFile.relativeName)} });`); await response.addFileResult(resolvedFile, serializedState); }, }); @@ -58,7 +59,7 @@ const setStorageState = defineTool({ const resolvedFilename = await response.resolveClientFilename(params.filename); await browserContext.setStorageState(resolvedFilename); response.addTextResult(`Storage state restored from ${params.filename}`); - response.addCode(`await page.context().setStorageState('${params.filename}');`); + response.addCode(`await page.context().setStorageState(${escapeWithQuotes(params.filename)});`); }, }); diff --git a/packages/playwright-core/src/tools/backend/tabs.ts b/packages/playwright-core/src/tools/backend/tabs.ts index bd4a53e40fb91..8e97521d3f46b 100644 --- a/packages/playwright-core/src/tools/backend/tabs.ts +++ b/packages/playwright-core/src/tools/backend/tabs.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTool } from './tool'; import { renderTabsMarkdown } from './response'; @@ -44,7 +45,7 @@ const browserTabs = defineTool({ if (params.url) { const url = await tab.checkUrlAndNavigate(params.url); response.setIncludeSnapshot(); - response.addCode(`await page.goto('${url}');`); + response.addCode(`await page.goto(${escapeWithQuotes(url)});`); } break; } diff --git a/packages/playwright-core/src/tools/backend/webstorage.ts b/packages/playwright-core/src/tools/backend/webstorage.ts index 34003079b4793..00b01caecdd25 100644 --- a/packages/playwright-core/src/tools/backend/webstorage.ts +++ b/packages/playwright-core/src/tools/backend/webstorage.ts @@ -15,6 +15,7 @@ */ import * as z from 'zod'; +import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { defineTabTool } from './tool'; const localStorageList = defineTabTool({ @@ -59,7 +60,7 @@ const localStorageGet = defineTabTool({ response.addTextResult(`localStorage key '${params.key}' not found`); else response.addTextResult(`${params.key}=${value}`); - response.addCode(`await page.localStorage.getItem('${params.key}');`); + response.addCode(`await page.localStorage.getItem(${escapeWithQuotes(params.key)});`); }, }); @@ -79,7 +80,7 @@ const localStorageSet = defineTabTool({ handle: async (tab, params, response) => { await tab.page.localStorage.setItem(params.key, params.value); - response.addCode(`await page.localStorage.setItem('${params.key}', '${params.value}');`); + response.addCode(`await page.localStorage.setItem(${escapeWithQuotes(params.key)}, ${escapeWithQuotes(params.value)});`); }, }); @@ -98,7 +99,7 @@ const localStorageDelete = defineTabTool({ handle: async (tab, params, response) => { await tab.page.localStorage.removeItem(params.key); - response.addCode(`await page.localStorage.removeItem('${params.key}');`); + response.addCode(`await page.localStorage.removeItem(${escapeWithQuotes(params.key)});`); }, }); @@ -163,7 +164,7 @@ const sessionStorageGet = defineTabTool({ response.addTextResult(`sessionStorage key '${params.key}' not found`); else response.addTextResult(`${params.key}=${value}`); - response.addCode(`await page.sessionStorage.getItem('${params.key}');`); + response.addCode(`await page.sessionStorage.getItem(${escapeWithQuotes(params.key)});`); }, }); @@ -183,7 +184,7 @@ const sessionStorageSet = defineTabTool({ handle: async (tab, params, response) => { await tab.page.sessionStorage.setItem(params.key, params.value); - response.addCode(`await page.sessionStorage.setItem('${params.key}', '${params.value}');`); + response.addCode(`await page.sessionStorage.setItem(${escapeWithQuotes(params.key)}, ${escapeWithQuotes(params.value)});`); }, }); @@ -202,7 +203,7 @@ const sessionStorageDelete = defineTabTool({ handle: async (tab, params, response) => { await tab.page.sessionStorage.removeItem(params.key); - response.addCode(`await page.sessionStorage.removeItem('${params.key}');`); + response.addCode(`await page.sessionStorage.removeItem(${escapeWithQuotes(params.key)});`); }, }); diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 2cfd7ac60d2b3..83536e46341e8 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -163,6 +163,7 @@ export type TerminalReporterOptions = { screen?: TerminalScreen; omitFailures?: boolean; includeTestId?: boolean; + omitTags?: boolean; }; export class TerminalReporter implements ReporterV2 { @@ -366,7 +367,7 @@ export class TerminalReporter implements ReporterV2 { } formatTestHeader(test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string { - return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId }); + return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId, omitTags: this._options.omitTags }); } formatFailure(test: TestCase, index?: number): string { @@ -407,7 +408,7 @@ export function formatFailure(screen: Screen, config: FullConfig, test: TestCase if (!errors.length) continue; if (!printedHeader) { - const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error', includeTestId: options?.includeTestId }); + const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error', includeTestId: options?.includeTestId, omitTags: options?.omitTags }); lines.push(screen.colors.red(header)); printedHeader = true; } @@ -539,7 +540,7 @@ export function stepSuffix(step: TestStep | undefined) { return stepTitles.map(t => t.split('\n')[0]).map(t => ' › ' + t).join(''); } -function formatTestTitle(screen: Screen, config: FullConfig, test: TestCase, step?: TestStep, options: { includeTestId?: boolean } = {}): string { +function formatTestTitle(screen: Screen, config: FullConfig, test: TestCase, step?: TestStep, options: { includeTestId?: boolean, omitTags?: boolean } = {}): string { // root, project, file, ...describes, test const [, projectName, , ...titles] = test.titlePath(); const location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`; @@ -547,11 +548,11 @@ function formatTestTitle(screen: Screen, config: FullConfig, test: TestCase, ste const projectLabel = options.includeTestId ? `project=` : ''; const projectTitle = projectName ? `[${projectLabel}${projectName}] › ` : ''; const testTitle = `${testId}${projectTitle}${location} › ${titles.join(' › ')}`; - const extraTags = test.tags.filter(t => !testTitle.includes(t) && !config.tags.includes(t)); + const extraTags = options.omitTags ? [] : test.tags.filter(t => !testTitle.includes(t) && !config.tags.includes(t)); return `${testTitle}${stepSuffix(step)}${extraTags.length ? ' ' + extraTags.join(' ') : ''}`; } -function formatTestHeader(screen: Screen, config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error', includeTestId?: boolean } = {}): string { +function formatTestHeader(screen: Screen, config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error', includeTestId?: boolean, omitTags?: boolean } = {}): string { const title = formatTestTitle(screen, config, test, undefined, options); const header = `${options.indent || ''}${options.index ? options.index + ') ' : ''}${title}`; let fullHeader = header; diff --git a/packages/playwright/src/reporters/dot.ts b/packages/playwright/src/reporters/dot.ts index 723ee6ad93159..dd3812b44a8ae 100644 --- a/packages/playwright/src/reporters/dot.ts +++ b/packages/playwright/src/reporters/dot.ts @@ -14,13 +14,21 @@ * limitations under the License. */ +import { getAsBooleanFromENV } from '@utils/env'; + import { markErrorsAsReported, TerminalReporter } from './base'; +import type { DotReporterOptions } from '../../types/test'; import type { FullResult, Suite, TestCase, TestError, TestResult } from '../../types/testReporter'; +import type { CommonReporterOptions, TerminalReporterOptions } from './base'; class DotReporter extends TerminalReporter { private _counter = 0; + constructor(options?: DotReporterOptions & CommonReporterOptions & TerminalReporterOptions) { + super({ ...options, omitTags: getAsBooleanFromENV('PLAYWRIGHT_DOT_OMIT_TAGS', options?.omitTags) }); + } + override onBegin(suite: Suite) { super.onBegin(suite); this.writeLine(this.generateStartingMessage()); diff --git a/packages/playwright/src/reporters/github.ts b/packages/playwright/src/reporters/github.ts index 6975adb1dd098..e21fae59e6d06 100644 --- a/packages/playwright/src/reporters/github.ts +++ b/packages/playwright/src/reporters/github.ts @@ -18,10 +18,12 @@ import path from 'path'; import { noColors } from '@isomorphic/colors'; import { msToString } from '@isomorphic/formatUtils'; +import { getAsBooleanFromENV } from '@utils/env'; import { TerminalReporter, formatResultFailure, formatRetry } from './base'; import { stripAnsiEscapes } from '../util'; +import type { TerminalReporterOptions } from './base'; import type { FullResult, TestCase, TestError, TestResult } from '../../types/testReporter'; type GitHubLogType = 'debug' | 'notice' | 'warning' | 'error'; @@ -71,8 +73,8 @@ export class GitHubReporter extends TerminalReporter { githubLogger = new GitHubLogger(); private _failedTestCount = 0; - constructor(options: { omitFailures?: boolean } = {}) { - super(options); + constructor(options: TerminalReporterOptions = {}) { + super({ ...options, omitTags: getAsBooleanFromENV('PLAYWRIGHT_GITHUB_OMIT_TAGS', options.omitTags) }); this.screen = { ...this.screen, colors: noColors }; } diff --git a/packages/playwright/src/reporters/junit.ts b/packages/playwright/src/reporters/junit.ts index c5a516274eda0..82df28228344b 100644 --- a/packages/playwright/src/reporters/junit.ts +++ b/packages/playwright/src/reporters/junit.ts @@ -39,11 +39,13 @@ class JUnitReporter implements ReporterV2 { private stripANSIControlSequences = false; private includeProjectInTestName = false; private includeRetries = false; + private omitTags = false; constructor(options: JUnitReporterOptions & CommonReporterOptions) { this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences); this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName); this.includeRetries = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_RETRIES', !!options.includeRetries); + this.omitTags = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_OMIT_TAGS', !!options.omitTags); this.configDir = options.configDir; this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile; } @@ -222,7 +224,7 @@ class JUnitReporter implements ReporterV2 { entry.children!.push({ name: errorInfo.elementName, attributes: { message: errorInfo.message, type: errorInfo.type }, - text: stripAnsiEscapes(formatFailure(nonTerminalScreen, this.config, test)) + text: stripAnsiEscapes(formatFailure(nonTerminalScreen, this.config, test, undefined, { omitTags: this.omitTags })) }); return errorInfo.elementName; } @@ -232,7 +234,7 @@ class JUnitReporter implements ReporterV2 { message: `${path.basename(test.location.file)}:${test.location.line}:${test.location.column} ${test.title}`, type: 'FAILURE', }, - text: stripAnsiEscapes(formatFailure(nonTerminalScreen, this.config, test)) + text: stripAnsiEscapes(formatFailure(nonTerminalScreen, this.config, test, undefined, { omitTags: this.omitTags })) }); return 'failure'; } diff --git a/packages/playwright/src/reporters/line.ts b/packages/playwright/src/reporters/line.ts index 81f408f86a44f..c703d3cc01e18 100644 --- a/packages/playwright/src/reporters/line.ts +++ b/packages/playwright/src/reporters/line.ts @@ -14,9 +14,13 @@ * limitations under the License. */ +import { getAsBooleanFromENV } from '@utils/env'; + import { markErrorsAsReported, TerminalReporter } from './base'; +import type { LineReporterOptions } from '../../types/test'; import type { FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter'; +import type { CommonReporterOptions, TerminalReporterOptions } from './base'; class LineReporter extends TerminalReporter { private _current = 0; @@ -24,6 +28,10 @@ class LineReporter extends TerminalReporter { private _lastTest: TestCase | undefined; private _didBegin = false; + constructor(options?: LineReporterOptions & CommonReporterOptions & TerminalReporterOptions) { + super({ ...options, omitTags: getAsBooleanFromENV('PLAYWRIGHT_LINE_OMIT_TAGS', options?.omitTags) }); + } + override onBegin(suite: Suite) { super.onBegin(suite); const startingMessage = this.generateStartingMessage(); diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index 58dff8deeca26..eb128a33dbd24 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -43,7 +43,7 @@ class ListReporter extends TerminalReporter { private _paused = new Set(); constructor(options?: ListReporterOptions & CommonReporterOptions & TerminalReporterOptions) { - super(options); + super({ ...options, omitTags: getAsBooleanFromENV('PLAYWRIGHT_LIST_OMIT_TAGS', options?.omitTags) }); this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options?.printSteps); this._printFailuresInline = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE', options?.printFailuresInline); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 7a1ec114f0b33..ef7d2b1f49582 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -19,8 +19,11 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, export * from 'playwright-core'; export type BlobReporterOptions = { outputDir?: string, fileName?: string }; -export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean }; -export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean }; +export type DotReporterOptions = { omitTags?: boolean }; +export type LineReporterOptions = { omitTags?: boolean }; +export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean, omitTags?: boolean }; +export type GitHubReporterOptions = { omitTags?: boolean }; +export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean, omitTags?: boolean }; export type JsonReporterOptions = { outputFile?: string }; export type HtmlReporterOptions = { outputFolder?: string; @@ -37,10 +40,10 @@ export type HtmlReporterOptions = { export type ReporterDescription = Readonly< ['blob'] | ['blob', BlobReporterOptions] | - ['dot'] | - ['line'] | + ['dot'] | ['dot', DotReporterOptions] | + ['line'] | ['line', LineReporterOptions] | ['list'] | ['list', ListReporterOptions] | - ['github'] | + ['github'] | ['github', GitHubReporterOptions] | ['junit'] | ['junit', JUnitReporterOptions] | ['json'] | ['json', JsonReporterOptions] | ['html'] | ['html', HtmlReporterOptions] | diff --git a/tests/mcp/cli-core.spec.ts b/tests/mcp/cli-core.spec.ts index 575b182cade57..4b3cd8cecd81e 100644 --- a/tests/mcp/cli-core.spec.ts +++ b/tests/mcp/cli-core.spec.ts @@ -368,3 +368,10 @@ test('--raw on command without output', async ({ cli, server }) => { expect(output).not.toContain('### '); expect(output).not.toContain('Page URL'); }); + +test('codegen escapes single quotes in user input', async ({ cli, server }) => { + server.setContent('/', ``, 'text/html'); + await cli('open', server.PREFIX); + const { output } = await cli('type', "it's working"); + expect(output).toContain(`await page.keyboard.type('it\\'s working');`); +}); diff --git a/tests/playwright-test/reporter-base.spec.ts b/tests/playwright-test/reporter-base.spec.ts index afd5aa0543a9a..fb8b8176b7133 100644 --- a/tests/playwright-test/reporter-base.spec.ts +++ b/tests/playwright-test/reporter-base.spec.ts @@ -486,5 +486,106 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(text).toContain('› passes @bar1 @bar2 ('); expect(text).toContain('› passes @baz1 @baz2 ('); }); + + test('should omit tags when omitTags is set', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + test('passes @bar1 @bar2', async ({}) => { + expect(0).toBe(0); + }); + test('passes @baz1', { tag: ['@baz2'] }, async ({}) => { + expect(0).toBe(0); + }); + `, + }); + const text = stripAnsi(result.output); + // Tags appended from the `tag` annotation are omitted. + expect(text).toContain('› passes ('); + expect(text).not.toContain('@foo1'); + expect(text).not.toContain('@foo2'); + expect(text).not.toContain('@baz2'); + // Tags authored directly in the title are preserved. + expect(text).toContain('› passes @bar1 @bar2 ('); + expect(text).toContain('› passes @baz1 ('); + expect(result.exitCode).toBe(0); + }); + + test('should omit tags from failures when omitTags is set', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('fails', { tag: ['@qux1'] }, async ({}) => { + expect(1).toBe(2); + }); + `, + }); + const text = stripAnsi(result.output); + const titleLines = text.split('\n').filter(line => line.includes('a.test.ts') && line.includes('› fails')); + expect(titleLines.length).toBeGreaterThan(0); + for (const line of titleLines) + expect(line).not.toContain('@qux1'); + expect(result.exitCode).toBe(1); + }); + + test('should omit tags via the PLAYWRIGHT_LIST_OMIT_TAGS environment variable', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list']], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + test('passes @bar1 @bar2', async ({}) => { + expect(0).toBe(0); + }); + `, + }, undefined, { PLAYWRIGHT_LIST_OMIT_TAGS: '1' }); + const text = stripAnsi(result.output); + // Tags appended from the `tag` annotation are omitted. + expect(text).toContain('› passes ('); + expect(text).not.toContain('@foo1'); + expect(text).not.toContain('@foo2'); + // Tags authored directly in the title are preserved. + expect(text).toContain('› passes @bar1 @bar2 ('); + expect(result.exitCode).toBe(0); + }); + + test('should let PLAYWRIGHT_LIST_OMIT_TAGS override the omitTags option', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + `, + }, undefined, { PLAYWRIGHT_LIST_OMIT_TAGS: '0' }); + const text = stripAnsi(result.output); + // The environment variable disables omitTags, so the appended tags are shown. + expect(text).toContain('› passes @foo1 @foo2 ('); + expect(result.exitCode).toBe(0); + }); }); } diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index ee7f2e13246f6..41719262e7c1d 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -18,8 +18,11 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, export * from 'playwright-core'; export type BlobReporterOptions = { outputDir?: string, fileName?: string }; -export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean }; -export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean }; +export type DotReporterOptions = { omitTags?: boolean }; +export type LineReporterOptions = { omitTags?: boolean }; +export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean, omitTags?: boolean }; +export type GitHubReporterOptions = { omitTags?: boolean }; +export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean, omitTags?: boolean }; export type JsonReporterOptions = { outputFile?: string }; export type HtmlReporterOptions = { outputFolder?: string; @@ -36,10 +39,10 @@ export type HtmlReporterOptions = { export type ReporterDescription = Readonly< ['blob'] | ['blob', BlobReporterOptions] | - ['dot'] | - ['line'] | + ['dot'] | ['dot', DotReporterOptions] | + ['line'] | ['line', LineReporterOptions] | ['list'] | ['list', ListReporterOptions] | - ['github'] | + ['github'] | ['github', GitHubReporterOptions] | ['junit'] | ['junit', JUnitReporterOptions] | ['json'] | ['json', JsonReporterOptions] | ['html'] | ['html', HtmlReporterOptions] |