Skip to content

Commit

Permalink
support scoped step definitions (#205)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Oct 23, 2024
1 parent 904b4bf commit 1a7e019
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 17 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Changelog

## Dev
* feature: provide full localized step titles to Playwright HTML reporter ([#229](https://github.com/vitalets/playwright-bdd/issues/229), [#122](https://github.com/vitalets/playwright-bdd/issues/122))
* show background title in Playwright HTML reporter ([#122](https://github.com/vitalets/playwright-bdd/issues/122))
* increase minimal Playwright version from 1.35 to 1.38
* support scoped step definitions ([#205](https://github.com/vitalets/playwright-bdd/issues/205))
* new config option `missingSteps` to setup different behavior when step definitions are missing ([#158](https://github.com/vitalets/playwright-bdd/issues/158))
* new config option `matchKeywords` to enable keyword matching when searching for step definitions ([#221](https://github.com/vitalets/playwright-bdd/issues/221))
* make config option `quote` default to `"single"` to have less escapes in the generated files
* config option `featuresRoot` serves as a default directory for both `features` and `steps`, if these options are not explicitly defined
* increase minimal Playwright version from 1.35 to 1.38
* feature: provide full localized step titles to Playwright HTML reporter ([#229](https://github.com/vitalets/playwright-bdd/issues/229), [#122](https://github.com/vitalets/playwright-bdd/issues/122))
* show background title in Playwright HTML reporter ([#122](https://github.com/vitalets/playwright-bdd/issues/122))


## 7.5.0
* support external attachments in Cucumber HTML reporter ([#182](https://github.com/vitalets/playwright-bdd/issues/182))
Expand Down
11 changes: 8 additions & 3 deletions src/gen/testFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export class TestFile {
this.bddMetaBuilder.registerStep(step, keywordType);
// pickleStep contains step text with inserted example values and argument
const pickleStep = this.findPickleStep(step, outlineExampleRowId);
const stepDefinition = this.findStepDefinition(keywordType, step, pickleStep);
const stepDefinition = this.findStepDefinition(keywordType, step, pickleStep, tags);
if (!stepDefinition) {
hasMissingSteps = true;
return this.handleMissingStep(keywordEng, keywordType, pickleStep, step);
Expand Down Expand Up @@ -339,8 +339,13 @@ export class TestFile {
return { fixtures: testFixtureNames, lines, hasMissingSteps };
}

private findStepDefinition(keywordType: KeywordType, scenarioStep: Step, pickleStep: PickleStep) {
const stepDefinitions = this.stepFinder.findDefinitions(keywordType, pickleStep.text);
private findStepDefinition(
keywordType: KeywordType,
scenarioStep: Step,
pickleStep: PickleStep,
tags?: string[],
) {
const stepDefinitions = this.stepFinder.findDefinitions(keywordType, pickleStep.text, tags);

if (stepDefinitions.length > 1) {
const stepTextWithKeyword = getStepTextWithKeyword(scenarioStep.keyword, pickleStep.text);
Expand Down
6 changes: 5 additions & 1 deletion src/run/StepInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ class StepInvoker {

private findStepDefinition(stepText: string) {
const [_keyword, keywordType, stepLocation] = this.getStepMeta();
const stepDefinitions = this.stepFinder.findDefinitions(keywordType, stepText);
const stepDefinitions = this.stepFinder.findDefinitions(
keywordType,
stepText,
this.bddContext.tags,
);

if (stepDefinitions.length === 1) return stepDefinitions[0];

Expand Down
21 changes: 15 additions & 6 deletions src/steps/finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ import { StepDefinition } from './stepDefinition';
export class StepFinder {
constructor(private config: BDDConfig) {}

findDefinitions(keywordType: KeywordType, stepText: string) {
const matchedStepsByText = this.filterByText(stepDefinitions, stepText);
return this.config.matchKeywords
? this.filterByKeyword(matchedStepsByText, keywordType)
: matchedStepsByText;
findDefinitions(keywordType: KeywordType, stepText: string, tags?: string[]) {
let matchedSteps = this.filterByText(stepDefinitions, stepText);

if (this.config.matchKeywords) {
matchedSteps = this.filterByKeyword(matchedSteps, keywordType);
}

if (tags) {
matchedSteps = this.filterByTags(matchedSteps, tags);
}

return matchedSteps;
}

private filterByText(steps: StepDefinition[], stepText: string) {
Expand All @@ -39,7 +46,9 @@ export class StepFinder {
});
}

// todo: filterByTags
private filterByTags(steps: StepDefinition[], tags: string[]) {
return steps.filter((step) => step.matchesTags(tags));
}
}

export function formatDuplicateStepsMessage(
Expand Down
21 changes: 20 additions & 1 deletion src/steps/stepDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
*/

import { CucumberExpression, RegularExpression, Expression } from '@cucumber/cucumber-expressions';
import parseTagsExpression from '@cucumber/tag-expressions';
import { parameterTypeRegistry } from './parameterTypes';
import { PlaywrightLocation, TestTypeCommon } from '../playwright/types';
import { PomNode } from './decorators/pomGraph';

export type GherkinStepKeyword = 'Unknown' | 'Given' | 'When' | 'Then';
export type StepPattern = string | RegExp;
export type ProvidedStepOptions = {
tags?: string;
};

export type StepDefinitionOptions = {
keyword: GherkinStepKeyword;
Expand All @@ -19,12 +23,16 @@ export type StepDefinitionOptions = {
customTest?: TestTypeCommon;
pomNode?: PomNode; // for decorator steps
worldFixture?: string; // for new cucumber-style steps
providedOptions?: ProvidedStepOptions; // options passed as second argument
};

export class StepDefinition {
#expression?: Expression;
#tagsExpression?: ReturnType<typeof parseTagsExpression>;

constructor(private options: StepDefinitionOptions) {}
constructor(private options: StepDefinitionOptions) {
this.buildTagsExpression();
}

get keyword() {
return this.options.keyword;
Expand Down Expand Up @@ -87,4 +95,15 @@ export class StepDefinition {
isCucumberStyle(): this is this & { worldFixture: string } {
return Boolean(this.options.worldFixture);
}

matchesTags(tags: string[]) {
return this.#tagsExpression ? this.#tagsExpression.evaluate(tags) : true;
}

private buildTagsExpression() {
const tags = this.options.providedOptions?.tags;
if (tags) {
this.#tagsExpression = parseTagsExpression(tags);
}
}
}
8 changes: 6 additions & 2 deletions src/steps/styles/cucumberStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { getLocationByOffset } from '../../playwright/getLocationInFile';
import { registerStepDefinition } from '../stepRegistry';
import { BddAutoInjectFixtures } from '../../run/bddFixtures/autoInject';
import { TestTypeCommon } from '../../playwright/types';
import { StepPattern, GherkinStepKeyword, StepDefinitionOptions } from '../stepDefinition';
import { GherkinStepKeyword, StepDefinitionOptions } from '../stepDefinition';
import { parseStepDefinitionArgs, StepDefinitionArgs } from './shared';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CucumberStyleStepFn<World> = (this: World, ...args: any[]) => unknown;
Expand All @@ -17,13 +18,16 @@ export function cucumberStepCtor<StepFn extends StepDefinitionOptions['fn']>(
customTest: TestTypeCommon,
worldFixture: string,
) {
return (pattern: StepPattern, fn: StepFn) => {
return (...args: StepDefinitionArgs<StepFn>) => {
const { pattern, providedOptions, fn } = parseStepDefinitionArgs(args);

registerStepDefinition({
keyword,
pattern,
location: getLocationByOffset(3),
customTest,
worldFixture,
providedOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: ({ $bddContext }: BddAutoInjectFixtures, ...args: any[]) => {
return fn.call($bddContext.world, ...args);
Expand Down
6 changes: 5 additions & 1 deletion src/steps/styles/playwrightStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getLocationByOffset } from '../../playwright/getLocationInFile';
import { ParametersExceptFirst } from '../../utils/types';
import { registerStepDefinition } from '../stepRegistry';
import { StepPattern, GherkinStepKeyword, StepDefinitionOptions } from '../stepDefinition';
import { parseStepDefinitionArgs, StepDefinitionArgs } from './shared';

export type PlaywrightStyleStepFn<T extends KeyValue, W extends KeyValue> = (
fixtures: T & W,
Expand All @@ -17,13 +18,16 @@ export function playwrightStepCtor<StepFn extends StepDefinitionOptions['fn']>(
keyword: GherkinStepKeyword,
customTest?: TestTypeCommon,
) {
return (pattern: StepPattern, fn: StepFn) => {
return (...args: StepDefinitionArgs<StepFn>) => {
const { pattern, providedOptions, fn } = parseStepDefinitionArgs(args);

registerStepDefinition({
keyword,
pattern,
fn,
location: getLocationByOffset(3),
customTest,
providedOptions,
});

return getCallableStepFn(pattern, fn);
Expand Down
12 changes: 12 additions & 0 deletions src/steps/styles/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ProvidedStepOptions, StepDefinitionOptions, StepPattern } from '../stepDefinition';

export type StepDefinitionArgs<StepFn extends StepDefinitionOptions['fn']> =
| [pattern: StepPattern, fn: StepFn]
| [pattern: StepPattern, providedOptions: ProvidedStepOptions, fn: StepFn];

export function parseStepDefinitionArgs<StepFn extends StepDefinitionOptions['fn']>(
args: StepDefinitionArgs<StepFn>,
) {
const [pattern, providedOptions, fn] = args.length === 3 ? args : [args[0], {}, args[1]];
return { pattern, providedOptions, fn };
}
6 changes: 6 additions & 0 deletions test/scoped-steps/features/bar.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@bar
Feature: feature bar

Scenario: scenario for bar
Given step without tags
Given step bound to feature
11 changes: 11 additions & 0 deletions test/scoped-steps/features/baz.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Feature: feature baz

@baz1
Scenario: scenario for baz 1
Given step without tags
Given step bound to scenario

@baz2
Scenario: scenario for baz 2
Given step without tags
Given step bound to scenario
6 changes: 6 additions & 0 deletions test/scoped-steps/features/foo.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@foo
Feature: feature foo

Scenario: scenario for foo
Given step without tags
Given step bound to feature
24 changes: 24 additions & 0 deletions test/scoped-steps/features/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createBdd } from 'playwright-bdd';

const { Given } = createBdd();
const logger = console;

Given('step without tags', async ({ $testInfo }) => {
logger.log(`running step without tags: ${$testInfo.title}`);
});

Given('step bound to feature', { tags: '@foo' }, async ({ $testInfo }) => {
logger.log(`running step for @foo: ${$testInfo.title}`);
});

Given('step bound to feature', { tags: '@bar' }, async ({ $testInfo }) => {
logger.log(`running step for @bar: ${$testInfo.title}`);
});

Given('step bound to scenario', { tags: '@baz1' }, async ({ $testInfo }) => {
logger.log(`running step for @baz1: ${$testInfo.title}`);
});

Given('step bound to scenario', { tags: '@baz2' }, async ({ $testInfo }) => {
logger.log(`running step for @baz2: ${$testInfo.title}`);
});
3 changes: 3 additions & 0 deletions test/scoped-steps/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"description": "This file is required for Playwright to consider this dir as a <package-json dir>. It ensures to load 'playwright-bdd' from './test/node_modules/playwright-bdd' and output './test-results' here to avoid conflicts."
}
10 changes: 10 additions & 0 deletions test/scoped-steps/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from '@playwright/test';
import { defineBddConfig } from 'playwright-bdd';

const testDir = defineBddConfig({
featuresRoot: 'features',
});

export default defineConfig({
testDir,
});
18 changes: 18 additions & 0 deletions test/scoped-steps/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test, expect, TestDir, execPlaywrightTest } from '../_helpers/index.mjs';

const testDir = new TestDir(import.meta);

test(testDir.name, () => {
const stdout = execPlaywrightTest(testDir.name);

expect(stdout).toContain('running step for @foo: scenario for foo');
expect(stdout).toContain('running step for @bar: scenario for bar');

expect(stdout).toContain('running step for @baz1: scenario for baz 1');
expect(stdout).toContain('running step for @baz2: scenario for baz 2');

expect(stdout).toContain('running step without tags: scenario for foo');
expect(stdout).toContain('running step without tags: scenario for bar');
expect(stdout).toContain('running step without tags: scenario for baz 1');
expect(stdout).toContain('running step without tags: scenario for baz 2');
});

0 comments on commit 1a7e019

Please sign in to comment.