From 1a7e019898292c48b7d517442f470fee5dbe28c6 Mon Sep 17 00:00:00 2001 From: Vitaliy Potapov Date: Wed, 23 Oct 2024 13:45:55 +0400 Subject: [PATCH] support scoped step definitions (#205) --- CHANGELOG.md | 8 +++++--- src/gen/testFile.ts | 11 ++++++++--- src/run/StepInvoker.ts | 6 +++++- src/steps/finder.ts | 21 +++++++++++++++------ src/steps/stepDefinition.ts | 21 ++++++++++++++++++++- src/steps/styles/cucumberStyle.ts | 8 ++++++-- src/steps/styles/playwrightStyle.ts | 6 +++++- src/steps/styles/shared.ts | 12 ++++++++++++ test/scoped-steps/features/bar.feature | 6 ++++++ test/scoped-steps/features/baz.feature | 11 +++++++++++ test/scoped-steps/features/foo.feature | 6 ++++++ test/scoped-steps/features/steps.ts | 24 ++++++++++++++++++++++++ test/scoped-steps/package.json | 3 +++ test/scoped-steps/playwright.config.ts | 10 ++++++++++ test/scoped-steps/test.mjs | 18 ++++++++++++++++++ 15 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 src/steps/styles/shared.ts create mode 100644 test/scoped-steps/features/bar.feature create mode 100644 test/scoped-steps/features/baz.feature create mode 100644 test/scoped-steps/features/foo.feature create mode 100644 test/scoped-steps/features/steps.ts create mode 100644 test/scoped-steps/package.json create mode 100644 test/scoped-steps/playwright.config.ts create mode 100644 test/scoped-steps/test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c40040d..0a248c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src/gen/testFile.ts b/src/gen/testFile.ts index c7c59d5a..9baf01c4 100644 --- a/src/gen/testFile.ts +++ b/src/gen/testFile.ts @@ -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); @@ -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); diff --git a/src/run/StepInvoker.ts b/src/run/StepInvoker.ts index 58caab4b..19817963 100644 --- a/src/run/StepInvoker.ts +++ b/src/run/StepInvoker.ts @@ -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]; diff --git a/src/steps/finder.ts b/src/steps/finder.ts index 657357ff..a9f03fe2 100644 --- a/src/steps/finder.ts +++ b/src/steps/finder.ts @@ -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) { @@ -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( diff --git a/src/steps/stepDefinition.ts b/src/steps/stepDefinition.ts index e30d613b..932d6f96 100644 --- a/src/steps/stepDefinition.ts +++ b/src/steps/stepDefinition.ts @@ -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; @@ -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; - constructor(private options: StepDefinitionOptions) {} + constructor(private options: StepDefinitionOptions) { + this.buildTagsExpression(); + } get keyword() { return this.options.keyword; @@ -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); + } + } } diff --git a/src/steps/styles/cucumberStyle.ts b/src/steps/styles/cucumberStyle.ts index 03d22018..595f342a 100644 --- a/src/steps/styles/cucumberStyle.ts +++ b/src/steps/styles/cucumberStyle.ts @@ -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 = (this: World, ...args: any[]) => unknown; @@ -17,13 +18,16 @@ export function cucumberStepCtor( customTest: TestTypeCommon, worldFixture: string, ) { - return (pattern: StepPattern, fn: StepFn) => { + return (...args: StepDefinitionArgs) => { + 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); diff --git a/src/steps/styles/playwrightStyle.ts b/src/steps/styles/playwrightStyle.ts index 997acc47..06d271e3 100644 --- a/src/steps/styles/playwrightStyle.ts +++ b/src/steps/styles/playwrightStyle.ts @@ -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 = ( fixtures: T & W, @@ -17,13 +18,16 @@ export function playwrightStepCtor( keyword: GherkinStepKeyword, customTest?: TestTypeCommon, ) { - return (pattern: StepPattern, fn: StepFn) => { + return (...args: StepDefinitionArgs) => { + const { pattern, providedOptions, fn } = parseStepDefinitionArgs(args); + registerStepDefinition({ keyword, pattern, fn, location: getLocationByOffset(3), customTest, + providedOptions, }); return getCallableStepFn(pattern, fn); diff --git a/src/steps/styles/shared.ts b/src/steps/styles/shared.ts new file mode 100644 index 00000000..feed6b85 --- /dev/null +++ b/src/steps/styles/shared.ts @@ -0,0 +1,12 @@ +import { ProvidedStepOptions, StepDefinitionOptions, StepPattern } from '../stepDefinition'; + +export type StepDefinitionArgs = + | [pattern: StepPattern, fn: StepFn] + | [pattern: StepPattern, providedOptions: ProvidedStepOptions, fn: StepFn]; + +export function parseStepDefinitionArgs( + args: StepDefinitionArgs, +) { + const [pattern, providedOptions, fn] = args.length === 3 ? args : [args[0], {}, args[1]]; + return { pattern, providedOptions, fn }; +} diff --git a/test/scoped-steps/features/bar.feature b/test/scoped-steps/features/bar.feature new file mode 100644 index 00000000..27dab29b --- /dev/null +++ b/test/scoped-steps/features/bar.feature @@ -0,0 +1,6 @@ +@bar +Feature: feature bar + + Scenario: scenario for bar + Given step without tags + Given step bound to feature diff --git a/test/scoped-steps/features/baz.feature b/test/scoped-steps/features/baz.feature new file mode 100644 index 00000000..3f07da9c --- /dev/null +++ b/test/scoped-steps/features/baz.feature @@ -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 diff --git a/test/scoped-steps/features/foo.feature b/test/scoped-steps/features/foo.feature new file mode 100644 index 00000000..01855958 --- /dev/null +++ b/test/scoped-steps/features/foo.feature @@ -0,0 +1,6 @@ +@foo +Feature: feature foo + + Scenario: scenario for foo + Given step without tags + Given step bound to feature diff --git a/test/scoped-steps/features/steps.ts b/test/scoped-steps/features/steps.ts new file mode 100644 index 00000000..6b519a8b --- /dev/null +++ b/test/scoped-steps/features/steps.ts @@ -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}`); +}); diff --git a/test/scoped-steps/package.json b/test/scoped-steps/package.json new file mode 100644 index 00000000..de610253 --- /dev/null +++ b/test/scoped-steps/package.json @@ -0,0 +1,3 @@ +{ + "description": "This file is required for Playwright to consider this dir as a . It ensures to load 'playwright-bdd' from './test/node_modules/playwright-bdd' and output './test-results' here to avoid conflicts." +} diff --git a/test/scoped-steps/playwright.config.ts b/test/scoped-steps/playwright.config.ts new file mode 100644 index 00000000..631672f8 --- /dev/null +++ b/test/scoped-steps/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@playwright/test'; +import { defineBddConfig } from 'playwright-bdd'; + +const testDir = defineBddConfig({ + featuresRoot: 'features', +}); + +export default defineConfig({ + testDir, +}); diff --git a/test/scoped-steps/test.mjs b/test/scoped-steps/test.mjs new file mode 100644 index 00000000..a2ef3ab3 --- /dev/null +++ b/test/scoped-steps/test.mjs @@ -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'); +});