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

add BeforeStep / AfterStep hooks #281

Merged
merged 13 commits into from
Feb 17, 2025
Merged
15 changes: 12 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,27 @@ To contribute code:
## Development Setup

1. Install dependencies:
```
```sh
cd playwright-bdd
npm install
```
2. Install Playwright browsers:
```
```sh
npx playwright install chromium
```
3. Run tests:
```
```sh
npm t
# run specific test
npm run only test/<test-name-in-directory>
```
4. Run examples:
```sh
npm run examples
# run specific example
npm run examples examples/<example-name-in-directory>
```
note: you may need to run `npm run examples` before commiting, as some pre-commit checks rely on its generated output.

## Useful Dev Commands

Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ blob-report
/test/reporter-cucumber-merge/check-report
/test/reporter-cucumber-merge/.features-gen/features/sample.feature.spec.js-snapshots

/examples/**/playwright/.auth
/examples/**/playwright/.auth

.prototools
34 changes: 34 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,40 @@ Functions for step definitions.

**Returns:** *function* - A function to call this step from other steps.

### BeforeStep

Defines a hook that runs **before each step**. You can target the hook to specific step by providing the `tags` option.

**Usage:** `BeforeStep([options,] hookFn)`

**Params:**
* `options` *string | object*
- `tags` *string* - [Tag expression](https://github.com/cucumber/tag-expressions) to target this hook to specific features/steps.
- `name` *string* - An optional name for this hook for reporting.
- `timeout` *number* - Timeout for this hook in milliseconds.
* `hookFn` *Function* - Hook function `(fixtures?) => void`:
- `fixtures` *object* - Playwright fixtures:
- `$testInfo` *object* - Playwright [testInfo](https://playwright.dev/docs/api/class-testinfo).
- `$tags` *string[]* - List of tags for the current step.
- Any other built-in and custom fixtures.

### AfterStep

Defines a hook that runs **after each scenario**. You can target the hook to specific step by providing the `tags` option.

**Usage:** `AfterStep([options,] hookFn)`

**Params:**
* `options` *string | object*
- `tags` *string* - [Tag expression](https://github.com/cucumber/tag-expressions) to target this hook to specific features/steps.
- `name` *string* - An optional name for this hook for reporting.
- `timeout` *number* - Timeout for this hook in milliseconds.
* `hookFn` *Function* - Hook function `(fixtures?) => void`:
- `fixtures` *object* - Playwright fixtures:
- `$testInfo` *object* - Playwright [testInfo](https://playwright.dev/docs/api/class-testinfo).
- `$tags` *string[]* - List of tags for the current step.
- Any other built-in and custom fixtures.

### BeforeScenario / Before

Defines a hook that runs **before each scenario**. You can target the hook to specific scenarios by providing the `tags` option. `BeforeScenario` and `Before` are aliases.
Expand Down
111 changes: 110 additions & 1 deletion docs/writing-steps/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Hooks are functions that automatically run before/after workers or scenarios:
* `AfterWorker / AfterAll` - runs **once in each worker**, after all scenarios
* `BeforeScenario / Before` - runs **before each scenario**
* `AfterScenario / After` - runs **after each scenario**
* `BeforeStep` - runs **before each step**
* `AfterStep` - runs **after each step**

> If you need to run some code **before/after overall test execution**, check out Playwright's [project dependencies](https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies) or [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown#option-2-configure-globalsetup-and-globalteardown)

Expand Down Expand Up @@ -286,4 +288,111 @@ AfterScenario(async () => {
});
```

All options and behavior are similar to [BeforeScenario / Before](#beforescenario-before).
All options and behavior are similar to [BeforeScenario / Before](#beforescenario-before).

## BeforeStep

Playwright-BDD supports the step-level hook `BeforeStep`. It runs **before each step**.

Usage:
```ts
import { test as base, createBdd } from 'playwright-bdd';

export const test = base.extend({ /* ...your fixtures */ });

const { BeforeStep } = createBdd(test);

BeforeStep(async () => {
// runs before each step
});
```

If you don't use custom fixtures, you can create `BeforeStep` without passing `test` argument:
```ts
import { createBdd } from 'playwright-bdd';

const { BeforeStep } = createBdd();
```

You can target step hook to the steps of the specific feature/scenario by `tags`:

```ts
BeforeStep({ tags: '@mobile and not @slow' }, async function () {
// runs for scenarios with @mobile and not @slow
});
```
If you want to pass only tags, you can use a shortcut:
```ts
BeforeStep('@mobile and not @slow', async function () {
// runs for scenarios with @mobile and not @slow
});
```
You can also provide default tags via `createBdd()`:
```ts
const { BeforeStep } = createBdd(test, { tags: '@mobile' });

BeforeStep(async () => {
// runs only for scenarios with @mobile
});
```

If the hook has both default and own tags, they are combined using `AND` logic:
```ts
const { BeforeStep } = createBdd(test, { tags: '@mobile' });

BeforeStep({ tags: '@slow' }, async function () {
// runs for scenarios with @mobile and @slow
});
```

Additionally, you can set `name` and `timeout` for the hook:
```ts
BeforeStep({ name: 'my hook', timeout: 5000 }, async function () {
// ...
});
```

The hook function can accept **1 argument** - [test-scoped fixtures](https://playwright.dev/docs/test-fixtures#built-in-fixtures).
You can access [$testInfo](https://playwright.dev/docs/api/class-testinfo), [$tags](writing-steps/bdd-fixtures.md#tags) and any built-in or custom fixtures. See more details in [BeforeScenario / Before API](api.md#beforescenario-before).

## AfterStep

> Consider using [fixtures](#fixtures) instead of hooks.

Playwright-BDD supports the scenario-level hook `AfterStep`. It runs **after each step**.

Usage:
```ts
import { test as base, createBdd } from 'playwright-bdd';

export const test = base.extend({ /* ...your fixtures */ });

const { AfterStep } = createBdd(test);

AfterStep(async () => {
// runs after each scenario
});
```

All options and behavior are similar to [BeforeStep](#beforestep).

#### Example of using `AfterStep` to capture screenshot after each step

Create `fixtures.ts`:
```ts
export const { AfterStep } = createBdd(test);
```

Import `fixtures.ts` in step definition
```ts
import { AfterStep } from './fixtures';

AfterStep(async ({ page, $testInfo, $step }) => {
await $testInfo.attach(`screenshot after ${$step.title}`, {
contentType: 'image/png',
body: await page.screenshot()
});
});

// ...rest of the step definitions
```
13 changes: 13 additions & 0 deletions src/generate/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
SpecialTags,
} from '../specialTags';
import { DecoratorFixtureResolver } from './decoratorFixtureResolver';
import { getStepHooksFixtureNames, getStepHooksToRun } from '../../hooks/step';

export type StepData = {
pickleStep: PickleStep;
Expand All @@ -44,6 +45,7 @@ export class TestGen {
public skippedByTag: boolean;
private skippedByMissingSteps = false;
public slow: boolean;
private stepHooksFixtureNames: string[] = [];

// eslint-disable-next-line max-params
constructor(
Expand All @@ -62,6 +64,7 @@ export class TestGen {
this.specialTags = new SpecialTags(ownTestTags);
this.skippedByTag = isTestSkippedByCollectedTags(this.tags);
this.slow = isTestSlowByCollectedTags(this.tags);
this.fillStepHooksFixtureNames();
this.fillStepsData();
this.resolveFixtureNamesForDecoratorSteps();
}
Expand Down Expand Up @@ -110,6 +113,7 @@ export class TestGen {
const location = `${this.featureUri}:${stringifyLocation(gherkinStep.location)}`;
const matchedDefinition = this.findMatchedDefinition(pickleStep, gherkinStep);
const fixtureNames = this.getStepFixtureNames(matchedDefinition);
fixtureNames.push(...this.stepHooksFixtureNames);
const pomNode = matchedDefinition?.definition.pomNode;
const stepData: StepData = {
pickleStep,
Expand All @@ -126,6 +130,15 @@ export class TestGen {
});
}

private fillStepHooksFixtureNames() {
const beforeStepHooksToRun = getStepHooksToRun('beforeStep', this.tags);
const afterStepHooksToRun = getStepHooksToRun('afterStep', this.tags);
this.stepHooksFixtureNames = getStepHooksFixtureNames([
...beforeStepHooksToRun,
...afterStepHooksToRun,
]);
}

private handleMissingDefinitions() {
if (
!this.skippedByTag &&
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TestTypeCommon } from '../playwright/types';
import { buildTagsExpression, extractTagsFromPath } from '../steps/tags';
import { relativeToCwd } from '../utils/paths';
import { GeneralScenarioHook } from './scenario';
import { GeneralStepHook } from './step';
import { WorkerHook } from './worker';

/**
Expand All @@ -17,7 +18,7 @@ export type HookConstructorOptions = {
defaultTags?: string;
};

export function setTagsExpression(hook: WorkerHook | GeneralScenarioHook) {
export function setTagsExpression(hook: WorkerHook | GeneralScenarioHook | GeneralStepHook) {
const { defaultTags, options, location } = hook;
// Possibly, we should use relative to configDir
const relFilePath = relativeToCwd(location.file);
Expand Down
Loading