diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index f60e63a5a8181..57517fd5b5d54 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1850,6 +1850,12 @@ Specifies a custom location for the step to be shown in test reports and trace v Arbitrary serializable parameters describing the step. They are reported to the reporters as `testStep.params` and are shown in the trace viewer. +### option: Test.step.subtitle +* since: v1.64 +- `subtitle` <[string]> + +Step subtitle that complements the title, for example the target of the step. It is reported to the reporters as `testStep.subtitle` and is shown next to the title in test reports and the trace viewer. + ## async method: Test.step.skip * since: v1.50 - returns: <[void]> @@ -1903,6 +1909,12 @@ Specifies a custom location for the step to be shown in test reports and trace v Arbitrary serializable parameters describing the step. They are reported to the reporters as `testStep.params` and are shown in the trace viewer. +### option: Test.step.skip.subtitle +* since: v1.64 +- `subtitle` <[string]> + +Step subtitle that complements the title, for example the target of the step. It is reported to the reporters as `testStep.subtitle` and is shown next to the title in test reports and the trace viewer. + ### option: Test.step.skip.timeout * since: v1.50 - `timeout` <[float]> diff --git a/docs/src/test-reporter-api/class-teststep.md b/docs/src/test-reporter-api/class-teststep.md index 64df4d6989b2b..bcad398970749 100644 --- a/docs/src/test-reporter-api/class-teststep.md +++ b/docs/src/test-reporter-api/class-teststep.md @@ -112,8 +112,8 @@ User-friendly test step title, for example `Click` or `Navigate`. User-friendly test step subtitle that complements the title, when available. For Playwright API calls, it is the target locator or the navigation url. For example, a `Click` step has the clicked -locator as a subtitle. User interfaces typically render the subtitle next to the title or on a -separate line. +locator as a subtitle. [`method: Test.step`] steps carry the subtitle passed by the test author. +User interfaces typically render the subtitle next to the title or on a separate line. ```js // title `Click`, subtitle `getByRole('button')` @@ -121,6 +121,11 @@ await page.getByRole('button').click(); // title `Navigate`, subtitle `example.com/index.html` await page.goto('https://example.com/index.html'); + +// title `Add to cart`, subtitle `SKU 42` +await test.step('Add to cart', async () => { + // ... +}, { subtitle: 'SKU 42' }); ``` ## method: TestStep.titlePath diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 81eb262f6d5c7..43d028bdf93a6 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -274,12 +274,12 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number, params?: Record } = {}): Promise { + async _step(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number, params?: Record, subtitle?: string } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); await testInfo._onUserStepBegin?.(title); - const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box, params: options.params }); + const step = testInfo._addStep({ category: 'test.step', title, subtitle: options.subtitle, location: options.location, box: options.box, params: options.params }); return await currentZone().with('stepZone', step).run(async () => { try { let result: Awaited>> | undefined = undefined; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 040450342836f..907a6dae7d170 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6727,7 +6727,7 @@ export interface TestType { * @param body Step body. * @param options */ - (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any } }): Promise; + (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; /** * Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and * planned for a near-term fix. Playwright will not run the step. See also @@ -6755,7 +6755,7 @@ export interface TestType { * @param body Step body. * @param options */ - skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any } }): Promise; + skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; } /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index a3f5327d4725c..a7962739df19e 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -921,8 +921,9 @@ export interface TestStep { /** * User-friendly test step subtitle that complements the title, when available. For Playwright API calls, it is the - * target locator or the navigation url. For example, a `Click` step has the clicked locator as a subtitle. User - * interfaces typically render the subtitle next to the title or on a separate line. + * target locator or the navigation url. For example, a `Click` step has the clicked locator as a subtitle. + * [test.step(title, body[, options])](https://playwright.dev/docs/api/class-test#test-step) steps carry the subtitle + * passed by the test author. User interfaces typically render the subtitle next to the title or on a separate line. * * ```js * // title `Click`, subtitle `getByRole('button')` @@ -930,6 +931,11 @@ export interface TestStep { * * // title `Navigate`, subtitle `example.com/index.html` * await page.goto('https://example.com/index.html'); + * + * // title `Add to cart`, subtitle `SKU 42` + * await test.step('Add to cart', async () => { + * // ... + * }, { subtitle: 'SKU 42' }); * ``` * */ diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 354d022290374..093f61597ab9b 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -1553,3 +1553,19 @@ test('should record step params in trace', async ({ runInlineTest }, testInfo) = expect(actionByTitle('my step').params).toEqual({ foo: 'bar' }); expect(actionByTitle('Expect "toBe"').params).toEqual({ expected: '1' }); }); + +test('should record step subtitle in trace', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + await test.step('my step', async () => {}, { subtitle: 'my subtitle' }); + }); + `, + }, { trace: 'on' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + expect(trace.model.actions.find(a => a.title === 'my step')!.subtitle).toBe('my subtitle'); +}); diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 25a6c659aa4fd..d6899c520409b 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -1277,7 +1277,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => { import { test, expect } from '@playwright/test'; test('test 1', async ({ page }) => { await page.goto('about:blank'); - await test.step('my step', async () => {}, { params: { foo: 'bar', count: 7 } }); + await test.step('my step', async () => {}, { subtitle: 'my subtitle', params: { foo: 'bar', count: 7 } }); }); `, }; @@ -1286,7 +1286,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => { expect(exitCode).toBe(0); expect(outputLines).toEqual([ `Navigate about:blank | {"url":"about:blank"}`, - `my step | {"foo":"bar","count":7}`, + `my step my subtitle | {"foo":"bar","count":7}`, ]); }); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index b4e06167548a6..5a0b7f59c80fd 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -960,6 +960,24 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.step-subtitle .step-title-highlight')).toHaveText(['#target']); }); + test('should render test.step subtitle', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('has steps', async ({}) => { + await test.step('Add to cart', async () => {}, { subtitle: 'SKU 42' }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.getByRole('link', { name: 'has steps' }).click(); + await expect(page.locator('.step-title-container', { hasText: 'Add to cart' })).toHaveAttribute('aria-label', 'Add to cart SKU 42'); + await expect(page.locator('.step-subtitle')).toHaveText('SKU 42'); + }); + test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index ce57fb3b17bc4..af90f0215dbbf 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -1899,6 +1899,38 @@ test('should report step params', async ({ runInlineTest }) => { ]); }); +test('should report step subtitle', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter'; + export default class MyReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.category === 'test.step') + console.log('%%' + step.title + ' | ' + step.subtitle); + } + } + `, + 'playwright.config.ts': ` + module.exports = { reporter: './reporter' }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + await test.step('my step', async () => {}, { subtitle: 'my subtitle' }); + await test.step.skip('skipped step', async () => {}, { subtitle: 'skipped subtitle' }); + await test.step('plain step', async () => {}); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + `my step | my subtitle`, + `skipped step | skipped subtitle`, + `plain step | undefined`, + ]); +}); + test('should report input step params', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': ` diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index e7de7655c4c52..90a983fdcea29 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -197,8 +197,8 @@ export interface TestType { afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; step: { - (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any } }): Promise; - skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any } }): Promise; + (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; + skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; } expect: Expect<{}>; extend(fixtures: Fixtures): TestType;