diff --git a/.changeset/config.json b/.changeset/config.json index dcd3d8b..26b0e6b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": "@changesets/cli/changelog", + "changelog": ["@changesets/changelog-github", { "repo": "sebastianSedzik/playwright-decorators" }], "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/funny-pugs-kick.md b/.changeset/funny-pugs-kick.md index c8dd35f..7106a40 100644 --- a/.changeset/funny-pugs-kick.md +++ b/.changeset/funny-pugs-kick.md @@ -2,7 +2,7 @@ 'playwright-decorators': patch --- -- Display `reason` from `@skip` decorator. -- Show tags from `@skip` tests. -- Provide more detailed info in error messages. -- Cosmetic changes in readme file +Fixes of `@skip` and `@annotate` decorators: + +- Pass `reason` from `@skip` decorator to the reporter. +- Added support for annotations on skipped tests. diff --git a/.changeset/strong-kangaroos-fly.md b/.changeset/strong-kangaroos-fly.md new file mode 100644 index 0000000..6b73727 --- /dev/null +++ b/.changeset/strong-kangaroos-fly.md @@ -0,0 +1,22 @@ +--- +'playwright-decorators': minor +--- + + +Enhanced TypeScript support: + +- constrained the `@suite` decorator to class contexts only. +- constrained the `@test` decorator to class method context only. Type check of test method arguments. +- exported the `TestArgs` type to provide validity within test methods. + +```ts +import { suite, test, TestArgs } from 'playwright-decorators'; + +@suite() +class ExampleSuite { + @test() + async exampleTest({ page }: TestArgs) { // <- TestArgs ensures correct types of arguments + // ... + } +} +``` diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7cdc90a..57c615b 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -37,6 +37,7 @@ jobs: run: | cd examples npx playwright install chromium + npm run build npm t - name: Publish or release proposal uses: changesets/action@v1 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c050bef..a993cf3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -33,4 +33,5 @@ jobs: run: | cd examples npx playwright install chromium + npm run build npm t diff --git a/README.md b/README.md index 921e2db..e95e54f 100644 --- a/README.md +++ b/README.md @@ -13,25 +13,25 @@ npm i playwright-decorators ## 🏗️ Usage Declare tests using `@suite` and `@test` decorators ```ts -import { suite, test, slow, tag } from 'playwright-decorators'; +import { suite, test, slow, tag, TestArgs, TestInfo } from 'playwright-decorators'; @suite() // <-- Decorate class with @suite class MyTestSuite { @test() // <-- Decorate test method with @test - async myTest({ page }) { + async myTest({ page }: TestArgs, testInfo: TestInfo) { // ... } @tag(['team-x']) @slow('Response from pasword reset service takes a long time') @test() - async userShouldBeAbleToResetPassword({ page }) { + async userShouldBeAbleToResetPassword({ page }: TestArgs) { // ... } @withUser({ features: ['payment'] }) // <- Use your own custom decorators @test() - async userShouldBeAbleToCancelSubscription({ page }) { + async userShouldBeAbleToCancelSubscription({ page }: TestArgs) { // ... } } @@ -63,12 +63,12 @@ Mark class method as test. Under the hood, decorator creates a `test` block and runs the method inside it. ```ts -import { suite, test } from 'playwright-decorators'; +import { suite, test, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { @test() // <-- Decorate test method with @test() or @test(options) - async myTest({ page }) { + async myTest({ page }: TestArgs) { // ... } } @@ -82,12 +82,12 @@ class MyTestSuite { Mark method as `beforeAll` book. ```ts -import { suite, test, beforeAll } from 'playwright-decorators'; +import { suite, test, beforeAll, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { @beforeAll() // <-- Decorate method with @beforeAll() - async beforeAll({page}) { + async beforeAll({ page }: TestArgs) { // ... } } @@ -98,12 +98,12 @@ class MyTestSuite { Mark method as `beforeEach` book. ```ts -import { suite, test, beforeEach } from 'playwright-decorators'; +import { suite, test, beforeEach, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { @beforeEach() // <-- Decorate method with @beforeEach() - async beforeEach({ page }) { + async beforeEach({ page }: TestArgs) { // ... } } @@ -114,12 +114,12 @@ class MyTestSuite { Mark method as `afterAll` book. ```ts -import { suite, test, afterAll } from 'playwright-decorators'; +import { suite, test, afterAll, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { @afterAll() // <-- Decorate method with @afterAll() - async afterAll({page}) { + async afterAll({ page }: TestArgs) { // ... } } @@ -130,12 +130,12 @@ class MyTestSuite { Mark method as `afterEach` book. ```ts -import { suite, test, afterEach } from 'playwright-decorators'; +import { suite, test, afterEach, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { @afterEach() // <-- Decorate method with @afterEach() - async afterEach({ page }) { + async afterEach({ page }: TestArgs) { // ... } } @@ -146,7 +146,7 @@ class MyTestSuite { Skip single `@test` or `@suite`. ```ts -import { suite, test, skip } from 'playwright-decorators'; +import { suite, test, skip, TestArgs } from 'playwright-decorators'; // Skip test suite @skip() // <-- Decorate suite with @skip() @@ -159,7 +159,7 @@ class SkippedTestSuite { class MyTestSuite { @skip() // <-- Decorate test with @skip() @test() - async skippedTest({ page }) { + async skippedTest({ page }: TestArgs) { // ... } } @@ -175,7 +175,7 @@ Playwright Test runs this test and ensures that it is actually failing. This is useful for documentation purposes to acknowledge that some functionality is broken until it is fixed. ```ts -import { suite, test, fail } from 'playwright-decorators'; +import { suite, test, fail, TestArgs } from 'playwright-decorators'; // Mark suite as "fail", ensure that all tests from suite fail @fail() // <-- Decorate suite with @fail() @@ -188,7 +188,7 @@ class FailTestSuite { class MyTestSuite { @fail() // <-- Decorate test with @fail() @test() - async failingTest({ page }) { + async failingTest({ page }: TestArgs) { // ... } } @@ -203,7 +203,7 @@ Marks a `@test` or `@suite` as "fixme", with the intention to fix (with optional Decorated tests or suites will not be run. ```ts -import { suite, test, fixme } from 'playwright-decorators'; +import { suite, test, fixme, TestArgs } from 'playwright-decorators'; // Mark test suite as "fixme" @fixme() // <-- Decorate suite with @fixme() @@ -216,7 +216,7 @@ class FixmeTestSuite { class MyTestSuite { @fixme() // <-- Decorate test with @fixme() @test() - async fixmeTest({ page }) { + async fixmeTest({ page }: TestArgs) { // ... } } @@ -231,7 +231,7 @@ Mark single `@test` or `@suite` as "slow". Slow test will be given triple the default timeout. ```ts -import { suite, test, skip } from 'playwright-decorators'; +import { suite, test, skip, TestArgs } from 'playwright-decorators'; // Mark test suite as "slow" @slow() // <-- Decorate suite with @slow() @@ -244,7 +244,7 @@ class SlowTestSuite { class MyTestSuite { @slow() // <-- Decorate test with @slow() @test() - async slowTest({ page }) { + async slowTest({ page }: TestArgs) { // ... } } @@ -259,7 +259,7 @@ Declares a focused `@test` or `@suite`. If there are some focused tests or suites, all of them will be run but nothing else. ```ts -import { suite, test, only } from 'playwright-decorators'; +import { suite, test, only, TestArgs } from 'playwright-decorators'; // Run only selected test suite(s) @only() // <-- Decorate suite with @only() @@ -272,7 +272,7 @@ class FocusedTestSuite { class TestSuite { @only() // <-- Decorate test with @only() @test() - async focusedTest({ page }) { + async focusedTest({ page }: TestArgs) { // ... } } @@ -285,7 +285,7 @@ You can later run test(s) or suite(s) with specific tag, using `npx playwright t For example: to run tests/suites with `x` tag, please run `npx playwright test --grep "@x"` ```ts -import { suite, test, tag } from 'playwright-decorators'; +import { suite, test, tag, TestArgs } from 'playwright-decorators'; // Run only selected test suite(s) @tag(['x-api-consumer']) // <-- Decorate suite with @tag() @@ -298,7 +298,7 @@ class ApiConsumerTestSuite { class TestSuite { @tag(['x-api-consumer']) // <-- Decorate test with @tag() @test() - async apiConsumerTest({ page }) { + async apiConsumerTest({ page }: TestArgs) { // ... } } @@ -318,13 +318,13 @@ Add custom annotation to a test. Annotations are accessible via test.info().annotations. Many reporters show annotations, for example 'html'. ```ts -import { suite, test, annotation } from 'playwright-decorators'; +import { suite, test, annotation, TestArgs } from 'playwright-decorators'; @suite() class MyTestSuite { - @annotate({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/' }) // <-- Decorate test with @annotate() + @annotation({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/' }) // <-- Decorate test with @annotate() @test() - async testWithCustomAnnotation({ page }) { + async testWithCustomAnnotation({ page }: TestArgs) { // ... } } @@ -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, context }) => { // create code using hooks provided by test decorator... test.beforeTest(() => { /* ... */ }) test.afterTest(() => { /* ... */ }) @@ -366,7 +366,7 @@ Then use it on `@test` decorator: class MyTestSuite { @customTestDecorator() // <-- Decorate test with custom decorator @test() - async myTest({ page }) { + async myTest({ page }: TestArgs) { // ... } } @@ -379,7 +379,7 @@ 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, context }) => { // ... }); ``` diff --git a/examples/package.json b/examples/package.json index 239f708..3a3a43f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "start": "node app.js", + "build": "tsc", "test": "playwright test" }, "devDependencies": { diff --git a/examples/tests/basic.spec.ts b/examples/tests/basic.spec.ts index f28a442..6b435ab 100644 --- a/examples/tests/basic.spec.ts +++ b/examples/tests/basic.spec.ts @@ -1,23 +1,26 @@ -import {expect} from "@playwright/test"; +import { expect } from "@playwright/test"; import { beforeEach, suite, test, tag, - fixme, slow, annotation + fixme, + slow, + annotation, + TestArgs } from "playwright-decorators"; @tag(['x-api-consumer']) @suite() class SignInSuite { @beforeEach() - async setPageRoute({ page }) { + async setPageRoute({ page }: TestArgs) { // set sign-in page context for each test in the suite await page.goto('http://localhost:3000/sign-in'); } @test() - async userShouldNotBeAbleToSignInWithInvalidCredentials({ page }) { + async userShouldNotBeAbleToSignInWithInvalidCredentials({ page }: TestArgs) { // when user fills invalid credentials & submits the form await page.getByTestId('sign-in-email').fill("example@email.com"); await page.getByTestId('sign-in-password').fill("example password"); @@ -32,7 +35,7 @@ class SignInSuite { description: 'jira.com/issue-123' }) @test() - async userShouldBeAbleToResetPassword({ page }) { + async userShouldBeAbleToResetPassword({ page }: TestArgs) { await page.goto('http://localhost:3000/sign-in/reset'); // ... } @@ -40,7 +43,7 @@ class SignInSuite { @tag(['team-y']) @slow("/sign-in/sso page is under the development, and needs more then 500ms to load") @test() - async userShouldBeAbleToLoginViaSSO({ page }) { + async userShouldBeAbleToLoginViaSSO({ page }: TestArgs) { await page.goto('http://localhost:3000/sign-in/sso'); await expect(page.getByTestId('page-title')).toHaveText('SSO Login'); // ... diff --git a/examples/tests/custom-decorators.spec.ts b/examples/tests/custom-decorators.spec.ts index 8d16c01..17310d4 100644 --- a/examples/tests/custom-decorators.spec.ts +++ b/examples/tests/custom-decorators.spec.ts @@ -1,4 +1,4 @@ -import { suite, test } from "playwright-decorators"; +import { suite, test, TestArgs } from "playwright-decorators"; import { withUser, withRoute } from './decorators'; import { expect } from "@playwright/test"; @@ -11,7 +11,7 @@ import { expect } from "@playwright/test"; @suite() class AuthorizedUserSuite { @test() - async shouldBeLogged({ page }) { + async shouldBeLogged({ page }: TestArgs) { // When on `/` route await page.goto('http://localhost:3000/') @@ -21,7 +21,7 @@ class AuthorizedUserSuite { @withRoute('settings') // <- usage of custom `withRoute` decorator @test() - async shouldHaveRequestedFeatures({ page }) { + async shouldHaveRequestedFeatures({ page }: TestArgs) { // When on `/settings` route await expect(page).toHaveURL('http://localhost:3000/settings') @@ -36,7 +36,7 @@ class AuthorizedUserSuite { class UnauthorizedUserSuite { @withRoute('settings') // <- usage of custom `withRoute` decorator @test() - async shouldBeRedirectedToSignInPage({ page }) { + async shouldBeRedirectedToSignInPage({ page }: TestArgs) { await expect(page).toHaveURL(/sign-in/) } } diff --git a/examples/tests/decorators/withRoute.ts b/examples/tests/decorators/withRoute.ts index c78ac26..df3ac52 100644 --- a/examples/tests/decorators/withRoute.ts +++ b/examples/tests/decorators/withRoute.ts @@ -1,4 +1,4 @@ -import {createTestDecorator} from "playwright-decorators"; +import { createTestDecorator } from "playwright-decorators"; import playwright, {Page} from "@playwright/test"; /** @@ -8,7 +8,7 @@ import playwright, {Page} from "@playwright/test"; */ export const withRoute = (url: string) => createTestDecorator('withRoute', ({ test }) => { let _page: Page; - + // #1 Extract `page` from test context playwright.beforeEach(({ page }) => { _page = page; diff --git a/examples/tests/decorators/withUser.ts b/examples/tests/decorators/withUser.ts index 1ad458c..886cf90 100644 --- a/examples/tests/decorators/withUser.ts +++ b/examples/tests/decorators/withUser.ts @@ -1,5 +1,5 @@ -import {createSuiteDecorator} from "playwright-decorators"; import playwright from "@playwright/test"; +import { createSuiteDecorator } from "playwright-decorators"; /** * Provide context of logged-in user for each @test in the given @suite. diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..73ac3f6 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "rootDir": "./", + "target": "es6", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "types": ["node"], + "declaration": true, + "noEmit": true + }, + "include": [ + "." + ], + "exclude": [ + "dist" + ] +} diff --git a/lib/afterAll.decorator.ts b/lib/afterAll.decorator.ts index 5a64d31..fb30504 100644 --- a/lib/afterAll.decorator.ts +++ b/lib/afterAll.decorator.ts @@ -1,16 +1,17 @@ import playwright from '@playwright/test'; import {decoratePlaywrightTest} from "./helpers"; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface AfterAllOptions {} +import {TestMethod} from "./common"; /** * Run method after all tests in the suite. * Target class should be marked by @suite decorator. */ -export const afterAll = (options: AfterAllOptions = {}) => function(originalMethod: any, context: any) { - (context as ClassMemberDecoratorContext ).addInitializer(function () { - +export const afterAll = () => function( + originalMethod: TestMethod, + context: ClassMethodDecoratorContext +) { + context.addInitializer(function () { + const decoratedBeforeAll = decoratePlaywrightTest( originalMethod, originalMethod => (...args) => originalMethod.call(this, ...args) diff --git a/lib/afterEach.decorator.ts b/lib/afterEach.decorator.ts index 65e25b0..6cafdce 100644 --- a/lib/afterEach.decorator.ts +++ b/lib/afterEach.decorator.ts @@ -1,16 +1,17 @@ import playwright from '@playwright/test'; import {decoratePlaywrightTest} from "./helpers"; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface AfterEachOptions {} +import {TestMethod} from "./common"; /** * Run method after each test in suite. * Target class should be marked by @suite decorator. */ -export const afterEach = (options: AfterEachOptions = {}) => function(originalMethod: any, context: any) { - (context as ClassMemberDecoratorContext ).addInitializer(function () { - +export const afterEach = () => function( + originalMethod: TestMethod, + context: ClassMethodDecoratorContext +) { + context.addInitializer(function () { + const decoratedBeforeEach = decoratePlaywrightTest( originalMethod, originalMethod => (...args) => originalMethod.call(this, ...args) diff --git a/lib/annotation.decorator.ts b/lib/annotation.decorator.ts index ca2107a..6dd8db2 100644 --- a/lib/annotation.decorator.ts +++ b/lib/annotation.decorator.ts @@ -1,5 +1,6 @@ import {isTestDecoratedMethod} from "./test.decorator"; import {NotTestDecoratedMethodError} from "./errors"; +import {TestMethod} from "./common"; interface AnnotationDecoratorOptions { type: 'skip' | 'fail' | 'issue' | 'slow' | string; @@ -10,7 +11,11 @@ interface AnnotationDecoratorOptions { * Add custom annotation to a @test. * Annotations are accessible via test.info().annotations. Many reporters show annotations, for example 'html'. */ -export const annotation = (options: AnnotationDecoratorOptions) => function(originalMethod: any, context?: any) { +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 { diff --git a/lib/beforeAll.decorator.ts b/lib/beforeAll.decorator.ts index 7830866..6583833 100644 --- a/lib/beforeAll.decorator.ts +++ b/lib/beforeAll.decorator.ts @@ -1,15 +1,16 @@ import playwright from '@playwright/test'; import {decoratePlaywrightTest} from "./helpers"; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface BeforeAllOptions {} +import {TestMethod} from "./common"; /** * Run method before all tests in the suite. * Target class should be marked by @suite decorator. */ -export const beforeAll = (options: BeforeAllOptions = {}) => function(originalMethod: any, context: any) { - (context as ClassMemberDecoratorContext ).addInitializer(function () { +export const beforeAll = () => function( + originalMethod: TestMethod, + context: ClassMethodDecoratorContext +) { + context.addInitializer(function () { const decoratedBeforeAll = decoratePlaywrightTest( originalMethod, diff --git a/lib/beforeEach.decorator.ts b/lib/beforeEach.decorator.ts index 97d9f8b..3051887 100644 --- a/lib/beforeEach.decorator.ts +++ b/lib/beforeEach.decorator.ts @@ -1,16 +1,17 @@ import playwright from '@playwright/test'; import {decoratePlaywrightTest} from "./helpers"; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface BeforeEachOption {} +import {TestMethod} from "./common"; /** * Run method before each test in the suite. * Target class should be marked by @suite decorator. */ -export const beforeEach = (options: BeforeEachOption = {}) => function(originalMethod: any, context: any) { - (context as ClassMemberDecoratorContext ).addInitializer(function () { - +export const beforeEach = () => function( + originalMethod: TestMethod, + context: ClassMethodDecoratorContext +) { + context.addInitializer(function () { + const decoratedBeforeEach = decoratePlaywrightTest( originalMethod, originalMethod => (...args) => originalMethod.call(this, ...args) diff --git a/lib/common.ts b/lib/common.ts new file mode 100644 index 0000000..ddc47c4 --- /dev/null +++ b/lib/common.ts @@ -0,0 +1,7 @@ +import {PlaywrightTestArgs, TestInfo, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions} from "@playwright/test"; + +export { TestInfo } from "@playwright/test"; +export type TestArgs = PlaywrightTestArgs & PlaywrightTestOptions & PlaywrightWorkerArgs & PlaywrightWorkerOptions; +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 }; diff --git a/lib/custom.ts b/lib/custom.ts index 3ee363f..a2a7ea8 100644 --- a/lib/custom.ts +++ b/lib/custom.ts @@ -1,8 +1,9 @@ import {isSuiteDecoratedMethod, SuiteDecorator} from "./suite.decorator"; import {isTestDecoratedMethod, TestDecorator} from "./test.decorator"; import {NotSuiteDecoratedMethodError, NotTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; -type CustomSuiteDecorator = (params: { suite: SuiteDecorator, context?: ClassMethodDecoratorContext }) => void; +type CustomSuiteDecorator = (params: { suite: SuiteDecorator, context: ClassDecoratorContext }) => void; /** * Generates a decorator specifically intended for use with the @suite. @@ -11,7 +12,7 @@ type CustomSuiteDecorator = (params: { suite: SuiteDecorator, context?: ClassMet * @param suiteDecorator a custom decorator function */ export const createSuiteDecorator = (name: string, suiteDecorator: CustomSuiteDecorator) => { - return function(originalMethod: any, context?: any) { + return function(originalMethod: TestClass, context: ClassDecoratorContext) { if (!isSuiteDecoratedMethod(originalMethod)) { throw new NotSuiteDecoratedMethodError(name, originalMethod); } @@ -25,7 +26,7 @@ export const createSuiteDecorator = (name: string, suiteDecorator: CustomSuiteDe } } -type CustomTestDecorator = (params: { test: TestDecorator, context?: any }) => void; +type CustomTestDecorator = (params: { test: TestDecorator, context: ClassMethodDecoratorContext }) => void; /** * Generates a decorator specifically intended for use with the @test. @@ -34,7 +35,7 @@ type CustomTestDecorator = (params: { test: TestDecorator, context?: any }) => v * @param testDecorator a custom decorator function */ export const createTestDecorator = (name: string, testDecorator: CustomTestDecorator) => { - return function(originalMethod: any, context?: any) { + return function(originalMethod: TestMethod, context: ClassMethodDecoratorContext) { if (!isTestDecoratedMethod(originalMethod)) { throw new NotTestDecoratedMethodError(name, originalMethod); } diff --git a/lib/errors.ts b/lib/errors.ts index d39295e..b8614b9 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -1,5 +1,7 @@ +import {TestClass, TestMethod} from "./common"; + export class NotSuiteDecoratedMethodError extends Error { - constructor(decoratorName: string ,method: any) { + 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: @@ -11,7 +13,7 @@ ${method?.name}() {}`); } export class NotTestDecoratedMethodError extends Error { - constructor(decoratorName: string, method: any) { + 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: @@ -24,7 +26,7 @@ ${method?.name}() {}` } export class NotSuiteOrTestDecoratedMethodError extends Error { - constructor(decoratorName: string, method: any) { + 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: diff --git a/lib/fail.decorator.ts b/lib/fail.decorator.ts index 10d5591..ac41a34 100644 --- a/lib/fail.decorator.ts +++ b/lib/fail.decorator.ts @@ -1,13 +1,18 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Marks a @test or @suite as "should fail". * Playwright Test runs this test and ensures that it is actually failing. * This is useful for documentation purposes to acknowledge that some functionality is broken until it is fixed. */ -export const fail = (reason?: string) => function(originalMethod: any, context?: any) { +export const fail = (reason?: string) => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.fail = reason || true; return; diff --git a/lib/fixme.decorator.ts b/lib/fixme.decorator.ts index d9ccbd8..d21c79d 100644 --- a/lib/fixme.decorator.ts +++ b/lib/fixme.decorator.ts @@ -1,12 +1,17 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Marks a @test or @suite as "fixme", with the intention to fix (with optional reason). * Decorated tests or suites will not be run. */ -export const fixme = (reason?: string) => function(originalMethod: any, context?: any) { +export const fixme = (reason?: string) => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.fixme = reason || true; return; diff --git a/lib/helpers.ts b/lib/helpers.ts index 0bc695c..57fc94d 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,13 +1,12 @@ -import {TestInfo} from "@playwright/test"; +import {TestMethod} from "./common"; -export type PlaywrightTestFunction = (args: any, testInfo: TestInfo ) => Promise | void; -export type TestDecoratorFunction = (testFunction: PlaywrightTestFunction) => PlaywrightTestFunction; +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: PlaywrightTestFunction, decorationFunction: TestDecoratorFunction) => { +export const decoratePlaywrightTest = (testFunction: TestMethod, decorationFunction: TestDecoratorFunction) => { const decoratedTestFunction = decorationFunction(testFunction); // expose original function description diff --git a/lib/index.ts b/lib/index.ts index b394f99..a074396 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -30,6 +30,12 @@ export { NotTestDecoratedMethodError } from './errors'; +// common +export type { + TestInfo, + TestArgs +} from './common'; + // custom export { createSuiteDecorator, diff --git a/lib/only.decorator.ts b/lib/only.decorator.ts index 6ae728a..58106ab 100644 --- a/lib/only.decorator.ts +++ b/lib/only.decorator.ts @@ -1,12 +1,17 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Declares a focused test. * If there are some focused @test(s) or @suite(s), all of them will be run but nothing else. */ -export const only = () => function(originalMethod: any, context?: any) { +export const only = () => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.only = true; return; diff --git a/lib/skip.decorator.ts b/lib/skip.decorator.ts index f716f6d..ca66f6d 100644 --- a/lib/skip.decorator.ts +++ b/lib/skip.decorator.ts @@ -1,11 +1,16 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Skip @test or @suite (with optional reason). */ -export const skip = (reason?: string) => function(originalMethod: any, context?: any) { +export const skip = (reason?: string) => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.skip = reason || true; return; diff --git a/lib/slow.decorator.ts b/lib/slow.decorator.ts index fd69ea2..960bac2 100644 --- a/lib/slow.decorator.ts +++ b/lib/slow.decorator.ts @@ -1,12 +1,17 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Marks a @test or @suite as "slow" (with optional reason). * Slow test will be given triple the default timeout. */ -export const slow = (reason?: string) => function(originalMethod: any, context?: any) { +export const slow = (reason?: string) => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.slow = reason || true; return; diff --git a/lib/suite.decorator.ts b/lib/suite.decorator.ts index 48472fb..6ef59e0 100644 --- a/lib/suite.decorator.ts +++ b/lib/suite.decorator.ts @@ -1,6 +1,6 @@ import playwright from '@playwright/test'; +import {TestClass} from "./common"; -type Constructor = { new (...args: any[]): any }; type SuiteHook = () => void; interface SuiteDecoratorOptions { @@ -45,7 +45,7 @@ export class SuiteDecorator implements SuiteDecoratorOptions { private initializedHooks: SuiteHook[] = []; - constructor(private suiteClass: Constructor, options: SuiteDecoratorOptions) { + constructor(private suiteClass: TestClass, options: SuiteDecoratorOptions) { this.name = suiteClass.name; Object.assign(this, options); @@ -103,7 +103,7 @@ export class SuiteDecorator implements SuiteDecoratorOptions { return Promise.all(this.initializedHooks.map(hookFn => hookFn())); } - private async runSuite(userSuiteCode: () => Promise) { + private async runSuite(userSuiteCode: () => Promise) { this.handleSkip(); this.handleSlow(); this.handleFail(); @@ -144,15 +144,15 @@ export function isSuiteDecoratedMethod(method: any): method is SuiteDecoratedMet * * Behaviour of decorator can be modified by other decorators using injected `suiteDecorator` property. */ -export const suite = (options: SuiteDecoratorOptions = {}) => function(constructor: T, context?: ClassMethodDecoratorContext) { +export const suite = (options: SuiteDecoratorOptions = {}) => function(constructor: T, context: ClassDecoratorContext) { const suiteDecorator = new SuiteDecorator(constructor, options); - + /** * Decorate class by `suiteDecorator` property, to allow other decorators to modify suite behaviour / options. */ Object.assign(constructor, { suiteDecorator }); - context?.addInitializer(() => { + context.addInitializer(() => { suiteDecorator.run(); }) } diff --git a/lib/tag.decorator.ts b/lib/tag.decorator.ts index c5868e1..0e625c3 100644 --- a/lib/tag.decorator.ts +++ b/lib/tag.decorator.ts @@ -1,20 +1,25 @@ import {isSuiteDecoratedMethod} from "./suite.decorator"; import {isTestDecoratedMethod} from "./test.decorator"; import {NotSuiteOrTestDecoratedMethodError} from "./errors"; +import {TestClass, TestMethod} from "./common"; /** * Adds tags to `@test` or `@suite`. * You can later run test(s) or suite(s) with specific tag, using `npx playwright test --grep "@nameOfTag"` command. * For example: to run tests/suites with `x` tag, please run `npx playwright test --grep "@x"` */ -export const tag = (tags: string[]) => function(originalMethod: any, context?: any) { +export const tag = (tags: string[]) => function( + originalMethod: TestClass | TestMethod, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ClassDecoratorContext | ClassMethodDecoratorContext +) { const tagsAsPlaywrightAnnotations = tags.map(tag => `@${tag}`).join(' '); if (isSuiteDecoratedMethod(originalMethod)) { originalMethod.suiteDecorator.name = `${originalMethod.suiteDecorator.name} ${tagsAsPlaywrightAnnotations}`; return; } - + if (isTestDecoratedMethod(originalMethod)) { originalMethod.testDecorator.name = `${originalMethod.testDecorator.name} ${tagsAsPlaywrightAnnotations}`; return; diff --git a/lib/test.decorator.ts b/lib/test.decorator.ts index b634272..591b900 100644 --- a/lib/test.decorator.ts +++ b/lib/test.decorator.ts @@ -1,5 +1,6 @@ import playwright from '@playwright/test'; import {decoratePlaywrightTest, TestDecoratorFunction} from "./helpers"; +import {TestMethod} from "./common"; type TestHook = () => void | Promise; @@ -123,7 +124,7 @@ export class TestDecorator implements TestDecoratorOptions { /** * Run playwright.test function using all collected data. */ - run(executionContext: any) { + run(executionContext: ClassMethodDecoratorContext) { const decoratedTest: TestDecoratorFunction = (testFunction) => async (...args) => { this.handleAnnotations(); this.handleSkip(); @@ -176,12 +177,12 @@ 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: any, context: any) { +export const test = (options: TestDecoratorOptions = {}) => function(originalMethod: TestMethod, context: ClassMethodDecoratorContext) { const testDecorator = new TestDecorator(originalMethod, options); Object.assign(originalMethod, { testDecorator }); - (context as ClassMemberDecoratorContext).addInitializer(function () { - testDecorator.run(this); + context.addInitializer(function () { + testDecorator.run(this as ClassMethodDecoratorContext); }); } diff --git a/package-lock.json b/package-lock.json index e7c90aa..9ef5ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,20 @@ { "name": "playwright-decorators", - "version": "0.11.1", + "version": "0.11.2", "lockfileVersion": 3, "requires": true, "dev": true, "packages": { "": { "name": "playwright-decorators", - "version": "0.11.1", + "version": "0.11.2", "license": "MIT", "workspaces": [ "examples", "." ], "devDependencies": { + "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@playwright/test": "1.36.1", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -80,26 +81,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "examples/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "examples/node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -324,6 +305,17 @@ "@changesets/types": "^6.0.0" } }, + "node_modules/@changesets/changelog-github": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.0.tgz", + "integrity": "sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==", + "dev": true, + "dependencies": { + "@changesets/get-github-info": "^0.6.0", + "@changesets/types": "^6.0.0", + "dotenv": "^8.1.0" + } + }, "node_modules/@changesets/cli": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.27.1.tgz", @@ -612,6 +604,16 @@ "node": ">=4" } }, + "node_modules/@changesets/get-github-info": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", + "dev": true, + "dependencies": { + "dataloader": "^1.4.0", + "node-fetch": "^2.5.0" + } + }, "node_modules/@changesets/get-release-plan": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.0.tgz", @@ -2282,6 +2284,12 @@ "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==", "dev": true }, + "node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2419,6 +2427,15 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/dts-bundle-generator": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-8.0.1.tgz", @@ -4278,6 +4295,26 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", diff --git a/package.json b/package.json index 4864ff0..6adb374 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "." ], "scripts": { - "build:lib": "vite build", + "build:lib": "tsc && vite build", "build:types": "dts-bundle-generator --config ./dts-bundle-generator.config.ts", "build": "npm run build:lib", "postbuild": "npm run build:types", @@ -39,6 +39,7 @@ "release": "changeset publish" }, "devDependencies": { + "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@playwright/test": "1.36.1", "@typescript-eslint/eslint-plugin": "5.62.0", diff --git a/tests/annotation.spec.ts b/tests/annotation.spec.ts index 055b51c..824314a 100644 --- a/tests/annotation.spec.ts +++ b/tests/annotation.spec.ts @@ -23,6 +23,8 @@ playwright.describe('@annotate decorator', () => { playwright.describe('without @test', () => { playwright('should throw NotTestDecoratedMethodError', () => { try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore @annotation({ type: 'issue', description: 'url to issue' }) class ExampleClass {} } catch (e) { diff --git a/tests/custom.spec.ts b/tests/custom.spec.ts index 9d730d7..4d0cd0f 100644 --- a/tests/custom.spec.ts +++ b/tests/custom.spec.ts @@ -99,6 +99,8 @@ playwright.describe('custom decorators', () => { playwright('Should throw error if decorator is not used on @test', () => { expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore @customTestDecorator class ExampleClass {} }).toThrowError(NotTestDecoratedMethodError) diff --git a/tests/hooks.spec.ts b/tests/hooks.spec.ts index 6af5760..1ac5e27 100644 --- a/tests/hooks.spec.ts +++ b/tests/hooks.spec.ts @@ -1,4 +1,4 @@ -import { suite, test, afterAll, afterEach, beforeAll, beforeEach } from '../lib'; +import {suite, test, afterAll, afterEach, beforeAll, beforeEach, TestArgs} from '../lib'; import playwright, {expect} from "@playwright/test"; playwright.describe('hooks decorators', () => { @@ -7,7 +7,7 @@ playwright.describe('hooks decorators', () => { @suite() class HooksSuite { @beforeAll() - beforeAll({ browser }) { + beforeAll({ browser }: TestArgs) { called.push('beforeAll'); // ensure correctness of `this` context @@ -18,7 +18,7 @@ playwright.describe('hooks decorators', () => { } @beforeEach() - beforeEach({ browser }) { + beforeEach({ browser }: TestArgs) { called.push('beforeEach'); // ensure correctness of `this` context @@ -29,7 +29,7 @@ playwright.describe('hooks decorators', () => { } @afterAll() - afterAll({ browser }) { + afterAll({ browser }: TestArgs) { called.push('afterAll'); // ensure correctness of `this` context @@ -40,7 +40,7 @@ playwright.describe('hooks decorators', () => { } @afterEach() - afterEach({ browser }) { + afterEach({ browser }: TestArgs) { called.push('afterEach'); // ensure correctness of `this` context @@ -49,7 +49,7 @@ playwright.describe('hooks decorators', () => { // ensure fixture is passed expect(browser).not.toBeUndefined(); } - + @test() testMethod() { called.push('testMethod'); diff --git a/tests/tag.spec.ts b/tests/tag.spec.ts index 831c931..72ea64e 100644 --- a/tests/tag.spec.ts +++ b/tests/tag.spec.ts @@ -1,6 +1,5 @@ -import playwright, {expect} from "@playwright/test"; -import {mockFn} from "./__mocks__/mockFn"; -import {fail, NotSuiteOrTestDecoratedMethodError, suite, tag, test} from "../lib"; +import playwright, {expect, TestInfo} from "@playwright/test"; +import {NotSuiteOrTestDecoratedMethodError, suite, tag, test, TestArgs} from "../lib"; playwright.describe('@tag decorator', () => { playwright.describe('with @suite', () => { @@ -10,7 +9,7 @@ playwright.describe('@tag decorator', () => { @suite() class SuiteWithTag { @test() - async test({}, testInfo) { + async test({}: TestArgs, testInfo: TestInfo) { titlePath.push(...testInfo.titlePath); } } @@ -28,7 +27,7 @@ playwright.describe('@tag decorator', () => { class TestSuite { @tag(['x']) @test() - async testWithTag({}, testInfo) { + async testWithTag({}: TestArgs, testInfo: TestInfo) { titlePath.push(...testInfo.titlePath); } } diff --git a/tests/test.spec.ts b/tests/test.spec.ts index 8cdc483..df5a83f 100644 --- a/tests/test.spec.ts +++ b/tests/test.spec.ts @@ -1,5 +1,5 @@ import playwright, {expect} from "@playwright/test"; -import {suite, test} from "../lib"; +import {suite, test, TestArgs} from "../lib"; playwright.describe('@test decorator', () => { const called: string[] = []; @@ -27,7 +27,7 @@ playwright.describe('@test decorator', () => { } @test() - testShouldHaveAccessToPage({ page }) { + testShouldHaveAccessToPage({ page }: TestArgs) { called.push('testShouldHaveAccessToPage'); expect(page).not.toBeUndefined(); } diff --git a/tsconfig.json b/tsconfig.json index 5996ba1..1f6ba44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "strict": true, "sourceMap": true, "resolveJsonModule": true, - "experimentalDecorators": true, "esModuleInterop": true, "skipLibCheck": true, "noUnusedLocals": false, @@ -25,6 +24,7 @@ "." ], "exclude": [ - "dist" + "dist", + "examples" ] }