# Eslint Plugin Playwright > Ensure that there is at least one`expect`call made in a test. --- # Enforce assertion to be made in a test body (`expect-expect`) Ensure that there is at least one `expect` call made in a test. ## Rule Details Examples of **incorrect** code for this rule: ```javascript test('should be a test', () => { console.log('no assertion') }) test('should assert something', () => {}) ``` Examples of **correct** code for this rule: ```javascript test('should be a test', async () => { await expect(page).toHaveTitle('foo') }) test('should work with callbacks/async', async () => { await test.step('step 1', async () => { await expect(page).toHaveTitle('foo') }) }) ``` ## Options ```json { "playwright/expect-expect": [ "error", { "assertFunctionNames": ["assertCustomCondition"], "assertFunctionPatterns": ["^assert.*", "^verify.*"] } ] } ``` ### `assertFunctionNames` This array option specifies the names of functions that should be considered to be asserting functions. ```ts /* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["assertScrolledToBottom"] }] */ function assertScrolledToBottom(page) { // ... } test('should scroll', async ({ page }) => { await assertScrolledToBottom(page) }) ``` ### `assertFunctionPatterns` This array option specifies regular expression patterns that should match function names to be considered as asserting functions. This is useful when you have multiple assertion functions following a naming convention. ```ts /* eslint playwright/expect-expect: ["error", { "assertFunctionPatterns": ["^assert.*", "^verify.*"] }] */ function assertScrolledToBottom(page) { // ... } function verifyPageLoaded(page) { // ... } test('should scroll', async ({ page }) => { await assertScrolledToBottom(page) await verifyPageLoaded(page) }) ``` You can use both `assertFunctionNames` and `assertFunctionPatterns` together. The rule will consider a function as an assertion if it matches either an exact name or a pattern. --- # Enforces a maximum number assertion calls in a test body (`max-expects`) As more assertions are made, there is a possible tendency for the test to be more likely to mix multiple objectives. To avoid this, this rule reports when the maximum number of assertions is exceeded. ## Rule details This rule enforces a maximum number of `expect()` calls. The following patterns are considered warnings (with the default option of `{ "max": 5 } `): ```js test('should not pass', () => { expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() }) ``` The following patterns are **not** considered warnings (with the default option of `{ "max": 5 } `): ```js test('shout pass') test('shout pass', () => {}) test.skip('shout pass', () => {}) test('should pass', function () { expect(true).toBeDefined() }) test('should pass', () => { expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() expect(true).toBeDefined() }) ``` ## Options ```json { "playwright/max-expects": [ "error", { "max": 5 } ] } ``` ### `max` Enforces a maximum number of `expect()`. This has a default value of `5`. --- # Enforces a maximum depth to nested describe calls (`max-nested-describe`) While it's useful to be able to group your tests together within the same file using `describe()`, having too many levels of nesting throughout your tests make them difficult to read. ## Rule Details Examples of **incorrect** code for this rule (with the default option of `{ "max": 5 }` ): ```javascript test.describe('foo', () => { test.describe('bar', () => { test.describe('baz', () => { test.describe('qux', () => { test.describe('quxx', () => { test.describe('too many', () => { test('this test', async ({ page }) => {}) }) }) }) }) }) }) ``` Examples of **correct** code for this rule (with the default option of `{ "max": 5 }` ): ```javascript test.describe('foo', () => { test.describe('bar', () => { test('this test', async ({ page }) => {}) }) }) ``` ## Options ```json { "playwright/max-nested-describe": ["error", { "max": 5 }] } ``` ### `max` Enforces a maximum depth for nested `describe()`. This has a default value of `5`. Examples of **correct** code with options set to `{ "max": 2 }`: ```javascript test.describe('foo', () => { test.describe('bar', () => { test('this test', async ({ page }) => {}) }) }) ``` --- # Enforce Playwright APIs to be awaited (`missing-playwright-await`) Identify false positives when async Playwright APIs are not properly awaited. ## Rule Details Example of **incorrect** code for this rule: ```javascript expect(page).toMatchText('text') expect.poll(() => foo).toBe(true) test.step('clicks the button', async () => { await page.click('button') }) ``` Example of **correct** code for this rule: ```javascript await expect(page).toMatchText('text') await expect.poll(() => foo).toBe(true) await test.step('clicks the button', async () => { await page.click('button') }) ``` ## Options The rule accepts a non-required option which can be used to specify custom matchers which this rule should also warn about. This is useful when creating your own async `expect` matchers. ```json { "playwright/missing-playwright-await": [ "error", { "customMatchers": ["toBeCustomThing"] } ] } ``` --- # Disallow commented out tests (`no-commented-out-tests`) This rule raises a warning about commented out tests. It's similar to `no-skipped-test` rule. ## Rule details The rule uses fuzzy matching to do its best to determine what constitutes a commented out test, checking for a presence of `test(`, `test.describe(`, `test.skip(`, etc. in code comments. The following patterns are considered warnings: ```js // describe('foo', () => {}); // test.describe('foo', () => {}); // test('foo', () => {}); // test.describe.skip('foo', () => {}); // test.skip('foo', () => {}); // test.describe['skip']('bar', () => {}); // test['skip']('bar', () => {}); /* test.describe('foo', () => {}); */ ``` These patterns would not be considered warnings: ```js describe('foo', () => {}) test.describe('foo', () => {}) test('foo', () => {}) test.describe.only('bar', () => {}) test.only('bar', () => {}) // foo('bar', () => {}); ``` ### Limitations The plugin looks at the literal function names within test code, so will not catch more complex examples of commented out tests, such as: ```js // const testSkip = test.skip; // testSkip('skipped test', () => {}); // const myTest = test; // myTest('does not have function body'); ``` --- # Disallow calling `expect` conditionally (`no-conditional-expect`) This rule prevents the use of `expect` in conditional blocks, such as `if`s & `catch`s. This includes using `expect` in callbacks to functions named `catch`, which are assumed to be promises. ## Rule details Playwright only considers a test to have failed if it throws an error, meaning if calls to assertion functions like `expect` occur in conditional code such as a `catch` statement, tests can end up passing but not actually test anything. Additionally, conditionals tend to make tests more brittle and complex, as they increase the amount of mental thinking needed to understand what is actually being tested. The following patterns are warnings: ```js test('foo', () => { doTest && expect(1).toBe(2) }) test('bar', () => { if (!skipTest) { expect(1).toEqual(2) } }) test('baz', async () => { try { await foo() } catch (err) { expect(err).toMatchObject({ code: 'MODULE_NOT_FOUND' }) } }) test('throws an error', async () => { await foo().catch((error) => expect(error).toBeInstanceOf(error)) }) ``` The following patterns are not warnings: ```js test('foo', () => { expect(!value).toBe(false) }) function getValue() { if (process.env.FAIL) { return 1 } return 2 } test('foo', () => { expect(getValue()).toBe(2) }) test('validates the request', () => { try { processRequest(request) } catch { // ignore errors } finally { expect(validRequest).toHaveBeenCalledWith(request) } }) test('throws an error', async () => { await expect(foo).rejects.toThrow(Error) }) ``` ### How to catch a thrown error for testing without violating this rule A common situation that comes up with this rule is when wanting to test properties on a thrown error, as Playwright's `toThrow` matcher only checks the `message` property. Most people write something like this: ```typescript test.describe('when the http request fails', () => { test('includes the status code in the error', async () => { try { await makeRequest(url) } catch (error) { expect(error).toHaveProperty('statusCode', 404) } }) }) ``` As stated above, the problem with this is that if `makeRequest()` doesn't throw the test will still pass as if the `expect` had been called. A better way to handle this situation is to introduce a wrapper to handle the catching, and otherwise return a specific "no error thrown" error if nothing is thrown by the wrapped function: ```typescript class NoErrorThrownError extends Error {} const getError = async (call: () => unknown): Promise => { 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) }) }) ``` --- # Disallow conditional logic in tests (`no-conditional-in-test`) Conditional logic in tests is usually an indication that a test is attempting to cover too much, and not testing the logic it intends to. Each branch of code executing within a conditional statement will usually be better served by a test devoted to it. ## Rule Details Examples of **incorrect** code for this rule: ```javascript test('foo', async ({ page }) => { if (someCondition) { bar() } }) test('bar', async ({ page }) => { switch (mode) { case 'single': generateOne() break case 'double': generateTwo() break case 'multiple': generateMany() break } await expect(page.locator('.my-image').count()).toBeGreaterThan(0) }) test('baz', async ({ page }) => { const hotkey = process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'] await Promise.all(hotkey.map((x) => page.keyboard.down(x))) expect(actionIsPerformed()).toBe(true) }) ``` Examples of **correct** code for this rule: ```javascript test.describe('my tests', () => { if (someCondition) { test('foo', async ({ page }) => { bar() }) } }) beforeEach(() => { switch (mode) { case 'single': generateOne() break case 'double': generateTwo() break case 'multiple': generateMany() break } }) test('bar', async ({ page }) => { await expect(page.locator('.my-image').count()).toBeGreaterThan(0) }) const hotkey = process.platform === 'linux' ? ['Control', 'Alt', 'f'] : ['Alt', 'f'] test('baz', async ({ page }) => { await Promise.all(hotkey.map((x) => page.keyboard.down(x))) expect(actionIsPerformed()).toBe(true) }) ``` --- # Disallow duplicate setup and teardown hooks (`no-duplicate-hooks`) A `describe` block should not contain duplicate hooks. ## Rule details Examples of **incorrect** code for this rule ```js /* eslint playwright/no-duplicate-hooks: "error" */ test.describe('foo', () => { test.beforeEach(() => { // some setup }) test.beforeEach(() => { // some setup }) test('foo_test', () => { // some test }) }) // Nested describe scenario test.describe('foo', () => { test.beforeEach(() => { // some setup }) test('foo_test', () => { // some test }) test.describe('bar', () => { test('bar_test', () => { test.afterAll(() => { // some teardown }) test.afterAll(() => { // some teardown }) }) }) }) ``` Examples of **correct** code for this rule ```js /* eslint playwright/no-duplicate-hooks: "error" */ test.describe('foo', () => { test.beforeEach(() => { // some setup }) test('foo_test', () => { // some test }) }) // Nested describe scenario test.describe('foo', () => { test.beforeEach(() => { // some setup }) test('foo_test', () => { // some test }) test.describe('bar', () => { test('bar_test', () => { test.beforeEach(() => { // some setup }) }) }) }) ``` --- ## Disallow usage of element handles (`no-element-handle`) Disallow the creation of element handles with `page.$` or `page.$$`. ## Rule Details Examples of **incorrect** code for this rule: ```javascript // Element Handle const buttonHandle = await page.$('button') await buttonHandle.click() // Element Handles const linkHandles = await page.$$('a') ``` Example of **correct** code for this rule: ```javascript const buttonLocator = page.locator('button') await buttonLocator.click() ``` --- # Disallow usage of `page.$eval` and `page.$$eval` (`no-eval`) ## Rule Details Examples of **incorrect** code for this rule: ```javascript const searchValue = await page.$eval('#search', (el) => el.value) const divCounts = await page.$$eval( 'div', (divs, min) => divs.length >= min, 10, ) await page.$eval('#search', (el) => el.value) await page.$$eval('#search', (el) => el.value) ``` Example of **correct** code for this rule: ```javascript await page.locator('button').evaluate((node) => node.innerText) await page.locator('div').evaluateAll((divs, min) => divs.length >= min, 10) ``` --- # Disallow usage of `.only` annotation (`no-focused-test`) Examples of **incorrect** code for this rule: ```javascript test.only('focus this test', async ({ page }) => {}) test.describe.only('focus two tests', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) test.describe.parallel.only('focus two tests in parallel mode', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) test.describe.serial.only('focus two tests in serial mode', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) ``` Examples of **correct** code for this rule: ```javascript test('this test', async ({ page }) => {}) test.describe('two tests', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) test.describe.parallel('two tests in parallel mode', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) test.describe.serial('two tests in serial mode', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) ``` --- # Disallow usage of the `{ force: true }` option (`no-force-option`) ## Rule Details Examples of **incorrect** code for this rule: ```javascript await page.locator('button').click({ force: true }) await page.locator('check').check({ force: true }) await page.locator('input').fill('something', { force: true }) ``` Examples of **correct** code for this rule: ```javascript await page.locator('button').click() await page.locator('check').check() await page.locator('input').fill('something') ``` --- ## Disallow using `getByTitle()` (`no-get-by-title`) The HTML `title` attribute does not provide a fully accessible tooltip for elements so relying on it to identify elements can hide accessibility issues in your code. This rule helps to prevent that by disallowing use of the `getByTitle` method. ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.getByTitle('Delete product').click() ``` Example of **correct** code for this rule: ```javascript await page.getByRole('button', { name: 'Delete product' }).click() ``` --- # Disallow setup and teardown hooks (`no-hooks`) Playwright provides global functions for setup and teardown tasks, which are called before/after each test case and each test suite. The use of these hooks promotes shared state between tests. ## Rule details This rule reports for the following function calls: - `beforeAll` - `beforeEach` - `afterAll` - `afterEach` Examples of **incorrect** code for this rule: ```js /* eslint playwright/no-hooks: "error" */ function setupFoo(options) { /* ... */ } function setupBar(options) { /* ... */ } test.describe('foo', () => { let foo test.beforeEach(() => { foo = setupFoo() }) test.afterEach(() => { foo = null }) test('does something', () => { expect(foo.doesSomething()).toBe(true) }) test.describe('with bar', () => { let bar test.beforeEach(() => { bar = setupBar() }) test.afterEach(() => { bar = null }) test('does something with bar', () => { expect(foo.doesSomething(bar)).toBe(true) }) }) }) ``` Examples of **correct** code for this rule: ```js /* eslint playwright/no-hooks: "error" */ function setupFoo(options) { /* ... */ } function setupBar(options) { /* ... */ } test.describe('foo', () => { test('does something', () => { const foo = setupFoo() expect(foo.doesSomething()).toBe(true) }) test('does something with bar', () => { const foo = setupFoo() const bar = setupBar() expect(foo.doesSomething(bar)).toBe(true) }) }) ``` ## Options ```json { "playwright/no-hooks": [ "error", { "allow": ["afterEach", "afterAll"] } ] } ``` ### `allow` This array option controls which Playwright hooks are checked by this rule. There are four possible values: - `"beforeAll"` - `"beforeEach"` - `"afterAll"` - `"afterEach"` By default, none of these options are enabled (the equivalent of `{ "allow": [] }`). Examples of **incorrect** code for the `{ "allow": ["afterEach"] }` option: ```js /* eslint playwright/no-hooks: ["error", { "allow": ["afterEach"] }] */ function setupFoo(options) { /* ... */ } let foo test.beforeEach(() => { foo = setupFoo() }) test.afterEach(() => { playwright.resetModules() }) test('foo does this', () => { // ... }) test('foo does that', () => { // ... }) ``` Examples of **correct** code for the `{ "allow": ["afterEach"] }` option: ```js /* eslint playwright/no-hooks: ["error", { "allow": ["afterEach"] }] */ function setupFoo(options) { /* ... */ } test.afterEach(() => { playwright.resetModules() }) test('foo does this', () => { const foo = setupFoo() // ... }) test('foo does that', () => { const foo = setupFoo() // ... }) ``` ## When Not To Use It If you prefer using the setup and teardown hooks provided by Playwright, you can safely disable this rule. --- # Disallow nested `test.step()` methods (`no-nested-step`) Nesting `test.step()` methods can make your tests difficult to read. ## Rule Details Examples of **incorrect** code for this rule: ```javascript test('foo', async () => { await test.step('step1', async () => { await test.step('nest step', async () => { await expect(true).toBe(true) }) }) }) ``` Examples of **correct** code for this rule: ```javascript test('foo', async () => { await test.step('step1', async () => { await expect(true).toBe(true) }) await test.step('step2', async () => { await expect(true).toBe(true) }) }) ``` --- # Disallow usage of the `networkidle` option (`no-networkidle`) Using `networkidle` is discouraged in favor of using [web first assertions](https://playwright.dev/docs/best-practices#use-web-first-assertions). ## Rule Details Examples of **incorrect** code for this rule: ```javascript await page.waitForLoadState('networkidle') await page.waitForURL('...', { waitUntil: 'networkidle' }) await page.goto('...', { waitUntil: 'networkidle' }) ``` --- # Disallow usage of `nth` methods (`no-nth-methods`) This rule prevents the usage of `nth` methods (`first()`, `last()`, and `nth()`). These methods can be prone to flakiness if the DOM structure changes. ## Rule Details Examples of **incorrect** code for this rule: ```javascript page.locator('button').first() page.locator('button').last() page.locator('button').nth(3) ``` --- ## Disallow using `page.pause` (`no-page-pause`) Prevent usage of `page.pause()`. ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.click('button') await page.pause() ``` Example of **correct** code for this rule: ```javascript await page.click('button') ``` --- ## Disallow using raw locators (`no-raw-locators`) Prefer using user-facing locators over raw locators to make tests more robust. Check out the [Playwright documentation](https://playwright.dev/docs/locators) for more information. ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.locator('button').click() ``` Example of **correct** code for this rule: ```javascript await page.getByRole('button').click() ``` ```javascript await page.getByRole('button', { name: 'Submit', }) ``` ## Options ```json { "playwright/no-raw-locators": [ "error", { "allowed": ["iframe", "[aria-busy='false']"] } ] } ``` ### `allowed` An array of raw locators that are allowed. This helps for locators such as `iframe` which does not have a ARIA role that you can select using `getByRole`. By default, no raw locators are allowed (the equivalent of `{ "ignore": [] }`). Example of **incorrect** code for the `{ "allowed": ["[aria-busy=false]"] }` option: ```javascript page.getByRole('navigation').and(page.locator('iframe')) ``` Example of **correct** code for the `{ "allowed": ["[aria-busy=false]"] }` option: ```javascript page.getByRole('navigation').and(page.locator('[aria-busy="false"]')) ``` --- # Disallow specific matchers & modifiers (`no-restricted-matchers`) This rule bans specific matchers & modifiers from being used, and can suggest alternatives. ## Rule Details Bans are expressed in the form of a map, with the value being either a string message to be shown, or `null` if the default rule message should be used. Both matchers, modifiers, and chains of the two are checked, allowing for specific variations of a matcher to be banned if desired. By default, this map is empty, meaning no matchers or modifiers are banned. For example: ```json { "playwright/no-restricted-matchers": [ "error", { "toBeFalsy": "Use `toBe(false)` instead.", "not": null, "not.toHaveText": null } ] } ``` Examples of **incorrect** code for this rule with the above configuration ```javascript test('is false', () => { expect(a).toBeFalsy() }) test('not', () => { expect(a).not.toBe(true) }) test('chain', async () => { await expect(foo).not.toHaveText('bar') }) ``` --- # Disallow usage of the `.skip` annotation (`no-skipped-test`) ## Rule Details Examples of **incorrect** code for this rule: ```javascript test.skip('skip this test', async ({ page }) => {}) test.describe.skip('skip two tests', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) test.describe('skip test inside describe', () => { test.skip() }) test.describe('skip test conditionally', async ({ browserName }) => { test.skip(browserName === 'firefox', 'Working on it') }) ``` Examples of **correct** code for this rule: ```javascript test('this test', async ({ page }) => {}) test.describe('two tests', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) ``` ## Options ```json { "playwright/no-skipped-test": [ "error", { "allowConditional": false } ] } ``` ### `allowConditional` Setting this option to `true` will allow using `test.skip()` to [conditionally skip a test](https://playwright.dev/docs/test-annotations#conditionally-skip-a-test). This can be helpful if you want to prevent usage of `test.skip` being added by mistake but still allow conditional tests based on browser/environment setup. Examples of **incorrect** code for the `{ "allowConditional": true }` option: ```javascript test.skip('foo', ({}) => { expect(1).toBe(1) }) test('foo', ({}) => { test.skip() expect(1).toBe(1) }) ``` Example of **correct** code for the `{ "allowConditional": true }` option: ```javascript test('foo', ({ browserName }) => { test.skip(browserName === 'firefox', 'Still working on it') expect(1).toBe(1) }) ``` --- # Disallow usage of the `.slow` annotation (`no-slowed-test`) ## Rule Details Examples of **incorrect** code for this rule: ```javascript test.slow('slow this test', async ({ page }) => {}) test.describe('slow test inside describe', () => { test.slow() }) test.describe('slow test conditionally', async ({ browserName }) => { test.slow(browserName === 'firefox', 'Working on it') }) ``` Examples of **correct** code for this rule: ```javascript test('this test', async ({ page }) => {}) test.describe('two tests', () => { test('one', async ({ page }) => {}) test('two', async ({ page }) => {}) }) ``` ## Options ```json { "playwright/no-slowed-test": [ "error", { "allowConditional": false } ] } ``` ### `allowConditional` Setting this option to `true` will allow using `test.slow()` to conditionally mark a test as slow. This can be helpful if you want to prevent usage of `test.slow` being added by mistake but still allow slow tests based on browser/environment setup. Examples of **incorrect** code for the `{ "allowConditional": true }` option: ```javascript test.slow('foo', ({}) => { expect(1).toBe(1) }) test('foo', ({}) => { test.slow() expect(1).toBe(1) }) ``` Example of **correct** code for the `{ "allowConditional": true }` option: ```javascript test('foo', ({ browserName }) => { test.slow(browserName === 'firefox', 'Still working on it') expect(1).toBe(1) }) ``` --- # Disallow using `expect` outside of `test` blocks (`no-standalone-expect`) Prevents `expect` statements outside of a `test` block. An `expect` within a helper function (but outside of a `test` block) will not trigger this rule. ## Rule details This rule aims to eliminate `expect` statements outside of `test` blocks to encourage good testing practices. Using `expect` statements outside of `test` blocks may partially work, but their intent is to be used within a test as doing so makes it clear the purpose of each test. Using `expect` in helper functions is allowed to support grouping several expect statements into a helper function or page object method. Test hooks such as `beforeEach` are also allowed to support use cases such as waiting for an element on the page before each test is executed. While these uses cases are supported, they should be used sparingly as moving too many `expect` statements outside of the body of a `test` block can make it difficult to understand the purpose and primary assertions being made by a given test. Examples of **incorrect** code for this rule: ```js // in describe test.describe('a test', () => { expect(1).toBe(1) }) // below other tests test.describe('a test', () => { test('an it', () => { expect(1).toBe(1) }) expect(1).toBe(1) }) ``` Examples of **correct** code for this rule: ```js // in it block test.describe('a test', () => { test('an it', () => { expect(1).toBe(1) }) }) // in helper function test.describe('a test', () => { const helper = () => { expect(1).toBe(1) } test('an it', () => { helper() }) }) ``` _Note that this rule will not trigger if the helper function is never used even though the `expect` will not execute. Rely on a rule like no-unused-vars for this case._ ## When Not To Use It Don't use this rule on non-playwright test files. --- ## Prevent unsafe variable references in `page.evaluate()` and `page.addInitScript()` (`no-unsafe-references`) This rule prevents common mistakes when using `page.evaluate()` or `page.addInitScript()` with variables referenced from the parent scope. When referencing variables from the parent scope with these methods, you must pass them as an argument so Playwright can properly serialize and send them to the browser page where the function being evaluated is executed. ## Rule Details Example of **incorrect** code for this rule: ```javascript const x = 7 const y = 8 await page.evaluate(() => Promise.resolve(x * y), []) await page.addInitScript(() => Promise.resolve(x * y), []) ``` Example of **correct** code for this rule: ```javascript await page.evaluate(([x, y]) => Promise.resolve(x * y), [7, 8]) await page.addInitScript(([x, y]) => Promise.resolve(x * y), [7, 8]) const x = 7 const y = 8 await page.evaluate(([x, y]) => Promise.resolve(x * y), [x, y]) await page.addInitScript(([x, y]) => Promise.resolve(x * y), [x, y]) ``` --- # Disallow usage of page locators that are not used (`no-unused-locators`) Using locators without performing any actions or assertions on them can lead to unexpected behavior/flakiness in tests. This rule helps ensure that locators are used in some way by requiring that they are either acted upon or asserted against. ## Rule Details Examples of **incorrect** code for this rule: ```javascript page.getByRole('button', { name: 'Sign in' }) ``` Examples of **correct** code for this rule: ```javascript const btn = page.getByRole('button', { name: 'Sign in' }) await page.getByRole('button', { name: 'Sign in' }).click() await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible() ``` --- # Disallow unnecessary `await`s for Playwright methods (`no-useless-await`) Some Playwright methods are frequently, yet incorrectly, awaited when the await expression has no effect. ## Rule Details Examples of **incorrect** code for this rule: ```javascript await page.locator('.my-element') await page.getByRole('.my-element') await expect(1).toBe(1) await expect(true).toBeTruthy() ``` Examples of **correct** code for this rule: ```javascript page.locator('.my-element') page.getByRole('.my-element') await page.$('.my-element') await page.goto('.my-element') expect(1).toBe(1) expect(true).toBeTruthy() await expect(page.locator('.foo')).toBeVisible() await expect(page.locator('.foo')).toHaveText('bar') ``` --- # Disallow usage of `not` matchers when a specific matcher exists (`no-useless-not`) Several Playwright matchers are complimentary such as `toBeVisible`/`toBeHidden` and `toBeEnabled`/`toBeDisabled`. While the `not` variants of each of these matchers can be used, it's preferred to use the complimentary matcher instead. ## Rule Details Examples of **incorrect** code for this rule: ```javascript expect(locator).not.toBeVisible() expect(locator).not.toBeHidden() expect(locator).not.toBeEnabled() expect(locator).not.toBeDisabled() ``` Example of **correct** code for this rule: ```javascript expect(locator).toBeHidden() expect(locator).toBeVisible() expect(locator).toBeDisabled() expect(locator).toBeEnabled() ``` --- # Disallow usage of `page.waitForNavigation` (`no-wait-for-navigation`) ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.waitForNavigation() const navigationPromise = page.waitForNavigation() await page.getByText('Navigate after timeout').click() await navigationPromise ``` Examples of **correct** code for this rule: ```javascript await page.waitForURL('**/target') await page.click('delayed-navigation') ``` --- # Disallow usage of `page.waitForSelector` (`no-wait-for-selector`) ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.waitForSelector('#foo') ``` Examples of **correct** code for this rule: ```javascript await page.waitForLoadState() await page.waitForURL('/home') await page.waitForFunction(() => window.innerWidth < 100) ``` --- # Disallow usage of `page.waitForTimeout` (`no-wait-for-timeout`) ## Rule Details Example of **incorrect** code for this rule: ```javascript await page.waitForTimeout(5000) ``` Examples of **correct** code for this rule: ```javascript // Use signals such as network events, selectors becoming visible and others instead. await page.waitForLoadState() await page.waitForURL('/home') await page.waitForFunction(() => window.innerWidth < 100) ``` --- # Suggest using the built-in comparison matchers (`prefer-comparison-matcher`) Playwright has a number of built-in matchers for comparing numbers, which allow for more readable tests and error messages if an expectation fails. ## Rule details This rule checks for comparisons in tests that could be replaced with one of the following built-in comparison matchers: - `toBeGreaterThan` - `toBeGreaterThanOrEqual` - `toBeLessThan` - `toBeLessThanOrEqual` Examples of **incorrect** code for this rule: ```js expect(x > 5).toBe(true) expect(x < 7).not.toEqual(true) expect(x <= y).toStrictEqual(true) ``` Examples of **correct** code for this rule: ```js expect(x).toBeGreaterThan(5) expect(x).not.toBeLessThanOrEqual(7) expect(x).toBeLessThanOrEqual(y) // special case - see below expect(x < 'Carl').toBe(true) ``` Note that these matchers only work with numbers and bigints, and that the rule assumes that any variables on either side of the comparison operator are of one of those types - this means if you're using the comparison operator with strings, the fix applied by this rule will result in an error. ```js expect(myName).toBeGreaterThanOrEqual(theirName) // Matcher error: received value must be a number or bigint ``` The reason for this is that comparing strings with these operators is expected to be very rare and would mean not being able to have an automatic fixer for this rule. If for some reason you are using these operators to compare strings, you can disable this rule using an inline [configuration comment](https://eslint.org/docs/user-guide/configuring/rules#disabling-rules): ```js // eslint-disable-next-line playwright/prefer-comparison-matcher expect(myName > theirName).toBe(true) ``` --- # Suggest using the built-in equality matchers (`prefer-equality-matcher`) Playwright has built-in matchers for expecting equality, which allow for more readable tests and error messages if an expectation fails. ## Rule details This rule checks for _strict_ equality checks (`===` & `!==`) in tests that could be replaced with one of the following built-in equality matchers: - `toBe` - `toEqual` - `toStrictEqual` Examples of **incorrect** code for this rule: ```js expect(x === 5).toBe(true) expect(name === 'Carl').not.toEqual(true) expect(myObj !== thatObj).toStrictEqual(true) ``` Examples of **correct** code for this rule: ```js expect(x).toBe(5) expect(name).not.toEqual('Carl') expect(myObj).toStrictEqual(thatObj) ``` --- # Prefer having hooks in a consistent order (`prefer-hooks-in-order`) While hooks can be setup in any order, they're always called by `playwright` in this specific order: 1. `beforeAll` 1. `beforeEach` 1. `afterEach` 1. `afterAll` This rule aims to make that more obvious by enforcing grouped hooks be setup in that order within tests. ## Rule details Examples of **incorrect** code for this rule ```js /* eslint playwright/prefer-hooks-in-order: "error" */ test.describe('foo', () => { test.beforeEach(() => { seedMyDatabase() }) test.beforeAll(() => { createMyDatabase() }) test('accepts this input', () => { // ... }) test('returns that value', () => { // ... }) test.describe('when the database has specific values', () => { const specificValue = '...' test.beforeEach(() => { seedMyDatabase(specificValue) }) test('accepts that input', () => { // ... }) test('throws an error', () => { // ... }) test.afterEach(() => { clearLogger() }) test.beforeEach(() => { mockLogger() }) test('logs a message', () => { // ... }) }) test.afterAll(() => { removeMyDatabase() }) }) ``` Examples of **correct** code for this rule ```js /* eslint playwright/prefer-hooks-in-order: "error" */ test.describe('foo', () => { test.beforeAll(() => { createMyDatabase() }) test.beforeEach(() => { seedMyDatabase() }) test('accepts this input', () => { // ... }) test('returns that value', () => { // ... }) test.describe('when the database has specific values', () => { const specificValue = '...' test.beforeEach(() => { seedMyDatabase(specificValue) }) test('accepts that input', () => { // ... }) test('throws an error', () => { // ... }) test.beforeEach(() => { mockLogger() }) test.afterEach(() => { clearLogger() }) test('logs a message', () => { // ... }) }) test.afterAll(() => { removeMyDatabase() }) }) ``` ## Also See - [`prefer-hooks-on-top`](prefer-hooks-on-top.md) --- # Suggest having hooks before any test cases (`prefer-hooks-on-top`) While hooks can be setup anywhere in a test file, they are always called in a specific order, which means it can be confusing if they're intermixed with test cases. This rule helps to ensure that hooks are always defined before test cases. ## Rule details Examples of **incorrect** code for this rule ```js /* eslint playwright/prefer-hooks-on-top: "error" */ test.describe('foo', () => { test.beforeEach(() => { seedMyDatabase() }) test('accepts this input', () => { // ... }) test.beforeAll(() => { createMyDatabase() }) test('returns that value', () => { // ... }) test.describe('when the database has specific values', () => { const specificValue = '...' test.beforeEach(() => { seedMyDatabase(specificValue) }) test('accepts that input', () => { // ... }) test('throws an error', () => { // ... }) test.afterEach(() => { clearLogger() }) test.beforeEach(() => { mockLogger() }) test('logs a message', () => { // ... }) }) test.afterAll(() => { removeMyDatabase() }) }) ``` Examples of **correct** code for this rule ```js /* eslint playwright/prefer-hooks-on-top: "error" */ test.describe('foo', () => { test.beforeAll(() => { createMyDatabase() }) test.beforeEach(() => { seedMyDatabase() }) test.afterAll(() => { clearMyDatabase() }) test('accepts this input', () => { // ... }) test('returns that value', () => { // ... }) test.describe('when the database has specific values', () => { const specificValue = '...' beforeEach(() => { seedMyDatabase(specificValue) }) beforeEach(() => { mockLogger() }) afterEach(() => { clearLogger() }) test('accepts that input', () => { // ... }) test('throws an error', () => { // ... }) test('logs a message', () => { // ... }) }) }) ``` --- # Suggest using `page.locator()` (`prefer-locator`) Suggest using locators and their associated methods instead of page methods for performing actions. ## Rule details This rule triggers a warning if page methods are used, instead of locators. The following patterns are considered warnings: ```javascript page.click('css=button') await page.click('css=button') await page.dblclick('xpath=//button') await page.fill('input[type="password"]', 'password') await page.frame('frame-name').click('css=button') ``` The following pattern are **not** warnings: ```javascript const locator = page.locator('css=button') await page.getByRole('password').fill('password') await page.getByLabel('User Name').fill('John') await page.getByRole('button', { name: 'Sign in' }).click() await page.locator('input[type="password"]').fill('password') await page.locator('css=button').click() await page.locator('xpath=//button').dblclick() await page.frameLocator('#my-iframe').getByText('Submit').click() ``` --- # Enforce lowercase test names (`prefer-lowercase-title`) ## Rule details Enforce `test` and `test.describe` to have descriptions that begin with a lowercase letter. This provides more readable test failures. This rule is not enabled by default. The following pattern is considered a warning: ```javascript test('Adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3) }) ``` The following pattern is **not** considered a warning: ```javascript test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3) }) ``` ## Options ```json { "playwright/prefer-lowercase-title": [ "error", { "allowedPrefixes": ["GET", "POST"], "ignore": ["test.describe", "test"], "ignoreTopLevelDescribe": true } ] } ``` ### `ignore` This array option controls which Playwright functions are checked by this rule. There are two possible values: - `"test.describe"` - `"test"` By default, none of these options are enabled (the equivalent of `{ "ignore": [] }`). Example of **correct** code for the `{ "ignore": ["test.describe"] }` option: ```javascript test.describe('Uppercase description') ``` Example of **correct** code for the `{ "ignore": ["test"] }` option: ```javascript test('Uppercase description') ``` ### `allowedPrefixes` This array option allows specifying prefixes, which contain capitals that titles can start with. This can be useful when writing tests for API endpoints, where you'd like to prefix with the HTTP method. By default, nothing is allowed (the equivalent of `{ "allowedPrefixes": [] }`). Example of **correct** code for the `{ "allowedPrefixes": ["GET"] }` option: ```javascript test.describe('GET /live') ``` ### `ignoreTopLevelDescribe` This option can be set to allow only the top-level `test.describe` blocks to have a title starting with an upper-case letter. Example of **correct** code for the `{ "ignoreTopLevelDescribe": true }` option: ```javascript test.describe('MyClass', () => { test.describe('#myMethod', () => { test('does things', () => {}) }) }) ``` --- # Suggest using native Playwright locators (`prefer-native-locators`) Playwright has built-in locators for common query selectors such as finding elements by placeholder text, ARIA role, accessible name, and more. This rule suggests using these native locators instead of using `page.locator()` with an equivalent selector. In some cases this can be more robust too, such as finding elements by ARIA role or accessible name, because some elements have implicit roles, and there are multiple ways to specify accessible names. ## Rule details Examples of **incorrect** code for this rule: ```javascript page.locator('[aria-label="View more"]') page.locator('[role="button"]') page.locator('[placeholder="Enter some text..."]') page.locator('[alt="Playwright logo"]') page.locator('[title="Additional context"]') page.locator('[data-testid="password-input"]') ``` Examples of **correct** code for this rule: ```javascript page.getByLabel('View more') page.getByRole('Button') page.getByPlaceholder('Enter some text...') page.getByAltText('Playwright logo') page.getByTestId('password-input') page.getByTitle('Additional context') ``` ## Options ```json { "playwright/prefer-native-locators": [ "error", { "testIdAttribute": "data-testid" } ] } ``` ### `testIdAttribute` Default: `data-testid` This string option specifies the test ID attribute to look for and replace with `page.getByTestId()` calls. If you are using [`page.setTestIdAttribute()`](https://playwright.dev/docs/api/class-selectors#selectors-set-test-id-attribute), this should be set to the same value as what you pass in to that method. Examples of **incorrect** code when using `{ "testIdAttribute": "data-custom-testid" }` option: ```js page.locator('[data-custom-testid="password-input"]') ``` Examples of **correct** code when using `{ "testIdAttribute": "data-custom-testid" }` option: ```js page.getByTestId('password-input') ``` --- # Suggest using `toStrictEqual()` (`prefer-strict-equal`) `toStrictEqual` not only checks that two objects contain the same data but also that they have the same structure. It is common to expect objects to not only have identical values but also to have identical keys. A stricter equality will catch cases where two objects do not have identical keys. ## Rule details This rule triggers a warning if `toEqual()` is used to assert equality. ### Default configuration The following pattern is considered warning: ```javascript expect({ a: 'a', b: undefined }).toEqual({ a: 'a' }) // true ``` The following pattern is not warning: ```javascript expect({ a: 'a', b: undefined }).toStrictEqual({ a: 'a' }) // false ``` --- # Suggest using `toBe()` for primitive literals (`prefer-to-be`) When asserting against primitive literals such as numbers and strings, the equality matchers all operate the same, but read slightly differently in code. This rule recommends using the `toBe` matcher in these situations, as it forms the most grammatically natural sentence. For `null`, `undefined`, and `NaN` this rule recommends using their specific `toBe` matchers, as they give better error messages as well. ## Rule details This rule triggers a warning if `toEqual()` or `toStrictEqual()` are used to assert a primitive literal value such as numbers, strings, and booleans. The following patterns are considered warnings: ```javascript expect(value).not.toEqual(5) expect(getMessage()).toStrictEqual('hello world') expect(loadMessage()).resolves.toEqual('hello world') ``` The following pattern is not warning: ```javascript expect(value).not.toBe(5) expect(getMessage()).toBe('hello world') expect(loadMessage()).resolves.toBe('hello world') expect(didError).not.toBe(true) expect(catchError()).toStrictEqual({ message: 'oh noes!' }) ``` For `null`, `undefined`, and `NaN`, this rule triggers a warning if `toBe` is used to assert against those literal values instead of their more specific `toBe` counterparts: ```javascript expect(value).not.toBe(undefined) expect(getMessage()).toBe(null) expect(countMessages()).resolves.not.toBe(NaN) ``` The following pattern is not warning: ```javascript expect(value).toBeDefined() expect(getMessage()).toBeNull() expect(countMessages()).resolves.not.toBeNaN() expect(catchError()).toStrictEqual({ message: undefined }) ``` --- # Suggest using `toContain()` (`prefer-to-contain`) In order to have a better failure message, `toContain()` should be used upon asserting expectations on an array containing an object. ## Rule Details Example of **incorrect** code for this rule: ```javascript expect(a.includes(b)).toBe(true) expect(a.includes(b)).not.toBe(true) expect(a.includes(b)).toBe(false) expect(a.includes(b)).toEqual(true) expect(a.includes(b)).toStrictEqual(true) ``` Example of **correct** code for this rule: ```javascript expect(a).toContain(b) expect(a).not.toContain(b) ``` --- # Suggest using `toHaveCount()` (`prefer-to-have-count`) In order to have a better failure message, `toHaveCount()` should be used upon asserting expectations on locators `count()` method. ## Rule details This rule triggers a warning if `toBe()`, `toEqual()` or `toStrictEqual()` is used to assert locators `count()` method. The following patterns are considered warnings: ```javascript expect(await files.count()).toBe(1) expect(await files.count()).toEqual(1) expect(await files.count()).toStrictEqual(1) ``` The following pattern is **not** a warning: ```javascript await expect(files).toHaveCount(1) ``` --- # Suggest using `toHaveLength()` (`prefer-to-have-length`) In order to have a better failure message, `toHaveLength()` should be used upon asserting expectations on objects length property. ## Rule details This rule triggers a warning if `toBe()`, `toEqual()` or `toStrictEqual()` is used to assert objects length property. The following patterns are considered warnings: ```javascript expect(files.length).toBe(1) expect(files.length).toEqual(1) expect(files.length).toStrictEqual(1) ``` The following pattern is **not** a warning: ```javascript expect(files).toHaveLength(1) ``` --- # Prefer web first assertions (`prefer-web-first-assertions`) Playwright supports many web first assertions to assert properties or conditions on elements. These assertions are preferred over instance methods as the web first assertions will automatically wait for the conditions to be fulfilled resulting in more resilient tests. ## Rule Details Examples of **incorrect** code for this rule: ```javascript expect(await page.locator('.tweet').isVisible()).toBe(true) expect(await page.locator('.tweet').isEnabled()).toBe(true) expect(await page.locator('.tweet').innerText()).toBe('bar') ``` Example of **correct** code for this rule: ```javascript await expect(page.locator('.tweet')).toBeVisible() await expect(page.locator('.tweet')).toBeEnabled() await expect(page.locator('.tweet')).toHaveText('bar') ``` --- # Require setup and teardown code to be within a hook (`require-hook`) It's common when writing tests to need to perform setup work that has to happen before tests run, and finishing work after tests run. Because Playwright executes all `describe` handlers in a test file _before_ it executes any of the actual tests, it's important to ensure setup and teardown work is done inside `before*` and `after*` handlers respectively, rather than inside the `describe` blocks. ## Rule details This rule flags any expression that is either at the toplevel of a test file or directly within the body of a `describe`, _except_ for the following: - `import` statements - `const` variables - `let` _declarations_, and initializations to `null` or `undefined` - Classes - Types - Calls to the standard Playwright globals This rule flags any function calls within test files that are directly within the body of a `describe`, and suggests wrapping them in one of the four lifecycle hooks. Here is a slightly contrived test file showcasing some common cases that would be flagged: ```js const initializeCityDatabase = () => { database.addCity('Vienna') database.addCity('San Juan') database.addCity('Wellington') } const clearCityDatabase = () => { database.clear() } initializeCityDatabase() test('that persists cities', () => { expect(database.cities.length).toHaveLength(3) }) test('city database has Vienna', () => { expect(isCity('Vienna')).toBeTruthy() }) test('city database has San Juan', () => { expect(isCity('San Juan')).toBeTruthy() }) test.describe('when loading cities from the api', () => { clearCityDatabase() test('does not duplicate cities', async () => { await database.loadCities() expect(database.cities).toHaveLength(4) }) }) clearCityDatabase() ``` Here is the same slightly contrived test file showcasing the same common cases but in ways that would be **not** flagged: ```js const initializeCityDatabase = () => { database.addCity('Vienna') database.addCity('San Juan') database.addCity('Wellington') } const clearCityDatabase = () => { database.clear() } test.beforeEach(() => { initializeCityDatabase() }) test('that persists cities', () => { expect(database.cities.length).toHaveLength(3) }) test('city database has Vienna', () => { expect(isCity('Vienna')).toBeTruthy() }) test('city database has San Juan', () => { expect(isCity('San Juan')).toBeTruthy() }) test.describe('when loading cities from the api', () => { test.beforeEach(() => { clearCityDatabase() }) test('does not duplicate cities', async () => { await database.loadCities() expect(database.cities).toHaveLength(4) }) }) test.afterEach(() => { clearCityDatabase() }) ``` ## Options If there are methods that you want to call outside of hooks and tests, you can mark them as allowed using the `allowedFunctionCalls` option. ```json { "playwright/require-hook": [ "error", { "allowedFunctionCalls": ["enableAutoDestroy"] } ] } ``` Examples of **correct** code when using `{ "allowedFunctionCalls": ["enableAutoDestroy"] }` option: ```js /* eslint playwright/require-hook: ["error", { "allowedFunctionCalls": ["enableAutoDestroy"] }] */ enableAutoDestroy(test.afterEach) test.beforeEach(initDatabase) test.afterEach(tearDownDatabase) test.describe('Foo', () => { test('always returns 42', () => { expect(global.getAnswer()).toBe(42) }) }) ``` --- # Require soft assertions (`require-soft-assertions`) Some find it easier to write longer test that perform more assertions per test. In cases like these, it can be helpful to require [soft assertions](https://playwright.dev/docs/test-assertions#soft-assertions) in your tests. This rule is not enabled by default and is only intended to be used it if fits your workflow. If you aren't sure if you should use this rule, you probably shouldn't 🙂. ## Rule Details Examples of **incorrect** code for this rule: ```javascript await expect(page.locator('foo')).toHaveText('bar') await expect(page).toHaveTitle('baz') ``` Examples of **correct** code for this rule: ```javascript await expect.soft(page.locator('foo')).toHaveText('bar') await expect.soft(page).toHaveTitle('baz') ``` --- # Require a message for `toThrow()` (`require-to-throw-message`) `toThrow()` (and its alias `toThrowError()`) is used to check if an error is thrown by a function call, such as in `expect(() => a()).toThrow()`. However, if no message is defined, then the test will pass for any thrown error. Requiring a message ensures that the intended error is thrown. ## Rule details This rule triggers a warning if `toThrow()` or `toThrowError()` is used without an error message. The following patterns are considered warnings: ```js test('all the things', async () => { expect(() => a()).toThrow() expect(() => a()).toThrowError() await expect(a()).rejects.toThrow() await expect(a()).rejects.toThrowError() }) ``` The following patterns are **not** considered warnings: ```js test('all the things', async () => { expect(() => a()).toThrow('a') expect(() => a()).toThrowError('a') await expect(a()).rejects.toThrow('a') await expect(a()).rejects.toThrowError('a') }) ``` --- # Require test cases and hooks to be inside a `test.describe` block (`require-top-level-describe`) Playwright allows you to organise your test files the way you want it. However, the more your codebase grows, the more it becomes hard to navigate in your test files. This rule makes sure you provide at least a top-level `describe` block in your test file. ## Rule Details This rule triggers a warning if a test case (`test`) or a hook (`test.beforeAll`, `test.beforeEach`, `test.afterEach`, `test.afterAll`) is not located in a top-level `test.describe` block. The following patterns are considered warnings: ```javascript // Above a describe block test('my test', () => {}) test.describe('test suite', () => { test('test', () => {}) }) // Below a describe block test.describe('test suite', () => {}) test('my test', () => {}) // Same for hooks test.beforeAll('my beforeAll', () => {}) test.describe('test suite', () => {}) test.afterEach('my afterEach', () => {}) ``` The following patterns are **not** considered warnings: ```javascript // In a describe block test.describe('test suite', () => { test('my test', () => {}) }) // In a nested describe block test.describe('test suite', () => { test('my test', () => {}) test.describe('another test suite', () => { test('my other test', () => {}) }) }) ``` You can also enforce a limit on the number of describes allowed at the top-level using the `maxTopLevelDescribes` option: ```json { "playwright/require-top-level-describe": [ "error", { "maxTopLevelDescribes": 2 } ] } ``` Examples of **incorrect** code with the above config: ```javascript test.describe('test suite', () => { test('test', () => {}) }) test.describe('test suite', () => {}) test.describe('test suite', () => {}) ``` This option defaults to `Infinity`, allowing any number of top-level describes. --- # Enforce valid `describe()` callback (`valid-describe-callback`) Using an improper `describe()` callback function can lead to unexpected test errors. ## Rule details This rule validates that the second parameter of a `describe()` function is a callback function. This callback function: - should not be [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) - should not contain any parameters - should not contain any `return` statements The following patterns are considered warnings: ```js // Async callback functions are not allowed test.describe('myFunction()', async () => { // ... }) // Callback function parameters are not allowed test.describe('myFunction()', (done) => { // ... }) // No return statements are allowed in block of a callback function test.describe('myFunction', () => { return Promise.resolve().then(() => { test('breaks', () => { throw new Error('Fail') }) }) }) // Returning a value from a describe block is not allowed test.describe('myFunction', () => test('returns a truthy value', () => { expect(myFunction()).toBeTruthy() })) ``` The following patterns are **not** considered warnings: ```js test.describe('myFunction()', () => { test('returns a truthy value', () => { expect(myFunction()).toBeTruthy() }) }) ``` --- # Require promises that have expectations in their chain to be valid (`valid-expect-in-promise`) Ensure promises that include expectations are returned or awaited. ## Rule details This rule flags any promises within the body of a test that include expectations that have either not been returned or awaited. The following patterns are considered warnings: ```js test('promises a person', () => { api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) }) test('promises a counted person', () => { const promise = api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) promise.then(() => { expect(analytics.gottenPeopleCount).toBe(1) }) }) test('promises multiple people', () => { const firstPromise = api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) const secondPromise = api.getPersonByName('alice').then((person) => { expect(person).toHaveProperty('name', 'Alice') }) return Promise.any([firstPromise, secondPromise]) }) ``` The following pattern is not a warning: ```js test('promises a person', async () => { await api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) }) test('promises a counted person', () => { let promise = api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) promise = promise.then(() => { expect(analytics.gottenPeopleCount).toBe(1) }) return promise }) test('promises multiple people', () => { const firstPromise = api.getPersonByName('bob').then((person) => { expect(person).toHaveProperty('name', 'Bob') }) const secondPromise = api.getPersonByName('alice').then((person) => { expect(person).toHaveProperty('name', 'Alice') }) return Promise.allSettled([firstPromise, secondPromise]) }) ``` --- # Enforce valid `expect()` usage (`valid-expect`) Ensure `expect()` is called with a matcher. ## Rule details Examples of **incorrect** code for this rule: ```javascript expect() expect('something') expect(true).toBeDefined ``` Example of **correct** code for this rule: ```javascript expect(locator).toHaveText('howdy') expect('something').toBe('something') expect(true).toBeDefined() ``` ## Options ```json { "minArgs": 1, "maxArgs": 2 } ``` ### `minArgs` & `maxArgs` Enforces the minimum and maximum number of arguments that `expect` can take, and is required to take. `minArgs` defaults to 1 while `maxArgs` deafults to `2` to support custom expect messages. If you want to enforce `expect` always or never has a custom message, you can adjust these two option values to your preference. --- # Valid Test Tags This rule ensures that test tags in Playwright test files follow the correct format and meet any configured requirements. ## Rule Details This rule enforces the following: 1. Tags must start with `@` (e.g., `@e2e`, `@regression`) 2. (Optional, exclusive of 3) Tags must match one of the values in the `allowedTags` property 3. (Optional, exclusive of 2) Tags must not match one of the values in the `disallowedTags` property ### Examples ```ts // Valid test('my test', { tag: '@e2e' }, async ({ page }) => {}) test('my test', { tag: ['@e2e', '@login'] }, async ({ page }) => {}) test.describe('my suite', { tag: '@regression' }, () => {}) test.step('my step', { tag: '@critical' }, async () => {}) // Valid with test.skip, test.fixme, test.only test.skip('my test', { tag: '@e2e' }, async ({ page }) => {}) test.fixme('my test', { tag: '@e2e' }, async ({ page }) => {}) test.only('my test', { tag: '@e2e' }, async ({ page }) => {}) // Valid with annotation test( 'my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' }, }, async ({ page }) => {}, ) // Valid with array of annotations test( 'my test', { tag: '@e2e', annotation: [{ type: 'issue', description: 'BUG-123' }, { type: 'flaky' }], }, async ({ page }) => {}, ) // Invalid test('my test', { tag: 'e2e' }, async ({ page }) => {}) // Missing @ prefix test('my test', { tag: ['e2e', 'login'] }, async ({ page }) => {}) // Missing @ prefix ``` ## Options This rule accepts an options object with the following properties: ```ts type RuleOptions = { allowedTags?: (string | RegExp)[] // List of allowed tags or patterns disallowedTags?: (string | RegExp)[] // List of disallowed tags or patterns } ``` ### `allowedTags` When specified, only the listed tags are allowed. You can use either exact strings or regular expressions to match patterns. ```ts // Only allow specific tags { "rules": { "playwright/valid-test-tags": ["error", { "allowedTags": ["@e2e", "@regression"] }] } } // Allow tags matching a pattern { "rules": { "playwright/valid-test-tags": ["error", { "allowedTags": ["@e2e", /^@my-tag-\d+$/] }] } } ``` ### `disallowedTags` When specified, the listed tags are not allowed. You can use either exact strings or regular expressions to match patterns. ```ts // Disallow specific tags { "rules": { "playwright/valid-test-tags": ["error", { "disallowedTags": ["@skip", "@todo"] }] } } // Disallow tags matching a pattern { "rules": { "playwright/valid-test-tags": ["error", { "disallowedTags": ["@skip", /^@temp-/] }] } } ``` Note: You cannot use both `allowedTags` and `disallowedTags` together. Choose one approach based on your needs. ## Further Reading - [Playwright Test Tags Documentation](https://playwright.dev/docs/test-annotations#tag-tests) - [Playwright Test Annotations Documentation](https://playwright.dev/docs/test-annotations) --- # Enforce valid titles (`valid-title`) Checks that the title of test blocks are valid by ensuring that titles are: - not empty, - is a string, - not prefixed with their block name, - have no leading or trailing spaces ## Rule details ### `emptyTitle` An empty title is not informative, and serves little purpose. Examples of **incorrect** code for this rule: ```javascript test.describe('', () => {}) test.describe('foo', () => { test('', () => {}) }) test('', () => {}) ``` Examples of **correct** code for this rule: ```javascript test.describe('foo', () => {}) test.describe('foo', () => { test('bar', () => {}) }) test('foo', () => {}) ``` ### `titleMustBeString` Titles for `describe` and `test` blocks should always be a string; you can disable this with the `ignoreTypeOfDescribeName` and `ignoreTypeOfTestName` options. Examples of **incorrect** code for this rule: ```javascript test(123, () => {}) test.describe(String(/.+/), () => {}) test.describe(myFunction, () => {}) test.describe(6, function () {}) const title = 123 test(title, () => {}) ``` Examples of **correct** code for this rule: ```javascript test('is a string', () => {}) test.describe('is a string', () => {}) const title = 'is a string' test(title, () => {}) ``` Examples of **correct** code when `ignoreTypeOfDescribeName` is `true`: ```javascript test('is a string', () => {}) test.describe('is a string', () => {}) test.describe(String(/.+/), () => {}) test.describe(myFunction, () => {}) test.describe(6, function () {}) ``` Examples of **correct** code when `ignoreTypeOfTestName` is `true`: ```javascript const myTestName = 'is a string' test(String(/.+/), () => {}) test(myFunction, () => {}) test(myTestName, () => {}) test(6, function () {}) ``` ### `duplicatePrefix` A `describe` / `test` block should not start with `duplicatePrefix` Examples of **incorrect** code for this rule ```javascript test('test foo', () => {}) test.describe('foo', () => { test('test bar', () => {}) }) test.describe('describe foo', () => { test('bar', () => {}) }) ``` Examples of **correct** code for this rule ```javascript test('foo', () => {}) test.describe('foo', () => { test('bar', () => {}) }) ``` ### `accidentalSpace` A `describe` / `test` block should not contain accidentalSpace, but can be turned off via the `ignoreSpaces` option: Examples of **incorrect** code for this rule ```javascript test(' foo', () => {}) test.describe('foo', () => { test(' bar', () => {}) }) test.describe(' foo', () => { test('bar', () => {}) }) test.describe('foo ', () => { test('bar', () => {}) }) ``` Examples of **correct** code for this rule ```javascript test('foo', () => {}) test.describe('foo', () => { test('bar', () => {}) }) ``` ## Options ```ts interface Options { ignoreSpaces?: boolean ignoreTypeOfStepName?: boolean ignoreTypeOfTestName?: boolean ignoreTypeOfDescribeName?: boolean disallowedWords?: string[] mustNotMatch?: Partial> | string mustMatch?: Partial> | string } ``` #### `ignoreSpaces` Default: `false` When enabled, the leading and trailing spaces won't be checked. #### `ignoreTypeOfStepName` Default: `true` When enabled, the type of the first argument to `test.step` blocks won't be checked. #### `ignoreTypeOfDescribeName` Default: `false` When enabled, the type of the first argument to `describe` blocks won't be checked. #### `disallowedWords` Default: `[]` A string array of words that are not allowed to be used in test titles. Matching is not case-sensitive, and looks for complete words: Examples of **incorrect** code when using `disallowedWords`: ```javascript // with disallowedWords: ['correct', 'all', 'every', 'properly'] test.describe('the correct way to do things', () => {}) test.describe('every single one of them', () => {}) test('has ALL the things', () => {}) test(`that the value is set properly`, () => {}) ``` Examples of **correct** code when using `disallowedWords`: ```javascript // with disallowedWords: ['correct', 'all', 'every', 'properly'] test('correctly sets the value', () => {}) test('that everything is as it should be', () => {}) test.describe('the proper way to handle things', () => {}) ``` #### `mustMatch` & `mustNotMatch` Defaults: `{}` Allows enforcing that titles must match or must not match a given Regular Expression, with an optional message. An object can be provided to apply different Regular Expressions (with optional messages) to specific Playwright test function groups (`describe`, `test`). Examples of **incorrect** code when using `mustMatch`: ```javascript // with mustMatch: '^that' test.describe('the correct way to do things', () => {}) test('this there!', () => {}) // with mustMatch: { test: '^that' } test.describe('the tests that will be run', () => {}) test('the stuff works', () => {}) test('errors that are thrown have messages', () => {}) ``` Examples of **correct** code when using `mustMatch`: ```javascript // with mustMatch: '^that' test.describe('that thing that needs to be done', () => {}) test('that this there!', () => {}) // with mustMatch: { test: '^that' } test.describe('the tests will be run', () => {}) test('that the stuff works', () => {}) ``` Optionally you can provide a custom message to show for a particular matcher by using a tuple at any level where you can provide a matcher: ```javascript const prefixes = ['when', 'with', 'without', 'if', 'unless', 'for'] const prefixesList = prefixes.join(' - \n') module.exports = { rules: { 'playwright/valid-title': [ 'error', { mustNotMatch: ['\\.$', 'Titles should not end with a full-stop'], mustMatch: { describe: [ new RegExp(`^(?:[A-Z]|\\b(${prefixes.join('|')})\\b`, 'u').source, `Describe titles should either start with a capital letter or one of the following prefixes: ${prefixesList}`, ], test: /[^A-Z]/u.source, }, }, ], }, } ```