Skip to content

Commit d11380e

Browse files
authored
docs: improve test.step documentation (#27535)
1 parent 9edb811 commit d11380e

File tree

2 files changed

+233
-29
lines changed

2 files changed

+233
-29
lines changed

docs/src/test-api/class-test.md

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,7 +1331,7 @@ Optional description that will be reflected in a test report.
13311331
* since: v1.10
13321332
- returns: <[any]>
13331333

1334-
Declares a test step.
1334+
Declares a test step that is shown in the report.
13351335

13361336
**Usage**
13371337

@@ -1342,6 +1342,14 @@ test('test', async ({ page }) => {
13421342
await test.step('Log in', async () => {
13431343
// ...
13441344
});
1345+
1346+
await test.step('Outer step', async () => {
1347+
// ...
1348+
// You can nest steps inside each other.
1349+
await test.step('Inner step', async () => {
1350+
// ...
1351+
});
1352+
});
13451353
});
13461354
```
13471355

@@ -1361,64 +1369,137 @@ test('test', async ({ page }) => {
13611369
});
13621370
```
13631371

1364-
### param: Test.step.title
1365-
* since: v1.10
1366-
- `title` <[string]>
1372+
**Decorator**
13671373

1368-
Step name.
1374+
You can use TypeScript method decorators to turn a method into a step.
1375+
Each call to the decorated method will show up as a step in the report.
13691376

1377+
```js
1378+
function step(target: Function, context: ClassMethodDecoratorContext) {
1379+
return function replacementMethod(...args: any) {
1380+
const name = this.constructor.name + '.' + (context.name as string);
1381+
return test.step(name, async () => {
1382+
return await target.call(this, ...args);
1383+
});
1384+
};
1385+
}
13701386

1371-
### param: Test.step.body
1372-
* since: v1.10
1373-
- `body` <[function]\(\):[Promise]<[any]>>
1387+
class LoginPage {
1388+
constructor(readonly page: Page) {}
13741389

1375-
Step body.
1390+
@step
1391+
async login() {
1392+
const account = { username: 'Alice', password: 's3cr3t' };
1393+
await this.page.getByLabel('Username or email address').fill(account.username);
1394+
await this.page.getByLabel('Password').fill(account.password);
1395+
await this.page.getByRole('button', { name: 'Sign in' }).click();
1396+
await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
1397+
}
1398+
}
13761399

1377-
### option: Test.step.box
1378-
* since: v1.39
1379-
- `box` <boolean>
1400+
test('example', async ({ page }) => {
1401+
const loginPage = new LoginPage(page);
1402+
await loginPage.login();
1403+
});
1404+
```
1405+
1406+
**Boxing**
13801407

1381-
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.
1408+
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:
13821409

13831410
```js
1384-
const assertGoodPage = async page => {
1385-
await test.step('assertGoodPage', async () => {
1386-
await expect(page.getByText('does-not-exist')).toBeVisible();
1387-
}, { box: true });
1388-
};
1411+
async function login(page) {
1412+
await test.step('login', async () => {
1413+
const account = { username: 'Alice', password: 's3cr3t' };
1414+
await page.getByLabel('Username or email address').fill(account.username);
1415+
await page.getByLabel('Password').fill(account.password);
1416+
await page.getByRole('button', { name: 'Sign in' }).click();
1417+
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
1418+
});
1419+
}
13891420

1390-
test('box', async ({ page }) => {
1391-
await assertGoodPage(page); // <-- Errors will be reported on this line.
1421+
test('example', async ({ page }) => {
1422+
await page.goto('https://github.com/login');
1423+
await login(page);
13921424
});
13931425
```
13941426

1395-
You can also use TypeScript method decorators to annotate method as a boxed step:
1427+
```txt
1428+
Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
1429+
... error details omitted ...
1430+
1431+
8 | await page.getByRole('button', { name: 'Sign in' }).click();
1432+
> 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
1433+
| ^
1434+
10 | });
1435+
```
1436+
1437+
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.
1438+
1439+
```js
1440+
async function login(page) {
1441+
await test.step('login', async () => {
1442+
// ...
1443+
}, { box: true }); // Note the "box" option here.
1444+
}
1445+
```
1446+
1447+
```txt
1448+
Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
1449+
... error details omitted ...
1450+
1451+
14 | await page.goto('https://github.com/login');
1452+
> 15 | await login(page);
1453+
| ^
1454+
16 | });
1455+
```
1456+
1457+
You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above:
13961458

13971459
```js
13981460
function boxedStep(target: Function, context: ClassMethodDecoratorContext) {
13991461
return function replacementMethod(...args: any) {
14001462
const name = this.constructor.name + '.' + (context.name as string);
14011463
return test.step(name, async () => {
14021464
return await target.call(this, ...args);
1403-
}, { box: true });
1465+
}, { box: true }); // Note the "box" option here.
14041466
};
14051467
}
14061468

1407-
class Pom {
1469+
class LoginPage {
14081470
constructor(readonly page: Page) {}
14091471

14101472
@boxedStep
1411-
async assertGoodPage() {
1412-
await expect(this.page.getByText('does-not-exist')).toBeVisible({ timeout: 1 });
1473+
async login() {
1474+
// ....
14131475
}
14141476
}
14151477

1416-
test('box', async ({ page }) => {
1417-
const pom = new Pom(page);
1418-
await pom.assertGoodPage(); // <-- Error will be reported on this line.
1478+
test('example', async ({ page }) => {
1479+
const loginPage = new LoginPage(page);
1480+
await loginPage.login(); // <-- Error will be reported on this line.
14191481
});
14201482
```
14211483

1484+
### param: Test.step.title
1485+
* since: v1.10
1486+
- `title` <[string]>
1487+
1488+
Step name.
1489+
1490+
1491+
### param: Test.step.body
1492+
* since: v1.10
1493+
- `body` <[function]\(\):[Promise]<[any]>>
1494+
1495+
Step body.
1496+
1497+
### option: Test.step.box
1498+
* since: v1.39
1499+
- `box` <boolean>
1500+
1501+
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.
1502+
14221503
## method: Test.use
14231504
* since: v1.10
14241505

packages/playwright/types/test.d.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3318,7 +3318,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
33183318
*/
33193319
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
33203320
/**
3321-
* Declares a test step.
3321+
* Declares a test step that is shown in the report.
33223322
*
33233323
* **Usage**
33243324
*
@@ -3329,6 +3329,14 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
33293329
* await test.step('Log in', async () => {
33303330
* // ...
33313331
* });
3332+
*
3333+
* await test.step('Outer step', async () => {
3334+
* // ...
3335+
* // You can nest steps inside each other.
3336+
* await test.step('Inner step', async () => {
3337+
* // ...
3338+
* });
3339+
* });
33323340
* });
33333341
* ```
33343342
*
@@ -3348,6 +3356,121 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
33483356
* });
33493357
* ```
33503358
*
3359+
* **Decorator**
3360+
*
3361+
* You can use TypeScript method decorators to turn a method into a step. Each call to the decorated method will show
3362+
* up as a step in the report.
3363+
*
3364+
* ```js
3365+
* function step(target: Function, context: ClassMethodDecoratorContext) {
3366+
* return function replacementMethod(...args: any) {
3367+
* const name = this.constructor.name + '.' + (context.name as string);
3368+
* return test.step(name, async () => {
3369+
* return await target.call(this, ...args);
3370+
* });
3371+
* };
3372+
* }
3373+
*
3374+
* class LoginPage {
3375+
* constructor(readonly page: Page) {}
3376+
*
3377+
* @step
3378+
* async login() {
3379+
* const account = { username: 'Alice', password: 's3cr3t' };
3380+
* await this.page.getByLabel('Username or email address').fill(account.username);
3381+
* await this.page.getByLabel('Password').fill(account.password);
3382+
* await this.page.getByRole('button', { name: 'Sign in' }).click();
3383+
* await expect(this.page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
3384+
* }
3385+
* }
3386+
*
3387+
* test('example', async ({ page }) => {
3388+
* const loginPage = new LoginPage(page);
3389+
* await loginPage.login();
3390+
* });
3391+
* ```
3392+
*
3393+
* **Boxing**
3394+
*
3395+
* When something inside a step fails, you would usually see the error pointing to the exact action that failed. For
3396+
* example, consider the following login step:
3397+
*
3398+
* ```js
3399+
* async function login(page) {
3400+
* await test.step('login', async () => {
3401+
* const account = { username: 'Alice', password: 's3cr3t' };
3402+
* await page.getByLabel('Username or email address').fill(account.username);
3403+
* await page.getByLabel('Password').fill(account.password);
3404+
* await page.getByRole('button', { name: 'Sign in' }).click();
3405+
* await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
3406+
* });
3407+
* }
3408+
*
3409+
* test('example', async ({ page }) => {
3410+
* await page.goto('https://github.com/login');
3411+
* await login(page);
3412+
* });
3413+
* ```
3414+
*
3415+
* ```txt
3416+
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
3417+
* ... error details omitted ...
3418+
*
3419+
* 8 | await page.getByRole('button', { name: 'Sign in' }).click();
3420+
* > 9 | await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
3421+
* | ^
3422+
* 10 | });
3423+
* ```
3424+
*
3425+
* As we see above, the test may fail with an error pointing inside the step. If you would like the error to highlight
3426+
* the "login" step instead of its internals, use the `box` option. An error inside a boxed step points to the step
3427+
* call site.
3428+
*
3429+
* ```js
3430+
* async function login(page) {
3431+
* await test.step('login', async () => {
3432+
* // ...
3433+
* }, { box: true }); // Note the "box" option here.
3434+
* }
3435+
* ```
3436+
*
3437+
* ```txt
3438+
* Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
3439+
* ... error details omitted ...
3440+
*
3441+
* 14 | await page.goto('https://github.com/login');
3442+
* > 15 | await login(page);
3443+
* | ^
3444+
* 16 | });
3445+
* ```
3446+
*
3447+
* You can also create a TypeScript decorator for a boxed step, similar to a regular step decorator above:
3448+
*
3449+
* ```js
3450+
* function boxedStep(target: Function, context: ClassMethodDecoratorContext) {
3451+
* return function replacementMethod(...args: any) {
3452+
* const name = this.constructor.name + '.' + (context.name as string);
3453+
* return test.step(name, async () => {
3454+
* return await target.call(this, ...args);
3455+
* }, { box: true }); // Note the "box" option here.
3456+
* };
3457+
* }
3458+
*
3459+
* class LoginPage {
3460+
* constructor(readonly page: Page) {}
3461+
*
3462+
* @boxedStep
3463+
* async login() {
3464+
* // ....
3465+
* }
3466+
* }
3467+
*
3468+
* test('example', async ({ page }) => {
3469+
* const loginPage = new LoginPage(page);
3470+
* await loginPage.login(); // <-- Error will be reported on this line.
3471+
* });
3472+
* ```
3473+
*
33513474
* @param title Step name.
33523475
* @param body Step body.
33533476
* @param options

0 commit comments

Comments
 (0)