DEV Community

Cover image for 🛡️ Never Commit Broken Code Again: A Guide to ESLint and Husky in Playwright
idavidov13
idavidov13

Posted on • Originally published at idavidov.eu

🛡️ Never Commit Broken Code Again: A Guide to ESLint and Husky in Playwright

In the world of automated testing, consistency is king. When you're working on a team, it's easy for different coding styles and small mistakes to creep into the codebase. This leads to messy code, broken tests, and frustrating code reviews. But what if you could automatically enforce quality standards before bad code ever gets committed?

This guide is for any Automation QA who wants to build a safety net for their Playwright project. We'll focus on how even junior QAs can implement powerful checks to ensure every team member adheres to the same high standards.

By the end of this tutorial, you will have a fully automated system that lints, formats, and validates your code every time you try to commit.

Ready to build your fortress? Let's get started. 🏰


📝Prerequisites

Before we begin, you should have a basic Playwright project already set up. This guide assumes you have Node.js and npm installed and ready to go. You can check Playwright project Initial Setup.


🛠️The Dream Team: Our Tooling Explained

We'll be using a powerful combination of four tools to create our pre-commit safety net. Here's a quick rundown of what each one does:

  • ESLint 🧐: A static analysis tool that scans your code to find and report on patterns, potential bugs, and stylistic errors based on a configurable set of rules.

  • Prettier ✨: An opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules, saving you from any formatting debates.

  • Husky 🐕‍🦺: A tool that makes it incredibly simple to work with Git hooks. We'll use it to set up a "pre-commit" hook, which is a script that runs right before a commit is finalized.

  • lint-staged 🚫: The perfect partner for Husky. It allows you to run linters and formatters against only the files that have been staged in Git, making the process fast and efficient.

Now, let's integrate them into your Playwright project.

Dream Team


🪜Step-by-Step Implementation Guide

Follow these steps carefully to build your automated checks.

Step 1: 📦Install Your Dependencies

First, we need to add our new tools to the project. Below is the command that will install all the dependencies for our case:

npm install --save-dev @typescript-eslint/eslint-plugin@^8.34.1 @typescript-eslint/parser@^8.34.1 eslint@^9.29.0 eslint-config-prettier@^10.1.5 eslint-define-config@^2.1.0 eslint-plugin-playwright@^2.2.0 eslint-plugin-prettier@^5.5.0 husky@^9.1.7 jiti@^2.4.2 lint-staged@^16.1.2
Enter fullscreen mode Exit fullscreen mode

Open your package.json file and check the following packages to your devDependencies (most probably your versions will be newer):

"devDependencies": {
        "@playwright/test": "^1.53.2",
        "@types/node": "^24.0.12",
        "@typescript-eslint/eslint-plugin": "^8.36.0",
        "@typescript-eslint/parser": "^8.36.0",
        "eslint": "^9.30.1",
        "eslint-config-prettier": "^10.1.5",
        "eslint-define-config": "^2.1.0",
        "eslint-plugin-playwright": "^2.2.0",
        "eslint-plugin-prettier": "^5.5.1",
        "husky": "^9.1.7",
        "jiti": "^2.4.2",
        "lint-staged": "^16.1.2"
    },
Enter fullscreen mode Exit fullscreen mode

Step 2: ✨Configure Prettier for Consistent Formatting

  • Install Prettier extention in your IDE:

Prettier

  • Change the “Default Formatter” and “Format On Save” options in your IDE

Press Ctrl + Shift + P and type “Preferences: Open Settings (UI)”. Open it and In the search bar, type “format:. Make the changes from the screenshot below:

Settings

Create a new file in your project's root directory named .prettierrc. This file tells Prettier how you want your code to be formatted.

{
    "semi": true,
    "tabWidth": 4,
    "useTabs": false,
    "printWidth": 80,
    "singleQuote": true,
    "trailingComma": "es5",
    "bracketSpacing": true,
    "arrowParens": "always",
    "proseWrap": "preserve"
}
Enter fullscreen mode Exit fullscreen mode

Explanation of each Prettier option:

  • semi: (true) Always add a semicolon at the end of every statement.

  • tabWidth: (4) Use 4 spaces per indentation level.

  • useTabs: (false) Indent lines with spaces instead of tabs.

  • printWidth: (80) Wrap lines at 80 characters for better readability.

  • singleQuote: (true) Use single quotes instead of double quotes wherever possible.

  • trailingComma: ("es5") Add trailing commas wherever valid in ES5 (objects, arrays, etc.).

  • bracketSpacing: (true) Print spaces between brackets in object literals (e.g., { foo: bar }).

  • arrowParens: ("always") Always include parentheses around arrow function parameters, even if there is only one parameter.

  • proseWrap: ("preserve") Do not change wrapping in markdown text; respect the input's line breaks.

Step 3: 📝Set Up Your TypeScript Configuration

If you don't already have one, create a tsconfig.json file in your root directory. This file specifies the root files and the compiler options required to compile a TypeScript project.

{
    "compilerOptions": {
        "target": "ESNext",
        "module": "NodeNext",
        "lib": ["ESNext", "DOM"],
        "moduleResolution": "NodeNext",
        "esModuleInterop": true,
        "allowJs": true,
        "checkJs": false,
        "outDir": "./dist",
        "rootDir": ".",
        "strict": true,
        "noImplicitAny": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "resolveJsonModule": true,
        "types": ["node", "playwright"]
    },
    "include": [
        "*.ts",
        "*.mts",
        "tests/**/*.ts",
        "fixtures/**/*.ts",
        "pages/**/*.ts",
        "helpers/**/*.ts",
        "enums/**/*.ts"
    ],
    "exclude": ["node_modules", "dist", "playwright-report", "test-results"]
}
Enter fullscreen mode Exit fullscreen mode

The include and exclude arrays are crucial here—they tell the TypeScript compiler exactly which files to check and which to ignore.

Step 4: 📖Create the ESLint Rulebook

This is where the magic happens. Create a file named eslint.config.mts in your root directory. This file will contain all the rules for ensuring code quality.

import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import prettierPlugin from 'eslint-plugin-prettier';
import playwright from 'eslint-plugin-playwright';

const prettierConfig = {
    semi: true,
    tabWidth: 4,
    useTabs: false,
    printWidth: 80,
    singleQuote: true,
    trailingComma: 'es5',
    bracketSpacing: true,
    arrowParens: 'always',
    proseWrap: 'preserve',
};

const config = [
    {
        ignores: ['node_modules', 'dist', 'playwright-report', 'test-results'],
    },
    {
        files: ['**/*.ts', '**/*.tsx'],
        languageOptions: {
            parser: tsParser,
            parserOptions: {
                ecmaVersion: 2020,
                sourceType: 'module',
                project: ['./tsconfig.json'],
                tsconfigRootDir: __dirname,
            },
        },
        plugins: {
            '@typescript-eslint': tseslint,
            prettier: prettierPlugin,
            playwright,
        },
        rules: {
            ...((tseslint.configs.recommended as any).rules ?? {}),
            ...((playwright.configs['flat/recommended'] as any).rules ?? {}),
            'prettier/prettier': ['error', prettierConfig],
            '@typescript-eslint/explicit-function-return-type': 'error',
            '@typescript-eslint/no-explicit-any': 'error',
            '@typescript-eslint/no-unused-vars': [
                'error',
                { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
            ],
            'no-console': 'error',
            'prefer-const': 'error',
            '@typescript-eslint/no-inferrable-types': 'error',
            '@typescript-eslint/no-empty-function': 'error',
            '@typescript-eslint/no-floating-promises': 'error',
            'playwright/missing-playwright-await': 'error',
            'playwright/no-page-pause': 'error',
            'playwright/no-useless-await': 'error',
            'playwright/no-skipped-test': 'error',
        },
    },
];

export default config;
Enter fullscreen mode Exit fullscreen mode

What do all these rules do?

Your ESLint config enforces a comprehensive set of best practices and code quality standards. Here's what each part does:

  • ...tseslint.configs.recommended: This preset from @typescript-eslint enables a wide range of rules that catch common bugs and bad practices in TypeScript code. It enforces things like avoiding unsafe any types, preventing unused variables, requiring proper use of promises, and more. These rules help ensure your TypeScript code is robust, maintainable, and less error-prone.

  • ...playwright.configs['flat/recommended']: This preset from eslint-plugin-playwright enforces best practices for Playwright test code. It catches mistakes like missing await on Playwright actions, using forbidden test annotations (like .only or .skip), unsafe selectors, and more. This helps keep your tests reliable and consistent.

  • 'prettier/prettier': ['error', prettierConfig]: Enforces code formatting according to your Prettier settings. Any formatting issues will be reported as errors, ensuring a consistent code style across your project.

  • '@typescript-eslint/explicit-function-return-type': 'error': Requires all functions to have an explicit return type. This improves code readability and helps catch bugs where a function might return an unexpected type.

  • '@typescript-eslint/no-explicit-any': 'error': Disallows the use of the any type. This ensures you use more precise types, making your code safer and easier to maintain.

  • '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]: Prevents unused variables and function arguments, except those starting with an underscore (which are often intentionally unused). This helps keep your code clean and free of clutter.

  • 'no-console': 'error': Disallows console.log and similar statements. This prevents accidental logging in production code, which can be unprofessional or leak sensitive information.

  • 'prefer-const': 'error': Requires variables that are never reassigned after declaration to be declared with const. This helps prevent accidental reassignment and makes code intent clearer.

  • '@typescript-eslint/no-inferrable-types': 'error': Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean. This keeps code cleaner and leverages TypeScript's type inference.

  • '@typescript-eslint/no-empty-function': 'error': Disallows empty functions. Empty functions are often a sign of incomplete or placeholder code and should be avoided.

  • '@typescript-eslint/no-floating-promises': 'error': Requires that Promises are handled appropriately (with await or .then()). This prevents unhandled promise rejections and ensures async code is reliable.

  • 'playwright/missing-playwright-await': 'error': Ensures that all Playwright actions (like page.click()) are properly awaited. Missing await is a common cause of flaky tests.

  • 'playwright/no-page-pause': 'error': Disallows the use of page.pause(), which is meant for debugging and should not be left in committed test code.

  • 'playwright/no-useless-await': 'error': Prevents unnecessary use of await on synchronous Playwright methods, keeping your code clean and efficient.

  • 'playwright/no-skipped-test': 'error': Disallows skipping tests using .skip. This ensures that all tests are run and nothing is accidentally left out.

How do these rules help?

  • Rules set to 'error' will block a commit, forcing you to fix the issue before your code can be merged.

By combining these rules, your project is protected from common mistakes, code smells, and inconsistent styles—making your codebase more reliable, readable, and professional.

Step 5: 🔗Connect the Tools with lint-staged

Now, let's tell lint-staged what to do. Add the following block to your package.json at the root level (just after the devDependencies):

"lint-staged": {
  "*.{ts,tsx,js,jsx}": [
    "npx eslint --fix",
    "npx prettier --write"
  ]
}
Enter fullscreen mode Exit fullscreen mode

This configuration tells lint-staged: "For any staged TypeScript or JavaScript file, first run ESLint to automatically fix what it can, and then run Prettier to format the code."

Step 6: 🐕‍🦺Automate the Trigger with Husky

Finally, let's make this all run automatically before each commit using Husky.

First, initialize Husky:

npx husky init
Enter fullscreen mode Exit fullscreen mode

This command creates a .husky directory in your project.

⚠️Next, remove all files, except _/pre-commit and add the following script to it. This is the hook that Git will run.

Update the content of the _/pre-commit to:

#!/bin/sh
npx lint-staged
Enter fullscreen mode Exit fullscreen mode

This tiny script tells Git to run lint-staged before every commit. And that's it! Your setup is complete. 🎯


🚀See It In Action: 🤯Let's Break Some Rules

The best way to appreciate our new safety net is to see it in action. Let's try to commit some "bad" code.

Create a new test file named tests/bad-test-example.spec.ts. Paste the following code, which intentionally violates several of our rules:

// tests/bad-test-example.spec.ts
import { test, expect } from '@playwright/test';

// Rule violation: No explicit return type
function addNumbers(a: number, b: number) {
    //Rule violation: No explicit return type
    const unusedVar = 'I do nothing'; // Rule violation: Unused variable
    return a + b;
}

test('bad example test', async ({ page }) => {
    console.log('This should not be here!'); // Rule violation: no-console

    await page.goto('https://playwright.dev/');

    page.getByRole('button', { name: 'Get Started' }).click(); //Rule violation: Missing 'await' for a Playwright action

    // Rule violation: Missing 'await' for a Playwright action
    page.getByLabel('Search').click();

    await page.getByPlaceholder('Search docs').fill('assertions');

    // Rule violation: Using page.pause()
    await page.pause();

    await expect(page.getByText('Writing assertions')).toBeVisible(); //Rule violation: Missing 'await'

    let result;
    await test.step('test', async () => {
        result = addNumbers(1, 2); //Rule violation: no-explicit-any
    });
    console.log(result); //Rule violation: no-console
});
Enter fullscreen mode Exit fullscreen mode

Now, try to commit this file:

git add .
git commit -m "feat: add broken test file"
Enter fullscreen mode Exit fullscreen mode

What happens? Your commit will be REJECTED! 🛑

You will see an output in your terminal from ESLint, listing all the errors it found:

  • 🚫The no-console error.

  • 🏷️The missing function return type.

  • ⏳The missing await on page.getByRole('button', { name: 'Get Started' }).click(); and page.getByLabel('Search').click();.

  • ⏸️The use of page.pause().

  • 🗑️The unused variable.

lint-staged will prevent the commit from completing until you fix these errors. Prettier may have also fixed some formatting issues automatically.

Husky


The Payoff: Better Code, Happier Teams

You've just implemented a powerful, automated system that acts as a guardian for your codebase. This small setup provides enormous long-term benefits:

  • 🧹Better Code Quality: The code is cleaner, more readable, and free of common errors.
  • 👥Stick to Team Rules: Everyone on the team is automatically held to the same coding standards, ensuring consistency across the entire project.
  • Faster Code Reviews: Reviewers can focus on the logic of the code, not on trivial formatting or style issues.

By enforcing these standards automatically, you free up mental energy to focus on what really matters: writing great, reliable tests. 🧠

Clean Code


🏁Conclusion

Congratulations! 🎊 You've just built a powerful safety net for your Playwright project. This setup ensures that your codebase stays clean, readable, and free of common errors.


🙏🏻 Thank you for reading! Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals 🚀 who are passionate about mastering modern testing.

Join the community and get the latest articles and tips by signing up for the newsletter.

Top comments (2)

Collapse
 
dotallio profile image
Dotallio

Love how you broke this down for Playwright specifically, those test rule checks are a lifesaver.
Have you tried combining this with auto-fix CI workflows as well, or do you rely solely on pre-commit hooks?

Collapse
 
idavidov13 profile image
idavidov13

Thank you for the great feedback.

Once we implemented the pre-commit hook and started using it, we found that after several commits, you build a habbit to resolve potential commit problems during coding phase.

Thats why we stopped there, and resolve any issue by hand. There are situations, where auto-fix can fail with fixing or will implement fix, where not necessary (example is putting a log on the console which user is logged during auth phase).

I can suggest you to try this workflow and if you are not satisfied, you can easily extend it 😎