From ae7e1aa9ebe8dbe6f7b289a23fb3f7db99760cfb Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Thu, 13 Feb 2025 11:28:52 +0800 Subject: [PATCH 01/13] Before/AfterEach and Before/AfterStep hooks --- src/hooks/step.ts | 153 ++++++++++++++++++++++++++++++++++ src/runtime/bddStepInvoker.ts | 11 ++- src/steps/createBdd.ts | 7 ++ 3 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/hooks/step.ts diff --git a/src/hooks/step.ts b/src/hooks/step.ts new file mode 100644 index 00000000..3974985b --- /dev/null +++ b/src/hooks/step.ts @@ -0,0 +1,153 @@ +/** + * Step level hooks: Before / After. + */ + +/* eslint-disable max-depth */ + +import { KeyValue, PlaywrightLocation, TestTypeCommon } from '../playwright/types'; +import { callWithTimeout } from '../utils'; +import { getLocationByOffset } from '../playwright/getLocationInFile'; +import { runStepWithLocation } from '../playwright/runStepWithLocation'; +import { getBddAutoInjectFixtures } from '../runtime/bddTestFixturesAuto'; +import { HookConstructorOptions, setTagsExpression } from './shared'; +import { TagsExpression } from '../steps/tags'; +import { BddContext } from '../runtime/bddContext'; + +type StepHookType = 'before' | 'after'; + +type StepHookOptions = { + name?: string; + tags?: string; + timeout?: number; +}; + +type StepHookFixtures = { + $bddContext: BddContext; + [key: string]: unknown; +}; + +type StepHookFn = (this: World, fixtures: Fixtures) => unknown; + +type StepHook = { + type: StepHookType; + options: StepHookOptions; + fn: StepHookFn; + tagsExpression?: TagsExpression; + location: PlaywrightLocation; // absolute path to hook location, line and col + customTest?: TestTypeCommon; + defaultTags?: string; + worldFixture?: string; +}; + +/** + * When calling Before() / After() you can pass: + * 1. hook fn + * 2. tags string + hook fn + * 3. options object + hook fn + * + * See: https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/api_reference.md#afteroptions-fn + */ +type StepHookDefinitionArgs = + | [StepHookFn] + | [NonNullable, StepHookFn] + | [StepHookOptions, StepHookFn]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GeneralStepHook = StepHook; + +const stepHooks: GeneralStepHook[] = []; + +/** + * Returns Before() / After() functions. + */ +export function stepHookFactory< + TestFixtures extends KeyValue, + WorkerFixtures extends KeyValue, + World, +>(type: StepHookType, { customTest, defaultTags, worldFixture }: HookConstructorOptions) { + type AllFixtures = TestFixtures & WorkerFixtures; + type Args = StepHookDefinitionArgs; + + return (...args: Args) => { + addHook({ + type, + options: getOptionsFromArgs(args) as StepHookOptions, + fn: getFnFromArgs(args) as StepHookFn, + // offset = 3 b/c this call is 3 steps below the user's code + location: getLocationByOffset(3), + customTest, + defaultTags, + worldFixture, + }); + }; +} + +// eslint-disable-next-line visual/complexity +export async function runStepHooks(hooks: GeneralStepHook[], fixtures: StepHookFixtures) { + let error; + for (const hook of hooks) { + try { + await runStepHook(hook, fixtures); + } catch (e) { + if (hook.type === 'before') throw e; + if (!error) error = e; + } + } + if (error) throw error; +} + +async function runStepHook(hook: GeneralStepHook, fixtures: StepHookFixtures) { + const fn = wrapHookFnWithTimeout(hook, fixtures); + const stepTitle = getHookStepTitle(hook); + await runStepWithLocation(fixtures.$bddContext.test, stepTitle, hook.location, fn); +} + +export function getStepHooksToRun(type: StepHookType, tags: string[] = []) { + return stepHooks + .filter((hook) => hook.type === type) + .filter((hook) => !hook.tagsExpression || hook.tagsExpression.evaluate(tags)); +} + +/** + * Wraps hook fn with timeout. + */ +function wrapHookFnWithTimeout(hook: GeneralStepHook, fixtures: StepHookFixtures) { + const { timeout } = hook.options; + const { $bddContext } = fixtures; + const fixturesArg = { + ...fixtures, + ...getBddAutoInjectFixtures($bddContext), + }; + + return async () => { + await callWithTimeout( + () => hook.fn.call($bddContext.world, fixturesArg), + timeout, + getTimeoutMessage(hook), + ); + }; +} + +function getOptionsFromArgs(args: unknown[]) { + if (typeof args[0] === 'string') return { tags: args[0] }; + if (typeof args[0] === 'object') return args[0]; + return {}; +} + +function getFnFromArgs(args: unknown[]) { + return args.length === 1 ? args[0] : args[1]; +} + +function addHook(hook: GeneralStepHook) { + setTagsExpression(hook); + stepHooks.push(hook); +} + +function getTimeoutMessage(hook: GeneralStepHook) { + const { timeout, name: hookName } = hook.options; + return `${hook.type} hook ${hookName ? `"${hookName}" ` : ''}timeout (${timeout} ms)`; +} + +function getHookStepTitle(hook: GeneralStepHook) { + return hook.options.name ?? (hook.type === 'before' ? 'BeforeEach hook' : 'AfterEach hook'); +} diff --git a/src/runtime/bddStepInvoker.ts b/src/runtime/bddStepInvoker.ts index e4dd89b5..9c2156c8 100644 --- a/src/runtime/bddStepInvoker.ts +++ b/src/runtime/bddStepInvoker.ts @@ -10,6 +10,7 @@ import { runStepWithLocation } from '../playwright/runStepWithLocation'; import { formatDuplicateStepsMessage, StepFinder } from '../steps/finder'; import { MatchedStepDefinition } from '../steps/matchedStepDefinition'; import { BddContext } from './bddContext'; +import { getStepHooksToRun, runStepHooks } from '../hooks/step'; export type BddStepFn = BddStepInvoker['invoke']; @@ -46,16 +47,22 @@ export class BddStepInvoker { const fixturesArg = this.getStepFixtures(stepFixtures || {}); - await runStepWithLocation(this.bddContext.test, stepTextWithKeyword, location, () => { + await runStepWithLocation(this.bddContext.test, stepTextWithKeyword, location, async () => { // 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. // See: https://github.com/vitalets/playwright-bdd/issues/208 - return matchedDefinition.definition.fn.call( + const beforeHooksToRun = getStepHooksToRun('before', this.bddContext.tags); + await runStepHooks(beforeHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); + const use = await matchedDefinition.definition.fn.call( this.bddContext.world, fixturesArg, ...stepParameters, ); + const afterHooksToRun = getStepHooksToRun('after', this.bddContext.tags); + afterHooksToRun.reverse(); + await runStepHooks(afterHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); + return use; }); } diff --git a/src/steps/createBdd.ts b/src/steps/createBdd.ts index 0f017d08..78369f94 100644 --- a/src/steps/createBdd.ts +++ b/src/steps/createBdd.ts @@ -24,6 +24,7 @@ import { playwrightStepCtor, } from './styles/playwrightStyle'; import { BddWorkerFixtures } from '../runtime/bddWorkerFixtures'; +import { stepHookFactory } from '../hooks/step'; type CreateBddOptions = { worldFixture?: WorldFixtureName; @@ -79,6 +80,8 @@ export function createBdd< const AfterAll = workerHookFactory('afterAll', ctorOptions); const Before = scenarioHookFactory('before', ctorOptions); const After = scenarioHookFactory('after', ctorOptions); + const BeforeStep = stepHookFactory('before', ctorOptions); + const AfterStep = stepHookFactory('after', ctorOptions); // aliases const [BeforeWorker, AfterWorker] = [BeforeAll, AfterAll]; @@ -106,6 +109,8 @@ export function createBdd< AfterWorker, BeforeScenario, AfterScenario, + BeforeStep, + AfterStep, }; } @@ -128,6 +133,8 @@ export function createBdd< AfterWorker, BeforeScenario, AfterScenario, + BeforeStep, + AfterStep, }; } From 96186652fa5f3fc8e35c08274fd3141f5b55ad6c Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Thu, 13 Feb 2025 17:44:32 +0800 Subject: [PATCH 02/13] add tests and update step hook names --- src/hooks/step.ts | 2 +- src/runtime/bddStepInvoker.ts | 4 +- test/hooks-step/features/fixtures.ts | 24 +++++ test/hooks-step/features/sample.feature | 11 +++ test/hooks-step/features/scenarioHooks.ts | 26 ++++++ test/hooks-step/features/stepHooks.ts | 26 ++++++ test/hooks-step/features/steps.ts | 9 ++ test/hooks-step/features/workerHooks.ts | 9 ++ test/hooks-step/package.json | 3 + test/hooks-step/playwright.config.ts | 23 +++++ test/hooks-step/test.mjs | 108 ++++++++++++++++++++++ 11 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 test/hooks-step/features/fixtures.ts create mode 100644 test/hooks-step/features/sample.feature create mode 100644 test/hooks-step/features/scenarioHooks.ts create mode 100644 test/hooks-step/features/stepHooks.ts create mode 100644 test/hooks-step/features/steps.ts create mode 100644 test/hooks-step/features/workerHooks.ts create mode 100644 test/hooks-step/package.json create mode 100644 test/hooks-step/playwright.config.ts create mode 100644 test/hooks-step/test.mjs diff --git a/src/hooks/step.ts b/src/hooks/step.ts index 3974985b..e337d870 100644 --- a/src/hooks/step.ts +++ b/src/hooks/step.ts @@ -149,5 +149,5 @@ function getTimeoutMessage(hook: GeneralStepHook) { } function getHookStepTitle(hook: GeneralStepHook) { - return hook.options.name ?? (hook.type === 'before' ? 'BeforeEach hook' : 'AfterEach hook'); + return hook.options.name ?? (hook.type === 'before' ? 'BeforeStep hook' : 'AfterStep hook'); } diff --git a/src/runtime/bddStepInvoker.ts b/src/runtime/bddStepInvoker.ts index 9c2156c8..b1119c1d 100644 --- a/src/runtime/bddStepInvoker.ts +++ b/src/runtime/bddStepInvoker.ts @@ -54,7 +54,7 @@ export class BddStepInvoker { // See: https://github.com/vitalets/playwright-bdd/issues/208 const beforeHooksToRun = getStepHooksToRun('before', this.bddContext.tags); await runStepHooks(beforeHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); - const use = await matchedDefinition.definition.fn.call( + const result = await matchedDefinition.definition.fn.call( this.bddContext.world, fixturesArg, ...stepParameters, @@ -62,7 +62,7 @@ export class BddStepInvoker { const afterHooksToRun = getStepHooksToRun('after', this.bddContext.tags); afterHooksToRun.reverse(); await runStepHooks(afterHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); - return use; + return result; }); } diff --git a/test/hooks-step/features/fixtures.ts b/test/hooks-step/features/fixtures.ts new file mode 100644 index 00000000..dadc0023 --- /dev/null +++ b/test/hooks-step/features/fixtures.ts @@ -0,0 +1,24 @@ +import timers from 'node:timers/promises'; +import { createBdd } from 'playwright-bdd'; +import { testWithLog } from '../../_helpers/withLog'; + +type TestFixtures = { + testFixtureCommon: void; + testFixtureScenario1: void; +}; + +export const test = testWithLog.extend({ + testFixtureCommon: async ({ log }, use) => { + log(`testFixtureCommon setup`); + await use(); + }, + + testFixtureScenario1: async ({ log }, use) => { + // tiny delay to have always foo after bar + await timers.setTimeout(50); + log(`testFixtureScenario1 setup`); + await use(); + }, +}); + +export const { Given, Before, BeforeAll, After, AfterAll, BeforeStep, AfterStep } = createBdd(test); diff --git a/test/hooks-step/features/sample.feature b/test/hooks-step/features/sample.feature new file mode 100644 index 00000000..69a47d9c --- /dev/null +++ b/test/hooks-step/features/sample.feature @@ -0,0 +1,11 @@ +Feature: sample + + Background: bg + Given bg step + + @scenario1 + Scenario: scenario 1 + Given a step + + Scenario: scenario 2 + Given a step diff --git a/test/hooks-step/features/scenarioHooks.ts b/test/hooks-step/features/scenarioHooks.ts new file mode 100644 index 00000000..957bbdfd --- /dev/null +++ b/test/hooks-step/features/scenarioHooks.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Before, After } from './fixtures'; + +Before(async ({ log, testFixtureCommon }) => { + log(`Before 1`); +}); + +Before(async ({ log }) => { + log(`Before 2`); +}); + +Before({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { + log(`Before 3 (@scenario1)`); +}); + +After(async ({ log, testFixtureCommon }) => { + log(`After 1`); +}); + +After(async ({ log }) => { + log(`After 2`); +}); + +After({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { + log(`After 3 (@scenario1)`); +}); diff --git a/test/hooks-step/features/stepHooks.ts b/test/hooks-step/features/stepHooks.ts new file mode 100644 index 00000000..c3a883be --- /dev/null +++ b/test/hooks-step/features/stepHooks.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { BeforeStep, AfterStep } from './fixtures'; + +BeforeStep(async ({ log, testFixtureCommon }) => { + log(`BeforeStep 1`); +}); + +BeforeStep(async ({ log }) => { + log(`BeforeStep 2`); +}); + +BeforeStep({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { + log(`BeforeStep 3 (@scenario1)`); +}); + +AfterStep(async ({ log, testFixtureCommon }) => { + log(`AfterStep 1`); +}); + +AfterStep(async ({ log }) => { + log(`AfterStep 2`); +}); + +AfterStep({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { + log(`AfterStep 3 (@scenario1)`); +}); diff --git a/test/hooks-step/features/steps.ts b/test/hooks-step/features/steps.ts new file mode 100644 index 00000000..176fc242 --- /dev/null +++ b/test/hooks-step/features/steps.ts @@ -0,0 +1,9 @@ +import { Given } from './fixtures'; + +Given('bg step', async ({ log, $testInfo }) => { + log(`bg step of ${$testInfo.title}`); +}); + +Given('a step', async ({ log, $testInfo }) => { + log(`a step of ${$testInfo.title}`); +}); diff --git a/test/hooks-step/features/workerHooks.ts b/test/hooks-step/features/workerHooks.ts new file mode 100644 index 00000000..dfed5ed3 --- /dev/null +++ b/test/hooks-step/features/workerHooks.ts @@ -0,0 +1,9 @@ +import { BeforeAll, AfterAll } from './fixtures'; + +BeforeAll(async ({ log }) => { + log(`BeforeAll 1`); +}); + +AfterAll(async ({ log }) => { + log(`AfterAll 1`); +}); diff --git a/test/hooks-step/package.json b/test/hooks-step/package.json new file mode 100644 index 00000000..de610253 --- /dev/null +++ b/test/hooks-step/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/hooks-step/playwright.config.ts b/test/hooks-step/playwright.config.ts new file mode 100644 index 00000000..57a7234a --- /dev/null +++ b/test/hooks-step/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@playwright/test'; +import { defineBddConfig, cucumberReporter } from 'playwright-bdd'; + +const testDir = defineBddConfig({ + featuresRoot: 'features', +}); + +const workers = Number(process.env.WORKERS); +const fullyParallel = Boolean(process.env.FULLY_PARALLEL); + +export default defineConfig({ + testDir, + workers, + fullyParallel, + reporter: [ + cucumberReporter('message', { + outputFile: `actual-reports/report-workers-${workers}.jsonl`, + }), + cucumberReporter('html', { + outputFile: `actual-reports/report-workers-${workers}.html`, + }), + ], +}); diff --git a/test/hooks-step/test.mjs b/test/hooks-step/test.mjs new file mode 100644 index 00000000..cf4ba0d9 --- /dev/null +++ b/test/hooks-step/test.mjs @@ -0,0 +1,108 @@ +import { test, expectCalls, TestDir, execPlaywrightTest } from '../_helpers/index.mjs'; + +const testDir = new TestDir(import.meta); + +test(`${testDir.name} (1 worker)`, () => { + const stdout = execPlaywrightTest(testDir.name, { env: { WORKERS: 1 } }); + expectCalls('worker 0: ', stdout, [ + // scenario 1 + 'BeforeAll 1', + 'testFixtureCommon setup', + 'testFixtureScenario1 setup', + 'Before 1', + 'Before 2', + 'Before 3 (@scenario1)', + 'BeforeStep 1', + 'BeforeStep 2', + 'BeforeStep 3 (@scenario1)', + 'bg step of scenario 1', + 'AfterStep 3 (@scenario1)', + 'AfterStep 2', + 'AfterStep 1', + 'BeforeStep 1', + 'BeforeStep 2', + 'BeforeStep 3 (@scenario1)', + 'a step of scenario 1', + 'AfterStep 3 (@scenario1)', + 'AfterStep 2', + 'AfterStep 1', + 'After 3 (@scenario1)', + 'After 2', + 'After 1', + // scenario 2 + 'testFixtureCommon setup', + // Currently testFixtureScenario1 is executed for scenario 2 as well, + // b/c we have all scenario hooks fixtures in a single object ($beforeEachFixtures, ) + // todo: setup only needed fixtures for each test + 'testFixtureScenario1 setup', + 'Before 1', + 'Before 2', + 'BeforeStep 1', + 'BeforeStep 2', + 'bg step of scenario 2', + 'AfterStep 2', + 'AfterStep 1', + 'BeforeStep 1', + 'BeforeStep 2', + 'a step of scenario 2', + 'AfterStep 2', + 'AfterStep 1', + 'After 2', + 'After 1', + 'AfterAll 1', + ]); +}); + +test(`${testDir.name} (2 workers, fully-parallel)`, () => { + const stdout = execPlaywrightTest(testDir.name, { env: { WORKERS: 2, FULLY_PARALLEL: 1 } }); + + // worker 0 + expectCalls('worker 0: ', stdout, [ + 'BeforeAll 1', + 'testFixtureCommon setup', + 'testFixtureScenario1 setup', + 'Before 1', + 'Before 2', + 'Before 3 (@scenario1)', + 'BeforeStep 1', + 'BeforeStep 2', + 'BeforeStep 3 (@scenario1)', + 'bg step of scenario 1', + 'AfterStep 3 (@scenario1)', + 'AfterStep 2', + 'AfterStep 1', + 'BeforeStep 1', + 'BeforeStep 2', + 'BeforeStep 3 (@scenario1)', + 'a step of scenario 1', + 'AfterStep 3 (@scenario1)', + 'AfterStep 2', + 'AfterStep 1', + 'After 3 (@scenario1)', + 'After 2', + 'After 1', + 'AfterAll 1', + ]); + + // worker 1 + expectCalls('worker 1: ', stdout, [ + 'BeforeAll 1', + 'testFixtureCommon setup', + 'testFixtureScenario1 setup', + 'Before 1', + 'Before 2', + 'BeforeStep 1', + 'BeforeStep 2', + 'bg step of scenario 2', + 'AfterStep 2', + 'AfterStep 1', + 'BeforeStep 1', + 'BeforeStep 2', + 'a step of scenario 2', + 'AfterStep 2', + 'AfterStep 1', + 'After 2', + 'After 1', + 'AfterAll 1', + ]); +}); From e5451e44f40ed722bd2f485187f9798ec4c28a77 Mon Sep 17 00:00:00 2001 From: vitalets Date: Thu, 13 Feb 2025 15:16:11 +0400 Subject: [PATCH 03/13] test: simplify test/hooks-step --- test/hooks-step/features/fixtures.ts | 11 +-- test/hooks-step/features/sample.feature | 8 +- test/hooks-step/features/scenarioHooks.ts | 26 ------- test/hooks-step/features/stepHooks.ts | 8 +- test/hooks-step/features/steps.ts | 4 +- test/hooks-step/features/workerHooks.ts | 9 --- test/hooks-step/playwright.config.ts | 10 +-- test/hooks-step/test.mjs | 94 ++++++----------------- 8 files changed, 42 insertions(+), 128 deletions(-) delete mode 100644 test/hooks-step/features/scenarioHooks.ts delete mode 100644 test/hooks-step/features/workerHooks.ts diff --git a/test/hooks-step/features/fixtures.ts b/test/hooks-step/features/fixtures.ts index dadc0023..9e433233 100644 --- a/test/hooks-step/features/fixtures.ts +++ b/test/hooks-step/features/fixtures.ts @@ -4,7 +4,7 @@ import { testWithLog } from '../../_helpers/withLog'; type TestFixtures = { testFixtureCommon: void; - testFixtureScenario1: void; + testFixtureScenario2: void; }; export const test = testWithLog.extend({ @@ -13,12 +13,13 @@ export const test = testWithLog.extend({ await use(); }, - testFixtureScenario1: async ({ log }, use) => { - // tiny delay to have always foo after bar + // this fixture is used only by step hooks tagged with @scenario2 + testFixtureScenario2: async ({ log }, use) => { + // tiny delay to always setup this fixture after testFixtureCommon await timers.setTimeout(50); - log(`testFixtureScenario1 setup`); + log(`testFixtureScenario2 setup`); await use(); }, }); -export const { Given, Before, BeforeAll, After, AfterAll, BeforeStep, AfterStep } = createBdd(test); +export const { Given, BeforeStep, AfterStep } = createBdd(test); diff --git a/test/hooks-step/features/sample.feature b/test/hooks-step/features/sample.feature index 69a47d9c..c93368ba 100644 --- a/test/hooks-step/features/sample.feature +++ b/test/hooks-step/features/sample.feature @@ -3,9 +3,11 @@ Feature: sample Background: bg Given bg step - @scenario1 Scenario: scenario 1 - Given a step + Given step 1 + Then step 2 + @scenario2 Scenario: scenario 2 - Given a step + Given step 3 + Then step 4 diff --git a/test/hooks-step/features/scenarioHooks.ts b/test/hooks-step/features/scenarioHooks.ts deleted file mode 100644 index 957bbdfd..00000000 --- a/test/hooks-step/features/scenarioHooks.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Before, After } from './fixtures'; - -Before(async ({ log, testFixtureCommon }) => { - log(`Before 1`); -}); - -Before(async ({ log }) => { - log(`Before 2`); -}); - -Before({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { - log(`Before 3 (@scenario1)`); -}); - -After(async ({ log, testFixtureCommon }) => { - log(`After 1`); -}); - -After(async ({ log }) => { - log(`After 2`); -}); - -After({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { - log(`After 3 (@scenario1)`); -}); diff --git a/test/hooks-step/features/stepHooks.ts b/test/hooks-step/features/stepHooks.ts index c3a883be..bff343c0 100644 --- a/test/hooks-step/features/stepHooks.ts +++ b/test/hooks-step/features/stepHooks.ts @@ -9,8 +9,8 @@ BeforeStep(async ({ log }) => { log(`BeforeStep 2`); }); -BeforeStep({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { - log(`BeforeStep 3 (@scenario1)`); +BeforeStep({ tags: '@scenario2' }, async ({ log, testFixtureScenario2 }) => { + log(`BeforeStep 3 (@scenario2)`); }); AfterStep(async ({ log, testFixtureCommon }) => { @@ -21,6 +21,6 @@ AfterStep(async ({ log }) => { log(`AfterStep 2`); }); -AfterStep({ tags: '@scenario1' }, async ({ log, testFixtureScenario1 }) => { - log(`AfterStep 3 (@scenario1)`); +AfterStep({ tags: '@scenario2' }, async ({ log, testFixtureScenario2 }) => { + log(`AfterStep 3 (@scenario2)`); }); diff --git a/test/hooks-step/features/steps.ts b/test/hooks-step/features/steps.ts index 176fc242..2ff45489 100644 --- a/test/hooks-step/features/steps.ts +++ b/test/hooks-step/features/steps.ts @@ -4,6 +4,6 @@ Given('bg step', async ({ log, $testInfo }) => { log(`bg step of ${$testInfo.title}`); }); -Given('a step', async ({ log, $testInfo }) => { - log(`a step of ${$testInfo.title}`); +Given('step {int}', async ({ log }, n: number) => { + log(`step ${n}`); }); diff --git a/test/hooks-step/features/workerHooks.ts b/test/hooks-step/features/workerHooks.ts deleted file mode 100644 index dfed5ed3..00000000 --- a/test/hooks-step/features/workerHooks.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BeforeAll, AfterAll } from './fixtures'; - -BeforeAll(async ({ log }) => { - log(`BeforeAll 1`); -}); - -AfterAll(async ({ log }) => { - log(`AfterAll 1`); -}); diff --git a/test/hooks-step/playwright.config.ts b/test/hooks-step/playwright.config.ts index 57a7234a..604f2580 100644 --- a/test/hooks-step/playwright.config.ts +++ b/test/hooks-step/playwright.config.ts @@ -5,19 +5,15 @@ const testDir = defineBddConfig({ featuresRoot: 'features', }); -const workers = Number(process.env.WORKERS); -const fullyParallel = Boolean(process.env.FULLY_PARALLEL); - export default defineConfig({ testDir, - workers, - fullyParallel, + workers: 1, reporter: [ cucumberReporter('message', { - outputFile: `actual-reports/report-workers-${workers}.jsonl`, + outputFile: `actual-reports/report.jsonl`, }), cucumberReporter('html', { - outputFile: `actual-reports/report-workers-${workers}.html`, + outputFile: `actual-reports/report.html`, }), ], }); diff --git a/test/hooks-step/test.mjs b/test/hooks-step/test.mjs index cf4ba0d9..ebb41824 100644 --- a/test/hooks-step/test.mjs +++ b/test/hooks-step/test.mjs @@ -2,107 +2,57 @@ import { test, expectCalls, TestDir, execPlaywrightTest } from '../_helpers/inde const testDir = new TestDir(import.meta); -test(`${testDir.name} (1 worker)`, () => { - const stdout = execPlaywrightTest(testDir.name, { env: { WORKERS: 1 } }); +test(testDir.name, () => { + const stdout = execPlaywrightTest(testDir.name); expectCalls('worker 0: ', stdout, [ // scenario 1 - 'BeforeAll 1', - 'testFixtureCommon setup', - 'testFixtureScenario1 setup', - 'Before 1', - 'Before 2', - 'Before 3 (@scenario1)', + // TODO: commented, b/c now fixtures don't initialize + // 'testFixtureCommon setup', + 'BeforeStep 1', 'BeforeStep 2', - 'BeforeStep 3 (@scenario1)', 'bg step of scenario 1', - 'AfterStep 3 (@scenario1)', - 'AfterStep 2', - 'AfterStep 1', - 'BeforeStep 1', - 'BeforeStep 2', - 'BeforeStep 3 (@scenario1)', - 'a step of scenario 1', - 'AfterStep 3 (@scenario1)', 'AfterStep 2', 'AfterStep 1', - 'After 3 (@scenario1)', - 'After 2', - 'After 1', - // scenario 2 - 'testFixtureCommon setup', - // Currently testFixtureScenario1 is executed for scenario 2 as well, - // b/c we have all scenario hooks fixtures in a single object ($beforeEachFixtures, ) - // todo: setup only needed fixtures for each test - 'testFixtureScenario1 setup', - 'Before 1', - 'Before 2', + 'BeforeStep 1', 'BeforeStep 2', - 'bg step of scenario 2', + 'step 1', 'AfterStep 2', 'AfterStep 1', + 'BeforeStep 1', 'BeforeStep 2', - 'a step of scenario 2', + 'step 2', 'AfterStep 2', 'AfterStep 1', - 'After 2', - 'After 1', - 'AfterAll 1', - ]); -}); -test(`${testDir.name} (2 workers, fully-parallel)`, () => { - const stdout = execPlaywrightTest(testDir.name, { env: { WORKERS: 2, FULLY_PARALLEL: 1 } }); + // scenario 2 + // TODO: commented, b/c now fixtures don't initialize + // 'testFixtureScenario2 setup', - // worker 0 - expectCalls('worker 0: ', stdout, [ - 'BeforeAll 1', - 'testFixtureCommon setup', - 'testFixtureScenario1 setup', - 'Before 1', - 'Before 2', - 'Before 3 (@scenario1)', 'BeforeStep 1', 'BeforeStep 2', - 'BeforeStep 3 (@scenario1)', - 'bg step of scenario 1', - 'AfterStep 3 (@scenario1)', - 'AfterStep 2', - 'AfterStep 1', - 'BeforeStep 1', - 'BeforeStep 2', - 'BeforeStep 3 (@scenario1)', - 'a step of scenario 1', - 'AfterStep 3 (@scenario1)', + 'BeforeStep 3 (@scenario2)', + 'bg step of scenario 2', + 'AfterStep 3 (@scenario2)', 'AfterStep 2', 'AfterStep 1', - 'After 3 (@scenario1)', - 'After 2', - 'After 1', - 'AfterAll 1', - ]); - // worker 1 - expectCalls('worker 1: ', stdout, [ - 'BeforeAll 1', - 'testFixtureCommon setup', - 'testFixtureScenario1 setup', - 'Before 1', - 'Before 2', 'BeforeStep 1', 'BeforeStep 2', - 'bg step of scenario 2', + 'BeforeStep 3 (@scenario2)', + 'step 3', + 'AfterStep 3 (@scenario2)', 'AfterStep 2', 'AfterStep 1', + 'BeforeStep 1', 'BeforeStep 2', - 'a step of scenario 2', + 'BeforeStep 3 (@scenario2)', + 'step 4', + 'AfterStep 3 (@scenario2)', 'AfterStep 2', 'AfterStep 1', - 'After 2', - 'After 1', - 'AfterAll 1', ]); }); From 5b2b9bd127c78431df98f2c8e817c8451960723c Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Fri, 14 Feb 2025 13:53:45 +0800 Subject: [PATCH 04/13] update StepHookType to beforeStep/afterStep --- src/hooks/shared.ts | 3 ++- src/hooks/step.ts | 14 +++++++------- src/runtime/bddStepInvoker.ts | 4 ++-- src/steps/createBdd.ts | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/hooks/shared.ts b/src/hooks/shared.ts index 853df930..de71e738 100644 --- a/src/hooks/shared.ts +++ b/src/hooks/shared.ts @@ -6,6 +6,7 @@ import { TestTypeCommon } from '../playwright/types'; import { buildTagsExpression, extractTagsFromPath } from '../steps/tags'; import { relativeToCwd } from '../utils/paths'; import { GeneralScenarioHook } from './scenario'; +import { GeneralStepHook } from './step'; import { WorkerHook } from './worker'; /** @@ -17,7 +18,7 @@ export type HookConstructorOptions = { defaultTags?: string; }; -export function setTagsExpression(hook: WorkerHook | GeneralScenarioHook) { +export function setTagsExpression(hook: WorkerHook | GeneralScenarioHook | GeneralStepHook) { const { defaultTags, options, location } = hook; // Possibly, we should use relative to configDir const relFilePath = relativeToCwd(location.file); diff --git a/src/hooks/step.ts b/src/hooks/step.ts index e337d870..636f0b6c 100644 --- a/src/hooks/step.ts +++ b/src/hooks/step.ts @@ -1,5 +1,5 @@ /** - * Step level hooks: Before / After. + * Step level hooks: BeforeStep / AfterStep. */ /* eslint-disable max-depth */ @@ -13,7 +13,7 @@ import { HookConstructorOptions, setTagsExpression } from './shared'; import { TagsExpression } from '../steps/tags'; import { BddContext } from '../runtime/bddContext'; -type StepHookType = 'before' | 'after'; +type StepHookType = 'beforeStep' | 'afterStep'; type StepHookOptions = { name?: string; @@ -40,7 +40,7 @@ type StepHook = { }; /** - * When calling Before() / After() you can pass: + * When calling BeforeStep() / After() you can pass: * 1. hook fn * 2. tags string + hook fn * 3. options object + hook fn @@ -53,12 +53,12 @@ type StepHookDefinitionArgs = | [StepHookOptions, StepHookFn]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type GeneralStepHook = StepHook; +export type GeneralStepHook = StepHook; const stepHooks: GeneralStepHook[] = []; /** - * Returns Before() / After() functions. + * Returns BeforeStep() / AfterStep() functions. */ export function stepHookFactory< TestFixtures extends KeyValue, @@ -89,7 +89,7 @@ export async function runStepHooks(hooks: GeneralStepHook[], fixtures: StepHookF try { await runStepHook(hook, fixtures); } catch (e) { - if (hook.type === 'before') throw e; + if (hook.type === 'beforeStep') throw e; if (!error) error = e; } } @@ -149,5 +149,5 @@ function getTimeoutMessage(hook: GeneralStepHook) { } function getHookStepTitle(hook: GeneralStepHook) { - return hook.options.name ?? (hook.type === 'before' ? 'BeforeStep hook' : 'AfterStep hook'); + return hook.options.name ?? (hook.type === 'beforeStep' ? 'BeforeStep hook' : 'AfterStep hook'); } diff --git a/src/runtime/bddStepInvoker.ts b/src/runtime/bddStepInvoker.ts index b1119c1d..5035d5d8 100644 --- a/src/runtime/bddStepInvoker.ts +++ b/src/runtime/bddStepInvoker.ts @@ -52,14 +52,14 @@ export class BddStepInvoker { // some projects request it for better migration process from cucumber. // Here, for pw-style we pass empty object as world. // See: https://github.com/vitalets/playwright-bdd/issues/208 - const beforeHooksToRun = getStepHooksToRun('before', this.bddContext.tags); + const beforeHooksToRun = getStepHooksToRun('beforeStep', this.bddContext.tags); await runStepHooks(beforeHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); const result = await matchedDefinition.definition.fn.call( this.bddContext.world, fixturesArg, ...stepParameters, ); - const afterHooksToRun = getStepHooksToRun('after', this.bddContext.tags); + const afterHooksToRun = getStepHooksToRun('afterStep', this.bddContext.tags); afterHooksToRun.reverse(); await runStepHooks(afterHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); return result; diff --git a/src/steps/createBdd.ts b/src/steps/createBdd.ts index 78369f94..be2d5751 100644 --- a/src/steps/createBdd.ts +++ b/src/steps/createBdd.ts @@ -80,8 +80,8 @@ export function createBdd< const AfterAll = workerHookFactory('afterAll', ctorOptions); const Before = scenarioHookFactory('before', ctorOptions); const After = scenarioHookFactory('after', ctorOptions); - const BeforeStep = stepHookFactory('before', ctorOptions); - const AfterStep = stepHookFactory('after', ctorOptions); + const BeforeStep = stepHookFactory('beforeStep', ctorOptions); + const AfterStep = stepHookFactory('afterStep', ctorOptions); // aliases const [BeforeWorker, AfterWorker] = [BeforeAll, AfterAll]; From 8befae1bafb5c0e7dd3b07c049f5597544e15f62 Mon Sep 17 00:00:00 2001 From: vitalets Date: Fri, 14 Feb 2025 10:59:24 +0400 Subject: [PATCH 05/13] pass fixtures to step hooks --- src/generate/test/index.ts | 13 ++++ src/hooks/step.ts | 32 ++++++--- src/runtime/bddStepInvoker.ts | 35 +++++++--- test/_helpers/rawJsonReporter.ts | 4 ++ test/hooks-step/features/decorators.feature | 5 ++ .../{sample.feature => simple.feature} | 5 +- test/hooks-step/features/steps/TodoPage.ts | 15 +++++ .../features/{ => steps}/fixtures.ts | 6 +- .../features/{ => steps}/stepHooks.ts | 10 +-- test/hooks-step/features/{ => steps}/steps.ts | 3 + test/hooks-step/features/with-bg.feature | 7 ++ test/hooks-step/playwright.config.ts | 19 ++++-- test/hooks-step/test.mjs | 66 ++++++++++++++----- 13 files changed, 170 insertions(+), 50 deletions(-) create mode 100644 test/hooks-step/features/decorators.feature rename test/hooks-step/features/{sample.feature => simple.feature} (70%) create mode 100644 test/hooks-step/features/steps/TodoPage.ts rename test/hooks-step/features/{ => steps}/fixtures.ts (79%) rename test/hooks-step/features/{ => steps}/stepHooks.ts (53%) rename test/hooks-step/features/{ => steps}/steps.ts (70%) create mode 100644 test/hooks-step/features/with-bg.feature diff --git a/src/generate/test/index.ts b/src/generate/test/index.ts index 0aadb86f..c9782bf5 100644 --- a/src/generate/test/index.ts +++ b/src/generate/test/index.ts @@ -21,6 +21,7 @@ import { SpecialTags, } from '../specialTags'; import { DecoratorFixtureResolver } from './decoratorFixtureResolver'; +import { getStepHooksFixtureNames, getStepHooksToRun } from '../../hooks/step'; export type StepData = { pickleStep: PickleStep; @@ -44,6 +45,7 @@ export class TestGen { public skippedByTag: boolean; private skippedByMissingSteps = false; public slow: boolean; + private stepHooksFixtureNames: string[] = []; // eslint-disable-next-line max-params constructor( @@ -62,6 +64,7 @@ export class TestGen { this.specialTags = new SpecialTags(ownTestTags); this.skippedByTag = isTestSkippedByCollectedTags(this.tags); this.slow = isTestSlowByCollectedTags(this.tags); + this.fillStepHooksFixtureNames(); this.fillStepsData(); this.resolveFixtureNamesForDecoratorSteps(); } @@ -110,6 +113,7 @@ export class TestGen { const location = `${this.featureUri}:${stringifyLocation(gherkinStep.location)}`; const matchedDefinition = this.findMatchedDefinition(pickleStep, gherkinStep); const fixtureNames = this.getStepFixtureNames(matchedDefinition); + fixtureNames.push(...this.stepHooksFixtureNames); const pomNode = matchedDefinition?.definition.pomNode; const stepData: StepData = { pickleStep, @@ -126,6 +130,15 @@ export class TestGen { }); } + private fillStepHooksFixtureNames() { + const beforeStepHooksToRun = getStepHooksToRun('beforeStep', this.tags); + const afterStepHooksToRun = getStepHooksToRun('afterStep', this.tags); + this.stepHooksFixtureNames = getStepHooksFixtureNames([ + ...beforeStepHooksToRun, + ...afterStepHooksToRun, + ]); + } + private handleMissingDefinitions() { if ( !this.skippedByTag && diff --git a/src/hooks/step.ts b/src/hooks/step.ts index 636f0b6c..c6b45c84 100644 --- a/src/hooks/step.ts +++ b/src/hooks/step.ts @@ -8,10 +8,11 @@ import { KeyValue, PlaywrightLocation, TestTypeCommon } from '../playwright/type import { callWithTimeout } from '../utils'; import { getLocationByOffset } from '../playwright/getLocationInFile'; import { runStepWithLocation } from '../playwright/runStepWithLocation'; -import { getBddAutoInjectFixtures } from '../runtime/bddTestFixturesAuto'; +import { getBddAutoInjectFixtures, isBddAutoInjectFixture } from '../runtime/bddTestFixturesAuto'; import { HookConstructorOptions, setTagsExpression } from './shared'; import { TagsExpression } from '../steps/tags'; import { BddContext } from '../runtime/bddContext'; +import { fixtureParameterNames } from '../playwright/fixtureParameterNames'; type StepHookType = 'beforeStep' | 'afterStep'; @@ -96,10 +97,29 @@ export async function runStepHooks(hooks: GeneralStepHook[], fixtures: StepHookF if (error) throw error; } +// todo: join with getScenarioHooksFixtureNames(), +// make universal function getHooksFixtureNames() +export function getStepHooksFixtureNames(hooks: GeneralStepHook[]) { + const fixtureNames = new Set(); + + hooks.forEach((hook) => { + const hookFixtureNames = fixtureParameterNames(hook.fn); + hookFixtureNames.forEach((fixtureName) => fixtureNames.add(fixtureName)); + }); + + return [...fixtureNames].filter((name) => !isBddAutoInjectFixture(name)); +} + async function runStepHook(hook: GeneralStepHook, fixtures: StepHookFixtures) { - const fn = wrapHookFnWithTimeout(hook, fixtures); - const stepTitle = getHookStepTitle(hook); - await runStepWithLocation(fixtures.$bddContext.test, stepTitle, hook.location, fn); + const hookFn = wrapHookFnWithTimeout(hook, fixtures); + const stepTitle = hook.options.name; + // wrap hookFn call into test.step() only if user provided a name for the hook, + // otherwise run as is to avoid extra level in the steps structure. + if (stepTitle) { + await runStepWithLocation(fixtures.$bddContext.test, stepTitle, hook.location, hookFn); + } else { + await hookFn(); + } } export function getStepHooksToRun(type: StepHookType, tags: string[] = []) { @@ -147,7 +167,3 @@ function getTimeoutMessage(hook: GeneralStepHook) { const { timeout, name: hookName } = hook.options; return `${hook.type} hook ${hookName ? `"${hookName}" ` : ''}timeout (${timeout} ms)`; } - -function getHookStepTitle(hook: GeneralStepHook) { - return hook.options.name ?? (hook.type === 'beforeStep' ? 'BeforeStep hook' : 'AfterStep hook'); -} diff --git a/src/runtime/bddStepInvoker.ts b/src/runtime/bddStepInvoker.ts index 5035d5d8..63a326b7 100644 --- a/src/runtime/bddStepInvoker.ts +++ b/src/runtime/bddStepInvoker.ts @@ -5,7 +5,7 @@ import { PickleStepArgument } from '@cucumber/messages'; import { getLocationInFile } from '../playwright/getLocationInFile'; import { DataTable } from '../cucumber/DataTable'; -import { getBddAutoInjectFixtures } from './bddTestFixturesAuto'; +import { BddAutoInjectFixtures, getBddAutoInjectFixtures } from './bddTestFixturesAuto'; import { runStepWithLocation } from '../playwright/runStepWithLocation'; import { formatDuplicateStepsMessage, StepFinder } from '../steps/finder'; import { MatchedStepDefinition } from '../steps/matchedStepDefinition'; @@ -27,7 +27,7 @@ export class BddStepInvoker { async invoke( stepText: string, // step text without keyword argument?: PickleStepArgument | null, - stepFixtures?: Record, + providedFixtures?: Record, ) { this.bddContext.stepIndex++; this.bddContext.step.title = stepText; @@ -45,27 +45,40 @@ export class BddStepInvoker { argument || undefined, ); - const fixturesArg = this.getStepFixtures(stepFixtures || {}); + const stepHookFixtures = this.getStepHookFixtures(providedFixtures || {}); + const stepFixtures = this.getStepFixtures(providedFixtures || {}); await runStepWithLocation(this.bddContext.test, stepTextWithKeyword, location, async () => { // 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. // See: https://github.com/vitalets/playwright-bdd/issues/208 - const beforeHooksToRun = getStepHooksToRun('beforeStep', this.bddContext.tags); - await runStepHooks(beforeHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); + await this.runBeforeStepHooks(stepHookFixtures); const result = await matchedDefinition.definition.fn.call( this.bddContext.world, - fixturesArg, + stepFixtures, ...stepParameters, ); - const afterHooksToRun = getStepHooksToRun('afterStep', this.bddContext.tags); - afterHooksToRun.reverse(); - await runStepHooks(afterHooksToRun, { ...fixturesArg, $bddContext: this.bddContext }); + await this.runAfterStepHooks(stepHookFixtures); return result; }); } + private async runBeforeStepHooks( + stepHookFixtures: Record & BddAutoInjectFixtures, + ) { + const beforeHooksToRun = getStepHooksToRun('beforeStep', this.bddContext.tags); + await runStepHooks(beforeHooksToRun, stepHookFixtures); + } + + private async runAfterStepHooks( + stepHookFixtures: Record & BddAutoInjectFixtures, + ) { + const afterHooksToRun = getStepHooksToRun('afterStep', this.bddContext.tags); + afterHooksToRun.reverse(); + await runStepHooks(afterHooksToRun, stepHookFixtures); + } + private findStepDefinition(stepText: string, stepTextWithKeyword: string) { const { keywordType, gherkinStepLine } = this.getBddStepData(); const stepDefinitions = this.stepFinder.findDefinitions( @@ -123,6 +136,10 @@ export class BddStepInvoker { return Object.assign({}, providedFixtures, getBddAutoInjectFixtures(this.bddContext)); } + private getStepHookFixtures(providedFixtures: Record) { + return Object.assign({}, providedFixtures, getBddAutoInjectFixtures(this.bddContext)); + } + private getBddStepData() { const { stepIndex, bddTestData } = this.bddContext; const bddStepData = bddTestData.steps[stepIndex]; diff --git a/test/_helpers/rawJsonReporter.ts b/test/_helpers/rawJsonReporter.ts index 5ab76ce8..b370f711 100644 --- a/test/_helpers/rawJsonReporter.ts +++ b/test/_helpers/rawJsonReporter.ts @@ -24,6 +24,10 @@ export default class RawJsonReporter implements Reporter { fs.writeFileSync(filePath, content); } + printsToStdio() { + return false; + } + private buildFilePathForTest(test: TestCase) { const fileName = test .titlePath() diff --git a/test/hooks-step/features/decorators.feature b/test/hooks-step/features/decorators.feature new file mode 100644 index 00000000..d4770a72 --- /dev/null +++ b/test/hooks-step/features/decorators.feature @@ -0,0 +1,5 @@ +Feature: decorators + + Scenario: scenario 1 + Given decorator step 1 + Then decorator step 2 diff --git a/test/hooks-step/features/sample.feature b/test/hooks-step/features/simple.feature similarity index 70% rename from test/hooks-step/features/sample.feature rename to test/hooks-step/features/simple.feature index c93368ba..802d5c6b 100644 --- a/test/hooks-step/features/sample.feature +++ b/test/hooks-step/features/simple.feature @@ -1,7 +1,4 @@ -Feature: sample - - Background: bg - Given bg step +Feature: simple Scenario: scenario 1 Given step 1 diff --git a/test/hooks-step/features/steps/TodoPage.ts b/test/hooks-step/features/steps/TodoPage.ts new file mode 100644 index 00000000..b2070696 --- /dev/null +++ b/test/hooks-step/features/steps/TodoPage.ts @@ -0,0 +1,15 @@ +import { Given, Fixture } from 'playwright-bdd/decorators'; +import { test } from './fixtures'; +import { expect } from '@playwright/test'; + +export +@Fixture('todoPage') +class TodoPage { + constructor(private log: (message: string) => unknown) {} + + @Given('decorator step {int}') + step(n: number) { + this.log(`decorator step ${n}`); + expect(1).toBe(1); + } +} diff --git a/test/hooks-step/features/fixtures.ts b/test/hooks-step/features/steps/fixtures.ts similarity index 79% rename from test/hooks-step/features/fixtures.ts rename to test/hooks-step/features/steps/fixtures.ts index 9e433233..96135bdc 100644 --- a/test/hooks-step/features/fixtures.ts +++ b/test/hooks-step/features/steps/fixtures.ts @@ -1,10 +1,12 @@ import timers from 'node:timers/promises'; import { createBdd } from 'playwright-bdd'; -import { testWithLog } from '../../_helpers/withLog'; +import { testWithLog } from '../../../_helpers/withLog'; +import { TodoPage } from './TodoPage'; type TestFixtures = { testFixtureCommon: void; testFixtureScenario2: void; + todoPage: TodoPage; }; export const test = testWithLog.extend({ @@ -20,6 +22,8 @@ export const test = testWithLog.extend({ log(`testFixtureScenario2 setup`); await use(); }, + + todoPage: async ({ log }, use) => use(new TodoPage(log)), }); export const { Given, BeforeStep, AfterStep } = createBdd(test); diff --git a/test/hooks-step/features/stepHooks.ts b/test/hooks-step/features/steps/stepHooks.ts similarity index 53% rename from test/hooks-step/features/stepHooks.ts rename to test/hooks-step/features/steps/stepHooks.ts index bff343c0..2314d381 100644 --- a/test/hooks-step/features/stepHooks.ts +++ b/test/hooks-step/features/steps/stepHooks.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { BeforeStep, AfterStep } from './fixtures'; -BeforeStep(async ({ log, testFixtureCommon }) => { +BeforeStep(async ({ log, testFixtureCommon, $testInfo }) => { log(`BeforeStep 1`); + await $testInfo.attach('BeforeStep 1 attachment', { body: 'some text' }); }); -BeforeStep(async ({ log }) => { +BeforeStep({ name: 'named BeforeStep' }, async ({ log }) => { log(`BeforeStep 2`); }); @@ -13,11 +14,12 @@ BeforeStep({ tags: '@scenario2' }, async ({ log, testFixtureScenario2 }) => { log(`BeforeStep 3 (@scenario2)`); }); -AfterStep(async ({ log, testFixtureCommon }) => { +AfterStep(async ({ log, testFixtureCommon, $testInfo }) => { log(`AfterStep 1`); + await $testInfo.attach('AfterStep 1 attachment', { body: 'another text' }); }); -AfterStep(async ({ log }) => { +AfterStep({ name: 'named AfterStep' }, async ({ log }) => { log(`AfterStep 2`); }); diff --git a/test/hooks-step/features/steps.ts b/test/hooks-step/features/steps/steps.ts similarity index 70% rename from test/hooks-step/features/steps.ts rename to test/hooks-step/features/steps/steps.ts index 2ff45489..603cf935 100644 --- a/test/hooks-step/features/steps.ts +++ b/test/hooks-step/features/steps/steps.ts @@ -1,9 +1,12 @@ +import { expect } from '@playwright/test'; import { Given } from './fixtures'; Given('bg step', async ({ log, $testInfo }) => { log(`bg step of ${$testInfo.title}`); + expect(1).toBe(1); }); Given('step {int}', async ({ log }, n: number) => { log(`step ${n}`); + expect(1).toBe(1); }); diff --git a/test/hooks-step/features/with-bg.feature b/test/hooks-step/features/with-bg.feature new file mode 100644 index 00000000..b5bb8415 --- /dev/null +++ b/test/hooks-step/features/with-bg.feature @@ -0,0 +1,7 @@ +Feature: with-bg + + Background: + Given bg step + + Scenario: scenario 1 + Given step 1 diff --git a/test/hooks-step/playwright.config.ts b/test/hooks-step/playwright.config.ts index 604f2580..6e2c9bf6 100644 --- a/test/hooks-step/playwright.config.ts +++ b/test/hooks-step/playwright.config.ts @@ -1,19 +1,24 @@ import { defineConfig } from '@playwright/test'; import { defineBddConfig, cucumberReporter } from 'playwright-bdd'; +const { FEATURE = '' } = process.env; + const testDir = defineBddConfig({ featuresRoot: 'features', + features: `features/${FEATURE}.feature`, }); export default defineConfig({ testDir, - workers: 1, reporter: [ - cucumberReporter('message', { - outputFile: `actual-reports/report.jsonl`, - }), - cucumberReporter('html', { - outputFile: `actual-reports/report.html`, - }), + ['html', { open: 'never' }], + cucumberReporter('html', { outputFile: `actual-reports/${FEATURE}/report.html` }), + [ + '../_helpers/stepsReporter.ts', + { + outputFile: `actual-reports/${FEATURE}/steps.txt`, + categories: ['hook', 'test.step', 'attach', 'expect'], + }, + ], ], }); diff --git a/test/hooks-step/test.mjs b/test/hooks-step/test.mjs index ebb41824..60a39038 100644 --- a/test/hooks-step/test.mjs +++ b/test/hooks-step/test.mjs @@ -2,18 +2,14 @@ import { test, expectCalls, TestDir, execPlaywrightTest } from '../_helpers/inde const testDir = new TestDir(import.meta); -test(testDir.name, () => { - const stdout = execPlaywrightTest(testDir.name); +test(`${testDir.name} (simple)`, () => { + const stdout = execPlaywrightTest(testDir.name, { + env: { FEATURE: 'simple' }, + }); + expectCalls('worker 0: ', stdout, [ // scenario 1 - // TODO: commented, b/c now fixtures don't initialize - // 'testFixtureCommon setup', - - 'BeforeStep 1', - 'BeforeStep 2', - 'bg step of scenario 1', - 'AfterStep 2', - 'AfterStep 1', + 'testFixtureCommon setup', 'BeforeStep 1', 'BeforeStep 2', @@ -28,13 +24,13 @@ test(testDir.name, () => { 'AfterStep 1', // scenario 2 - // TODO: commented, b/c now fixtures don't initialize - // 'testFixtureScenario2 setup', + 'testFixtureCommon setup', + 'testFixtureScenario2 setup', 'BeforeStep 1', 'BeforeStep 2', 'BeforeStep 3 (@scenario2)', - 'bg step of scenario 2', + 'step 3', 'AfterStep 3 (@scenario2)', 'AfterStep 2', 'AfterStep 1', @@ -42,16 +38,52 @@ test(testDir.name, () => { 'BeforeStep 1', 'BeforeStep 2', 'BeforeStep 3 (@scenario2)', - 'step 3', + 'step 4', 'AfterStep 3 (@scenario2)', 'AfterStep 2', 'AfterStep 1', + ]); +}); + +test(`${testDir.name} (with bg)`, () => { + const stdout = execPlaywrightTest(testDir.name, { + env: { FEATURE: 'with-bg' }, + }); + + expectCalls('worker 0: ', stdout, [ + 'testFixtureCommon setup', 'BeforeStep 1', 'BeforeStep 2', - 'BeforeStep 3 (@scenario2)', - 'step 4', - 'AfterStep 3 (@scenario2)', + 'bg step of scenario 1', + 'AfterStep 2', + 'AfterStep 1', + + 'BeforeStep 1', + 'BeforeStep 2', + 'step 1', + 'AfterStep 2', + 'AfterStep 1', + ]); +}); + +test(`${testDir.name} (decorators)`, () => { + const stdout = execPlaywrightTest(testDir.name, { + env: { FEATURE: 'decorators' }, + }); + + expectCalls('worker 0: ', stdout, [ + 'testFixtureCommon setup', + + 'BeforeStep 1', + 'BeforeStep 2', + 'decorator step 1', + 'AfterStep 2', + 'AfterStep 1', + + 'BeforeStep 1', + 'BeforeStep 2', + 'decorator step 2', 'AfterStep 2', 'AfterStep 1', ]); From d69715cd603ecde2b4fd470acc9f860f04a5059f Mon Sep 17 00:00:00 2001 From: vitalets Date: Fri, 14 Feb 2025 11:23:32 +0400 Subject: [PATCH 06/13] test: step hooks error test --- test/hooks-step/features/error.feature | 11 +++++++ test/hooks-step/features/steps/stepHooks.ts | 11 +++++++ test/hooks-step/test.mjs | 34 ++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/hooks-step/features/error.feature diff --git a/test/hooks-step/features/error.feature b/test/hooks-step/features/error.feature new file mode 100644 index 00000000..04f7dde3 --- /dev/null +++ b/test/hooks-step/features/error.feature @@ -0,0 +1,11 @@ +Feature: error + + @error-in-before-step-hook + Scenario: scenario 1 + Given step 1 + Then step 2 + + @error-in-after-step-hook + Scenario: scenario 2 + Given step 3 + Then step 4 diff --git a/test/hooks-step/features/steps/stepHooks.ts b/test/hooks-step/features/steps/stepHooks.ts index 2314d381..4c2f2692 100644 --- a/test/hooks-step/features/steps/stepHooks.ts +++ b/test/hooks-step/features/steps/stepHooks.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { expect } from '@playwright/test'; import { BeforeStep, AfterStep } from './fixtures'; BeforeStep(async ({ log, testFixtureCommon, $testInfo }) => { @@ -6,6 +7,11 @@ BeforeStep(async ({ log, testFixtureCommon, $testInfo }) => { await $testInfo.attach('BeforeStep 1 attachment', { body: 'some text' }); }); +BeforeStep({ tags: '@error-in-before-step-hook' }, async ({ log }) => { + log(`BeforeStep with error`); + expect(1).toBe(2); +}); + BeforeStep({ name: 'named BeforeStep' }, async ({ log }) => { log(`BeforeStep 2`); }); @@ -19,6 +25,11 @@ AfterStep(async ({ log, testFixtureCommon, $testInfo }) => { await $testInfo.attach('AfterStep 1 attachment', { body: 'another text' }); }); +AfterStep({ tags: '@error-in-after-step-hook' }, async ({ log }) => { + log(`AfterStep with error`); + expect(1).toBe(2); +}); + AfterStep({ name: 'named AfterStep' }, async ({ log }) => { log(`AfterStep 2`); }); diff --git a/test/hooks-step/test.mjs b/test/hooks-step/test.mjs index 60a39038..a690f5f9 100644 --- a/test/hooks-step/test.mjs +++ b/test/hooks-step/test.mjs @@ -1,4 +1,10 @@ -import { test, expectCalls, TestDir, execPlaywrightTest } from '../_helpers/index.mjs'; +import { + test, + expectCalls, + TestDir, + execPlaywrightTest, + execPlaywrightTestWithError, +} from '../_helpers/index.mjs'; const testDir = new TestDir(import.meta); @@ -88,3 +94,29 @@ test(`${testDir.name} (decorators)`, () => { 'AfterStep 1', ]); }); + +test(`${testDir.name} (error)`, () => { + const stdout = execPlaywrightTestWithError(testDir.name, '', { + env: { FEATURE: 'error' }, + }); + + expectCalls('worker 0: ', stdout, [ + // scenario 1 + 'testFixtureCommon setup', + + 'BeforeStep 1', + 'BeforeStep with error', + ]); + + expectCalls('worker 1: ', stdout, [ + // scenario 2 + 'testFixtureCommon setup', + + 'BeforeStep 1', + 'BeforeStep 2', + 'step 3', + 'AfterStep 2', + 'AfterStep with error', + 'AfterStep 1', + ]); +}); From 6c0b0539e5672c302ca7221a68143f86dbc5bd3c Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sat, 15 Feb 2025 12:44:28 +0800 Subject: [PATCH 07/13] update docs for BeforeStep/AfterStep --- .prototools | 2 + docs/api.md | 34 +++++++++++ docs/writing-steps/hooks.md | 114 +++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 .prototools diff --git a/.prototools b/.prototools new file mode 100644 index 00000000..1a94aa25 --- /dev/null +++ b/.prototools @@ -0,0 +1,2 @@ +node = "~20" +npm = "~9" diff --git a/docs/api.md b/docs/api.md index b42c22f2..07fa33d1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -103,6 +103,40 @@ Functions for step definitions. **Returns:** *function* - A function to call this step from other steps. +### BeforeStep + +Defines a hook that runs **before each step**. You can target the hook to specific step by providing the `tags` option. + +**Usage:** `BeforeStep([options,] hookFn)` + +**Params:** + * `options` *string | object* + - `tags` *string* - [Tag expression](https://github.com/cucumber/tag-expressions) to target this hook to specific features/steps. + - `name` *string* - An optional name for this hook for reporting. + - `timeout` *number* - Timeout for this hook in milliseconds. + * `hookFn` *Function* - Hook function `(fixtures?) => void`: + - `fixtures` *object* - Playwright fixtures: + - `$testInfo` *object* - Playwright [testInfo](https://playwright.dev/docs/api/class-testinfo). + - `$tags` *string[]* - List of tags for the current step. + - Any other built-in and custom fixtures. + +### AfterStep + +Defines a hook that runs **after each scenario**. You can target the hook to specific step by providing the `tags` option. + +**Usage:** `AfterStep([options,] hookFn)` + +**Params:** + * `options` *string | object* + - `tags` *string* - [Tag expression](https://github.com/cucumber/tag-expressions) to target this hook to specific features/steps. + - `name` *string* - An optional name for this hook for reporting. + - `timeout` *number* - Timeout for this hook in milliseconds. + * `hookFn` *Function* - Hook function `(fixtures?) => void`: + - `fixtures` *object* - Playwright fixtures: + - `$testInfo` *object* - Playwright [testInfo](https://playwright.dev/docs/api/class-testinfo). + - `$tags` *string[]* - List of tags for the current step. + - Any other built-in and custom fixtures. + ### BeforeScenario / Before Defines a hook that runs **before each scenario**. You can target the hook to specific scenarios by providing the `tags` option. `BeforeScenario` and `Before` are aliases. diff --git a/docs/writing-steps/hooks.md b/docs/writing-steps/hooks.md index 056c85f2..3614afec 100644 --- a/docs/writing-steps/hooks.md +++ b/docs/writing-steps/hooks.md @@ -6,6 +6,8 @@ Hooks are functions that automatically run before/after workers or scenarios: * `AfterWorker / AfterAll` - runs **once in each worker**, after all scenarios * `BeforeScenario / Before` - runs **before each scenario** * `AfterScenario / After` - runs **after each scenario** +* `BeforeStep` - runs **before each step** +* `AfterStep` - runs **after each step** > If you need to run some code **before/after overall test execution**, check out Playwright's [project dependencies](https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies) or [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown#option-2-configure-globalsetup-and-globalteardown) @@ -286,4 +288,114 @@ AfterScenario(async () => { }); ``` -All options and behavior are similar to [BeforeScenario / Before](#beforescenario-before). \ No newline at end of file +All options and behavior are similar to [BeforeScenario / Before](#beforescenario-before). + +## BeforeStep + +Playwright-BDD supports the step-level hook `BeforeStep`. It runs **before each step**. + +Usage: +```ts +import { test as base, createBdd } from 'playwright-bdd'; + +export const test = base.extend({ /* ...your fixtures */ }); + +const { BeforeStep } = createBdd(test); + +BeforeStep(async () => { + // runs before each step +}); +``` + +If you don't use custom fixtures, you can create `BeforeStep` without passing `test` argument: +```ts +import { createBdd } from 'playwright-bdd'; + +const { BeforeStep } = createBdd(); +``` + +Since Playwright-BDD **v8** you can target scenario hook to particular features/scenarios by `tags`: + +```ts +BeforeStep({ tags: '@mobile and not @slow' }, async function () { + // runs for scenarios with @mobile and not @slow +}); +``` +If you want to pass only tags, you can use a shortcut: +```ts +BeforeStep('@mobile and not @slow', async function () { + // runs for scenarios with @mobile and not @slow +}); +``` +You can also provide default tags via `createBdd()`: +```ts +const { BeforeStep } = createBdd(test, { tags: '@mobile' }); + +BeforeStep(async () => { + // runs only for scenarios with @mobile +}); +``` + +If the hook has both default and own tags, they are combined using `AND` logic: +```ts +const { BeforeStep } = createBdd(test, { tags: '@mobile' }); + +BeforeStep({ tags: '@slow' }, async function () { + // runs for scenarios with @mobile and @slow +}); +``` + +Additionally, you can set `name` and `timeout` for the hook: +```ts +BeforeStep({ name: 'my hook', timeout: 5000 }, async function () { + // ... +}); +``` + +The hook function can accept **1 argument** - [test-scoped fixtures](https://playwright.dev/docs/test-fixtures#built-in-fixtures). +You can access [$testInfo](https://playwright.dev/docs/api/class-testinfo), [$tags](writing-steps/bdd-fixtures.md#tags) and any built-in or custom fixtures. See more details in [BeforeScenario / Before API](api.md#beforescenario-before). + +#### Example of using `BeforeStep` with custom fixture + +Imagine you have defined a custom fixture `myFixture`: +```ts +import { test as base, createBdd } from 'playwright-bdd'; + +export const test = base.extend<{ myFixture: MyFixture }>({ + myFixture: async ({ page }, use) => { + // ... setup myFixture + } +}); + +export const { BeforeStep } = createBdd(test); +``` + +Now you can use `myFixture` in the produced hooks: +```ts +import { BeforeStep } from './fixtures'; + +BeforeStep(async ({ myFixture }) => { + // ... use myFixture in the hook +}); +``` + +## AfterStep + +> Consider using [fixtures](#fixtures) instead of hooks. + +Playwright-BDD supports the scenario-level hook `AfterStep`. It runs **after each step**. + +Usage: +```ts +import { test as base, createBdd } from 'playwright-bdd'; + +export const test = base.extend({ /* ...your fixtures */ }); + +const { AfterStep } = createBdd(test); + +AfterStep(async () => { + // runs after each scenario +}); +``` + +All options and behavior are similar to [BeforeStep](#beforestep). \ No newline at end of file From ec184e0952d76787826a4e65c1ba025b6ddccbf5 Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sat, 15 Feb 2025 21:40:32 +0800 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Vitaliy Potapov --- docs/writing-steps/hooks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/writing-steps/hooks.md b/docs/writing-steps/hooks.md index 3614afec..e544fbad 100644 --- a/docs/writing-steps/hooks.md +++ b/docs/writing-steps/hooks.md @@ -314,7 +314,7 @@ import { createBdd } from 'playwright-bdd'; const { BeforeStep } = createBdd(); ``` -Since Playwright-BDD **v8** you can target scenario hook to particular features/scenarios by `tags`: +You can target step hook to the steps of the specific feature/scenario by `tags`: ```ts BeforeStep({ tags: '@mobile and not @slow' }, async function () { @@ -355,7 +355,7 @@ BeforeStep({ name: 'my hook', timeout: 5000 }, async function () { The hook function can accept **1 argument** - [test-scoped fixtures](https://playwright.dev/docs/test-fixtures#built-in-fixtures). You can access [$testInfo](https://playwright.dev/docs/api/class-testinfo), [$tags](writing-steps/bdd-fixtures.md#tags) and any built-in or custom fixtures. See more details in [BeforeScenario / Before API](api.md#beforescenario-before). -#### Example of using `BeforeStep` with custom fixture +#### Example of using `BeforeStep` to capture screenshot after each step Imagine you have defined a custom fixture `myFixture`: ```ts From 7325e0fca0fee519744f2833f719c8820382dd95 Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sat, 15 Feb 2025 21:47:28 +0800 Subject: [PATCH 09/13] remove prototools --- .gitignore | 4 +++- .prototools | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 .prototools diff --git a/.gitignore b/.gitignore index 495c300a..09a2cec6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ blob-report /test/reporter-cucumber-merge/check-report /test/reporter-cucumber-merge/.features-gen/features/sample.feature.spec.js-snapshots -/examples/**/playwright/.auth \ No newline at end of file +/examples/**/playwright/.auth + +.prototools diff --git a/.prototools b/.prototools deleted file mode 100644 index 1a94aa25..00000000 --- a/.prototools +++ /dev/null @@ -1,2 +0,0 @@ -node = "~20" -npm = "~9" From 38d0182c6afa7532205faa69b7b02bf98fc1b976 Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sat, 15 Feb 2025 21:50:35 +0800 Subject: [PATCH 10/13] update documents of AfterStep example to take screenshots --- docs/writing-steps/hooks.md | 47 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/docs/writing-steps/hooks.md b/docs/writing-steps/hooks.md index e544fbad..b5aa729c 100644 --- a/docs/writing-steps/hooks.md +++ b/docs/writing-steps/hooks.md @@ -355,30 +355,6 @@ BeforeStep({ name: 'my hook', timeout: 5000 }, async function () { The hook function can accept **1 argument** - [test-scoped fixtures](https://playwright.dev/docs/test-fixtures#built-in-fixtures). You can access [$testInfo](https://playwright.dev/docs/api/class-testinfo), [$tags](writing-steps/bdd-fixtures.md#tags) and any built-in or custom fixtures. See more details in [BeforeScenario / Before API](api.md#beforescenario-before). -#### Example of using `BeforeStep` to capture screenshot after each step - -Imagine you have defined a custom fixture `myFixture`: -```ts -import { test as base, createBdd } from 'playwright-bdd'; - -export const test = base.extend<{ myFixture: MyFixture }>({ - myFixture: async ({ page }, use) => { - // ... setup myFixture - } -}); - -export const { BeforeStep } = createBdd(test); -``` - -Now you can use `myFixture` in the produced hooks: -```ts -import { BeforeStep } from './fixtures'; - -BeforeStep(async ({ myFixture }) => { - // ... use myFixture in the hook -}); -``` - ## AfterStep > Consider using [fixtures](#fixtures) instead of hooks. @@ -398,4 +374,25 @@ AfterStep(async () => { }); ``` -All options and behavior are similar to [BeforeStep](#beforestep). \ No newline at end of file +All options and behavior are similar to [BeforeStep](#beforestep). + +#### Example of using `AfterStep` to capture screenshot after each step + +Create `fixtures.ts`: +```ts +export const { AfterStep } = createBdd(test); +``` + +Import `fixture.ts` in step definition +```ts +import { AfterStep } from './fixtures'; + +AfterStep(async ({ page, $testInfo, $step }) => { + await $testInfo.attach(`screenshot after ${$step.title}`, { + contentType: 'image/png', + body: await page.screenshot() + }); +}); + +// ...rest of the step definitions +``` \ No newline at end of file From aad5d17f04578871e0bb591e4c7dc35a0ff94dd1 Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sat, 15 Feb 2025 22:00:36 +0800 Subject: [PATCH 11/13] update run examples command for contributing --- .github/CONTRIBUTING.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 04957fa4..f82bbccd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -46,18 +46,27 @@ To contribute code: ## Development Setup 1. Install dependencies: - ``` + ```sh cd playwright-bdd npm install ``` 2. Install Playwright browsers: - ``` + ```sh npx playwright install chromium ``` 3. Run tests: - ``` + ```sh npm t + # run specific test + npm run only test/ + ``` +4. Run examples: + ```sh + npm run examples + # run specific example + npm run examples examples/ ``` + note: you may need to run `npm run examples` before commiting, as some pre-commit checks rely on its generated output. ## Useful Dev Commands From 6234f39a506887911a83a4c3221cc56c378232f2 Mon Sep 17 00:00:00 2001 From: Shann Wei Yeh Date: Sun, 16 Feb 2025 19:12:24 +0800 Subject: [PATCH 12/13] Update docs/writing-steps/hooks.md Co-authored-by: Vitaliy Potapov --- docs/writing-steps/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/writing-steps/hooks.md b/docs/writing-steps/hooks.md index b5aa729c..18238d5a 100644 --- a/docs/writing-steps/hooks.md +++ b/docs/writing-steps/hooks.md @@ -383,7 +383,7 @@ Create `fixtures.ts`: export const { AfterStep } = createBdd(test); ``` -Import `fixture.ts` in step definition +Import `fixtures.ts` in step definition ```ts import { AfterStep } from './fixtures'; From 2938c94ab633b5790a66c599f09c8827d301fb65 Mon Sep 17 00:00:00 2001 From: vitalets Date: Mon, 17 Feb 2025 10:50:00 +0400 Subject: [PATCH 13/13] test: hooks-step: add case with error in step --- test/hooks-step/features/error.feature | 3 +++ test/hooks-step/features/steps/steps.ts | 5 +++++ test/hooks-step/test.mjs | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/test/hooks-step/features/error.feature b/test/hooks-step/features/error.feature index 04f7dde3..42457f6e 100644 --- a/test/hooks-step/features/error.feature +++ b/test/hooks-step/features/error.feature @@ -9,3 +9,6 @@ Feature: error Scenario: scenario 2 Given step 3 Then step 4 + + Scenario: scenario 3 error in step + Given step with error diff --git a/test/hooks-step/features/steps/steps.ts b/test/hooks-step/features/steps/steps.ts index 603cf935..a9f3151c 100644 --- a/test/hooks-step/features/steps/steps.ts +++ b/test/hooks-step/features/steps/steps.ts @@ -10,3 +10,8 @@ Given('step {int}', async ({ log }, n: number) => { log(`step ${n}`); expect(1).toBe(1); }); + +Given('step with error', async ({ log }) => { + log(`step with error`); + expect(1).toBe(2); +}); diff --git a/test/hooks-step/test.mjs b/test/hooks-step/test.mjs index a690f5f9..3fd7f91a 100644 --- a/test/hooks-step/test.mjs +++ b/test/hooks-step/test.mjs @@ -119,4 +119,14 @@ test(`${testDir.name} (error)`, () => { 'AfterStep with error', 'AfterStep 1', ]); + + expectCalls('worker 2: ', stdout, [ + // scenario 3 + 'testFixtureCommon setup', + + 'BeforeStep 1', + 'BeforeStep 2', + 'step with error', + // currently we don't run AfterStep hooks in case of step error + ]); });