Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support fixtures #56

Merged
merged 4 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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<

Check warning on line 19 in lib/common.ts

View workflow job for this annotation

GitHub Actions / Code formatting

Unexpected any. Specify a different type
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
Loading