Skip to content

Commit

Permalink
feat: support fixtures (#56)
Browse files Browse the repository at this point in the history
* feat: support fixtures

* docs: fixtures

* chore: generate changeset

* chore: improve typings
  • Loading branch information
SebastianSedzik authored Jan 24, 2024
1 parent 5bb418b commit 3a44870
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 37 deletions.
53 changes: 53 additions & 0 deletions .changeset/slimy-spiders-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
"playwright-decorators": minor
---

Add support for fixtures

This release introduce a new method `extend<T>(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<UserFixture>({
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<UserFixture>(withUser);

// #4 Use decorators
@suite()
class MyTestSuite {
@beforeAll()
async beforeAll({ user }: TestArgs<UserFixture>) { // have access to user fixture
// ...
}

@test()
async test({ user }: TestArgs<UserFixture>) { // have access to user fixture
// ...
}
}
```
79 changes: 73 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
// ...
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<T>(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
Expand All @@ -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<T>(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
Expand All @@ -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<T>(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
Expand All @@ -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<T>(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
Expand All @@ -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<T>(customFixture)` to ensure availability of custom fixture in the `afterEach` hook.


### Skip test or suite: `@skip(reason?: string)`
Skip single `@test` or `@suite`.
Expand Down Expand Up @@ -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<T>(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<UserFixture>({
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<UserFixture>(withUser);

// #4 Use decorators
@suite()
class MyTestSuite {
@beforeAll()
async beforeAll({ user }: TestArgs<UserFixture>) { // have access to user fixture
// ...
}

@test()
async test({ user }: TestArgs<UserFixture>) { // have access to user fixture
// ...
}
}
```
20 changes: 15 additions & 5 deletions lib/afterAll.decorator.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* Custom playwright instance to use instead of standard one.
* For example, provide result of `playwright.extend<T>(customFixture)` to ensure availability of custom fixture in the `afterAll` hook.
*/
playwright?: TestType<T>
}

/**
* 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 = <T = void>(options?: AfterAllDecoratorOptions<T>) =>
function (originalMethod: TestMethod<T>, context: ClassMethodDecoratorContext) {
context.addInitializer(function () {
const decoratedBeforeAll = decoratePlaywrightTest(
const decoratedBeforeAll = decoratePlaywrightTest<T>(
originalMethod,
(originalMethod) =>
(...args) =>
originalMethod.call(this, ...args)
)

playwright.afterAll(decoratedBeforeAll)
const { afterAll } = options?.playwright || (playwright as TestType<T>)

afterAll(decoratedBeforeAll)
})
}
20 changes: 15 additions & 5 deletions lib/afterEach.decorator.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* Custom playwright instance to use instead of standard one.
* For example, provide result of `playwright.extend<T>(customFixture)` to ensure availability of custom fixture in the `afterEach` hook.
*/
playwright?: TestType<T>
}

/**
* 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 = <T = void>(options?: AfterEachDecoratorOptions<T>) =>
function (originalMethod: TestMethod<T>, context: ClassMethodDecoratorContext) {
context.addInitializer(function () {
const decoratedBeforeEach = decoratePlaywrightTest(
const decoratedBeforeEach = decoratePlaywrightTest<T>(
originalMethod,
(originalMethod) =>
(...args) =>
originalMethod.call(this, ...args)
)

playwright.afterEach(decoratedBeforeEach)
const { afterEach } = options?.playwright || (playwright as TestType<T>)

afterEach(decoratedBeforeEach)
})
}
20 changes: 15 additions & 5 deletions lib/beforeAll.decorator.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* Custom playwright instance to use instead of standard one.
* For example, provide result of `playwright.extend<T>(customFixture)` to ensure availability of custom fixture in the `beforeAll` hook.
*/
playwright?: TestType<T>
}

/**
* 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 = <T = void>(options?: BeforeAllDecoratorOptions<T>) =>
function (originalMethod: TestMethod<T>, context: ClassMethodDecoratorContext) {
context.addInitializer(function () {
const decoratedBeforeAll = decoratePlaywrightTest(
const decoratedBeforeAll = decoratePlaywrightTest<T>(
originalMethod,
(originalMethod) =>
(...args) =>
originalMethod.call(this, ...args)
)

playwright.beforeAll(decoratedBeforeAll)
const { beforeAll } = options?.playwright || (playwright as TestType<T>)

beforeAll(decoratedBeforeAll)
})
}
20 changes: 15 additions & 5 deletions lib/beforeEach.decorator.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* Custom playwright instance to use instead of standard one.
* For example, provide result of `playwright.extend<T>(customFixture)` to ensure availability of custom fixture in the `beforeEach` hook.
*/
playwright?: TestType<T>
}

/**
* 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 = <T = void>(options?: BeforeEachDecoratorOptions<T>) =>
function (originalMethod: TestMethod<T>, context: ClassMethodDecoratorContext) {
context.addInitializer(function () {
const decoratedBeforeEach = decoratePlaywrightTest(
const decoratedBeforeEach = decoratePlaywrightTest<T>(
originalMethod,
(originalMethod) =>
(...args) =>
originalMethod.call(this, ...args)
)

playwright.beforeEach(decoratedBeforeEach)
const { beforeEach } = options?.playwright || (playwright as TestType<T>)

beforeEach(decoratedBeforeEach)
})
}
12 changes: 9 additions & 3 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = void> = T &
PlaywrightTestArgs &
PlaywrightTestOptions &
PlaywrightWorkerArgs &
PlaywrightWorkerOptions
export type TestMethod = (args: TestArgs, testInfo: TestInfo) => void | Promise<void>
export type TestMethod<T = void> = (args: TestArgs<T>, testInfo: TestInfo) => void | Promise<void>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TestClass = { new (...args: any[]): any }
export type TestType<T = any> = PlaywrightTestType<
TestArgs<T>,
PlaywrightWorkerArgs & PlaywrightWorkerOptions
>
25 changes: 25 additions & 0 deletions lib/extend.ts
Original file line number Diff line number Diff line change
@@ -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<T>
*/
export const extend = <T>(customPlaywright: TestType<T>) => {
return {
afterAll: (...options: Parameters<typeof afterAll>) =>
afterAll<T>({ ...options, playwright: customPlaywright }),
afterEach: (...options: Parameters<typeof afterEach>) =>
afterEach<T>({ ...options, playwright: customPlaywright }),
test: (...options: Parameters<typeof test>) =>
test<T>({ ...options, playwright: customPlaywright }),
beforeAll: (...options: Parameters<typeof beforeAll>) =>
beforeAll<T>({ ...options, playwright: customPlaywright }),
beforeEach: (...options: Parameters<typeof beforeEach>) =>
beforeEach<T>({ ...options, playwright: customPlaywright })
}
}
Loading

0 comments on commit 3a44870

Please sign in to comment.