Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: improve test.step documentation #27535

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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