Skip to content

Commit a98d72d

Browse files
committed
feat(lint): add playwright rules
1 parent edeba24 commit a98d72d

File tree

6 files changed

+1588
-7
lines changed

6 files changed

+1588
-7
lines changed

docs/style-guide.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,82 @@ test('every button click updates display', () => {
19811981
})
19821982
```
19831983

1984+
### Use Appropriate Queries
1985+
1986+
Follow the query priority order and avoid using container queries:
1987+
1988+
```tsx
1989+
// ✅ Good
1990+
screen.getByRole('textbox', { name: /username/i })
1991+
1992+
// ❌ Avoid
1993+
screen.getByTestId('username')
1994+
container.querySelector('.btn-primary')
1995+
```
1996+
1997+
### Use Query Variants Correctly
1998+
1999+
Only use query\* variants for checking non-existence:
2000+
2001+
```tsx
2002+
// ✅ Good
2003+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
2004+
2005+
// ❌ Avoid
2006+
expect(screen.queryByRole('alert')).toBeInTheDocument()
2007+
```
2008+
2009+
### Use find\* Over waitFor for Elements
2010+
2011+
Use find\* queries instead of waitFor for elements that may not be immediately
2012+
available:
2013+
2014+
```tsx
2015+
// ✅ Good
2016+
const submitButton = await screen.findByRole('button', { name: /submit/i })
2017+
2018+
// ❌ Avoid
2019+
const submitButton = await waitFor(() =>
2020+
screen.getByRole('button', { name: /submit/i }),
2021+
)
2022+
```
2023+
2024+
### Avoid Testing Implementation Details
2025+
2026+
Test components based on how users interact with them, not implementation
2027+
details:
2028+
2029+
```tsx
2030+
// ✅ Good
2031+
test('User can add items to cart', async () => {
2032+
render(<ProductList />)
2033+
await userEvent.click(screen.getByRole('button', { name: /add to cart/i }))
2034+
await expect(screen.getByText(/1 item in cart/i)).toBeInTheDocument()
2035+
})
2036+
2037+
// ❌ Avoid
2038+
test('Cart state updates when addToCart is called', () => {
2039+
const { container } = render(<ProductList />)
2040+
const addButton = container.querySelector('[data-testid="add-button"]')
2041+
fireEvent.click(addButton)
2042+
expect(
2043+
container.querySelector('[data-testid="cart-count"]'),
2044+
).toHaveTextContent('1')
2045+
})
2046+
```
2047+
2048+
### Use userEvent Over fireEvent
2049+
2050+
Use userEvent over fireEvent for more realistic user interactions:
2051+
2052+
```tsx
2053+
// ✅ Good
2054+
await userEvent.type(screen.getByRole('textbox'), 'Hello')
2055+
2056+
// ❌ Avoid
2057+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Hello' } })
2058+
```
2059+
19842060
## Misc
19852061

19862062
### File naming

eslint.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const hasReact = has('react')
1717
const hasTestingLibrary = has('@testing-library/dom')
1818
const hasJestDom = has('@testing-library/jest-dom')
1919
const hasVitest = has('vitest')
20+
const hasPlaywright = has('playwright')
21+
2022
const vitestFiles = ['**/__tests__/**/*', '**/*.test.*', '**/*.spec.*']
2123
const testFiles = ['**/tests/**', '**/#tests/**', ...vitestFiles]
2224
const playwrightFiles = ['**/e2e/**']
@@ -290,9 +292,50 @@ export const config = [
290292
vitest: (await import('@vitest/eslint-plugin')).default,
291293
},
292294
rules: {
295+
'vitest/expect-expect': ERROR,
293296
// you don't want the editor to autofix this, but we do want to be
294297
// made aware of it
295298
'vitest/no-focused-tests': [WARN, { fixable: false }],
299+
'vitest/no-import-node-test': ERROR,
300+
'vitest/no-standalone-expect': ERROR,
301+
'vitest/prefer-comparison-matcher': ERROR,
302+
'vitest/prefer-equality-matcher': ERROR,
303+
'vitest/prefer-to-be': ERROR,
304+
'vitest/prefer-to-contain': ERROR,
305+
'vitest/prefer-to-have-length': ERROR,
306+
'vitest/valid-expect-in-promise': ERROR,
307+
'vitest/valid-expect': ERROR,
308+
},
309+
}
310+
: null,
311+
312+
hasPlaywright
313+
? {
314+
files: ['**/tests/*.ts?(x)', '**/tests/*.js?(x)'],
315+
ignores: testFiles,
316+
plugins: {
317+
playwright: (await import('eslint-plugin-playwright')).default,
318+
},
319+
rules: {
320+
'playwright/expect-expect': ERROR,
321+
'playwright/max-nested-describe': ERROR,
322+
'playwright/missing-playwright-await': ERROR,
323+
'playwright/no-focused-test': WARN,
324+
'playwright/no-page-pause': ERROR,
325+
'playwright/no-raw-locators': [WARN, { allowed: ['iframe'] }],
326+
'playwright/no-slowed-test': ERROR,
327+
'playwright/no-standalone-expect': ERROR,
328+
'playwright/no-unsafe-references': ERROR,
329+
'playwright/prefer-comparison-matcher': ERROR,
330+
'playwright/prefer-equality-matcher': ERROR,
331+
'playwright/prefer-native-locators': ERROR,
332+
'playwright/prefer-to-be': ERROR,
333+
'playwright/prefer-to-contain': ERROR,
334+
'playwright/prefer-to-have-count': ERROR,
335+
'playwright/prefer-to-have-length': ERROR,
336+
'playwright/prefer-web-first-assertions': ERROR,
337+
'playwright/valid-expect-in-promise': ERROR,
338+
'playwright/valid-expect': ERROR,
296339
},
297340
}
298341
: null,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
import { expect, test } from 'vitest'
2+
13
export const MockAccordion = () => <div>Accordion</div>
4+
5+
test.skip('hello', () => {
6+
expect(true).toBe(true)
7+
})

fixture/tests/smoke.e2e.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '@playwright/test'
2+
3+
test('hello', async ({ page }) => {
4+
await page.goto('http://localhost:5173')
5+
})

0 commit comments

Comments
 (0)