eslint-plugin-playwright

repository·main·Indexed 18 days ago

https://github.com/mskelton/eslint-plugin-playwright

An ESLint plugin for Playwright testing frameworks that provides rules to enforce best practices and prevent common mistakes in test suites. It includes a comprehensive set of rules for assertions, locator usage, and async API handling, with support for both ESLint Flat Config and legacy configurations.

Tokens
28.5K
Snippets
107
Records
114
Agent score
63%

What's inside eslint-plugin-playwright

  1. Understand the limitations of `no-commented-out-tests`

    main

    The no-commented-out-tests rule relies on literal function name matching within comments. It cannot detect commented-out tests that use aliased functions or custom test extensions.

    For example, the rule will not catch these patterns:

    // const testSkip = test.skip;
    // testSkip('skipped test', () => {});
    
    // const custom = test.extend({});
    // custom('does not have function body');
  2. Use the `prefer-equality-matcher` rule to improve Playwright assertions

    main

    The prefer-equality-matcher rule suggests replacing strict equality checks (=== and !==) inside expect() calls with Playwright's built-in equality matchers. Using these built-in matchers makes tests more readable and provides better error messages when an assertion fails.

    // Incorrect: using strict equality inside expect
    expect(x === 5).toBe(true)
    
    // Correct: using built-in equality matcher
    expect(x).toBe(5)
  3. Configure eslint-plugin-playwright with Flat Config

    main

    To use the plugin with ESLint's Flat Config (eslint.config.js), target your Playwright test files using the files field and extend the recommended configuration via playwright.configs['flat/recommended'].

    import { defineConfig } from '@eslint/config'
    import playwright from 'eslint-plugin-playwright'
    
    export default defineConfig([
      {
        files: ['tests/**'],
        extends: [playwright.configs['flat/recommended']],
        rules: {
          // Customize Playwright rules
          // ...
        },
      },
    ])
  4. Disallow conditional logic in tests (`no-conditional-in-test`)

    main

    The no-conditional-in-test rule prevents the use of conditional logic (like if statements, switch cases, or ternary operators) inside the body of a Playwright test block.

    Using conditionals inside a test often indicates that a single test is attempting to cover too many scenarios. Instead of branching logic within a test, you should use Playwright's structural features to separate concerns:

    1. Use test.describe with conditionals: Wrap entire test blocks in a conditional within a describe block to target specific environments or states.
    2. Use beforeEach for setup logic: Move switch statements or setup conditionals into a beforeEach hook to prepare the state before the test runs.
    3. Pre-calculate values: Calculate variables (like platform-specific hotkeys) outside of the test block so the test body remains linear and deterministic.
    // Incorrect: Conditional logic inside the test body
    test('foo', async ({ page }) => {
      if (someCondition) {
        bar()
      }
    })
    
    // Correct: Conditional logic used to wrap the test in a describe block
    test.describe('my tests', () => {
      if (someCondition) {
        test('foo', async ({ page }) => {
          bar()
        })
      }
    })
  5. Use the `require-soft-assertions` rule to enforce soft assertions

    main

    The require-soft-assertions rule encourages the use of Playwright soft assertions instead of standard assertions. Soft assertions are useful when you want to perform multiple assertions within a single test without the test failing immediately upon the first assertion error.

    Note: This rule is not enabled by default and should only be used if it fits your specific testing workflow.

    // Correct: Using soft assertions
    await expect.soft(page.locator('foo')).toHaveText('bar');
    await expect.soft(page).toHaveTitle('baz');
    
    // Incorrect: Using standard assertions (will trigger rule violation)
    await expect(page.locator('foo')).toHaveText('bar');
    await expect(page).toHaveTitle('baz');
  6. Use the `prefer-web-first-assertions` rule for resilient tests

    main

    The prefer-web-first-assertions rule encourages the use of Playwright's built-in web first assertions instead of using standard Jest/Vitest matchers on the results of awaited locator methods.

    Web first assertions (e.g., toBeVisible(), toHaveText()) are preferred because they automatically wait for the condition to be met, making your tests more resilient to asynchronous UI changes and reducing flakiness. Using expect(await locator.isVisible()).toBe(true) is discouraged because it performs a single check at a specific moment in time without the automatic retry logic provided by Playwright's assertion engine.

    // ❌ Incorrect: Manual check of a boolean/value
    expect(await page.locator('.tweet').isVisible()).toBe(true)
    expect(await page.locator('.tweet').isEnabled()).toBe(true)
    expect(await page.locator('.tweet').innerText()).toBe('bar')
    
    // ✅ Correct: Using web first assertions with automatic waiting
    await expect(page.locator('.tweet')).toBeVisible()
    await expect(page.locator('.tweet')).toBeEnabled()
    await expect(page.locator('.tweet')).toHaveText('bar')
  7. Use specific matchers instead of `not` variants (`no-useless-not`)

    main

    The no-useless-not rule disallows the use of .not matchers when a direct, complimentary matcher exists. Using the specific matcher (e.g., toBeHidden() instead of not.toBeVisible()) improves test readability and intent.

    Incorrect usage: Using .not with a matcher that has a direct opposite.

    Correct usage: Using the direct opposite matcher instead of negating the original.

    // Incorrect
    expect(locator).not.toBeVisible()
    expect(locator).not.toBeHidden()
    expect(locator).not.toBeEnabled()
    expect(locator).not.toBeDisabled()
    
    // Correct
    expect(locator).toBeHidden()
    expect(locator).toBeVisible()
    expect(locator).toBeDisabled()
    expect(locator).toBeEnabled()
  8. Use the `prefer-hooks-on-top` rule to organize Playwright hooks

    main

    The playwright/prefer-hooks-on-top rule ensures that Playwright hooks (like beforeAll, beforeEach, afterEach, and afterAll) are defined before any test cases (test(...)) within the same scope.

    While Playwright allows hooks to be defined anywhere in a file, they are executed in a specific order. Intermixing hooks with test cases can make the execution flow confusing and difficult to read. This rule enforces a cleaner structure where all setup and teardown logic is grouped at the top of the test.describe block or scope, followed by the actual tests.

    /* eslint playwright/prefer-hooks-on-top: "error" */
    
    test.describe('foo', () => {
      test.beforeAll(() => {
        createMyDatabase()
      })
    
      test.beforeEach(() => {
        seedMyDatabase()
      })
    
      test.afterAll(() => {
        clearMyDatabase()
      })
    
      test('accepts this input', () => {
        // ...
      })
    });
  9. How to test error properties without violating `no-conditional-expect`

    main

    When you need to assert specific properties on a thrown error (since Playwright's toThrow matcher primarily checks the message property), avoid using try/catch blocks with expect inside the catch. Instead, use a wrapper function that catches the error and returns it, allowing you to perform assertions in the main execution flow.

    Recommended Pattern:

    1. Create a custom NoErrorThrownError class.
    2. Implement a helper function (e.g., getError) that wraps the async call. If the call succeeds, it throws the NoErrorThrownError. If it fails, it returns the caught error.
    3. In your test, await the helper and assert that the returned error is not an instance of NoErrorThrownError before checking its properties.
    class NoErrorThrownError extends Error {}
    
    const getError = async <TError>(call: () => unknown): Promise<TError> => {
      try {
        await call()
        throw new NoErrorThrownError()
      } catch (error: unknown) {
        return error as TError
      }
    }
    
    test.describe('when the http request fails', () => {
      test('includes the status code in the error', async () => {
        const error = await getError(async () => makeRequest(url))
    
        // check that the returned error wasn't that no error was thrown
        expect(error).not.toBeInstanceOf(NoErrorThrownError)
        expect(error).toHaveProperty('statusCode', 404)
      })
    })
  10. Disallow usage of `nth` methods (`no-nth-methods`)

    main

    The no-nth-methods rule prevents the use of Playwright locator methods that select elements based on their index: .first(), .last(), and .nth(). Using these methods can lead to flaky tests because they rely on the specific order and structure of the DOM, which may change during execution or updates.

    // Incorrect usage examples:
    page.locator('button').first()
    page.locator('button').last()
    page.locator('button').nth(3)