From 1e65a70220cdc9637d662c36a8ecbca88456b2b3 Mon Sep 17 00:00:00 2001 From: Vitaliy Potapov Date: Sun, 1 Dec 2024 16:48:04 +0400 Subject: [PATCH] docs: tags from path --- docs/_sidebar.md | 1 + docs/writing-features/index.md | 2 +- docs/writing-features/tags-from-path.md | 26 ++++++++++ docs/writing-steps/scoped.md | 68 +++++++++++++++++++++---- src/steps/tags.ts | 37 ++++++++++---- test/unit/test.mjs | 18 ++++++- 6 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 docs/writing-features/tags-from-path.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 34b85028..ba8a0b3c 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -10,6 +10,7 @@ * [**Writing features**](writing-features/index.md) - [Special tags](writing-features/special-tags.md) + - [Tags from path](writing-features/tags-from-path.md) - [Localization](writing-features/i18n.md) - [Customize examples title](writing-features/customize-examples-title.md) - [Use ChatGPT](writing-features/chatgpt.md) diff --git a/docs/writing-features/index.md b/docs/writing-features/index.md index a727a803..d070235c 100644 --- a/docs/writing-features/index.md +++ b/docs/writing-features/index.md @@ -19,7 +19,7 @@ Tags allow to run subset of tests using `--tags` option with [tags expression](h npx bddgen --tags "@desktop and not @slow" && npx playwright test ``` -?> Since Playwright **1.42** Cucumber tags are mapped to [Playwright tags](https://playwright.dev/docs/test-annotations#tag-tests) +?> Since Playwright **1.42** Gherkin tags are mapped to [Playwright tags](https://playwright.dev/docs/test-annotations#tag-tests) Also you can [access tags inside step definitions](writing-steps/builtin-fixtures.md#tags). diff --git a/docs/writing-features/tags-from-path.md b/docs/writing-features/tags-from-path.md new file mode 100644 index 00000000..715054fc --- /dev/null +++ b/docs/writing-features/tags-from-path.md @@ -0,0 +1,26 @@ +# Tags from path + +**Tags from path** is a powerful way to automatically assign tags to features via `@`-prefixed directories or filenames. It allows to bind feature with step definitions without explicitly defining tags in the code. + +Example: +``` +features +├── @game +│ ├── game.feature +│ └── steps.ts +└── @video-player + ├── video-player.feature + └── steps.ts +``` + +Storing `game.feature` inside `@game` directory is equivalent to have that tag defined in the feature file: +```gherkin +@game # <- you don't need this tag is stored in '@game' directory +Feature: Game + + Scenario: Start playing + ... + When I click the PLAY button +``` + +Step definitions are also [automatically tagged and scoped](writing-steps/scoped.md#tags-from-path) to the related features. \ No newline at end of file diff --git a/docs/writing-steps/scoped.md b/docs/writing-steps/scoped.md index d495ea6c..f873f373 100644 --- a/docs/writing-steps/scoped.md +++ b/docs/writing-steps/scoped.md @@ -30,13 +30,13 @@ The implementation of the step is different for each feature: ```js // game.steps.js When('I click the PLAY button', async () => { - // actions for game + // actions for game.feature }); ``` ```js // video-player.steps.js When('I click the PLAY button', async () => { - // actions for video-player + // actions for video-player.feature }); ``` If I run the example as is, I will get an error: @@ -46,20 +46,20 @@ Step: When I click the PLAY button # game.feature:6:5 - When 'I click the PLAY button' # game.steps.js:5 - When 'I click the PLAY button' # video-player.steps.js:5 ``` -To solve the issue I can scope step definition to the corresponding feature by tags: +To solve the issue you can scope step definition to the corresponding feature by `tags`: ```js // game.steps.js When('I click the PLAY button', { tags: '@game' }, async () => { - // actions for game + // actions for game.feature }); ``` ```js // video-player.steps.js When('I click the PLAY button', { tags: '@video-player' }, async () => { - // actions for video-player + // actions for video-player.feature }); ``` -And set these tags in feature files: +And set these tags in the feature files: ```gherkin @game Feature: Game @@ -76,17 +76,17 @@ Feature: Video player ... When I click the PLAY button ``` -Now code runs. Each feature uses respective step definition without any conflicts. +Now code runs. Each feature uses respective step definition without conflicts. ## Default tags -You can provide default tags for step functions via `createBdd()`: +You can provide default tags for step definitions and hooks via `createBdd()` options: ```ts // game.steps.ts const { Given, When, Then } = createBdd(test, { tags: '@game' }); When('I click the PLAY button', async () => { - // actions for game + // actions for game.feature }); ``` @@ -95,6 +95,52 @@ When('I click the PLAY button', async () => { const { Given, When, Then } = createBdd(test, { tags: '@video-player' }); When('I click the PLAY button', async () => { - // actions for video-player + // actions for video-player.feature }); -``` \ No newline at end of file +``` + +## Tags from path +You can provide default tags for step definitions and hooks via `@`-prefixed directory or file names. It is a convenient way for binding your steps and features. + +Example: +``` +features +├── @game +│ ├── game.feature +│ └── steps.ts +└── @video-player + ├── video-player.feature + └── steps.ts +``` +This is equivalent of having `@game` tag explicitly defined in the `game.feature` and in all step definitions inside `@game/steps.ts`. With tagged directory you can omit tags from code - step definitions will be scoped automatically: +```ts +// @game/steps.ts + +When('I click the PLAY button', async () => { + // actions for game.feature +}); +``` + +You can also add shared steps, to be used in both features: + +``` +features +├── @game +│ ├── game.feature +│ └── steps.ts +├── @video-player +│ ├── video-player.feature +│ └── steps.ts +└── shared-steps.ts +``` + +You can use `@`-tagged filenames as well. It allows to store features and steps separately: + +``` +features +├── @game.feature +└── @video-player.feature +steps +├── @game.ts +└── @video-player.ts +``` diff --git a/src/steps/tags.ts b/src/steps/tags.ts index a73cefbf..59b8bb14 100644 --- a/src/steps/tags.ts +++ b/src/steps/tags.ts @@ -1,5 +1,4 @@ import parseTagsExpression from '@cucumber/tag-expressions'; -import path from 'node:path'; import { removeDuplicates } from '../utils'; type TagString = string | undefined; @@ -16,15 +15,31 @@ export function buildTagsExpression(tagStrings: TagString[]): TagsExpression | u } /** - * Extracts tags from a path. - * /path/to/@tag1/file.feature -> ['@tag1'] + * Extracts all '@'-prefixed tags from a given file path. + * + * @param {string} path - The file path from which to extract tags. + * @returns {string[]} An array of tags found in the path. + * + * The function uses a regular expression to match tags that start with an '@' symbol + * and are followed by any characters except for specific delimiters. The delimiters + * that signal the end of a tag are: + * - '@': Indicates the start of a new tag. + * - '/': Forward slash, used in file paths. + * - '\\': Backslash, used in Windows file paths. + * - Whitespace characters (spaces, tabs, etc.). + * - ',': Comma, used to separate tags. + * - '.': Dot, used in file extensions. + * + * This allows tags to include a wide range of characters, including symbols like ':', '-', + * and others, while ensuring they are properly extracted from the path. + * + * **Example Usage:** + * ```typescript + * extractTagsFromPath('features/@foo-bar/@baz:qux'); + * // Returns: ['@foo-bar', '@baz:qux'] + * ``` */ -export function extractTagsFromPath(somePath = '') { - const tags: string[] = []; - const parts = somePath.split(path.sep); - parts.forEach((part) => { - const partTags = part.split(/\s+/).filter((s) => s.length > 1 && s.startsWith('@')); - tags.push(...partTags); - }); - return tags; +export function extractTagsFromPath(path: string): string[] { + const regex = /@[^@/\\\s,.]+/g; + return path.match(regex) || []; } diff --git a/test/unit/test.mjs b/test/unit/test.mjs index 05ae73a1..1ba91d77 100644 --- a/test/unit/test.mjs +++ b/test/unit/test.mjs @@ -27,9 +27,23 @@ test(`${testDir.name} (extractTagsFromPath)`, async () => { const modulePath = path.resolve('dist/steps/tags.js'); const { extractTagsFromPath } = await import(pathToFileURL(modulePath)); - expect(extractTagsFromPath(normalize('features/@foo'))).toEqual(['@foo']); - expect(extractTagsFromPath(normalize('features/@foo/@bar'))).toEqual(['@foo', '@bar']); + // directories + expect(extractTagsFromPath(normalize('features/@foo/'))).toEqual(['@foo']); + expect(extractTagsFromPath(normalize('features/@foo/@bar/'))).toEqual(['@foo', '@bar']); expect(extractTagsFromPath(normalize('features/@foo @bar/'))).toEqual(['@foo', '@bar']); + expect(extractTagsFromPath(normalize('features/tag @foo and @bar/'))).toEqual(['@foo', '@bar']); + expect(extractTagsFromPath(normalize('features/auth@foo@bar/'))).toEqual(['@foo', '@bar']); + expect(extractTagsFromPath(normalize('features/@foo,@bar/'))).toEqual(['@foo', '@bar']); + expect(extractTagsFromPath(normalize('features/@foo,bar/'))).toEqual(['@foo']); + expect(extractTagsFromPath(normalize('features/@foo:123/'))).toEqual(['@foo:123']); + expect(extractTagsFromPath(normalize('features/'))).toEqual([]); + expect(extractTagsFromPath(normalize('features/@/'))).toEqual([]); + + // filename + expect(extractTagsFromPath(normalize('features/@foo.feature'))).toEqual(['@foo']); + expect(extractTagsFromPath(normalize('features/@foo.ts'))).toEqual(['@foo']); + expect(extractTagsFromPath(normalize('features/@foo.steps.ts'))).toEqual(['@foo']); + expect(extractTagsFromPath(normalize('features/.steps.ts'))).toEqual([]); expect(extractTagsFromPath(normalize('features/@'))).toEqual([]); });