From 4d0629596b93d1161df90c4e86b4f66d60cff1a4 Mon Sep 17 00:00:00 2001 From: Vitaliy Potapov Date: Tue, 22 Oct 2024 10:31:03 +0400 Subject: [PATCH] new config option matchKeywords to enable keyword matching when searching for step definitions (#221) --- CHANGELOG.md | 5 +- eslint.config.mjs | 5 +- src/config/types.ts | 2 + src/features/helpers.ts | 8 +-- src/gen/bddMetaBuilder.ts | 38 ++++++------ src/gen/testFile.ts | 27 +++++++-- src/run/StepInvoker.ts | 53 ++++++++++------ src/run/bddFixtures/test.ts | 2 + src/steps/finder.ts | 60 +++++++++++++++++++ src/steps/registry.ts | 45 +++----------- src/utils/index.ts | 2 +- test/duplicate-steps/playwright.config.ts | 2 +- test/duplicate-steps/test.mjs | 37 +++++++----- test/i18n/playwright.config.ts | 4 +- .../features/fail/sample.feature | 4 ++ test/match-keywords/features/fail/steps.ts | 6 ++ .../features/success/sample.feature | 6 ++ test/match-keywords/features/success/steps.ts | 9 +++ test/match-keywords/package.json | 3 + test/match-keywords/playwright.config.ts | 29 +++++++++ test/match-keywords/test.mjs | 27 +++++++++ test/missing-steps/test.mjs | 6 +- 22 files changed, 269 insertions(+), 111 deletions(-) create mode 100644 src/steps/finder.ts create mode 100644 test/match-keywords/features/fail/sample.feature create mode 100644 test/match-keywords/features/fail/steps.ts create mode 100644 test/match-keywords/features/success/sample.feature create mode 100644 test/match-keywords/features/success/steps.ts create mode 100644 test/match-keywords/package.json create mode 100644 test/match-keywords/playwright.config.ts create mode 100644 test/match-keywords/test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index b9691f4b..d5ccf420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ ## 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)) -* new config option `missingSteps` to setup different behavior when missing step definitions detected ([#158](https://github.com/vitalets/playwright-bdd/issues/158)) -* make config option `quote` default to `"single"` to have less escapes in generated files +* 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 * increase minimal Playwright version from 1.35 to 1.38 ## 7.5.0 diff --git a/eslint.config.mjs b/eslint.config.mjs index 6bc01eac..604839b9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,7 +29,10 @@ export default [ 'no-console': 'error', 'no-undef': 0, 'no-empty-pattern': 0, - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, + ], '@typescript-eslint/no-require-imports': 0, }, }, diff --git a/src/config/types.ts b/src/config/types.ts index 9dd22a9f..d9c2beb6 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -52,6 +52,8 @@ export type BDDInputConfig = CucumberConfigDeprecated & { disableWarnings?: DisableWarningsConfig; /** Behavior for missing step definitions */ missingSteps?: 'fail-on-gen' | 'fail-on-run' | 'skip-scenario'; + /** Enables additional matching by keywords in step definitions */ + matchKeywords?: boolean; }; export type BDDConfig = BDDInputConfig & diff --git a/src/features/helpers.ts b/src/features/helpers.ts index b712fe68..286b426a 100644 --- a/src/features/helpers.ts +++ b/src/features/helpers.ts @@ -1,11 +1,7 @@ -import { Step, PickleStep } from '@cucumber/messages'; - -export function getStepTextWithKeyword(scenarioStep: Step | undefined, pickleStep: PickleStep) { - // fallback to empty keyword if scenario step is not provided - const keyword = scenarioStep?.keyword || ''; +export function getStepTextWithKeyword(keyword: string | undefined, pickleStepText: string) { // There is no full original step text in gherkin document. // Build it by concatenation of keyword and pickle text. // Cucumber html-formatter does the same: // https://github.com/cucumber/react-components/blob/27b02543a5d7abeded3410a58588ee4b493b4a8f/src/components/gherkin/GherkinStep.tsx#L114 - return `${keyword}${pickleStep.text}`; + return `${keyword || ''}${pickleStepText}`; } diff --git a/src/gen/bddMetaBuilder.ts b/src/gen/bddMetaBuilder.ts index ec27252d..b840f9c2 100644 --- a/src/gen/bddMetaBuilder.ts +++ b/src/gen/bddMetaBuilder.ts @@ -1,12 +1,13 @@ +/* eslint max-len: ['error', { code: 130 }] */ /** * Class to build and print meta - an object containing meta info about each test. * Tests are identified by special key constructed from title path. * * Example: * const bddFileMeta = { - * "Simple scenario": { pickleLocation: "3:10", tags: ["@foo"], pickleSteps: ["Given step"] }, - * "Scenario with examples|Example #1": { pickleLocation: "8:26", tags: [], pickleSteps: ["Given step"] }, - * "Rule 1|Scenario with examples|Example #1": { pickleLocation: "9:42", tags: [], pickleSteps: ["Given step"] }, + * "Simple scenario": { pickleLocation: "3:10", tags: ["@foo"], pickleSteps: [["Given ", "precondition"]] }, + * "Scenario with examples|Example #1": { pickleLocation: "8:26", tags: [], pickleSteps: [["Given ", "precondition"]] }, + * "Rule 1|Scenario with examples|Example #1": { pickleLocation: "9:42", tags: [], pickleSteps: [["Given ", "precondition"]] }, * }; */ @@ -17,7 +18,7 @@ import { stringifyLocation } from '../utils'; import { GherkinDocumentQuery } from '../features/documentQuery'; import { indent } from './formatter'; import { PickleWithLocation } from '../features/types'; -import { getStepTextWithKeyword } from '../features/helpers'; +import { KeywordType } from '../cucumber/keywordType'; const TEST_KEY_SEPARATOR = '|'; @@ -27,9 +28,15 @@ export type BddTestMeta = { pickleLocation: string; tags?: string[]; ownTags?: string[]; - pickleSteps: string[]; // array of step titles with keyword (including bg steps) + pickleSteps: BddStepMeta[]; // array of steps meta (including bg) }; +type BddStepMeta = [ + string /* original keyword*/, + KeywordType, + string /* step location 'line:col' */, +]; + type BddTestData = { pickle: PickleWithLocation; node: TestNode; @@ -37,7 +44,7 @@ type BddTestData = { export class BddMetaBuilder { private tests: BddTestData[] = []; - private pickleStepToScenarioStep = new Map(); + private stepsMap = new Map(); constructor(private gherkinDocumentQuery: GherkinDocumentQuery) {} @@ -53,10 +60,11 @@ export class BddMetaBuilder { this.tests.push({ node, pickle: pickles[0] }); } - registerStep(scenarioStep: messages.Step) { + registerStep(scenarioStep: messages.Step, keywordType: KeywordType) { this.gherkinDocumentQuery.getPickleSteps(scenarioStep.id).forEach((pickleStep) => { // map each pickle step to scenario step to get full original step later - this.pickleStepToScenarioStep.set(pickleStep, scenarioStep); + const stepLocation = stringifyLocation(scenarioStep.location); + this.stepsMap.set(pickleStep, [scenarioStep.keyword, keywordType, stepLocation]); }); } @@ -82,24 +90,20 @@ export class BddMetaBuilder { }); } - private buildTestKey(node: TestNode) { - // .slice(1) -> b/c we remove top describe title (it's same for all tests) - return node.titlePath.slice(1).join(TEST_KEY_SEPARATOR); - } - private buildTestMeta({ node, pickle }: BddTestData): BddTestMeta { return { pickleLocation: stringifyLocation(pickle.location), tags: node.tags.length ? node.tags : undefined, // todo: avoid duplication of tags and ownTags ownTags: node.ownTags.length ? node.ownTags : undefined, - pickleSteps: pickle.steps.map((pickleStep) => this.buildStepTitleWithKeyword(pickleStep)), + // all pickle steps should be registered to that moment + pickleSteps: pickle.steps.map((pickleStep) => this.stepsMap.get(pickleStep)!), }; } - private buildStepTitleWithKeyword(pickleStep: messages.PickleStep) { - const scenarioStep = this.pickleStepToScenarioStep.get(pickleStep); - return getStepTextWithKeyword(scenarioStep, pickleStep); + private buildTestKey(node: TestNode) { + // .slice(1) -> b/c we remove top describe title (it's same for all tests) + return node.titlePath.slice(1).join(TEST_KEY_SEPARATOR); } } diff --git a/src/gen/testFile.ts b/src/gen/testFile.ts index 2c8d1bff..4c3280cd 100644 --- a/src/gen/testFile.ts +++ b/src/gen/testFile.ts @@ -19,7 +19,7 @@ import { import path from 'node:path'; import { Formatter } from './formatter'; import { KeywordsMap, getKeywordsMap } from './i18n'; -import { throwIf } from '../utils'; +import { stringifyLocation, throwIf } from '../utils'; import parseTagsExpression from '@cucumber/tag-expressions'; import { TestNode } from './testNode'; import { isCucumberStyleStep, isDecorator } from '../steps/stepConfig'; @@ -30,7 +30,7 @@ import { BddMetaBuilder } from './bddMetaBuilder'; import { GherkinDocumentWithPickles } from '../features/types'; import { DecoratorSteps } from './decoratorSteps'; import { BDDConfig } from '../config/types'; -import { StepDefinition, findStepDefinition } from '../steps/registry'; +import { StepDefinition } from '../steps/registry'; import { KeywordType, mapStepsToKeywordTypes } from '../cucumber/keywordType'; import { ImportTestFromGuesser } from './importTestFrom'; import { isBddAutoInjectFixture } from '../run/bddFixtures/autoInject'; @@ -40,6 +40,8 @@ import { GherkinDocumentQuery } from '../features/documentQuery'; import { ExamplesTitleBuilder } from './examplesTitleBuilder'; import { MissingStep } from '../snippets/types'; import { getStepTextWithKeyword } from '../features/helpers'; +import { formatDuplicateStepsMessage, StepFinder } from '../steps/finder'; +import { exit } from '../utils/exit'; type TestFileOptions = { gherkinDocument: GherkinDocumentWithPickles; @@ -55,6 +57,7 @@ export class TestFile { private usedDecoratorFixtures = new Set(); private gherkinDocumentQuery: GherkinDocumentQuery; private bddMetaBuilder: BddMetaBuilder; + private stepFinder: StepFinder; public missingSteps: MissingStep[] = []; public featureUri: string; @@ -65,6 +68,7 @@ export class TestFile { this.gherkinDocumentQuery = new GherkinDocumentQuery(this.gherkinDocument); this.bddMetaBuilder = new BddMetaBuilder(this.gherkinDocumentQuery); this.featureUri = this.getFeatureUri(); + this.stepFinder = new StepFinder(options.config); } get gherkinDocument() { @@ -282,10 +286,10 @@ export class TestFile { const keywordType = stepToKeywordType.get(step)!; const keywordEng = this.getStepEnglishKeyword(step); testFixtureNames.add(keywordEng); - this.bddMetaBuilder.registerStep(step); + this.bddMetaBuilder.registerStep(step, keywordType); // pickleStep contains step text with inserted example values and argument const pickleStep = this.findPickleStep(step, outlineExampleRowId); - const stepDefinition = findStepDefinition(pickleStep.text, this.featureUri); + const stepDefinition = this.findStepDefinition(keywordType, step, pickleStep); if (!stepDefinition) { hasMissingSteps = true; return this.handleMissingStep(keywordEng, keywordType, pickleStep, step); @@ -338,6 +342,19 @@ export class TestFile { return { fixtures: testFixtureNames, lines, hasMissingSteps }; } + private findStepDefinition(keywordType: KeywordType, scenarioStep: Step, pickleStep: PickleStep) { + const stepDefinitions = this.stepFinder.findDefinitions(keywordType, pickleStep.text); + + if (stepDefinitions.length > 1) { + const stepTextWithKeyword = getStepTextWithKeyword(scenarioStep.keyword, pickleStep.text); + const stepLocation = `${this.featureUri}:${stringifyLocation(scenarioStep.location)}`; + // todo: maybe not exit and collect all duplicates? + exit(formatDuplicateStepsMessage(stepDefinitions, stepTextWithKeyword, stepLocation)); + } + + return stepDefinitions[0]; + } + private handleMissingStep( keywordEng: StepKeyword, keywordType: KeywordType, @@ -347,7 +364,7 @@ export class TestFile { const { line, column } = step.location; this.missingSteps.push({ location: { uri: this.featureUri, line, column }, - textWithKeyword: getStepTextWithKeyword(step, pickleStep), + textWithKeyword: getStepTextWithKeyword(step.keyword, pickleStep.text), keywordType, pickleStep, }); diff --git a/src/run/StepInvoker.ts b/src/run/StepInvoker.ts index b7469729..4842a90c 100644 --- a/src/run/StepInvoker.ts +++ b/src/run/StepInvoker.ts @@ -6,10 +6,12 @@ import { PickleStepArgument } from '@cucumber/messages'; import { getLocationInFile } from '../playwright/getLocationInFile'; import { DataTable } from '../cucumber/DataTable'; import { getBddAutoInjectFixtures } from './bddFixtures/autoInject'; -import { StepDefinition, findStepDefinition } from '../steps/registry'; +import { StepDefinition } from '../steps/registry'; import { runStepWithLocation } from '../playwright/runStepWithLocation'; import { BddContext } from './bddFixtures/test'; import { StepKeyword } from '../steps/types'; +import { formatDuplicateStepsMessage, StepFinder } from '../steps/finder'; +import { getStepTextWithKeyword } from '../features/helpers'; export type StepKeywordFixture = ReturnType; @@ -19,11 +21,10 @@ export function createStepInvoker(bddContext: BddContext, _keyword: StepKeyword) } class StepInvoker { - constructor( - private bddContext: BddContext, - // keyword in not needed now, b/c we can get all info about step from test meta - // private keyword: StepKeyword, - ) {} + private stepFinder: StepFinder; + constructor(private bddContext: BddContext) { + this.stepFinder = new StepFinder(bddContext.config); + } /** * Invokes particular step. @@ -37,13 +38,13 @@ class StepInvoker { this.bddContext.stepIndex++; this.bddContext.step.title = stepText; - const stepDefinition = this.getStepDefinition(stepText); + const stepDefinition = this.findStepDefinition(stepText); + const stepTextWithKeyword = this.getStepTextWithKeyword(stepText); // Get location of step call in generated test file. // This call must be exactly here to have correct call stack (before async calls) const location = getLocationInFile(this.bddContext.testInfo.file); - const stepTitleWithKeyword = this.getStepTitleWithKeyword(); const parameters = await this.getStepParameters( stepDefinition, stepText, @@ -54,7 +55,7 @@ class StepInvoker { this.bddContext.bddAnnotation?.registerStep(stepDefinition, stepText, location); - await runStepWithLocation(this.bddContext.test, stepTitleWithKeyword, location, () => { + await runStepWithLocation(this.bddContext.test, stepTextWithKeyword, location, () => { // Although pw-style does not expect usage of world / this in steps, // some projects request it for better migration process from cucumber. // Here, for pw-style we pass empty object as world. @@ -63,18 +64,27 @@ class StepInvoker { }); } - private getStepDefinition(text: string) { - const stepDefinition = findStepDefinition( - text, - // todo: change to feature uri - this.bddContext.testInfo.file, - ); + private findStepDefinition(stepText: string) { + const [_keyword, keywordType, stepLocation] = this.getStepMeta(); + const stepDefinitions = this.stepFinder.findDefinitions(keywordType, stepText); + + if (stepDefinitions.length === 1) return stepDefinitions[0]; + + const stepTextWithKeyword = this.getStepTextWithKeyword(stepText); + const fullStepLocation = `${this.bddContext.featureUri}:${stepLocation}`; - if (!stepDefinition) { - throw new Error(`Missing step: ${text}`); + if (stepDefinitions.length === 0) { + // todo: better error? + const message = `Missing step: ${stepTextWithKeyword}`; + throw new Error(message); } - return stepDefinition; + const message = formatDuplicateStepsMessage( + stepDefinitions, + stepTextWithKeyword, + fullStepLocation, + ); + throw new Error(message); } private async getStepParameters( @@ -99,7 +109,12 @@ class StepInvoker { return parameters; } - private getStepTitleWithKeyword() { + private getStepTextWithKeyword(stepText: string) { + const [keyword] = this.getStepMeta(); + return getStepTextWithKeyword(keyword, stepText); + } + + private getStepMeta() { const { stepIndex, bddTestMeta } = this.bddContext; return bddTestMeta.pickleSteps[stepIndex]; } diff --git a/src/run/bddFixtures/test.ts b/src/run/bddFixtures/test.ts index afbfe322..a2e93d87 100644 --- a/src/run/bddFixtures/test.ts +++ b/src/run/bddFixtures/test.ts @@ -49,6 +49,7 @@ export type BddFixturesTest = { export type BddContext = { config: BDDConfig; + featureUri: string; test: TestTypeCommon; testInfo: TestInfo; tags: string[]; @@ -83,6 +84,7 @@ export const test = base.extend({ await use({ config: $bddConfig, + featureUri: $uri, testInfo, test: $test, tags: $tags, diff --git a/src/steps/finder.ts b/src/steps/finder.ts new file mode 100644 index 00000000..0b08d68d --- /dev/null +++ b/src/steps/finder.ts @@ -0,0 +1,60 @@ +/** + * Finding step definitions. + */ +import { relativeToCwd } from '../utils/paths'; +import { StepDefinition, stepDefinitions } from './registry'; +import { BDDConfig } from '../config/types'; +import { KeywordType } from '../cucumber/keywordType'; + +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; + } + + private filterByText(steps: StepDefinition[], stepText: string) { + return steps.filter((step) => { + // todo: store result to reuse later (MatchedStepDefinition) + return Boolean(step.expression.match(stepText)); + }); + } + + private filterByKeyword(steps: StepDefinition[], keywordType: KeywordType) { + return steps.filter((step) => { + switch (step.keyword) { + case 'Unknown': + return true; + case 'Given': + return keywordType === 'precondition'; + case 'When': + return keywordType === 'event'; + case 'Then': + return keywordType === 'outcome'; + } + }); + } + + // todo: filterByTags +} + +export function formatDuplicateStepsMessage( + matchedSteps: StepDefinition[], + stepTextWithKeyword: string, + stepLocation: string, +) { + const stepLines = matchedSteps.map((step) => { + const file = step.uri ? relativeToCwd(step.uri) : ''; + return ` - ${step.keyword} '${step.patternString}' # ${file}:${step.line}`; + }); + + return [ + `Multiple step definitions found!`, + `Step: ${stepTextWithKeyword}`, + `File: ${stepLocation}`, + ...stepLines, + ].join('\n'); +} diff --git a/src/steps/registry.ts b/src/steps/registry.ts index 9293c11b..7f06c975 100644 --- a/src/steps/registry.ts +++ b/src/steps/registry.ts @@ -5,12 +5,17 @@ import { CucumberExpression, RegularExpression, Expression } from '@cucumber/cucumber-expressions'; import { parameterTypeRegistry } from './parameterTypes'; import { StepConfig } from './stepConfig'; -import { relativeToCwd } from '../utils/paths'; -import { exit } from '../utils/exit'; export type GherkinStepKeyword = 'Unknown' | 'Given' | 'When' | 'Then'; export type DefineStepPattern = string | RegExp; +export const stepDefinitions: StepDefinition[] = []; + +export function registerStepDefinition(stepConfig: StepConfig) { + const stepDefinition = new StepDefinition(stepConfig); + stepDefinitions.push(stepDefinition); +} + // todo: merge with StepConfig to have single class export class StepDefinition { #expression?: Expression; @@ -53,39 +58,3 @@ export class StepDefinition { return typeof this.pattern === 'string' ? this.pattern : this.pattern.source; } } - -export const stepDefinitions: StepDefinition[] = []; - -export function registerStepDefinition(stepConfig: StepConfig) { - const stepDefinition = new StepDefinition(stepConfig); - stepDefinitions.push(stepDefinition); -} - -// todo: don't call exit here, call it upper -export function findStepDefinition(stepText: string, featureFile: string) { - const matchedSteps = stepDefinitions.filter((step) => { - return Boolean(step.expression.match(stepText)); - }); - if (matchedSteps.length === 0) return; - if (matchedSteps.length > 1) { - exit(formatDuplicateStepsError(stepText, featureFile, matchedSteps)); - } - - return matchedSteps[0]; -} - -function formatDuplicateStepsError( - stepText: string, - featureFile: string, - matchedSteps: StepDefinition[], -) { - const stepLines = matchedSteps.map((step) => { - const file = step.uri ? relativeToCwd(step.uri) : ''; - return ` ${step.patternString} - ${file}:${step.line}`; - }); - - return [ - `Multiple step definitions matched for text: "${stepText}" (${featureFile})`, - ...stepLines, - ].join('\n'); -} diff --git a/src/utils/index.ts b/src/utils/index.ts index d498b555..5f080092 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -53,7 +53,7 @@ export async function callWithTimeout(fn: () => T, timeout?: number, timeoutM } export function stringifyLocation({ line, column }: { line: number; column?: number }) { - return `${line}:${column}`; + return `${line}:${column || 0}`; } export function omit(obj: T, key: K) { diff --git a/test/duplicate-steps/playwright.config.ts b/test/duplicate-steps/playwright.config.ts index a73273ef..cbcfe830 100644 --- a/test/duplicate-steps/playwright.config.ts +++ b/test/duplicate-steps/playwright.config.ts @@ -5,6 +5,7 @@ const PROJECTS = (process.env.PROJECTS || '').split(','); export default defineConfig({ projects: [ + // 'no-duplicates' project must be first as it's needed to run the second project in a worker ...(PROJECTS.includes('no-duplicates') ? [noDuplicates()] : []), ...(PROJECTS.includes('duplicate-regular-steps') ? [duplicateRegularSteps()] : []), ...(PROJECTS.includes('duplicate-decorator-steps') ? [duplicateDecoratorSteps()] : []), @@ -13,7 +14,6 @@ export default defineConfig({ function noDuplicates(): Project { return { - // this project must be first and is needed to run the second project in a worker name: 'no-duplicates', testDir: defineBddConfig({ outputDir: `.features-gen/no-duplicates`, diff --git a/test/duplicate-steps/test.mjs b/test/duplicate-steps/test.mjs index dd481c02..e95abd0b 100644 --- a/test/duplicate-steps/test.mjs +++ b/test/duplicate-steps/test.mjs @@ -7,15 +7,16 @@ import { } from '../_helpers/index.mjs'; const testDir = new TestDir(import.meta); -const featureFile = normalize('features/sample.feature'); test(`${testDir.name} (main thread - regular steps)`, () => { const error = [ - `Multiple step definitions matched for text: "duplicate step" (${featureFile})`, - ` duplicate step - ${normalize('steps/steps.ts')}:6`, - ` duplicate step - ${normalize('steps/steps.ts')}:7`, - ` duplicate step - ${normalize('steps/steps.ts')}:8`, - ].join('\n'); + `Multiple step definitions found`, + `Step: Given duplicate step`, + `File: ${normalize('features/sample.feature:5:5')}`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:6`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:7`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:8`, + ]; execPlaywrightTestWithError(testDir.name, error, { cmd: BDDGEN_CMD, env: { PROJECTS: 'duplicate-regular-steps' }, @@ -24,11 +25,13 @@ test(`${testDir.name} (main thread - regular steps)`, () => { test(`${testDir.name} (main thread - decorator steps)`, () => { const error = [ - `Multiple step definitions matched for text: "duplicate decorator step" (${featureFile})`, - ` duplicate decorator step - ${normalize('steps/TodoPage.ts')}:7`, - ` duplicate decorator step - ${normalize('steps/TodoPage.ts')}:10`, - ` duplicate decorator step - ${normalize('steps/TodoPage.ts')}:13`, - ].join('\n'); + `Multiple step definitions found`, + `Step: Given duplicate decorator step`, + `File: ${normalize('features/sample.feature:9:5')}`, + ` - Given 'duplicate decorator step' # ${normalize('steps/TodoPage.ts')}:7`, + ` - Given 'duplicate decorator step' # ${normalize('steps/TodoPage.ts')}:10`, + ` - Given 'duplicate decorator step' # ${normalize('steps/TodoPage.ts')}:13`, + ]; execPlaywrightTestWithError(testDir.name, error, { cmd: BDDGEN_CMD, env: { PROJECTS: 'duplicate-decorator-steps' }, @@ -37,11 +40,13 @@ test(`${testDir.name} (main thread - decorator steps)`, () => { test(`${testDir.name} (worker - regular steps)`, () => { const error = [ - `Multiple step definitions matched for text: "duplicate step" (${featureFile})`, - ` duplicate step - ${normalize('steps/steps.ts')}:6`, - ` duplicate step - ${normalize('steps/steps.ts')}:7`, - ` duplicate step - ${normalize('steps/steps.ts')}:8`, - ].join('\n'); + `Multiple step definitions found`, + `Step: Given duplicate step`, + `File: ${normalize('features/sample.feature:5:5')}`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:6`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:7`, + ` - Given 'duplicate step' # ${normalize('steps/steps.ts')}:8`, + ]; execPlaywrightTestWithError(testDir.name, error, { cmd: BDDGEN_CMD, env: { PROJECTS: 'no-duplicates,duplicate-regular-steps' }, diff --git a/test/i18n/playwright.config.ts b/test/i18n/playwright.config.ts index e08a3835..07ab326c 100644 --- a/test/i18n/playwright.config.ts +++ b/test/i18n/playwright.config.ts @@ -3,8 +3,8 @@ import { defineBddConfig, cucumberReporter } from 'playwright-bdd'; const testDir = defineBddConfig({ language: 'ru', - paths: ['features/*.feature'], - require: ['steps/*.ts'], + features: 'features/*.feature', + steps: 'steps/*.ts', featuresRoot: 'features', }); diff --git a/test/match-keywords/features/fail/sample.feature b/test/match-keywords/features/fail/sample.feature new file mode 100644 index 00000000..e106b24e --- /dev/null +++ b/test/match-keywords/features/fail/sample.feature @@ -0,0 +1,4 @@ +Feature: match keywords + + Scenario: scenario 1 + Given duplicate step 1 diff --git a/test/match-keywords/features/fail/steps.ts b/test/match-keywords/features/fail/steps.ts new file mode 100644 index 00000000..b3358057 --- /dev/null +++ b/test/match-keywords/features/fail/steps.ts @@ -0,0 +1,6 @@ +import { createBdd } from 'playwright-bdd'; + +const { Given } = createBdd(); + +Given('duplicate step 1', async () => {}); +Given('duplicate step {int}', async () => {}); diff --git a/test/match-keywords/features/success/sample.feature b/test/match-keywords/features/success/sample.feature new file mode 100644 index 00000000..89b390c6 --- /dev/null +++ b/test/match-keywords/features/success/sample.feature @@ -0,0 +1,6 @@ +Feature: match keywords + + Scenario: scenario 1 + Given duplicate step + When duplicate step + Then duplicate step diff --git a/test/match-keywords/features/success/steps.ts b/test/match-keywords/features/success/steps.ts new file mode 100644 index 00000000..6120fdbd --- /dev/null +++ b/test/match-keywords/features/success/steps.ts @@ -0,0 +1,9 @@ +import { createBdd } from 'playwright-bdd'; + +const { Given, When, Then } = createBdd(); + +Given('unique step', async () => {}); + +Given('duplicate step', async () => {}); +When('duplicate step', async () => {}); +Then('duplicate step', async () => {}); diff --git a/test/match-keywords/package.json b/test/match-keywords/package.json new file mode 100644 index 00000000..de610253 --- /dev/null +++ b/test/match-keywords/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/match-keywords/playwright.config.ts b/test/match-keywords/playwright.config.ts new file mode 100644 index 00000000..f56ba459 --- /dev/null +++ b/test/match-keywords/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, Project } from '@playwright/test'; +import { defineBddProject } from 'playwright-bdd'; + +const PROJECT = process.env.PROJECT; + +export default defineConfig({ + projects: [ + ...(PROJECT === 'success' ? [successProject()] : []), + ...(PROJECT === 'fail' ? [failProject()] : []), + ], +}); + +function successProject(): Project { + return defineBddProject({ + name: 'success', + features: 'features/success', + steps: 'features/success', + matchKeywords: true, + }); +} + +function failProject(): Project { + return defineBddProject({ + name: 'fail', + features: 'features/fail', + steps: 'features/fail', + matchKeywords: true, + }); +} diff --git a/test/match-keywords/test.mjs b/test/match-keywords/test.mjs new file mode 100644 index 00000000..18051375 --- /dev/null +++ b/test/match-keywords/test.mjs @@ -0,0 +1,27 @@ +import { + test, + TestDir, + normalize, + execPlaywrightTest, + execPlaywrightTestWithError, +} from '../_helpers/index.mjs'; + +const testDir = new TestDir(import.meta); + +test(`${testDir.name} (success)`, () => { + execPlaywrightTest(testDir.name, { env: { PROJECT: 'success' } }); +}); + +test(`${testDir.name} (fail)`, () => { + execPlaywrightTestWithError( + testDir.name, + [ + `Multiple step definitions found`, // prettier-ignore + `Step: Given duplicate step 1`, + `File: ${normalize('features/fail/sample.feature:4:5')}`, + `Given 'duplicate step 1' # ${normalize('features/fail/steps.ts:5')}`, + `Given 'duplicate step {int}' # ${normalize('features/fail/steps.ts:6')}`, + ], + { env: { PROJECT: 'fail' } }, + ); +}); diff --git a/test/missing-steps/test.mjs b/test/missing-steps/test.mjs index 1a676049..fa559045 100644 --- a/test/missing-steps/test.mjs +++ b/test/missing-steps/test.mjs @@ -19,9 +19,9 @@ test(`${testDir.name} (fail-on-run)`, () => { execPlaywrightTestWithError( testDir.name, [ - `Error: Missing step: missing step 10`, // prettier-ignore - `Error: Missing step: missing step 20`, - `Error: Missing step: missing step 30`, + `Error: Missing step: Given missing step 10`, // prettier-ignore + `Error: Missing step: And missing step 20`, + `Error: Missing step: Given missing step 30`, ], { env: { MISSING_STEPS: 'fail-on-run' } }, );