From 7dc8495f32269942efc88e3152a1091c4f0a4fc6 Mon Sep 17 00:00:00 2001 From: Jimmy Guzman <30631540+jimmy-guzman@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:30:47 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20`eslint-plugin-playwr?= =?UTF-8?q?ight`=20rules=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🏁 Closes: #98 --- package.json | 2 + pnpm-lock.yaml | 66 ++++ src/configs/playwright.ts | 18 ++ src/constants.ts | 6 +- src/factory.spec.ts | 53 +-- src/factory.ts | 5 + src/rules.gen.d.ts | 306 ++++++++++++++++++ .../__snapshots__/playwright.spec.ts.snap | 31 ++ src/rules/playwright.spec.ts | 5 + src/rules/playwright.ts | 27 ++ src/types.ts | 7 +- src/utils/has-dependency.ts | 4 + 12 files changed, 502 insertions(+), 28 deletions(-) create mode 100644 src/configs/playwright.ts create mode 100644 src/rules/__snapshots__/playwright.spec.ts.snap create mode 100644 src/rules/playwright.spec.ts create mode 100644 src/rules/playwright.ts diff --git a/package.json b/package.json index 2ce86a0..37ac6a1 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-n": "^17.14.0", "eslint-plugin-perfectionist": "^4.0.3", + "eslint-plugin-playwright": "^2.1.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "0.4.14", @@ -77,6 +78,7 @@ "@commitlint/config-conventional": "19.6.0", "@eslint/config-inspector": "0.5.6", "@jimmy.codes/prettier-config": "1.3.0", + "@playwright/test": "1.49.0", "@semantic-release/changelog": "6.0.3", "@semantic-release/commit-analyzer": "13.0.0", "@semantic-release/git": "10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8419cd9..f915c44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: eslint-plugin-perfectionist: specifier: ^4.0.3 version: 4.0.3(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3) + eslint-plugin-playwright: + specifier: ^2.1.0 + version: 2.1.0(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-react: specifier: ^7.37.2 version: 7.37.2(eslint@9.15.0(jiti@2.4.0)) @@ -93,6 +96,9 @@ importers: '@jimmy.codes/prettier-config': specifier: 1.3.0 version: 1.3.0(prettier@3.3.3) + '@playwright/test': + specifier: 1.49.0 + version: 1.49.0 '@semantic-release/changelog': specifier: 6.0.3 version: 6.0.3(semantic-release@24.2.0(typescript@5.6.3)) @@ -1097,6 +1103,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.0': + resolution: {integrity: sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2485,6 +2496,12 @@ packages: peerDependencies: eslint: '>=8.0.0' + eslint-plugin-playwright@2.1.0: + resolution: {integrity: sha512-wMbHOehofSB1cBdzz2CLaCYaKNLeTQ0YnOW+7AHa281TJqlpEJUBgTHbRUYOUxiXphfWwOyTPvgr6vvEmArbSA==} + engines: {node: '>=16.6.0'} + peerDependencies: + eslint: '>=8.40.0' + eslint-plugin-react-hooks@5.0.0: resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} engines: {node: '>=10'} @@ -2691,6 +2708,11 @@ packages: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2788,6 +2810,10 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4077,6 +4103,16 @@ packages: typescript: optional: true + playwright-core@1.49.0: + resolution: {integrity: sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.0: + resolution: {integrity: sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4754,6 +4790,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -5966,6 +6006,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.49.0': + dependencies: + playwright: 1.49.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -7607,6 +7651,11 @@ snapshots: - supports-color - typescript + eslint-plugin-playwright@2.1.0(eslint@9.15.0(jiti@2.4.0)): + dependencies: + eslint: 9.15.0(jiti@2.4.0) + globals: 13.24.0 + eslint-plugin-react-hooks@5.0.0(eslint@9.15.0(jiti@2.4.0)): dependencies: eslint: 9.15.0(jiti@2.4.0) @@ -7905,6 +7954,9 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8005,6 +8057,10 @@ snapshots: globals@11.12.0: {} + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + globals@14.0.0: {} globals@15.12.0: {} @@ -9386,6 +9442,14 @@ snapshots: optionalDependencies: typescript: 5.6.3 + playwright-core@1.49.0: {} + + playwright@1.49.0: + dependencies: + playwright-core: 1.49.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.0.0: {} @@ -10170,6 +10234,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.20.2: {} + type-fest@0.6.0: {} type-fest@0.8.1: {} diff --git a/src/configs/playwright.ts b/src/configs/playwright.ts new file mode 100644 index 0000000..2ad94bc --- /dev/null +++ b/src/configs/playwright.ts @@ -0,0 +1,18 @@ +import { GLOB_PLAYWRIGHT } from "../constants"; +import { playwrightRules } from "../rules/playwright"; +import { interopDefault } from "../utils/interop-default"; + +export const playwrightConfig = async () => { + const playwrightPlugin = await interopDefault( + import("eslint-plugin-playwright"), + ); + + return [ + { + ...playwrightPlugin.configs["flat/recommended"], + files: GLOB_PLAYWRIGHT, + name: "jimmy.codes/playwright", + rules: await playwrightRules(), + }, + ]; +}; diff --git a/src/constants.ts b/src/constants.ts index f338f1c..9d269a0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -59,9 +59,13 @@ export const GLOB_TESTS = [ `**/*.benchmark.${GLOB_SRC_EXT}`, ]; -export const GLOB_E2E = [ +export const GLOB_PLAYWRIGHT = [ `**/e2e/**/*.spec.${GLOB_SRC_EXT}`, `**/e2e/**/*.test.${GLOB_SRC_EXT}`, +]; + +export const GLOB_E2E = [ + ...GLOB_PLAYWRIGHT, `**/cypress/**/*.spec.${GLOB_SRC_EXT}`, `**/cypress/**/*.test.${GLOB_SRC_EXT}`, ]; diff --git a/src/factory.spec.ts b/src/factory.spec.ts index f84cc9d..c61dab3 100644 --- a/src/factory.spec.ts +++ b/src/factory.spec.ts @@ -93,9 +93,7 @@ describe("jimmyDotCodes", () => { expect.arrayContaining([ expect.objectContaining({ name: "jimmy.codes/jest" }), expect.objectContaining({ name: "jimmy.codes/react" }), - expect.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -111,9 +109,7 @@ describe("jimmyDotCodes", () => { expect.arrayContaining([ expect.objectContaining({ name: "jimmy.codes/vitest" }), expect.objectContaining({ name: "jimmy.codes/react" }), - expect.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -136,6 +132,16 @@ describe("jimmyDotCodes", () => { ); }); + it("should create configuration w/ playwright", async () => { + await expect( + jimmyDotCodes({ autoDetect: false, playwright: true }), + ).resolves.toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "jimmy.codes/playwright" }), + ]), + ); + }); + describe("autoDetect", () => { it("should include typescript when auto detection is enabled", async () => { vi.mocked(isPackageExists).mockImplementation((name) => { @@ -145,14 +151,11 @@ describe("jimmyDotCodes", () => { await expect(jimmyDotCodes({ autoDetect: true })).resolves.toStrictEqual( expect.arrayContaining([ expect.objectContaining({ name: "jimmy.codes/typescript" }), - expect.not.objectContaining({ name: "jimmy.codes/testing" }), expect.not.objectContaining({ name: "jimmy.codes/vitest" }), expect.not.objectContaining({ name: "jimmy.codes/jest" }), expect.not.objectContaining({ name: "jimmy.codes/react" }), expect.not.objectContaining({ name: "jimmy.codes/react/query" }), - expect.not.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.not.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -165,14 +168,11 @@ describe("jimmyDotCodes", () => { await expect(jimmyDotCodes({ autoDetect: true })).resolves.toStrictEqual( expect.arrayContaining([ expect.not.objectContaining({ name: "jimmy.codes/typescript" }), - expect.not.objectContaining({ name: "jimmy.codes/testing" }), expect.not.objectContaining({ name: "jimmy.codes/vitest" }), expect.not.objectContaining({ name: "jimmy.codes/jest" }), expect.objectContaining({ name: "jimmy.codes/react" }), expect.not.objectContaining({ name: "jimmy.codes/react/query" }), - expect.not.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.not.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -186,14 +186,11 @@ describe("jimmyDotCodes", () => { await expect(jimmyDotCodes({ autoDetect: true })).resolves.toStrictEqual( expect.arrayContaining([ expect.not.objectContaining({ name: "jimmy.codes/typescript" }), - expect.not.objectContaining({ name: "jimmy.codes/testing" }), expect.not.objectContaining({ name: "jimmy.codes/vitest" }), expect.not.objectContaining({ name: "jimmy.codes/jest" }), expect.objectContaining({ name: "jimmy.codes/react" }), expect.objectContaining({ name: "jimmy.codes/react/query" }), - expect.not.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.not.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -211,9 +208,7 @@ describe("jimmyDotCodes", () => { expect.not.objectContaining({ name: "jimmy.codes/jest" }), expect.not.objectContaining({ name: "jimmy.codes/react" }), expect.not.objectContaining({ name: "jimmy.codes/react/query" }), - expect.not.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.not.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -231,9 +226,7 @@ describe("jimmyDotCodes", () => { expect.objectContaining({ name: "jimmy.codes/jest" }), expect.not.objectContaining({ name: "jimmy.codes/react" }), expect.not.objectContaining({ name: "jimmy.codes/react/query" }), - expect.not.objectContaining({ - name: "jimmy.codes/testing-library", - }), + expect.not.objectContaining({ name: "jimmy.codes/testing-library" }), ]), ); }); @@ -278,5 +271,17 @@ describe("jimmyDotCodes", () => { ]), ); }); + + it("should include playwright when auto detection is enabled", async () => { + vi.mocked(isPackageExists).mockImplementation((name) => { + return name === "@playwright/test"; + }); + + await expect(jimmyDotCodes({ autoDetect: true })).resolves.toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "jimmy.codes/playwright" }), + ]), + ); + }); }); }); diff --git a/src/factory.ts b/src/factory.ts index ecea4c7..7ff072d 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -10,6 +10,7 @@ import { importsConfig } from "./configs/imports"; import { javascriptConfig } from "./configs/javascript"; import { nodeConfig } from "./configs/node"; import { perfectionistConfig } from "./configs/perfectionist"; +import { playwrightConfig } from "./configs/playwright"; import { prettierConfig } from "./configs/prettier"; import { reactConfig } from "./configs/react"; import { regexpConfig } from "./configs/regexp"; @@ -25,6 +26,7 @@ import { } from "./utils/get-options"; import { hasAstro, + hasPlaywright, hasReact, hasTesting, hasTypescript, @@ -40,6 +42,7 @@ export const jimmyDotCodes = async ( autoDetect = true, configs = [], ignores = [], + playwright = false, react = false, testing = false, typescript = false, @@ -62,6 +65,7 @@ export const jimmyDotCodes = async ( testingOptions, autoDetect, ); + const isPlaywrightEnabled = playwright || (autoDetect && hasPlaywright()); return [ javascriptConfig(), @@ -77,6 +81,7 @@ export const jimmyDotCodes = async ( isAstroEnabled ? await astroConfig() : [], isTestingEnabled ? await testingConfig(testingOptions, autoDetect) : [], isTestingLibraryEnabled ? await testingLibrary() : [], + isPlaywrightEnabled ? await playwrightConfig() : [], prettierConfig(), commonjsConfig(), ignoresConfig(ignores), diff --git a/src/rules.gen.d.ts b/src/rules.gen.d.ts index 32afbb9..854078c 100644 --- a/src/rules.gen.d.ts +++ b/src/rules.gen.d.ts @@ -3374,6 +3374,246 @@ export interface RuleOptions { * @see https://perfectionist.dev/rules/sort-variable-declarations */ 'perfectionist/sort-variable-declarations'?: Linter.RuleEntry + /** + * Enforce assertion to be made in a test body + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md + */ + 'playwright/expect-expect'?: Linter.RuleEntry + /** + * Enforces a maximum number assertion calls in a test body + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md + */ + 'playwright/max-expects'?: Linter.RuleEntry + /** + * Enforces a maximum depth to nested describe calls + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md + */ + 'playwright/max-nested-describe'?: Linter.RuleEntry + /** + * Identify false positives when async Playwright APIs are not properly awaited. + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md + */ + 'playwright/missing-playwright-await'?: Linter.RuleEntry + /** + * Disallow commented out tests + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-tests.md + */ + 'playwright/no-commented-out-tests'?: Linter.RuleEntry<[]> + /** + * Disallow calling `expect` conditionally + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-expect.md + */ + 'playwright/no-conditional-expect'?: Linter.RuleEntry<[]> + /** + * Disallow conditional logic in tests + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md + */ + 'playwright/no-conditional-in-test'?: Linter.RuleEntry<[]> + /** + * Disallow duplicate setup and teardown hooks + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-duplicate-hooks.md + */ + 'playwright/no-duplicate-hooks'?: Linter.RuleEntry<[]> + /** + * The use of ElementHandle is discouraged, use Locator instead + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md + */ + 'playwright/no-element-handle'?: Linter.RuleEntry<[]> + /** + * The use of `page.$eval` and `page.$$eval` are discouraged, use `locator.evaluate` or `locator.evaluateAll` instead + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md + */ + 'playwright/no-eval'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `.only()` focus test annotation + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md + */ + 'playwright/no-focused-test'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `{ force: true }` option. + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md + */ + 'playwright/no-force-option'?: Linter.RuleEntry<[]> + /** + * Disallows the usage of getByTitle() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md + */ + 'playwright/no-get-by-title'?: Linter.RuleEntry<[]> + /** + * Disallow setup and teardown hooks + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-hooks.md + */ + 'playwright/no-hooks'?: Linter.RuleEntry + /** + * Disallow nested `test.step()` methods + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md + */ + 'playwright/no-nested-step'?: Linter.RuleEntry<[]> + /** + * Prevent usage of the networkidle option + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md + */ + 'playwright/no-networkidle'?: Linter.RuleEntry<[]> + /** + * Disallow usage of nth methods + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md + */ + 'playwright/no-nth-methods'?: Linter.RuleEntry<[]> + /** + * Prevent usage of page.pause() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md + */ + 'playwright/no-page-pause'?: Linter.RuleEntry<[]> + /** + * Disallows the usage of raw locators + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md + */ + 'playwright/no-raw-locators'?: Linter.RuleEntry + /** + * Disallow specific matchers & modifiers + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md + */ + 'playwright/no-restricted-matchers'?: Linter.RuleEntry + /** + * Prevent usage of the `.skip()` skip test annotation. + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md + */ + 'playwright/no-skipped-test'?: Linter.RuleEntry + /** + * Disallow using `expect` outside of `test` blocks + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md + */ + 'playwright/no-standalone-expect'?: Linter.RuleEntry<[]> + /** + * Prevent unsafe variable references in page.evaluate() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md + */ + 'playwright/no-unsafe-references'?: Linter.RuleEntry<[]> + /** + * Disallow unnecessary awaits for Playwright methods + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md + */ + 'playwright/no-useless-await'?: Linter.RuleEntry<[]> + /** + * Disallow usage of 'not' matchers when a more specific matcher exists + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md + */ + 'playwright/no-useless-not'?: Linter.RuleEntry<[]> + /** + * Prevent usage of page.waitForSelector() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md + */ + 'playwright/no-wait-for-selector'?: Linter.RuleEntry<[]> + /** + * Prevent usage of page.waitForTimeout() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md + */ + 'playwright/no-wait-for-timeout'?: Linter.RuleEntry<[]> + /** + * Suggest using the built-in comparison matchers + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparision-matcher.md + */ + 'playwright/prefer-comparison-matcher'?: Linter.RuleEntry<[]> + /** + * Suggest using the built-in equality matchers + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md + */ + 'playwright/prefer-equality-matcher'?: Linter.RuleEntry<[]> + /** + * Prefer having hooks in a consistent order + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md + */ + 'playwright/prefer-hooks-in-order'?: Linter.RuleEntry<[]> + /** + * Suggest having hooks before any test cases + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md + */ + 'playwright/prefer-hooks-on-top'?: Linter.RuleEntry<[]> + /** + * Suggest locators over page methods + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-locator.md + */ + 'playwright/prefer-locator'?: Linter.RuleEntry<[]> + /** + * Enforce lowercase test names + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md + */ + 'playwright/prefer-lowercase-title'?: Linter.RuleEntry + /** + * Prefer native locator functions + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-native-locators.md + */ + 'playwright/prefer-native-locators'?: Linter.RuleEntry + /** + * Suggest using `toStrictEqual()` + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md + */ + 'playwright/prefer-strict-equal'?: Linter.RuleEntry<[]> + /** + * Suggest using `toBe()` for primitive literals + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md + */ + 'playwright/prefer-to-be'?: Linter.RuleEntry<[]> + /** + * Suggest using toContain() + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md + */ + 'playwright/prefer-to-contain'?: Linter.RuleEntry<[]> + /** + * Suggest using `toHaveCount()` + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md + */ + 'playwright/prefer-to-have-count'?: Linter.RuleEntry<[]> + /** + * Suggest using `toHaveLength()` + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md + */ + 'playwright/prefer-to-have-length'?: Linter.RuleEntry<[]> + /** + * Prefer web first assertions + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md + */ + 'playwright/prefer-web-first-assertions'?: Linter.RuleEntry<[]> + /** + * Require setup and teardown code to be within a hook + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md + */ + 'playwright/require-hook'?: Linter.RuleEntry + /** + * Require all assertions to use `expect.soft` + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md + */ + 'playwright/require-soft-assertions'?: Linter.RuleEntry<[]> + /** + * Require a message for `toThrow()` + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-throw-message.md + */ + 'playwright/require-to-throw-message'?: Linter.RuleEntry<[]> + /** + * Require test cases and hooks to be inside a `test.describe` block + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md + */ + 'playwright/require-top-level-describe'?: Linter.RuleEntry + /** + * Enforce valid `describe()` callback + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-describe-callback.md + */ + 'playwright/valid-describe-callback'?: Linter.RuleEntry<[]> + /** + * Enforce valid `expect()` usage + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md + */ + 'playwright/valid-expect'?: Linter.RuleEntry + /** + * Require promises that have expectations in their chain to be valid + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect-in-promise.md + */ + 'playwright/valid-expect-in-promise'?: Linter.RuleEntry<[]> + /** + * Enforce valid titles + * @see https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-title.md + */ + 'playwright/valid-title'?: Linter.RuleEntry /** * Require using arrow functions for callbacks * @see https://eslint.org/docs/latest/rules/prefer-arrow-callback @@ -9341,6 +9581,72 @@ type PerfectionistSortVariableDeclarations = []|[{ type?: ("alphabetical" | "natural" | "line-length") }] +// ----- playwright/expect-expect ----- +type PlaywrightExpectExpect = []|[{ + assertFunctionNames?: []|[string] +}] +// ----- playwright/max-expects ----- +type PlaywrightMaxExpects = []|[{ + max?: number +}] +// ----- playwright/max-nested-describe ----- +type PlaywrightMaxNestedDescribe = []|[{ + max?: number +}] +// ----- playwright/missing-playwright-await ----- +type PlaywrightMissingPlaywrightAwait = []|[{ + customMatchers?: string[] +}] +// ----- playwright/no-hooks ----- +type PlaywrightNoHooks = []|[{ + allow?: unknown[] +}] +// ----- playwright/no-raw-locators ----- +type PlaywrightNoRawLocators = []|[{ + allowed?: string[] +}] +// ----- playwright/no-restricted-matchers ----- +type PlaywrightNoRestrictedMatchers = []|[{ + [k: string]: (string | null) | undefined +}] +// ----- playwright/no-skipped-test ----- +type PlaywrightNoSkippedTest = []|[{ + allowConditional?: boolean +}] +// ----- playwright/prefer-lowercase-title ----- +type PlaywrightPreferLowercaseTitle = []|[{ + allowedPrefixes?: string[] + ignore?: ("test.describe" | "test")[] + ignoreTopLevelDescribe?: boolean +}] +// ----- playwright/prefer-native-locators ----- +type PlaywrightPreferNativeLocators = []|[{ + testIdAttribute?: string +}] +// ----- playwright/require-hook ----- +type PlaywrightRequireHook = []|[{ + allowedFunctionCalls?: string[] +}] +// ----- playwright/require-top-level-describe ----- +type PlaywrightRequireTopLevelDescribe = []|[{ + maxTopLevelDescribes?: number +}] +// ----- playwright/valid-expect ----- +type PlaywrightValidExpect = []|[{ + maxArgs?: number + minArgs?: number +}] +// ----- playwright/valid-title ----- +type PlaywrightValidTitle = []|[{ + disallowedWords?: string[] + ignoreSpaces?: boolean + ignoreTypeOfDescribeName?: boolean + ignoreTypeOfStepName?: boolean + ignoreTypeOfTestName?: boolean + [k: string]: (string | [string]|[string, string] | { + [k: string]: (string | [string]|[string, string]) | undefined + }) +}] // ----- prefer-arrow-callback ----- type PreferArrowCallback = []|[{ allowNamedFunctions?: boolean diff --git a/src/rules/__snapshots__/playwright.spec.ts.snap b/src/rules/__snapshots__/playwright.spec.ts.snap new file mode 100644 index 0000000..ae1be69 --- /dev/null +++ b/src/rules/__snapshots__/playwright.spec.ts.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should create playwright rules 1`] = ` +{ + "no-empty-pattern": "off", + "playwright/expect-expect": "error", + "playwright/max-nested-describe": "error", + "playwright/missing-playwright-await": "error", + "playwright/no-conditional-expect": "error", + "playwright/no-conditional-in-test": "error", + "playwright/no-element-handle": "error", + "playwright/no-eval": "error", + "playwright/no-focused-test": "error", + "playwright/no-force-option": "error", + "playwright/no-nested-step": "error", + "playwright/no-networkidle": "error", + "playwright/no-page-pause": "error", + "playwright/no-skipped-test": "error", + "playwright/no-standalone-expect": "error", + "playwright/no-unsafe-references": "error", + "playwright/no-useless-await": "error", + "playwright/no-useless-not": "error", + "playwright/no-wait-for-selector": "error", + "playwright/no-wait-for-timeout": "error", + "playwright/prefer-web-first-assertions": "error", + "playwright/valid-describe-callback": "error", + "playwright/valid-expect": "error", + "playwright/valid-expect-in-promise": "error", + "playwright/valid-title": "error", +} +`; diff --git a/src/rules/playwright.spec.ts b/src/rules/playwright.spec.ts new file mode 100644 index 0000000..2bf4ddd --- /dev/null +++ b/src/rules/playwright.spec.ts @@ -0,0 +1,5 @@ +import { playwrightRules } from "./playwright"; + +test("should create playwright rules", async () => { + await expect(playwrightRules()).resolves.toMatchSnapshot(); +}); diff --git a/src/rules/playwright.ts b/src/rules/playwright.ts new file mode 100644 index 0000000..2c17cac --- /dev/null +++ b/src/rules/playwright.ts @@ -0,0 +1,27 @@ +import type { Rules } from "../types"; + +import { interopDefault } from "../utils/interop-default"; + +export const playwrightRules = async () => { + const playwrightPlugin = await interopDefault( + import("eslint-plugin-playwright"), + ); + + return { + ...playwrightPlugin.configs["flat/recommended"].rules, + "playwright/expect-expect": "error", + "playwright/max-nested-describe": "error", + "playwright/no-conditional-expect": "error", + "playwright/no-conditional-in-test": "error", + "playwright/no-element-handle": "error", + "playwright/no-eval": "error", + "playwright/no-force-option": "error", + "playwright/no-nested-step": "error", + "playwright/no-page-pause": "error", + "playwright/no-skipped-test": "error", + "playwright/no-useless-await": "error", + "playwright/no-useless-not": "error", + "playwright/no-wait-for-selector": "error", + "playwright/no-wait-for-timeout": "error", + } satisfies Rules; +}; diff --git a/src/types.ts b/src/types.ts index 2dc7fc1..9c0fbf0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,14 +71,15 @@ export interface Options { */ ignores?: string[]; /** - * Are React rules enabled? + * Are playwright rules enabled? * @default false */ - react?: boolean | ReactOptions; + playwright?: boolean; /** - * Are Jest rules enabled? + * Are React rules enabled? * @default false */ + react?: boolean | ReactOptions; /** * Are testing rules enabled? * @default false diff --git a/src/utils/has-dependency.ts b/src/utils/has-dependency.ts index a74b376..b5f8814 100644 --- a/src/utils/has-dependency.ts +++ b/src/utils/has-dependency.ts @@ -35,3 +35,7 @@ export const hasReactQuery = () => { export const hasAstro = () => { return isPackageExists("astro"); }; + +export const hasPlaywright = () => { + return isPackageExists("@playwright/test"); +};