Skip to content

Commit

Permalink
docs: tags from path
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Dec 1, 2024
1 parent 3711166 commit 1e65a70
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/writing-features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

26 changes: 26 additions & 0 deletions docs/writing-features/tags-from-path.md
Original file line number Diff line number Diff line change
@@ -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.
68 changes: 57 additions & 11 deletions docs/writing-steps/scoped.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
});
```

Expand All @@ -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
});
```
```

## 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
```
37 changes: 26 additions & 11 deletions src/steps/tags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import parseTagsExpression from '@cucumber/tag-expressions';
import path from 'node:path';
import { removeDuplicates } from '../utils';

type TagString = string | undefined;
Expand All @@ -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) || [];
}
18 changes: 16 additions & 2 deletions test/unit/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});

0 comments on commit 1e65a70

Please sign in to comment.