Skip to content

Commit

Permalink
feat: add support for creating custom test and suite decorator (#38)
Browse files Browse the repository at this point in the history
* feat: add `createSuiteAndTestDecorator` util

* chore: rewrite decorators to use `create*Decorator` utils

* chore: move errors to custom file

* docs: document `createTestAndSuiteDecorator`

* fix: export `TestInfo` type

* chore: add changeset

* chore: format code

* docs: reformat

* ci: fix step name
  • Loading branch information
SebastianSedzik committed Jan 3, 2024
1 parent bd10be1 commit dbea0b6
Show file tree
Hide file tree
Showing 24 changed files with 283 additions and 239 deletions.
18 changes: 18 additions & 0 deletions .changeset/great-doors-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'playwright-decorators': patch
---

Fix export of `TestInfo` type

```ts
import { suite, test, TestArgs, TestInfo } from '@playwright/test'

@suite()
class TestSuite {
@test()
myTest({ page }: TestArgs, testInfo: TestInfo) {
// ...
}
}

```
31 changes: 31 additions & 0 deletions .changeset/red-beans-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'playwright-decorators': minor
---

Added support for creating custom test and suite decorator

```ts
import { createSuiteAndTestDecorator } from 'playwright-decorators'
import playwright from '@playwright/test'

const mySuiteAndTestDecorator = createSuiteAndTestDecorator(
'mySuiteAndTestDecorator',
({suite}) => {
suite.initialized(() => {
/** run custom code when suite is initialized **/
})
},
({test}) => {
test.beforeTest(() => {
/** run custom code before test execution **/
})
test.afterTest(() => {
/** run custom code after test execution **/
})

playwright.beforeEach(() => {
/** run custom code before each test execution **/
})
}
);
```
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v4
- name: Prepare
uses: ./.github/actions/prepare
- name: Build
- name: Test
run: |
npx playwright install chromium
npm run test
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ Attempting to utilize a custom test decorator on a method that lacks the `@test`
import { suite, createTestDecorator } from 'playwright-decorators';
import playwright from '@playwright/test';

const customTestDecorator = createTestDecorator('customTestDecorator', ({ test, context }) => {
const customTestDecorator = createTestDecorator('customTestDecorator', ({ test }) => {
// create code using hooks provided by test decorator...
test.beforeTest(() => { /* ... */ })
test.afterTest(() => { /* ... */ })
Expand Down Expand Up @@ -379,8 +379,12 @@ Attempting to apply a custom suite decorator to a class that lacks the `@suite`
```ts
import { suite, createSuiteDecorator } from 'playwright-decorators';

const customSuiteDecorator = createSuiteDecorator('customSuiteDecorator', ({ suite, context }) => {
// ...
const customSuiteDecorator = createSuiteDecorator('customSuiteDecorator', ({ suite }) => {
// run your custom code imadiately
suite.name = 'Custom name';

// or attach to specific hooks...
suite.initialized(() => { /* ... */ })
});
```

Expand All @@ -392,3 +396,20 @@ class MyTestSuite {
// ...
}
```

### Suite and test decorator
The `createSuiteAndTestDecorator` function allows the creation of custom decorators that can be applied to both suites and tests.

```ts
import {createSuiteAndTestDecorator} from 'playwright-decorators';

const customSuiteAndTestDecorator = createSuiteAndTestDecorator(
'customSuiteAndTestDecorator',
({ suite }) => {
// custom suite decorator code
},
({ test }) => {
// code test decorator code
}
)
```
42 changes: 22 additions & 20 deletions examples/tests/decorators/withUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,31 @@ import { createSuiteDecorator } from 'playwright-decorators'
* Please use it with `@suite` decorator.
*/
export const withUser = (options: { features: string[] }) =>
createSuiteDecorator('withUser', () => {
let testUser: { email: string; password: string }
createSuiteDecorator('withUser', ({ suite }) => {
suite.initialized(() => {
let testUser: { email: string; password: string }

// #1 Get test user credentials before all tests
playwright.beforeAll(async () => {
const testUserPayload = { features: options.features }
// #1 Get test user credentials before all tests
playwright.beforeAll(async () => {
const testUserPayload = { features: options.features }

// #2 Send request to create a new test user
const testUserData = await fetch('http://localhost:3000/create-user', {
method: 'POST',
body: JSON.stringify(testUserPayload),
headers: { 'Content-Type': 'application/json' }
})
// #2 Send request to create a new test user
const testUserData = await fetch('http://localhost:3000/create-user', {
method: 'POST',
body: JSON.stringify(testUserPayload),
headers: { 'Content-Type': 'application/json' }
})

// #3 Keep credentials of test user
testUser = await testUserData.json()
})
// #3 Keep credentials of test user
testUser = await testUserData.json()
})

// #4 Login with test user credentials before each test
playwright.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000/sign-in')
await page.getByTestId('sign-in-email').fill(testUser.email)
await page.getByTestId('sign-in-password').fill(testUser.password)
await page.getByTestId('sign-in-submit').click()
// #4 Login with test user credentials before each test
playwright.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000/sign-in')
await page.getByTestId('sign-in-email').fill(testUser.email)
await page.getByTestId('sign-in-password').fill(testUser.password)
await page.getByTestId('sign-in-submit').click()
})
})
})
18 changes: 4 additions & 14 deletions lib/annotation.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { isTestDecoratedMethod } from './test.decorator'
import { NotTestDecoratedMethodError } from './errors'
import { TestMethod } from './common'
import { createTestDecorator } from './custom'

interface AnnotationDecoratorOptions {
type: 'skip' | 'fail' | 'issue' | 'slow' | string
Expand All @@ -12,14 +10,6 @@ interface AnnotationDecoratorOptions {
* Annotations are accessible via test.info().annotations. Many reporters show annotations, for example 'html'.
*/
export const annotation = (options: AnnotationDecoratorOptions) =>
function (
originalMethod: TestMethod,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
context: ClassMethodDecoratorContext
) {
if (isTestDecoratedMethod(originalMethod)) {
originalMethod.testDecorator.annotations.push(options)
} else {
throw new NotTestDecoratedMethodError('annotation', originalMethod)
}
}
createTestDecorator('annotation', ({ test }) => {
test.annotations.push(options)
})
6 changes: 3 additions & 3 deletions lib/common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
PlaywrightTestArgs,
TestInfo,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions
PlaywrightWorkerOptions,
TestInfo as PlaywrightTestInfo
} from '@playwright/test'

export { TestInfo } from '@playwright/test'
export type TestInfo = PlaywrightTestInfo
export type TestArgs = PlaywrightTestArgs &
PlaywrightTestOptions &
PlaywrightWorkerArgs &
Expand Down
119 changes: 106 additions & 13 deletions lib/custom.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
import { isSuiteDecoratedMethod, SuiteDecorator } from './suite.decorator'
import { isTestDecoratedMethod, TestDecorator } from './test.decorator'
import { NotSuiteDecoratedMethodError, NotTestDecoratedMethodError } from './errors'
import { TestClass, TestMethod } from './common'

export class NotSuiteDecoratedMethodError extends Error {
constructor(decoratorName: string, method: TestClass) {
super(`
The @${decoratorName} decorator can only be used on class that also have the @suite decorator.
Make sure ${method?.name} is marked with @suite, and that ${decoratorName} comes before @suite, like this:
@${decoratorName}
@suite()
${method?.name}() {}`)
}
}

export class NotTestDecoratedMethodError extends Error {
constructor(decoratorName: string, method: TestMethod) {
super(`
The @${decoratorName} decorator can only be used on methods that also have the @test decorator.
Make sure ${method?.name} is marked with @test, and that ${decoratorName} comes before @test, like this:
@${decoratorName}
@test()
${method?.name}() {}`)
}
}

export class NotSuiteOrTestDecoratedMethodError extends Error {
constructor(decoratorName: string, method: TestClass | TestMethod) {
super(`
The @${decoratorName} decorator can only be used on classes/methods that also have the @suite or @test decorator.
Make sure ${method?.name} is marked with @suite or @test, and that ${decoratorName} comes before @suite or @test, like this:
@${decoratorName}
@suite() / @test()
${method?.name}() {}
`)
}
}

type CustomSuiteDecorator = (params: {
/**
* @suite decorator context
*/
suite: SuiteDecorator
/**
* The suite class that is being decorated.
*/
suiteClass: TestClass
/**
* The context of the suite class that is being decorated.
*/
context: ClassDecoratorContext
}) => void

Expand All @@ -15,22 +61,31 @@ type CustomSuiteDecorator = (params: {
* @param suiteDecorator a custom decorator function
*/
export const createSuiteDecorator = (name: string, suiteDecorator: CustomSuiteDecorator) => {
return function (originalMethod: TestClass, context: ClassDecoratorContext) {
if (!isSuiteDecoratedMethod(originalMethod)) {
throw new NotSuiteDecoratedMethodError(name, originalMethod)
return function (suiteClass: TestClass, context: ClassDecoratorContext) {
if (!isSuiteDecoratedMethod(suiteClass)) {
throw new NotSuiteDecoratedMethodError(name, suiteClass)
}

originalMethod.suiteDecorator.initialized(() => {
suiteDecorator({
suite: originalMethod.suiteDecorator,
context
})
suiteDecorator({
suite: suiteClass.suiteDecorator,
suiteClass: suiteClass,
context
})
}
}

type CustomTestDecorator = (params: {
/**
* @test decorator context
*/
test: TestDecorator
/**
* The test method that is being decorated.
*/
testMethod: TestMethod
/**
* The context of the test method that is being decorated.
*/
context: ClassMethodDecoratorContext
}) => void

Expand All @@ -41,14 +96,52 @@ type CustomTestDecorator = (params: {
* @param testDecorator a custom decorator function
*/
export const createTestDecorator = (name: string, testDecorator: CustomTestDecorator) => {
return function (originalMethod: TestMethod, context: ClassMethodDecoratorContext) {
if (!isTestDecoratedMethod(originalMethod)) {
throw new NotTestDecoratedMethodError(name, originalMethod)
return function (testMethod: TestMethod, context: ClassMethodDecoratorContext) {
if (!isTestDecoratedMethod(testMethod)) {
throw new NotTestDecoratedMethodError(name, testMethod)
}

testDecorator({
test: originalMethod.testDecorator,
test: testMethod.testDecorator,
testMethod,
context
})
}
}

/**
* Generates a decorator specifically intended for use with both @suite and @test.
* @param name name of the decorator
* @param suiteDecorator a custom decorator function intended for use with @suite
* @param testDecorator a custom decorator function intended for use with @test
*/
export const createSuiteAndTestDecorator = (
name: string,
suiteDecorator: CustomSuiteDecorator,
testDecorator: CustomTestDecorator
) => {
return function (
originalMethod: TestClass | TestMethod,
context: ClassDecoratorContext | ClassMethodDecoratorContext
) {
if (isSuiteDecoratedMethod(originalMethod)) {
suiteDecorator({
suite: originalMethod.suiteDecorator,
suiteClass: originalMethod as TestClass,
context: context as ClassDecoratorContext
})
return
}

if (isTestDecoratedMethod(originalMethod)) {
testDecorator({
test: originalMethod.testDecorator,
testMethod: originalMethod as TestMethod,
context: context as ClassMethodDecoratorContext
})
return
}

throw new NotSuiteOrTestDecoratedMethodError(name, originalMethod)
}
}
38 changes: 0 additions & 38 deletions lib/errors.ts

This file was deleted.

Loading

0 comments on commit dbea0b6

Please sign in to comment.