diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b2c79d..def36111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## dev +* feature: show step locations in unused export and in duplicate steps error ([#113](https://github.com/vitalets/playwright-bdd/issues/113)) * feature: add `$step` fixture ([#133](https://github.com/vitalets/playwright-bdd/issues/133)) * feature: add experimental support of `steps` option (related to [#94](https://github.com/vitalets/playwright-bdd/issues/94)) diff --git a/src/cucumber/loadSteps.ts b/src/cucumber/loadSteps.ts index d64c408c..f8468e6b 100644 --- a/src/cucumber/loadSteps.ts +++ b/src/cucumber/loadSteps.ts @@ -1,7 +1,9 @@ import { IRunConfiguration, IRunEnvironment, loadSupport } from '@cucumber/cucumber/api'; import { installTransform } from '../playwright/transform'; import { exit } from '../utils/exit'; -import { ISupportCodeLibrary } from './types'; +import { ISupportCodeLibrary, StepDefinition } from './types'; +import { getStepConfig } from '../steps/stepConfig'; +import path from 'node:path'; const cache = new Map>(); @@ -35,20 +37,15 @@ export async function loadSteps( export function findStepDefinition( supportCodeLibrary: ISupportCodeLibrary, stepText: string, - file: string, + featureFile: string, ) { const matchedSteps = supportCodeLibrary.stepDefinitions.filter((step) => { return step.matchesStepName(stepText); }); if (matchedSteps.length === 0) return; - if (matchedSteps.length > 1) - exit( - [ - `Multiple step definitions matched for text: "${stepText}" (${file})`, - // todo: print location of every step definition (as in cucumber) - ...matchedSteps.map((s) => ` ${s.pattern}`), - ].join('\n'), - ); + if (matchedSteps.length > 1) { + exit(formtDuplicateStepsError(stepText, featureFile, matchedSteps)); + } return matchedSteps[0]; } @@ -56,3 +53,24 @@ export function findStepDefinition( export function hasTsNodeRegister(runConfiguration: IRunConfiguration) { return runConfiguration.support.requireModules.includes('ts-node/register'); } + +function formtDuplicateStepsError( + stepText: string, + featureFile: string, + matchedSteps: StepDefinition[], +) { + const stepLines = matchedSteps.map(formatDuplicateStep); + return [ + `Multiple step definitions matched for text: "${stepText}" (${featureFile})`, + ...stepLines, + ].join('\n'); +} + +function formatDuplicateStep(step: StepDefinition) { + const { pattern } = step; + const patternText = typeof pattern === 'string' ? pattern : pattern.source; + const { location } = getStepConfig(step) || {}; + const file = location ? path.relative(process.cwd(), location.file) : ''; + const locationStr = location ? ` - ${file}:${location.line}` : ''; + return ` ${patternText}${locationStr}`; +} diff --git a/src/cucumber/types.ts b/src/cucumber/types.ts index 82abfa77..0db8c1de 100644 --- a/src/cucumber/types.ts +++ b/src/cucumber/types.ts @@ -7,6 +7,8 @@ import type { ParameterTypeRegistry } from '@cucumber/cucumber-expressions'; import type StepDefinition from '@cucumber/cucumber/lib/models/step_definition'; +export { StepDefinition }; + export interface ISupportCodeLibrary { parameterTypeRegistry: ParameterTypeRegistry; stepDefinitions: StepDefinition[]; diff --git a/test/duplicate-steps/features/one.feature b/test/duplicate-steps/features/one.feature deleted file mode 100644 index 8aae8845..00000000 --- a/test/duplicate-steps/features/one.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: unique steps - - Scenario: scenario 1 - Given unique step diff --git a/test/duplicate-steps/features/sample.feature b/test/duplicate-steps/features/sample.feature new file mode 100644 index 00000000..f457af1a --- /dev/null +++ b/test/duplicate-steps/features/sample.feature @@ -0,0 +1,13 @@ +Feature: duplicate steps + + @duplicate-regular-steps + Scenario: duplicate regular steps + Given duplicate step + + @duplicate-decorator-steps + Scenario: duplicate decorator steps + Given duplicate decorator step + + @no-duplicates + Scenario: no duplicates + Given unique step diff --git a/test/duplicate-steps/features/two.feature b/test/duplicate-steps/features/two.feature deleted file mode 100644 index 5607cf9b..00000000 --- a/test/duplicate-steps/features/two.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: duplicate steps - - Scenario: scenario 1 - Given duplicate step diff --git a/test/duplicate-steps/playwright.config.ts b/test/duplicate-steps/playwright.config.ts index 7c23ac51..12b5f200 100644 --- a/test/duplicate-steps/playwright.config.ts +++ b/test/duplicate-steps/playwright.config.ts @@ -1,25 +1,49 @@ -import { defineConfig } from '@playwright/test'; +import { defineConfig, Project } from '@playwright/test'; import { defineBddConfig } from 'playwright-bdd'; +const PROJECTS = (process.env.PROJECTS || '').split(','); + export default defineConfig({ projects: [ - { - name: 'no duplicate steps', - testDir: defineBddConfig({ - outputDir: `.features-gen/one`, - paths: ['features/one.feature'], - importTestFrom: 'steps/fixtures.ts', - }), - }, - { - // important to have duplicate steps in the second project - // that runs in a worker process - name: 'duplicates', - testDir: defineBddConfig({ - outputDir: `.features-gen/two`, - paths: ['features/two.feature'], - importTestFrom: 'steps/fixtures.ts', - }), - }, + ...(PROJECTS.includes('no-duplicates') ? [noDuplicates()] : []), + ...(PROJECTS.includes('duplicate-regular-steps') ? [duplicateRegularSteps()] : []), + ...(PROJECTS.includes('duplicate-decorator-steps') ? [duplicateDecoratorSteps()] : []), ], }); + +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`, + paths: ['features/*.feature'], + require: ['steps/steps.ts'], + tags: '@no-duplicates', + }), + }; +} + +function duplicateRegularSteps(): Project { + return { + name: 'duplicate-regular-steps', + testDir: defineBddConfig({ + outputDir: `.features-gen/regular`, + paths: ['features/*.feature'], + require: ['steps/steps.ts'], + tags: '@duplicate-regular-steps', + }), + }; +} + +function duplicateDecoratorSteps(): Project { + return { + name: 'duplicate-decorator-steps', + testDir: defineBddConfig({ + outputDir: `.features-gen/decorator`, + paths: ['features/*.feature'], + importTestFrom: 'steps/fixtures.ts', + tags: '@duplicate-decorator-steps', + }), + }; +} diff --git a/test/duplicate-steps/steps/TodoPage.ts b/test/duplicate-steps/steps/TodoPage.ts index 5b602be5..fd22fe00 100644 --- a/test/duplicate-steps/steps/TodoPage.ts +++ b/test/duplicate-steps/steps/TodoPage.ts @@ -4,12 +4,12 @@ import { test } from './fixtures'; export @Fixture('todoPage') class TodoPage { - @Given('unique step') + @Given('duplicate decorator step') async step1() {} - @Given('duplicate step') + @Given('duplicate decorator step') async step2() {} - @Given('duplicate step') + @Given(/duplicate decorator step/) async step3() {} } diff --git a/test/duplicate-steps/steps/fixtures.ts b/test/duplicate-steps/steps/fixtures.ts index b99ea5e0..d89d58de 100644 --- a/test/duplicate-steps/steps/fixtures.ts +++ b/test/duplicate-steps/steps/fixtures.ts @@ -1,10 +1,6 @@ import { test as base } from 'playwright-bdd'; import { TodoPage } from './TodoPage'; -type Fixtures = { - todoPage: TodoPage; -}; - -export const test = base.extend({ +export const test = base.extend<{ todoPage: TodoPage }>({ todoPage: ({}, use) => use(new TodoPage()), }); diff --git a/test/duplicate-steps/steps/steps.ts b/test/duplicate-steps/steps/steps.ts new file mode 100644 index 00000000..1d20187a --- /dev/null +++ b/test/duplicate-steps/steps/steps.ts @@ -0,0 +1,8 @@ +import { createBdd } from 'playwright-bdd'; + +const { Given } = createBdd(); + +Given('unique step', async () => {}); +Given('duplicate step', async () => {}); +Given('duplicate step', async () => {}); +Given(/duplicate step/, async () => {}); diff --git a/test/duplicate-steps/test.mjs b/test/duplicate-steps/test.mjs index e3f99157..0d4c3991 100644 --- a/test/duplicate-steps/test.mjs +++ b/test/duplicate-steps/test.mjs @@ -1,24 +1,49 @@ -import { normalize } from 'node:path'; -import { test, TestDir, execPlaywrightTestWithError, DEFAULT_CMD } from '../_helpers/index.mjs'; +import { + test, + TestDir, + normalize, + execPlaywrightTestWithError, + BDDGEN_CMD, +} from '../_helpers/index.mjs'; const testDir = new TestDir(import.meta); +const featureFile = normalize('features/sample.feature'); -test(`${testDir.name} (main thread)`, () => { - execPlaywrightTestWithError( - testDir.name, - DUPLICATE_STEPS_ERROR, - `${DEFAULT_CMD} --project duplicates`, - ); +test(`${testDir.name} (main thread - regular steps)`, () => { + const error = [ + `Multiple step definitions matched for text: "duplicate step" (${featureFile})`, + ` duplicate step - steps/steps.ts:6`, + ` duplicate step - steps/steps.ts:7`, + ` duplicate step - steps/steps.ts:8`, + ].join('\n'); + execPlaywrightTestWithError(testDir.name, error, { + cmd: BDDGEN_CMD, + env: { PROJECTS: 'duplicate-regular-steps' }, + }); }); -test(`${testDir.name} (worker)`, () => { - execPlaywrightTestWithError(testDir.name, DUPLICATE_STEPS_ERROR); +test(`${testDir.name} (main thread - decorator steps)`, () => { + const error = [ + `Multiple step definitions matched for text: "duplicate decorator step" (${featureFile})`, + ` duplicate decorator step - steps/TodoPage.ts:7`, + ` duplicate decorator step - steps/TodoPage.ts:10`, + ` duplicate decorator step - steps/TodoPage.ts:13`, + ].join('\n'); + execPlaywrightTestWithError(testDir.name, error, { + cmd: BDDGEN_CMD, + env: { PROJECTS: 'duplicate-decorator-steps' }, + }); }); -const DUPLICATE_STEPS_ERROR = [ - `Multiple step definitions matched for text: "duplicate step" (${normalize( - 'features/two.feature', - )})`, - ' duplicate step', - ' duplicate step', -].join('\n'); +test(`${testDir.name} (worker - regular steps)`, () => { + const error = [ + `Multiple step definitions matched for text: "duplicate step" (${featureFile})`, + ` duplicate step - steps/steps.ts:6`, + ` duplicate step - steps/steps.ts:7`, + ` duplicate step - steps/steps.ts:8`, + ].join('\n'); + execPlaywrightTestWithError(testDir.name, error, { + cmd: BDDGEN_CMD, + env: { PROJECTS: 'no-duplicates,duplicate-regular-steps' }, + }); +});