Skip to content

Commit

Permalink
add default tags to @fixture decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Nov 28, 2024
1 parent 1c2ec01 commit bc3672d
Show file tree
Hide file tree
Showing 18 changed files with 186 additions and 63 deletions.
17 changes: 12 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ Functions for step implementations.
**Params:**
* `pattern` *string | regexp* - step pattern as [cucumber expression](https://github.com/cucumber/cucumber-expressions) or RegExp
* `options` *object* - step options
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to match this step to specific features/scenarios
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to bind this step to specific features/scenarios
* `fn` *function* - step function `(fixtures, ...args) => void`:
- `fixtures` *object* - Playwright fixtures (omitted in cucumber-style)
- `...args` *array* - arguments captured from step pattern

**Returns:** *function* - a function to call this step from other steps.

### BeforeScenario / Before

Defines a hook that runs **before each scenario**. You can target hook to specific scenarios by providing `tags` option. `BeforeScenario` and `Before` are aliases.

**Usage:** `BeforeScenario([options,] hookFn)`
Expand All @@ -117,6 +118,7 @@ Defines a hook that runs **before each scenario**. You can target hook to specif
- any other built-in and custom fixtures

### AfterScenario / After

Defines a hook that runs **after each scenario**. You can target hook to specific scenarios by providing `tags` option. `AfterScenario` and `After` are aliases.

**Usage:** `AfterScenario([options,] hookFn)`
Expand All @@ -133,6 +135,7 @@ Defines a hook that runs **after each scenario**. You can target hook to specifi
- any other built-in and custom fixtures

### BeforeWorker / BeforeAll

Defines a hook that runs **once in each worker**, before all scenarios.
You can target hook to specific scenarios by providing `tags` option.
`BeforeWorker` and `BeforeAll` are aliases.
Expand All @@ -152,6 +155,7 @@ You can target hook to specific scenarios by providing `tags` option.
- any other built-in and custom **worker-scoped** fixtures

### AfterWorker / AfterAll

Defines a hook that runs **once in each worker**, after all scenarios.
You can target hook to specific scenarios by providing `tags` option.
`AfterWorker` and `AfterAll` are aliases.
Expand All @@ -162,7 +166,7 @@ You can target hook to specific scenarios by providing `tags` option.

**Params:**
* `options` *string | object*
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to target this hook to specific features
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to bind this hook to specific features
- `name` *string* - an optional name for this hook for reporting
- `timeout` *number* - timeout for this hook in milliseconds
* `hookFn` *Function* hook function `(fixtures?) => void`:
Expand All @@ -173,10 +177,13 @@ You can target hook to specific scenarios by providing `tags` option.
### @Fixture
Class decorator to bind Page Object Model (POM) with fixture name.

**Usage:** `@Fixture(fixtureName)`
**Usage:** `@Fixture(nameOrOptions)`

**Params:**
* `fixtureName` *string* - fixture name for the given class.
* `nameOrOptions` *string* - fixture name for the given class
* `nameOrOptions` *object* - fixture options
- `name` *string* - fixture name for the given class
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to bind all steps of that class to specific features/scenarios

It is also possible to provide `test` type as a generic parameter to restrict `fixtureName` to available fixture names:
```ts
Expand All @@ -200,4 +207,4 @@ A decorator to mark method as BDD step.
**Params:**
* `pattern` *string | regexp* - step pattern as [cucumber expression](https://github.com/cucumber/cucumber-expressions) or RegExp
* `options` *object* - step options
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to match this step to specific features/scenarios
- `tags` *string* - [tag expression](https://github.com/cucumber/tag-expressions) to bind this step to specific features/scenarios
8 changes: 8 additions & 0 deletions docs/writing-steps/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ The benefits of using fixture:

## BeforeWorker / BeforeAll

> Consider using [fixtures](#fixtures) instead of hooks.
Playwright-bdd supports worker-level hook `BeforeWorker` (aliased as `BeforeAll`). It runs **once in each worker, before all scenarios**.

?> Although `BeforeAll` is more used name, `BeforeWorker` better expresses when the hook runs.
Expand Down Expand Up @@ -137,6 +139,8 @@ BeforeWorker(async ({ myWorkerFixture }) => {
## AfterWorker / AfterAll

> Consider using [fixtures](#fixtures) instead of hooks.
Playwright-bdd supports worker-level hook `AfterWorker` (aliased as `AfterAll`).
It runs **once in each worker, after all scenarios**.

Expand All @@ -157,6 +161,8 @@ For other options please see [BeforeWorker / BeforeAll](#beforeworker-beforeall)

## BeforeScenario / Before

> Consider using [fixtures](#fixtures) instead of hooks.
Playwright-bdd supports scenario-level hook `BeforeScenario` (aliased as `Before`). It runs **before each scenario**.

?> Although `Before` is more used name, `BeforeScenario` better expresses when the hook runs.
Expand Down Expand Up @@ -220,6 +226,8 @@ BeforeScenario(async ({ myFixture }) => {

## AfterScenario / After

> Consider using [fixtures](#fixtures) instead of hooks.
Playwright-bdd supports scenario-level hook `AfterScenario` (aliased as `After`). It runs **after each scenario**.

?> Although `After` is more used name, `AfterScenario` better expresses when the hook runs.
Expand Down
13 changes: 6 additions & 7 deletions src/generate/test/poms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,18 @@ export type UsedFixture = {

type UsedPom = {
byTag: boolean;
fixtures?: UsedFixture[];
fixtures?: UsedFixture[]; // todo: rename to resolvedFixtures
};

export class TestPoms {
// map of poms used in test
// poms used in test + info about resolved fixtures
// todo: rename to usedPomNodes
private usedPoms = new Map<PomNode, UsedPom>();
private resolved = false;

registerPomNode(pomNode: PomNode, { byTag = false } = {}) {
const usedPom = this.usedPoms.get(pomNode);
if (usedPom) {
// todo: optimize: if (usedPom && byTag) usedPom.byTag = true
if (byTag && !usedPom.byTag) usedPom.byTag = true;
} else {
this.usedPoms.set(pomNode, { byTag });
Expand All @@ -76,7 +77,6 @@ export class TestPoms {
* that does not have steps in the test, but should be considered.
*/
resolveAllFixtures() {
this.resolved = true;
this.usedPoms.forEach((_, pomNode) => {
this.getResolvedFixtures(pomNode);
});
Expand All @@ -87,13 +87,12 @@ export class TestPoms {
* Filter out pomNodes with empty fixture names (as they are not marked with @Fixture decorator)
*/
// eslint-disable-next-line visual/complexity
getResolvedFixtures(pomNode: PomNode) {
if (!this.resolved) this.resolveAllFixtures();
getResolvedFixtures(pomNode: PomNode): UsedFixture[] {
const usedPom = this.usedPoms.get(pomNode);
// fixtures already resolved
if (usedPom?.fixtures) return usedPom.fixtures;

// Recursively resolve children fixtures, used in test
// Recursively resolve children fixtures as deep as possible
let childFixtures: UsedFixture[] = [...pomNode.children]
.map((child) => this.getResolvedFixtures(child))
.flat()
Expand Down
27 changes: 25 additions & 2 deletions src/steps/decorators/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
/**
* Class level @Fixture decorator.
*
* This decorator is needed to get access to class Constructor,
* that in turn is needed to build POM inheritance graph using prototype chain.
* Method decorators don't have access to Constructor in decoration phase,
* only in runtime (that is too late).
*
* There was idea to use the following way of creating decorators,
* that eliminates usage of @Fixture:
* const { Given, When, Then } = createBddDecorators(test, { pomFixture: 'myPOM' });
* But due to above reason it's not possible.
* Also it leads to cyclic dependencies: fixture imports test, and test imports fixture.
*/

import { TestType } from '@playwright/test';
Expand All @@ -15,10 +26,22 @@ type CustomTestFixtureName<T extends KeyValue> = Exclude<
keyof PwBuiltInFixturesTest | number | symbol
>;

export function Fixture<T extends KeyValue>(fixtureName: CustomTestFixtureName<T>) {
export type FixtureOptions<T extends KeyValue> = {
name?: CustomTestFixtureName<T>;
tags?: string;
};

export function Fixture<T extends KeyValue>(arg: CustomTestFixtureName<T> | FixtureOptions<T>) {
const { name, tags } = resolveFixtureOptions(arg);
// context parameter is required for decorator by TS even though it's not used
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (Ctor: Function, _context: ClassDecoratorContext) => {
createPomNode(Ctor, fixtureName as string);
createPomNode(Ctor, name as string, tags);
};
}

function resolveFixtureOptions<T extends KeyValue>(
arg: CustomTestFixtureName<T> | FixtureOptions<T>,
): FixtureOptions<T> {
return typeof arg === 'string' ? { name: arg } : arg;
}
4 changes: 3 additions & 1 deletion src/steps/decorators/pomGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ type PomClass = Function;
// POM class with inherited children POMs: representation of classes inheritance.
export type PomNode = {
fixtureName: string;
fixtureTags?: string;
className: string;
children: Set<PomNode>;
};

const pomGraph = new Map<PomClass, PomNode>();

export function createPomNode(Ctor: PomClass, fixtureName: string) {
export function createPomNode(Ctor: PomClass, fixtureName: string, fixtureTags?: string) {
const pomNode: PomNode = {
fixtureName,
fixtureTags,
className: Ctor.name,
children: new Set(),
};
Expand Down
1 change: 1 addition & 0 deletions src/steps/decorators/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function linkStepsWithPomNode(Ctor: Function, pomNode: PomNode) {
const stepOptions = getStepOptionsFromMethod(descriptor);
if (!stepOptions) return;
stepOptions.pomNode = pomNode;
stepOptions.defaultTags = pomNode.fixtureTags;
registerDecoratorStep(stepOptions);
});
}
Expand Down
9 changes: 6 additions & 3 deletions src/steps/stepDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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
};

export class StepDefinition {
Expand Down Expand Up @@ -129,9 +130,11 @@ export class StepDefinition {
}

private buildTagsExpression() {
const { defaultTags } = this.options;
const tags = this.options.providedOptions?.tags;
if (tags) {
this.#tagsExpression = parseTagsExpression(tags);
}
const allTags = [defaultTags, tags].filter(Boolean);
if (!allTags.length) return;
const tagsString = allTags.map((tag) => `(${tag})`).join(' and ');
this.#tagsExpression = parseTagsExpression(tagsString);
}
}
16 changes: 16 additions & 0 deletions test/scoped-steps-decorators/features/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test as base } from 'playwright-bdd';
import { TodoPage, TodoPage2, TodoPage3, TodoPage4 } from './poms';

export const logger = console;

export const test = base.extend<{
todoPage: TodoPage;
todoPage2: TodoPage2;
todoPage3: TodoPage3;
todoPage4: TodoPage4;
}>({
todoPage: async ({}, use, testInfo) => use(new TodoPage(testInfo)),
todoPage2: async ({}, use, testInfo) => use(new TodoPage2(testInfo)),
todoPage3: async ({}, use, testInfo) => use(new TodoPage3(testInfo)),
todoPage4: async ({}, use, testInfo) => use(new TodoPage4(testInfo)),
});
65 changes: 65 additions & 0 deletions test/scoped-steps-decorators/features/poms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Given, Fixture } from 'playwright-bdd/decorators';
import { logger, test } from './fixtures';
import { TestInfo } from '@playwright/test';

export
@Fixture<typeof test>('todoPage')
class TodoPage {
constructor(private testInfo: TestInfo) {}

@Given('decorator step', { tags: '@todoPage' })
step() {
logger.log(`${this.testInfo.title}: todoPage - decorator step`);
}
}

export
@Fixture<typeof test>('todoPage2')
class TodoPage2 {
constructor(private testInfo: TestInfo) {}

@Given('decorator step', { tags: '@todoPage2' })
step() {
logger.log(`${this.testInfo.title}: todoPage2 - decorator step`);
}
}

export
@Fixture<typeof test>({ name: 'todoPage3', tags: '@todoPage3' })
class TodoPage3 {
constructor(private testInfo: TestInfo) {}

@Given('decorator step')
step() {
logger.log(`${this.testInfo.title}: todoPage3 - decorator step`);
}

@Given('decorator step', { tags: 'not @todoPage3' })
step2() {
// this step should not be used, because it does not match anything:
// (@todoPage3) and (not @todoPage3)
logger.log(`${this.testInfo.title}: todoPage3 - decorator step`);
}
}

/* inheritance */

export
@Fixture({ tags: '@todoPage4' })
class Base {
constructor(protected testInfo: TestInfo) {}

@Given('decorator step')
step() {
logger.log(`${this.testInfo.title}: todoPage4 - decorator step from base`);
}
}

export
@Fixture<typeof test>('todoPage4')
class TodoPage4 extends Base {
@Given('unique step of todoPage4')
step2() {
logger.log(`${this.testInfo.title}: todoPage4 - unique step`);
}
}
18 changes: 18 additions & 0 deletions test/scoped-steps-decorators/features/sample.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Feature: feature with poms

@todoPage
Scenario: scenario for TodoPage
Given decorator step

@todoPage2
Scenario: scenario for TodoPage2
Given decorator step

@todoPage3
Scenario: scenario for TodoPage3
Given decorator step

@todoPage4
Scenario: scenario for TodoPage4
Given decorator step
Given unique step of todoPage4
3 changes: 3 additions & 0 deletions test/scoped-steps-decorators/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-decorators/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: 'features',
});

export default defineConfig({
testDir,
});
13 changes: 13 additions & 0 deletions test/scoped-steps-decorators/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { test, expect, TestDir, execPlaywrightTest } from '../_helpers/index.mjs';

const testDir = new TestDir(import.meta);

test(testDir.name, () => {
const stdout = execPlaywrightTest(testDir.name);

expect(stdout).toContain('scenario for TodoPage: todoPage - decorator step');
expect(stdout).toContain('scenario for TodoPage2: todoPage2 - decorator step');
expect(stdout).toContain('scenario for TodoPage3: todoPage3 - decorator step');
expect(stdout).toContain('scenario for TodoPage4: todoPage4 - decorator step from base');
expect(stdout).toContain('scenario for TodoPage4: todoPage4 - unique step');
});
Loading

0 comments on commit bc3672d

Please sign in to comment.