diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 78cb947d0ecb0..6c3ac71d075dc 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1331,7 +1331,7 @@ Optional description that will be reflected in a test report. * since: v1.10 - returns: <[any]> -Declares a test step. +Declares a test step that is shown in the report. **Usage** @@ -1342,6 +1342,14 @@ test('test', async ({ page }) => { await test.step('Log in', async () => { // ... }); + + await test.step('Outer step', async () => { + // ... + // You can nest steps inside each other. + await test.step('Inner step', async () => { + // ... + }); + }); }); ``` @@ -1361,38 +1369,92 @@ test('test', async ({ page }) => { }); ``` -### param: Test.step.title -* since: v1.10 -- `title` <[string]> +**Decorator** -Step name. +You can use TypeScript method decorators to turn a method into a step. +Each call to the decorated method will show up as a step in the report. +```js +function step(target: Function, context: ClassMethodDecoratorContext) { + return function replacementMethod(...args: any) { + const name = this.constructor.name + '.' + (context.name as string); + return test.step(name, async () => { + return await target.call(this, ...args); + }); + }; +} -### param: Test.step.body -* since: v1.10 -- `body` <[function]\(\):[Promise]<[any]>> +class LoginPage { + constructor(readonly page: Page) {} -Step body. + @step + async login() { + const account = { username: 'Alice', password: 's3cr3t' }; + await this.page.getByLabel('Username or email address').fill(account.username); + await this.page.getByLabel('Password').fill(account.password); + await this.page.getByRole('button', { name: 'Sign in' }).click(); + await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + } +} -### option: Test.step.box -* since: v1.39 -- `box` +test('example', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.login(); +}); +``` + +**Boxing** -Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. +When something inside a step fails, you would usually see the error pointing to the exact action that failed. For example, consider the following login step: ```js -const assertGoodPage = async page => { - await test.step('assertGoodPage', async () => { - await expect(page.getByText('does-not-exist')).toBeVisible(); - }, { box: true }); -}; +async function login(page) { + await test.step('login', async () => { + const account = { username: 'Alice', password: 's3cr3t' }; + await page.getByLabel('Username or email address').fill(account.username); + await page.getByLabel('Password').fill(account.password); + await page.getByRole('button', { name: 'Sign in' }).click(); + await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + }); +} -test('box', async ({ page }) => { - await assertGoodPage(page); // <-- Errors will be reported on this line. +test('example', async ({ page }) => { + await page.goto('https://github.com/login'); + await login(page); }); ``` -You can also use TypeScript method decorators to annotate method as a boxed step: +```txt +Error: Timed out 5000ms waiting for expect(locator).toBeVisible() + ... error details omitted ... + + 8 | await page.getByRole('button', { name: 'Sign in' }).click(); +> 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + | ^ + 10 | }); +``` + +As we see above, the test may fail with an error pointing inside the step. If you would like the error to highlight the "login" step instead of its internals, use the `box` option. An error inside a boxed step points to the step call site. + +```js +async function login(page) { + await test.step('login', async () => { + // ... + }, { box: true }); // Note the "box" option here. +} +``` + +```txt +Error: Timed out 5000ms waiting for expect(locator).toBeVisible() + ... error details omitted ... + + 14 | await page.goto('https://github.com/login'); +> 15 | await login(page); + | ^ + 16 | }); +``` + +You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above: ```js function boxedStep(target: Function, context: ClassMethodDecoratorContext) { @@ -1400,25 +1462,44 @@ function boxedStep(target: Function, context: ClassMethodDecoratorContext) { const name = this.constructor.name + '.' + (context.name as string); return test.step(name, async () => { return await target.call(this, ...args); - }, { box: true }); + }, { box: true }); // Note the "box" option here. }; } -class Pom { +class LoginPage { constructor(readonly page: Page) {} @boxedStep - async assertGoodPage() { - await expect(this.page.getByText('does-not-exist')).toBeVisible({ timeout: 1 }); + async login() { + // .... } } -test('box', async ({ page }) => { - const pom = new Pom(page); - await pom.assertGoodPage(); // <-- Error will be reported on this line. +test('example', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.login(); // <-- Error will be reported on this line. }); ``` +### param: Test.step.title +* since: v1.10 +- `title` <[string]> + +Step name. + + +### param: Test.step.body +* since: v1.10 +- `body` <[function]\(\):[Promise]<[any]>> + +Step body. + +### option: Test.step.box +* since: v1.39 +- `box` + +Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details. + ## method: Test.use * since: v1.10 diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 7944d2349a67c..9fa455eb21cc5 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -3318,7 +3318,7 @@ export interface TestType): void; /** - * Declares a test step. + * Declares a test step that is shown in the report. * * **Usage** * @@ -3329,6 +3329,14 @@ export interface TestType { * // ... * }); + * + * await test.step('Outer step', async () => { + * // ... + * // You can nest steps inside each other. + * await test.step('Inner step', async () => { + * // ... + * }); + * }); * }); * ``` * @@ -3348,6 +3356,121 @@ export interface TestType { + * return await target.call(this, ...args); + * }); + * }; + * } + * + * class LoginPage { + * constructor(readonly page: Page) {} + * + * @step + * async login() { + * const account = { username: 'Alice', password: 's3cr3t' }; + * await this.page.getByLabel('Username or email address').fill(account.username); + * await this.page.getByLabel('Password').fill(account.password); + * await this.page.getByRole('button', { name: 'Sign in' }).click(); + * await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + * } + * } + * + * test('example', async ({ page }) => { + * const loginPage = new LoginPage(page); + * await loginPage.login(); + * }); + * ``` + * + * **Boxing** + * + * When something inside a step fails, you would usually see the error pointing to the exact action that failed. For + * example, consider the following login step: + * + * ```js + * async function login(page) { + * await test.step('login', async () => { + * const account = { username: 'Alice', password: 's3cr3t' }; + * await page.getByLabel('Username or email address').fill(account.username); + * await page.getByLabel('Password').fill(account.password); + * await page.getByRole('button', { name: 'Sign in' }).click(); + * await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + * }); + * } + * + * test('example', async ({ page }) => { + * await page.goto('https://github.com/login'); + * await login(page); + * }); + * ``` + * + * ```txt + * Error: Timed out 5000ms waiting for expect(locator).toBeVisible() + * ... error details omitted ... + * + * 8 | await page.getByRole('button', { name: 'Sign in' }).click(); + * > 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible(); + * | ^ + * 10 | }); + * ``` + * + * As we see above, the test may fail with an error pointing inside the step. If you would like the error to highlight + * the "login" step instead of its internals, use the `box` option. An error inside a boxed step points to the step + * call site. + * + * ```js + * async function login(page) { + * await test.step('login', async () => { + * // ... + * }, { box: true }); // Note the "box" option here. + * } + * ``` + * + * ```txt + * Error: Timed out 5000ms waiting for expect(locator).toBeVisible() + * ... error details omitted ... + * + * 14 | await page.goto('https://github.com/login'); + * > 15 | await login(page); + * | ^ + * 16 | }); + * ``` + * + * You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above: + * + * ```js + * function boxedStep(target: Function, context: ClassMethodDecoratorContext) { + * return function replacementMethod(...args: any) { + * const name = this.constructor.name + '.' + (context.name as string); + * return test.step(name, async () => { + * return await target.call(this, ...args); + * }, { box: true }); // Note the "box" option here. + * }; + * } + * + * class LoginPage { + * constructor(readonly page: Page) {} + * + * @boxedStep + * async login() { + * // .... + * } + * } + * + * test('example', async ({ page }) => { + * const loginPage = new LoginPage(page); + * await loginPage.login(); // <-- Error will be reported on this line. + * }); + * ``` + * * @param title Step name. * @param body Step body. * @param options