diff --git a/.changeset/slimy-spiders-cover.md b/.changeset/slimy-spiders-cover.md new file mode 100644 index 0000000..3f74aff --- /dev/null +++ b/.changeset/slimy-spiders-cover.md @@ -0,0 +1,53 @@ +--- +"playwright-decorators": minor +--- + +Add support for fixtures + +This release introduce a new method `extend(customFixture)` that allows to create decorators (`afterAll`, `afterEach`, `test`, `beforeAll`, `beforeEach`) with access to custom fixtures. + +```ts +import { test as base } from 'playwright'; +import { suite, test, extend } from 'playwright-decorators'; + +// #1 Create fixture type +type UserFixture = { + user: { + firstName: string; + lastName: string; + } +} + +// #2 Create user fixture +const withUser = base.extend({ + user: async ({}, use) => { + await use({ + firstName: 'John', + lastName: 'Doe' + }) + } +}) + +// #3 Generate afterAll, afterEach, test, beforeAll, beforeEach decorators with access to the user fixture +const { + afterAll, + afterEach, + test, + beforeAll, + beforeEach, +} = extend(withUser); + +// #4 Use decorators +@suite() +class MyTestSuite { + @beforeAll() + async beforeAll({ user }: TestArgs) { // have access to user fixture + // ... + } + + @test() + async test({ user }: TestArgs) { // have access to user fixture + // ... + } +} +``` diff --git a/README.md b/README.md index 860079b..4c11735 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ class MyTestSuite { } @tag(['team-x']) - @slow('Response from pasword reset service takes a long time') + @slow('Response from reset password service needs more time') @test() async userShouldBeAbleToResetPassword({ page }: TestArgs) { // ... @@ -57,6 +57,7 @@ class MyTestSuite { - [Run test(s) or suite(s) in debug mode: `@debug`](#run-tests-or-suites-in-debug-mode-debug) - [Run test(s) or suite(s) in preview mode: `@preview`](#run-tests-or-suites-in-preview-mode-preview) - [Create custom decorator: `createSuiteDecorator`, `createTestDecorator`, `createSuiteAndTestDecorator`](#custom-decorators) +- [Using custom fixtures: `extend`](#fixtures) ### Creating a test suite: `@suite(options?)` Mark class as test suite. @@ -95,9 +96,9 @@ class MyTestSuite { #### Options - `name` (optional) - name of the test. By default, name of the method. - `only` (optional) - declares focused test. If there are some focused @test(s) or @suite(s), all of them will be run but nothing else. +- `playwright` (optional) - Custom playwright instance to use instead of standard one. For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `test` method. - -### Run method before all tests in the suite: `@beforeAll()` +### Run method before all tests in the suite: `@beforeAll(options?)` Mark the method as `beforeAll` book. ```ts @@ -112,8 +113,11 @@ class MyTestSuite { } ``` +#### Options +- `playwright` (optional) - Custom playwright instance to use instead of standard one. For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `beforeAll` hook. + -### Run method before each test in the suite: `@beforeEach()` +### Run method before each test in the suite: `@beforeEach(options?)` Mark the method as `beforeEach` book. ```ts @@ -128,8 +132,11 @@ class MyTestSuite { } ``` +#### Options +- `playwright` (optional) - Custom playwright instance to use instead of standard one. For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `beforeEach` hook. + -### Run method after all tests in the suite: `@afterAll()` +### Run method after all tests in the suite: `@afterAll(options?)` Mark the method as `afterAll` book. ```ts @@ -144,8 +151,11 @@ class MyTestSuite { } ``` +#### Options +- `playwright` (optional) - Custom playwright instance to use instead of standard one. For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `afterAll` hook. + -### Run method after each test in the suite: `@afterEach()` +### Run method after each test in the suite: `@afterEach(options?)` Mark the method as `afterEach` book. ```ts @@ -160,6 +170,9 @@ class MyTestSuite { } ``` +#### Options +- `playwright` (optional) - Custom playwright instance to use instead of standard one. For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `afterEach` hook. + ### Skip test or suite: `@skip(reason?: string)` Skip single `@test` or `@suite`. @@ -488,3 +501,57 @@ const customSuiteAndTestDecorator = createSuiteAndTestDecorator( } ) ``` + + +### Fixtures +> If you are not familiar with concept of fixtures in Playwright, please read [this](https://playwright.dev/docs/test-fixtures) article first. + +The `extend(customFixture)` method generates decorators with access to custom fixture. + +The following example illustrates how to create decorators with access to `user` fixture: + +```ts +import { test as base } from 'playwright'; +import { suite, test, extend } from 'playwright-decorators'; + +// #1 Create fixture type +type UserFixture = { + user: { + firstName: string; + lastName: string; + } +} + +// #2 Create user fixture +const withUser = base.extend({ + user: async ({}, use) => { + await use({ + firstName: 'John', + lastName: 'Doe' + }) + } +}) + +// #3 Generate afterAll, afterEach, test, beforeAll, beforeEach decorators with access to the user fixture +const { + afterAll, + afterEach, + test, + beforeAll, + beforeEach, +} = extend(withUser); + +// #4 Use decorators +@suite() +class MyTestSuite { + @beforeAll() + async beforeAll({ user }: TestArgs) { // have access to user fixture + // ... + } + + @test() + async test({ user }: TestArgs) { // have access to user fixture + // ... + } +} +``` diff --git a/lib/afterAll.decorator.ts b/lib/afterAll.decorator.ts index 42f95c8..6f06589 100644 --- a/lib/afterAll.decorator.ts +++ b/lib/afterAll.decorator.ts @@ -1,21 +1,31 @@ import playwright from '@playwright/test' import { decoratePlaywrightTest } from './helpers' -import { TestMethod } from './common' +import { TestMethod, TestType } from './common' + +export interface AfterAllDecoratorOptions { + /** + * Custom playwright instance to use instead of standard one. + * For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `afterAll` hook. + */ + playwright?: TestType +} /** * Run method after all tests in the suite. * Target class should be marked by @suite decorator. */ -export const afterAll = () => - function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { +export const afterAll = (options?: AfterAllDecoratorOptions) => + function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { context.addInitializer(function () { - const decoratedBeforeAll = decoratePlaywrightTest( + const decoratedBeforeAll = decoratePlaywrightTest( originalMethod, (originalMethod) => (...args) => originalMethod.call(this, ...args) ) - playwright.afterAll(decoratedBeforeAll) + const { afterAll } = options?.playwright || (playwright as TestType) + + afterAll(decoratedBeforeAll) }) } diff --git a/lib/afterEach.decorator.ts b/lib/afterEach.decorator.ts index c4400a5..7bbb8d8 100644 --- a/lib/afterEach.decorator.ts +++ b/lib/afterEach.decorator.ts @@ -1,21 +1,31 @@ import playwright from '@playwright/test' import { decoratePlaywrightTest } from './helpers' -import { TestMethod } from './common' +import { TestMethod, TestType } from './common' + +export interface AfterEachDecoratorOptions { + /** + * Custom playwright instance to use instead of standard one. + * For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `afterEach` hook. + */ + playwright?: TestType +} /** * Run method after each test in suite. * Target class should be marked by @suite decorator. */ -export const afterEach = () => - function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { +export const afterEach = (options?: AfterEachDecoratorOptions) => + function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { context.addInitializer(function () { - const decoratedBeforeEach = decoratePlaywrightTest( + const decoratedBeforeEach = decoratePlaywrightTest( originalMethod, (originalMethod) => (...args) => originalMethod.call(this, ...args) ) - playwright.afterEach(decoratedBeforeEach) + const { afterEach } = options?.playwright || (playwright as TestType) + + afterEach(decoratedBeforeEach) }) } diff --git a/lib/beforeAll.decorator.ts b/lib/beforeAll.decorator.ts index b279660..aaafad1 100644 --- a/lib/beforeAll.decorator.ts +++ b/lib/beforeAll.decorator.ts @@ -1,21 +1,31 @@ import playwright from '@playwright/test' import { decoratePlaywrightTest } from './helpers' -import { TestMethod } from './common' +import { TestMethod, TestType } from './common' + +export interface BeforeAllDecoratorOptions { + /** + * Custom playwright instance to use instead of standard one. + * For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `beforeAll` hook. + */ + playwright?: TestType +} /** * Run method before all tests in the suite. * Target class should be marked by @suite decorator. */ -export const beforeAll = () => - function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { +export const beforeAll = (options?: BeforeAllDecoratorOptions) => + function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { context.addInitializer(function () { - const decoratedBeforeAll = decoratePlaywrightTest( + const decoratedBeforeAll = decoratePlaywrightTest( originalMethod, (originalMethod) => (...args) => originalMethod.call(this, ...args) ) - playwright.beforeAll(decoratedBeforeAll) + const { beforeAll } = options?.playwright || (playwright as TestType) + + beforeAll(decoratedBeforeAll) }) } diff --git a/lib/beforeEach.decorator.ts b/lib/beforeEach.decorator.ts index 778edc8..5dd59e1 100644 --- a/lib/beforeEach.decorator.ts +++ b/lib/beforeEach.decorator.ts @@ -1,21 +1,31 @@ import playwright from '@playwright/test' import { decoratePlaywrightTest } from './helpers' -import { TestMethod } from './common' +import { TestMethod, TestType } from './common' + +export interface BeforeEachDecoratorOptions { + /** + * Custom playwright instance to use instead of standard one. + * For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `beforeEach` hook. + */ + playwright?: TestType +} /** * Run method before each test in the suite. * Target class should be marked by @suite decorator. */ -export const beforeEach = () => - function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { +export const beforeEach = (options?: BeforeEachDecoratorOptions) => + function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { context.addInitializer(function () { - const decoratedBeforeEach = decoratePlaywrightTest( + const decoratedBeforeEach = decoratePlaywrightTest( originalMethod, (originalMethod) => (...args) => originalMethod.call(this, ...args) ) - playwright.beforeEach(decoratedBeforeEach) + const { beforeEach } = options?.playwright || (playwright as TestType) + + beforeEach(decoratedBeforeEach) }) } diff --git a/lib/common.ts b/lib/common.ts index 28c29f9..b64b3c9 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -3,14 +3,20 @@ import { PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, - TestInfo as PlaywrightTestInfo + TestInfo as PlaywrightTestInfo, + TestType as PlaywrightTestType } from '@playwright/test' export type TestInfo = PlaywrightTestInfo -export type TestArgs = PlaywrightTestArgs & +export type TestArgs = T & + PlaywrightTestArgs & PlaywrightTestOptions & PlaywrightWorkerArgs & PlaywrightWorkerOptions -export type TestMethod = (args: TestArgs, testInfo: TestInfo) => void | Promise +export type TestMethod = (args: TestArgs, testInfo: TestInfo) => void | Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any export type TestClass = { new (...args: any[]): any } +export type TestType = PlaywrightTestType< + TestArgs, + PlaywrightWorkerArgs & PlaywrightWorkerOptions +> diff --git a/lib/extend.ts b/lib/extend.ts new file mode 100644 index 0000000..abca7cb --- /dev/null +++ b/lib/extend.ts @@ -0,0 +1,25 @@ +import { afterAll } from './afterAll.decorator' +import { afterEach } from './afterEach.decorator' +import { beforeAll } from './beforeAll.decorator' +import { beforeEach } from './beforeEach.decorator' +import { test } from './test.decorator' +import { TestType } from './common' + +/** + * Generates afterAll, afterEach, test, beforeAll, beforeEach decorators with access to custom fixture. + * @param customPlaywright - method returned from playwright.extend + */ +export const extend = (customPlaywright: TestType) => { + return { + afterAll: (...options: Parameters) => + afterAll({ ...options, playwright: customPlaywright }), + afterEach: (...options: Parameters) => + afterEach({ ...options, playwright: customPlaywright }), + test: (...options: Parameters) => + test({ ...options, playwright: customPlaywright }), + beforeAll: (...options: Parameters) => + beforeAll({ ...options, playwright: customPlaywright }), + beforeEach: (...options: Parameters) => + beforeEach({ ...options, playwright: customPlaywright }) + } +} diff --git a/lib/helpers.ts b/lib/helpers.ts index 98fd7c8..d46528c 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,13 +1,13 @@ import { TestMethod } from './common' -export type TestDecoratorFunction = (testFunction: TestMethod) => TestMethod +export type TestDecoratorFunction = (testFunction: TestMethod) => TestMethod /** * Wrap a playwright test function with class method, and make it visible externally as original one (function description). * It is required, as @playwright/test function do not accept rest parameters. */ -export const decoratePlaywrightTest = ( - testFunction: TestMethod, +export const decoratePlaywrightTest = ( + testFunction: TestMethod, decorationFunction: TestDecoratorFunction ) => { const decoratedTestFunction = decorationFunction(testFunction) diff --git a/lib/index.ts b/lib/index.ts index b5b2da1..b810c16 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -27,6 +27,7 @@ export { preview } from './preview.decorator' // common export { type TestInfo, type TestArgs } from './common' +export { extend } from './extend' // custom export { createSuiteDecorator, createTestDecorator, createSuiteAndTestDecorator } from './custom' diff --git a/lib/test.decorator.ts b/lib/test.decorator.ts index d4777e2..88d3d09 100644 --- a/lib/test.decorator.ts +++ b/lib/test.decorator.ts @@ -1,10 +1,10 @@ import playwright from '@playwright/test' import { decoratePlaywrightTest, TestDecoratorFunction } from './helpers' -import { TestMethod } from './common' +import { TestMethod, TestType } from './common' type TestHook = () => void | Promise -interface TestDecoratorOptions { +export interface TestDecoratorOptions { /** * Name of the test. Default: name of the method */ @@ -14,11 +14,17 @@ interface TestDecoratorOptions { * If there are some focused @test(s) or @suite(s), all of them will be run but nothing else. */ only?: boolean + /** + * Custom playwright instance to use instead of standard one. + * For example, provide result of `playwright.extend(customFixture)` to ensure availability of custom fixture in the `test` method. + */ + playwright?: TestType } export class TestDecorator implements TestDecoratorOptions { name: string only = false + playwright = playwright private beforeTestHooks: TestHook[] = [] private afterTestHooks: TestHook[] = [] @@ -55,7 +61,7 @@ export class TestDecorator implements TestDecoratorOptions { await this.handleAfterTestHooks() } - const testRunner = this.only ? playwright.only : playwright + const testRunner = this.only ? this.playwright.only : this.playwright testRunner(this.name, decoratePlaywrightTest(this.testMethod, extendedTestMethod)) } @@ -90,8 +96,8 @@ export function isTestDecoratedMethod(method: any): method is TestDecoratedMetho * * Behaviour of decorator can be modified by other decorators using injected `testDecorator` property. */ -export const test = (options: TestDecoratorOptions = {}) => - function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { +export const test = (options: TestDecoratorOptions = {}) => + function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) { const testDecorator = new TestDecorator(originalMethod, options) Object.assign(originalMethod, { testDecorator }) diff --git a/tests/extend.spec.ts b/tests/extend.spec.ts new file mode 100644 index 0000000..871c951 --- /dev/null +++ b/tests/extend.spec.ts @@ -0,0 +1,52 @@ +import playwright, { expect, test as base } from '@playwright/test' +import { extend } from '../lib/extend' +import { suite, TestArgs } from '../lib' + +playwright.describe('extend', () => { + type UserFixture = { + user: { + firstName: string + lastName: string + } + } + + const withUser = base.extend({ + // eslint-disable-next-line no-empty-pattern + user: async ({}, use) => { + await use({ + firstName: 'John', + lastName: 'Doe' + }) + } + }) + + const { test, beforeEach, beforeAll, afterEach, afterAll } = extend(withUser) + + @suite() + class ExtendUtilSuite { + @beforeAll() + 'should custom fixture be available in before all'({ user }: TestArgs) { + expect(user).toBeDefined() + } + + @beforeEach() + 'should custom fixture be available in before each'({ user }: TestArgs) { + expect(user).toBeDefined() + } + + @afterAll() + 'should custom fixture be available in after all'({ user }: TestArgs) { + expect(user).toBeDefined() + } + + @afterEach() + 'should custom fixture be available in after each'({ user }: TestArgs) { + expect(user).toBeDefined() + } + + @test() + 'should custom fixture be available in test'({ user }: TestArgs) { + expect(user).toBeDefined() + } + } +})