Skip to content

Commit

Permalink
add default tags in createBdd()
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Nov 28, 2024
1 parent 9e9370d commit eec4e34
Show file tree
Hide file tree
Showing 19 changed files with 236 additions and 46 deletions.
15 changes: 6 additions & 9 deletions src/hooks/scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

/* eslint-disable max-depth */

import parseTagsExpression from '@cucumber/tag-expressions';
import { KeyValue, PlaywrightLocation, TestTypeCommon } from '../playwright/types';
import { fixtureParameterNames } from '../playwright/fixtureParameterNames';
import { callWithTimeout } from '../utils';
import { getLocationByOffset } from '../playwright/getLocationInFile';
import { runStepWithLocation } from '../playwright/runStepWithLocation';
import { BddContext } from '../runtime/bddTestFixtures';
import { getBddAutoInjectFixtures, isBddAutoInjectFixture } from '../runtime/bddTestFixturesAuto';
import { HookConstructorOptions, setTagsExpression } from './shared';
import { TagsExpression } from '../steps/tags';

export type ScenarioHookType = 'before' | 'after';

Expand All @@ -32,9 +33,10 @@ type ScenarioHook<Fixtures, World> = {
type: ScenarioHookType;
options: ScenarioHookOptions;
fn: ScenarioHookFn<Fixtures, World>;
tagsExpression?: ReturnType<typeof parseTagsExpression>;
tagsExpression?: TagsExpression;
location: PlaywrightLocation;
customTest?: TestTypeCommon;
defaultTags?: string;
};

/**
Expand Down Expand Up @@ -62,7 +64,7 @@ export function createBeforeAfter<
TestFixtures extends KeyValue,
WorkerFixtures extends KeyValue,
World,
>(type: ScenarioHookType, customTest: TestTypeCommon | undefined) {
>(type: ScenarioHookType, { customTest, defaultTags }: HookConstructorOptions) {
type AllFixtures = TestFixtures & WorkerFixtures;
type Args = ScenarioHookDefinitionArgs<AllFixtures, World>;

Expand All @@ -74,6 +76,7 @@ export function createBeforeAfter<
// offset = 3 b/c this call is 3 steps below the user's code
location: getLocationByOffset(3),
customTest,
defaultTags,
});
};
}
Expand Down Expand Up @@ -148,12 +151,6 @@ function getFnFromArgs(args: unknown[]) {
return args.length === 1 ? args[0] : args[1];
}

function setTagsExpression(hook: GeneralScenarioHook) {
if (hook.options.tags) {
hook.tagsExpression = parseTagsExpression(hook.options.tags);
}
}

function addHook(hook: GeneralScenarioHook) {
setTagsExpression(hook);
scenarioHooks.push(hook);
Expand Down
21 changes: 21 additions & 0 deletions src/hooks/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Shared stuff for worker and scenario hooks.
* todo: move more functions here.
*/
import { TestTypeCommon } from '../playwright/types';
import { buildTagsExpression } from '../steps/tags';
import { GeneralScenarioHook } from './scenario';
import { WorkerHook } from './worker';

/**
* Options passed when creating constructor for hooks.
*/
export type HookConstructorOptions = {
worldFixture?: string;
customTest?: TestTypeCommon;
defaultTags?: string;
};

export function setTagsExpression(hook: WorkerHook | GeneralScenarioHook) {
hook.tagsExpression = buildTagsExpression(hook.defaultTags, hook.options.tags);
}
15 changes: 6 additions & 9 deletions src/hooks/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
/* eslint-disable max-depth */

import { WorkerInfo } from '@playwright/test';
import parseTagsExpression from '@cucumber/tag-expressions';
import { fixtureParameterNames } from '../playwright/fixtureParameterNames';
import { KeyValue, PlaywrightLocation, TestTypeCommon } from '../playwright/types';
import { callWithTimeout } from '../utils';
import { getLocationByOffset } from '../playwright/getLocationInFile';
import { runStepWithLocation } from '../playwright/runStepWithLocation';
import { HookConstructorOptions, setTagsExpression } from './shared';
import { TagsExpression } from '../steps/tags';

export type WorkerHookType = 'beforeAll' | 'afterAll';

Expand All @@ -31,9 +32,10 @@ export type WorkerHook<Fixtures = object> = {
type: WorkerHookType;
options: WorkerHookOptions;
fn: WorkerHookFn<Fixtures>;
tagsExpression?: ReturnType<typeof parseTagsExpression>;
tagsExpression?: TagsExpression;
location: PlaywrightLocation;
customTest?: TestTypeCommon;
defaultTags?: string;
// Since playwright-bdd v8 we run worker hooks via test.beforeAll / test.afterAll in each test file,
// so we need to know if hook was executed to avoid double execution in every test file.
executed?: boolean;
Expand Down Expand Up @@ -63,7 +65,7 @@ const workerHooks: WorkerHook[] = [];
*/
export function createBeforeAllAfterAll<Fixtures extends KeyValue>(
type: WorkerHookType,
customTest: TestTypeCommon | undefined,
{ customTest, defaultTags }: HookConstructorOptions,
) {
type Args = WorkerHookDefinitionArgs<Fixtures>;
return (...args: Args) => {
Expand All @@ -74,6 +76,7 @@ export function createBeforeAllAfterAll<Fixtures extends KeyValue>(
// offset = 3 b/c this call is 3 steps below the user's code
location: getLocationByOffset(3),
customTest,
defaultTags,
});
};
}
Expand Down Expand Up @@ -161,9 +164,3 @@ function getTimeoutMessage(hook: WorkerHook) {
function getHookStepTitle(hook: WorkerHook) {
return hook.options.name || (hook.type === 'beforeAll' ? 'BeforeAll hook' : 'AfterAll hook');
}

function setTagsExpression(hook: WorkerHook) {
if (hook.options.tags) {
hook.tagsExpression = parseTagsExpression(hook.options.tags);
}
}
32 changes: 20 additions & 12 deletions src/steps/createBdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { BddWorkerFixtures } from '../runtime/bddWorkerFixtures';

type CreateBddOptions<WorldFixtureName> = {
worldFixture?: WorldFixtureName;
tags?: string;
};

type DefaultFixturesTest = PwBuiltInFixturesTest & BddTestFixtures;
Expand Down Expand Up @@ -58,10 +59,17 @@ export function createBdd<
if (customTest === (baseBddTest as TestTypeCommon)) customTest = undefined;
if (customTest) assertTestHasBddFixtures(customTest);

const BeforeAll = createBeforeAllAfterAll<W>('beforeAll', customTest);
const AfterAll = createBeforeAllAfterAll<W>('afterAll', customTest);
const Before = createBeforeAfter<T, W, World>('before', customTest);
const After = createBeforeAfter<T, W, World>('after', customTest);
// hooks and steps have the same constructor options
const ctorOptions = {
customTest,
worldFixture: options?.worldFixture,
defaultTags: options?.tags,
};

const BeforeAll = createBeforeAllAfterAll<W>('beforeAll', ctorOptions);
const AfterAll = createBeforeAllAfterAll<W>('afterAll', ctorOptions);
const Before = createBeforeAfter<T, W, World>('before', ctorOptions);
const After = createBeforeAfter<T, W, World>('after', ctorOptions);

// aliases
const [BeforeWorker, AfterWorker] = [BeforeAll, AfterAll];
Expand All @@ -72,10 +80,10 @@ export function createBdd<
if (!customTest) {
exit(`When using worldFixture, you should provide custom test to createBdd()`);
}
const Given = cucumberStepCtor('Given', customTest, options.worldFixture) as StepCtor;
const When = cucumberStepCtor('When', customTest, options.worldFixture) as StepCtor;
const Then = cucumberStepCtor('Then', customTest, options.worldFixture) as StepCtor;
const Step = cucumberStepCtor('Unknown', customTest, options.worldFixture) as StepCtor;
const Given = cucumberStepCtor('Given', ctorOptions) as StepCtor;
const When = cucumberStepCtor('When', ctorOptions) as StepCtor;
const Then = cucumberStepCtor('Then', ctorOptions) as StepCtor;
const Step = cucumberStepCtor('Unknown', ctorOptions) as StepCtor;
return {
Given,
When,
Expand All @@ -93,10 +101,10 @@ export function createBdd<
}

// playwright-style
const Given = playwrightStepCtor('Given', customTest) as StepCtor;
const When = playwrightStepCtor('When', customTest) as StepCtor;
const Then = playwrightStepCtor('Then', customTest) as StepCtor;
const Step = playwrightStepCtor('Unknown', customTest) as StepCtor;
const Given = playwrightStepCtor('Given', ctorOptions) as StepCtor;
const When = playwrightStepCtor('When', ctorOptions) as StepCtor;
const Then = playwrightStepCtor('Then', ctorOptions) as StepCtor;
const Step = playwrightStepCtor('Unknown', ctorOptions) as StepCtor;

return {
BeforeAll,
Expand Down
14 changes: 5 additions & 9 deletions src/steps/stepDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import { CucumberExpression, RegularExpression, Expression } from '@cucumber/cucumber-expressions';
import { PickleStepType } from '@cucumber/messages';
import parseTagsExpression from '@cucumber/tag-expressions';
import { parameterTypeRegistry } from './parameterTypes';
import { PlaywrightLocation, TestTypeCommon } from '../playwright/types';
import { PomNode } from './decorators/pomGraph';
import { MatchedStepDefinition } from './matchedStepDefinition';
import { buildTagsExpression, TagsExpression } from './tags';

export type GherkinStepKeyword = 'Unknown' | 'Given' | 'When' | 'Then';
export type StepPattern = string | RegExp;
Expand All @@ -26,12 +26,12 @@ export type StepDefinitionOptions = {
pomNode?: PomNode; // for decorator steps
worldFixture?: string; // for new cucumber-style steps
providedOptions?: ProvidedStepOptions; // options passed as second argument
defaultTags?: string; // tags from createBdd() or @Fixture
defaultTags?: string; // default tags from createBdd() or @Fixture
};

export class StepDefinition {
#expression?: Expression;
#tagsExpression?: ReturnType<typeof parseTagsExpression>;
#tagsExpression?: TagsExpression;

constructor(private options: StepDefinitionOptions) {
this.buildTagsExpression();
Expand Down Expand Up @@ -130,11 +130,7 @@ export class StepDefinition {
}

private buildTagsExpression() {
const { defaultTags } = this.options;
const tags = this.options.providedOptions?.tags;
const allTags = [defaultTags, tags].filter(Boolean);
if (!allTags.length) return;
const tagsString = allTags.map((tag) => `(${tag})`).join(' and ');
this.#tagsExpression = parseTagsExpression(tagsString);
const { defaultTags, providedOptions } = this.options;
this.#tagsExpression = buildTagsExpression(defaultTags, providedOptions?.tags);
}
}
7 changes: 3 additions & 4 deletions src/steps/styles/cucumberStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@
import { getLocationByOffset } from '../../playwright/getLocationInFile';
import { registerStepDefinition } from '../stepRegistry';
import { BddAutoInjectFixtures } from '../../runtime/bddTestFixturesAuto';
import { TestTypeCommon } from '../../playwright/types';
import { GherkinStepKeyword, StepDefinitionOptions } from '../stepDefinition';
import { parseStepDefinitionArgs, StepDefinitionArgs } from './shared';
import { parseStepDefinitionArgs, StepConstructorOptions, StepDefinitionArgs } from './shared';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CucumberStyleStepFn<World> = (this: World, ...args: any[]) => unknown;

export function cucumberStepCtor<StepFn extends StepDefinitionOptions['fn']>(
keyword: GherkinStepKeyword,
customTest: TestTypeCommon,
worldFixture: string,
{ customTest, worldFixture, defaultTags }: StepConstructorOptions,
) {
return (...args: StepDefinitionArgs<StepFn>) => {
const { pattern, providedOptions, fn } = parseStepDefinitionArgs(args);
Expand All @@ -27,6 +25,7 @@ export function cucumberStepCtor<StepFn extends StepDefinitionOptions['fn']>(
location: getLocationByOffset(3),
customTest,
worldFixture,
defaultTags,
providedOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: ({ $bddContext }: BddAutoInjectFixtures, ...args: any[]) => {
Expand Down
7 changes: 4 additions & 3 deletions src/steps/styles/playwrightStyle.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* Playwright-style steps.
*/
import { KeyValue, TestTypeCommon } from '../../playwright/types';
import { KeyValue } from '../../playwright/types';
import { fixtureParameterNames } from '../../playwright/fixtureParameterNames';
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';
import { parseStepDefinitionArgs, StepConstructorOptions, StepDefinitionArgs } from './shared';

export type PlaywrightStyleStepFn<T extends KeyValue, W extends KeyValue> = (
fixtures: T & W,
Expand All @@ -16,7 +16,7 @@ export type PlaywrightStyleStepFn<T extends KeyValue, W extends KeyValue> = (

export function playwrightStepCtor<StepFn extends StepDefinitionOptions['fn']>(
keyword: GherkinStepKeyword,
customTest?: TestTypeCommon,
{ customTest, defaultTags }: StepConstructorOptions,
) {
return (...args: StepDefinitionArgs<StepFn>) => {
const { pattern, providedOptions, fn } = parseStepDefinitionArgs(args);
Expand All @@ -27,6 +27,7 @@ export function playwrightStepCtor<StepFn extends StepDefinitionOptions['fn']>(
fn,
location: getLocationByOffset(3),
customTest,
defaultTags,
providedOptions,
});

Expand Down
7 changes: 7 additions & 0 deletions src/steps/styles/shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TestTypeCommon } from '../../playwright/types';
import { ProvidedStepOptions, StepDefinitionOptions, StepPattern } from '../stepDefinition';

export type StepDefinitionArgs<StepFn extends StepDefinitionOptions['fn']> =
Expand All @@ -10,3 +11,9 @@ export function parseStepDefinitionArgs<StepFn extends StepDefinitionOptions['fn
const [pattern, providedOptions, fn] = args.length === 3 ? args : [args[0], {}, args[1]];
return { pattern, providedOptions, fn };
}

export type StepConstructorOptions = {
worldFixture?: string;
customTest?: TestTypeCommon;
defaultTags?: string;
};
14 changes: 14 additions & 0 deletions src/steps/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import parseTagsExpression from '@cucumber/tag-expressions';

type Tags = string | undefined;
export type TagsExpression = ReturnType<typeof parseTagsExpression>;

/**
* Combines several tags strings and build tags expression.
*/
export function buildTagsExpression(defaultTags: Tags, tags: Tags): TagsExpression | undefined {
const allTags = [defaultTags, tags].filter(Boolean);
if (allTags.length === 0) return;
const tagsString = allTags.map((tag) => `(${tag})`).join(' and ');
return parseTagsExpression(tagsString);
}
3 changes: 3 additions & 0 deletions test/scoped-steps-default-tags/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"description": "This file is required for Playwright to consider this dir as a <package-json dir>. It ensures to load 'playwright-bdd' from './test/node_modules/playwright-bdd' and output './test-results' here to avoid conflicts."
}
10 changes: 10 additions & 0 deletions test/scoped-steps-default-tags/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from '@playwright/test';
import { defineBddConfig } from 'playwright-bdd';

const testDir = defineBddConfig({
featuresRoot: process.env.FEATURES_ROOT!,
});

export default defineConfig({
testDir,
});
9 changes: 9 additions & 0 deletions test/scoped-steps-default-tags/scenario-hooks/sample.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Feature: sample 1

@scenario1
Scenario: scenario 1
Given a step

@scenario2
Scenario: scenario 2
Given a step
27 changes: 27 additions & 0 deletions test/scoped-steps-default-tags/scenario-hooks/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test as base, createBdd } from 'playwright-bdd';
import { withLog } from '../../_helpers/withLog';

export const test = withLog(base);

const { BeforeScenario, AfterScenario } = createBdd(test, { tags: '@scenario1' });
const { Given } = createBdd(test);

BeforeScenario(async ({ log }) => {
log(`BeforeScenario`);
});

BeforeScenario({ tags: 'not @scenario1' }, async ({}) => {
throw new Error(`Should not be called`);
});

AfterScenario(async ({ log }) => {
log(`AfterScenario`);
});

AfterScenario({ tags: 'not @scenario1' }, async ({}) => {
throw new Error(`Should not be called`);
});

Given('a step', async ({ log, $testInfo }) => {
log(`${$testInfo.title}: a step`);
});
9 changes: 9 additions & 0 deletions test/scoped-steps-default-tags/steps-with-tags/sample.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Feature: sample 1

@scenario1
Scenario: scenario 1
Given a step

@scenario2
Scenario: scenario 2
Given a step
Loading

0 comments on commit eec4e34

Please sign in to comment.