Skip to content

Commit

Permalink
feature: add $step fixture (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Apr 11, 2024
1 parent 928a5af commit ccc6d65
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 33 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## dev
* feature: add `$step` fixture ([#133](https://github.com/vitalets/playwright-bdd/issues/133))

## 6.1.1
* fix: support stacktrace for Cucumber 10.4

Expand Down
24 changes: 22 additions & 2 deletions docs/writing-steps/playwright-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Playwright-style highlights:

* use `Given`, `When`, `Then` from `createBdd()` call (see example below)
* use arrow functions for step definitions
* don't use `World` and `before/after` hooks (use fixtures instead)
* don't use `World` / `this` inside steps

Example:

Expand All @@ -26,7 +26,7 @@ When('I click link {string}', async ({ page }, name: string) => {

> Usually step functions are async, but it is not required
?> Calling `const { Given, When, Then } = createBdd()` at the top of each step file is totally ok, because it returns lightweight wrappers without heavy operations
?> Calling `const { Given, When, Then } = createBdd()` at the top of each step file is normal, because it returns lightweight wrappers without heavy operations

## Fixtures
To use [fixtures](https://playwright.dev/docs/test-fixtures#with-fixtures) in step definitions:
Expand Down Expand Up @@ -96,6 +96,26 @@ Given('I do something', async ({ browserName, $test }) => {
});
```

## Accessing `$step`
You can access current step info by special `$step` fixture.
Currently, it contains only step title, but can be extended in the future.

One of the use-cases - additional matching in the step title.
Imagine you have a step that checks element visibility:
`Then('element with text {string} should( not) be displayed', ...)`.
As optional matches are [not a part](https://github.com/cucumber/cucumber-expressions/issues/125) of Cucumber expression result,
the easiest way to check for `( not)` is to use step title:
```ts
Then('element with text {string} should( not) be displayed', async ({ page, $step }, text: string) => {
const negate = /should not/.test($step.title);
if (negate) {
await expect(page.getByText(text)).toBeHidden();
} else {
await expect(page.getByText(text)).toBeVisible();
}
});
```

## Tags
You can access tags in step definitions by special `$tags` fixture:

Expand Down
2 changes: 1 addition & 1 deletion lint-staged.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function test(changedFiles) {
const testHelpersChanged = testDirFiles.length !== testFiles.length;
const srcFiles = micromatch(changedFiles, 'src/**/*.{js,mjs,ts}');
const packageFiles = micromatch(changedFiles, 'package*.json');
const testDirs = testDirFiles.map((file) => file.split(path.sep).slice(0, 2));
const testDirs = testDirFiles.map((file) => file.split(path.sep).slice(0, 2).join(path.sep));
// if changes only in test/* -> run only these tests
// if changes in src|package.json -> run tests on test dirs + smoke test dirs
if (testHelpersChanged || srcFiles.length || packageFiles.length) {
Expand Down
3 changes: 3 additions & 0 deletions src/gen/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export class Formatter {
];
}

/**
* These fixtures are added only in Cucumber-style
*/
bddWorldFixtures() {
const fixturesObj: Record<keyof BddWorldFixtures, null> = {
page: null,
Expand Down
1 change: 1 addition & 0 deletions src/run/StepInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class StepInvoker {
);

this.world.$internal.bddDataManager?.registerStep(stepDefinition, stepText, location);
this.world.step.title = stepText;

await runStepWithCustomLocation(this.world.test, stepTitle, location, () =>
code.apply(this.world, parameters),
Expand Down
36 changes: 31 additions & 5 deletions src/run/bddFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { BddDataManager } from './bddData';

// BDD fixtures prefixed with '$' to avoid collision with user's fixtures.

export type StepFixture = {
title: string;
};

export type BddFixtures = {
// fixtures injected into BddWorld:
// empty object for pw-style, builtin fixtures for cucumber-style
Expand All @@ -33,6 +37,7 @@ export type BddFixtures = {
$testMeta: TestMeta;
$tags: string[];
$test: TestTypeCommon;
$step: StepFixture;
$uri: string;
$scenarioHookFixtures: Record<string, unknown>;
$before: void;
Expand Down Expand Up @@ -80,7 +85,7 @@ export const test = base.extend<BddFixtures, BddFixturesWorker>({
// init $bddWorldFixtures with empty object, will be owerwritten in test file for cucumber-style
$bddWorldFixtures: ({}, use) => use({} as BddWorldFixtures),
$bddWorld: async (
{ $tags, $test, $bddWorldFixtures, $cucumber, $lang, $testMeta, $uri },
{ $tags, $test, $step, $bddWorldFixtures, $cucumber, $lang, $testMeta, $uri },
use,
testInfo,
) => {
Expand All @@ -90,6 +95,7 @@ export const test = base.extend<BddFixtures, BddFixturesWorker>({
supportCodeLibrary,
$tags,
$test,
$step,
$bddWorldFixtures,
lang: $lang,
parameters: runConfiguration.runtime.worldParameters || {},
Expand Down Expand Up @@ -122,6 +128,8 @@ export const test = base.extend<BddFixtures, BddFixturesWorker>({
// init $test with base test, but it will be overwritten in test file
$test: ({}, use) => use(base),

$step: ({}, use) => use({ title: '' }),

// feature file uri, relative to configDir, will be overwritten in test file
$uri: ({}, use) => use(''),

Expand Down Expand Up @@ -172,13 +180,31 @@ export const test = base.extend<BddFixtures, BddFixturesWorker>({
],
});

/** these fixtures automatically injected into every step call */
export type BddAutoInjectFixtures = Pick<BddFixtures, '$test' | '$tags'> & {
// Auto-inject-fixtures are automatically injected into every step call
// without explicitly passing them in the last argument of Given() / When() / Then()
export type BddAutoInjectFixtures = Pick<BddFixtures, '$test' | '$tags' | '$step'> & {
$testInfo: TestInfo;
};

const BDD_AUTO_INJECT_FIXTURES: (keyof BddAutoInjectFixtures)[] = ['$testInfo', '$test', '$tags'];
const BDD_AUTO_INJECT_FIXTURES: Record<keyof BddAutoInjectFixtures, null> = {
$tags: null,
$test: null,
$step: null,
$testInfo: null,
};

export function isBddAutoInjectFixture(name: string) {
return BDD_AUTO_INJECT_FIXTURES.includes(name as keyof BddAutoInjectFixtures);
return Object.prototype.hasOwnProperty.call(
BDD_AUTO_INJECT_FIXTURES,
name as keyof BddAutoInjectFixtures,
);
}

export function getBddAutoInjectsFixtures(bddWorld: BddWorld): BddAutoInjectFixtures {
return {
$testInfo: bddWorld.testInfo,
$tags: bddWorld.tags,
$test: bddWorld.test,
$step: bddWorld.step,
};
}
6 changes: 6 additions & 0 deletions src/run/bddWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { World as CucumberWorld, IWorldOptions } from '@cucumber/cucumber';
import { Fixtures, TestTypeCommon } from '../playwright/types';
import { ISupportCodeLibrary } from '../cucumber/types';
import { BddWorldInternal } from './bddWorldInternal';
import { StepFixture } from './bddFixtures';

export type BddWorldFixtures = {
page: Page;
Expand All @@ -21,6 +22,7 @@ export type BddWorldOptions<
supportCodeLibrary: ISupportCodeLibrary;
$tags: string[];
$test: TestType;
$step: StepFixture;
$bddWorldFixtures: BddWorldFixtures;
lang: string;
};
Expand Down Expand Up @@ -90,6 +92,10 @@ export class BddWorld<
return this.options.$test;
}

get step() {
return this.options.$step;
}

async init() {
// async setup before each test
}
Expand Down
12 changes: 6 additions & 6 deletions src/steps/defineStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GherkinStepKeyword } from '@cucumber/cucumber/lib/models/gherkin_step_k
import { CucumberStepFunction, StepConfig } from './stepConfig';
import StepDefinition from '@cucumber/cucumber/lib/models/step_definition';
import { exit } from '../utils/exit';
import { getBddAutoInjectsFixtures } from '../run/bddFixtures';

/**
* Defines step by config.
Expand Down Expand Up @@ -38,12 +39,11 @@ export function buildCucumberStepCode(stepConfig: StepConfig) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const code: CucumberStepFunction = function (...args: any[]) {
// build the first argument (fixtures) for step fn
const fixturesArg = Object.assign({}, this.$internal.currentStepFixtures, {
$testInfo: this.testInfo,
$test: this.test,
$tags: this.tags,
});

const fixturesArg = Object.assign(
{},
this.$internal.currentStepFixtures,
getBddAutoInjectsFixtures(this),
);
return stepConfig.fn.call(this, fixturesArg, ...args);
};

Expand Down
4 changes: 1 addition & 3 deletions test/fixtures/builtin-fixtures.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ Feature: custom fixtures in playwright-style


Scenario: playwright-bdd built-in fixtures
# cant make vscode extension to recognize these steps
Then $testInfo is available as a fixture and its title equals to "playwright-bdd built-in fixtures"
Then $test is available as a fixture and test.info().title equals to "playwright-bdd built-in fixtures"
Then bdd-fixtures should be defined
21 changes: 5 additions & 16 deletions test/fixtures/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,11 @@ Then('empty fixtures with int param {int}', ({}, n: number) => {
expect(typeof n).toEqual('number');
});

Then(
'$testInfo is available as a fixture and its title equals to {string}',
async ({ $testInfo }, title: string) => {
expect($testInfo).toBeDefined();
expect($testInfo.title).toEqual(title);
},
);

Then(
'$test is available as a fixture and test.info\\().title equals to {string}',
async ({ $test }, title: string) => {
expect($test).toBeDefined();
expect($test.skip).toBeDefined();
expect($test.info().title).toEqual(title);
},
);
Then('bdd-fixtures should be defined', async ({ $test, $step, $testInfo }) => {
expect($testInfo.title).toEqual('playwright-bdd built-in fixtures');
expect($test.info().title).toEqual('playwright-bdd built-in fixtures');
expect($step.title).toEqual('bdd-fixtures should be defined');
});

When('testScopedFixture set prop to {string}', async ({ testScopedFixture }, value: string) => {
testScopedFixture.prop = value;
Expand Down

0 comments on commit ccc6d65

Please sign in to comment.