seed-skills/eslint-testing/SKILL.md
Enforce test quality with ESLint - eslint-plugin-jest, eslint-plugin-playwright, and eslint-plugin-testing-library rules in flat config, blocking focused tests, missing assertions, and flaky waits via a CI lint gate.
npx skillsauth add PramodDutta/qaskills ESLint for Test QualityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent wire ESLint plugins that lint the tests themselves - catching focused tests that silently skip entire suites in CI, assertion-free tests, conditional expects, and flaky waitForTimeout calls before they merge. Trigger it when a project has test files but no test-specific lint rules, when an it.only ever reaches main, or when the user asks to "lint tests", "block .only", or "enforce testing best practices".
.only is a silent CI outage. it.only makes every other test in the file stop running while the build stays green. no-focused-tests set to error is the single highest-value test lint rule; it is non-negotiable.expect-expect (Jest/Playwright) turns these into lint errors and accepts custom assertion wrappers via configuration.files: ['**/*.test.ts'] globs in flat config so production code is not subjected to test rules and vice versa.--max-warnings 0 or set every rule you care about to error. A warning that scrolls by in CI logs changes nothing.flat/recommended for each plugin, then promote the high-signal rules (no-conditional-expect, no-standalone-expect, prefer-user-event) to error as the suite cleans up.npm install --save-dev eslint eslint-plugin-jest eslint-plugin-testing-library \
eslint-plugin-jest-dom eslint-plugin-playwright
// eslint.config.js
import jest from 'eslint-plugin-jest';
import testingLibrary from 'eslint-plugin-testing-library';
import jestDom from 'eslint-plugin-jest-dom';
import playwright from 'eslint-plugin-playwright';
export default [
// Unit and component tests (Jest + Testing Library)
{
files: ['src/**/*.test.{ts,tsx}', 'src/**/__tests__/**/*.{ts,tsx}'],
plugins: { jest, 'testing-library': testingLibrary, 'jest-dom': jestDom },
languageOptions: { globals: jest.environments.globals.globals },
rules: {
...jest.configs['flat/recommended'].rules,
...testingLibrary.configs['flat/react'].rules,
...jestDom.configs['flat/recommended'].rules,
'jest/no-focused-tests': 'error',
'jest/no-disabled-tests': 'warn',
'jest/no-conditional-expect': 'error',
'jest/no-standalone-expect': 'error',
'jest/valid-title': 'error',
'jest/prefer-hooks-on-top': 'error',
'jest/expect-expect': [
'error',
{ assertFunctionNames: ['expect', 'expectTypeOf', 'assertOrderShape'] },
],
'testing-library/prefer-user-event': 'error',
'testing-library/no-wait-for-side-effects': 'error',
'testing-library/no-manual-cleanup': 'error',
},
},
// Playwright E2E specs
{
files: ['e2e/**/*.spec.ts'],
plugins: { playwright },
rules: {
...playwright.configs['flat/recommended'].rules,
'playwright/no-focused-test': 'error',
'playwright/no-skipped-test': 'warn',
'playwright/no-wait-for-timeout': 'error',
'playwright/no-conditional-in-test': 'error',
'playwright/no-force-option': 'error',
'playwright/expect-expect': 'error',
'playwright/no-networkidle': 'error',
'playwright/prefer-web-first-assertions': 'error',
},
},
];
// e2e/checkout.spec.ts - every line below is a lint ERROR with the config above
test.only('applies a coupon', async ({ page }) => {
// playwright/no-focused-test: the other 84 specs in this project
// would silently not run in CI while the build stays green.
});
test('waits for the cart to update', async ({ page }) => {
await page.waitForTimeout(3000); // playwright/no-wait-for-timeout: flaky AND slow
await page.click('#checkout', { force: true }); // playwright/no-force-option: bypasses actionability
});
test('shows totals', async ({ page }) => {
const rows = await page.locator('.row').count();
if (rows > 0) {
expect(rows).toBeGreaterThan(0); // playwright/no-conditional-in-test
}
// playwright/expect-expect also fires if no assertion is reachable
});
// src/components/CouponForm.test.tsx - Jest + Testing Library violations
it('test 1', async () => {
// jest/valid-title: meaningless title
render(<CouponForm />);
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'SAVE10' } });
// testing-library/prefer-user-event: fireEvent skips focus/keyboard semantics
});
it('submits the coupon', async () => {
try {
await submitCoupon('SAVE10');
expect(api.apply).toHaveBeenCalled();
} catch {
expect(true).toBe(false); // jest/no-conditional-expect: may never run
}
});
// tests/helpers/assert-order-shape.ts
import { expect } from 'vitest';
// Custom assertion helper used across suites
export function assertOrderShape(order: unknown): void {
expect(order).toMatchObject({
id: expect.stringMatching(/^ord_/),
total: expect.any(Number),
items: expect.arrayContaining([expect.objectContaining({ sku: expect.any(String) })]),
});
}
// Registered above via expect-expect assertFunctionNames so tests
// that only call assertOrderShape(...) do not trip the rule.
# .github/workflows/lint.yml
name: lint
on: [pull_request]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Lint (zero warnings allowed)
run: npx eslint . --max-warnings 0
npm install --save-dev husky lint-staged
npx husky init
{
"lint-staged": {
"*.{ts,tsx,js}": ["eslint --fix --max-warnings 0"]
}
}
# .husky/pre-commit
npx lint-staged
eslint --fix first when adopting rules on an existing suite; many testing-library rules (such as query preferences) auto-fix.jest/no-disabled-tests as warn with a tracking convention: every it.skip requires a linked ticket in a comment.playwright/no-networkidle and prefer-web-first-assertions early; they remove the two most common Playwright flake sources.eslint-plugin-vitest (largely rule-compatible with eslint-plugin-jest) and the same scoping strategy.npx eslint . --max-warnings 0 locally before pushing; the gate exists to be unreachable, not to be hit.no-focused-tests to warn: the one severity that cannot stop the exact accident the rule exists for.// eslint-disable-next-line comments without a reason; require --report-unused-disable-directives (or linterOptions.reportUnusedDisableDirectives) to keep disables honest..only from last month - run the full lint, it is cheap.eslint-plugin-jest already ships; check the plugin's rule list first.eslint.config.js contains no test-specific plugins.it.only, test.only, or fdescribe was found on the main branch, or CI passed while most tests silently did not run..eslintrc to flat config and the test-file overrides need translating into files-scoped config objects.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.