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: add support for creating custom test and suite decorator #38

Merged
merged 9 commits into from
Jan 3, 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
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
Loading