Skip to content

Commit

Permalink
docs: improve test.step documentation (#27535)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Oct 10, 2023
1 parent 9edb811 commit d11380e
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 29 deletions.
137 changes: 109 additions & 28 deletions docs/src/test-api/class-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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 () => {
// ...
});
});
});
```

Expand All @@ -1361,64 +1369,137 @@ 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` <boolean>
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) {
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 });
}, { 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` <boolean>

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

Expand Down
125 changes: 124 additions & 1 deletion packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3318,7 +3318,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
*/
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
/**
* Declares a test step.
* Declares a test step that is shown in the report.
*
* **Usage**
*
Expand All @@ -3329,6 +3329,14 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* 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 () => {
* // ...
* });
* });
* });
* ```
*
Expand All @@ -3348,6 +3356,121 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* });
* ```
*
* **Decorator**
*
* 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);
* });
* };
* }
*
* 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
Expand Down

0 comments on commit d11380e

Please sign in to comment.