# Testing Audit

> Audit React tests against Testing Library query priority and well-known React Testing Library pitfalls. Static-first with optional --with-run coverage enrichment and implementation plan.

- **Type:** Skill
- **Install:** `agentstack add skill-bensheridanedwards-architectplaybook-testing-audit`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [BenSheridanEdwards](https://agentstack.voostack.com/s/bensheridanedwards)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [BenSheridanEdwards](https://github.com/BenSheridanEdwards)
- **Source:** https://github.com/BenSheridanEdwards/ArchitectPlaybook/tree/main/testing-audit

## Install

```sh
agentstack add skill-bensheridanedwards-architectplaybook-testing-audit
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# /testing-audit

Audit a TypeScript and React project's tests against an opinionated baseline organised in four layers — **test runner and tooling**, **query priority and selector hygiene**, **interaction and async patterns**, **test design and coverage** — preceded by a diagnostic snapshot. Then offer to generate an implementation plan for the gaps.

The default mental model is React component tests written with `@testing-library/react`, plus end-to-end tests in Playwright or Cypress. Vitest and Jest are both supported as the test runner. Mocha and other runners are out of scope.

## How this differs from neighbouring audits

| Concern | Owner |
| --- | --- |
| Whether tests *run* at every lifecycle stage (pre-commit, pre-push, continuous integration) | `/quality-gates-audit` |
| Whether `eslint-plugin-testing-library` and `eslint-plugin-jest-dom` are *configured* | `/linting-audit` |
| Whether tests *catch errors well* (error boundaries, async error paths) | `/error-handling-audit` |
| **Whether the tests themselves are well-formed**: query priority, async patterns, design, coverage | `/testing-audit` |
| End-to-end accessibility scans (axe in Playwright/Cypress) | `/accessibility-audit` |
| End-to-end performance measurement (Lighthouse CI) | `/performance-audit` |

When a single fix passes multiple audits (for example, configuring `eslint-plugin-testing-library` satisfies both `/linting-audit` and `/testing-audit`), every relevant audit surfaces the same gap so the user sees it once and resolves it once.

## Testing philosophy

This audit is opinionated. Three principles set the tone for everything below:

1. **Test behaviour, not implementation.** Assertions describe what the user perceives — what they see, click, type, hear, and read. Tests do not assert internal state shape, prop names, or specific function call sequences except where those *are* the public behaviour. The implementation plan's top-priority section addresses behaviour-vs-implementation drift before any other layer-4 work.
2. **Snapshots are a smell.** They almost always couple tests to implementation, churn on every harmless refactor, and rarely catch real regressions. Small, intentional, named snapshots are tolerable; large or whole-component snapshots are reported as `violation`. The skill's stated position is that snapshots are the exception, never the default.
3. **Assert against semantic tokens, not utility classes.** `expect(button).toHaveClass('bg-primary')` is resilient to design-system updates. `expect(button).toHaveClass('bg-gray-100')` breaks the moment the theme changes — and worse, it's testing what colour the button is rather than what the button is. Better still: assert on role, label, or text and skip the class assertion entirely.

These principles map directly to checks in Layer 4. They are not aspirations; they are how the audit grades.

## The query priority ladder

Layer 2's checks grade tests against this canonical Testing Library priority. It is reproduced here verbatim so the audit's stance is unmistakable.

**Priority 1 — Accessible to everyone (preferred):**

1. `getByRole` — the most reliable; queries elements exposed in the accessibility tree. Use with the `name` option: `getByRole('button', { name: /submit/i })`.
2. `getByLabelText` — the right tool for form fields. Mirrors how users navigate forms.
3. `getByPlaceholderText` — when a label isn't available. A placeholder is not a substitute for a label.
4. `getByText` — for non-interactive elements; how users find content outside forms.
5. `getByDisplayValue` — useful for navigating pages with pre-filled form values.

**Priority 2 — Semantic queries (variable user experience):**

1. `getByAltText` — for elements supporting `alt` (img, area, input, custom elements).
2. `getByTitle` — least reliable in this tier; the `title` attribute is not consistently read by screen readers and is not visible by default for sighted users.

**Priority 3 — Test IDs (last resort):**

1. `getByTestId` — the user cannot see or hear these. Use only when semantic matching is not feasible.

A healthy test suite sits heavily in Priority 1. The audit measures the distribution and flags codebases that lean on Priority 3 or fall back to `container.querySelector`.

## Static-first design with optional run enrichment

This skill is read-only. Two modes:

- **Static (default).** Read configuration files, source files, and test files. Pattern-detect query usage, async patterns, mocking shapes, structural test design, and styling-assertion hygiene.
- **Static plus opt-in `--with-run`.** Invoke the detected test runner in coverage mode (`vitest run --coverage --reporter=json`, `jest --coverage --json`) and parse the output. The coverage data feeds the diagnostic snapshot and enriches a small number of run-required checks (real coverage threshold verification, real test-count breakdown).

The skill **never modifies any test, configuration, or source file**, and **never runs Playwright or Cypress end-to-end suites in `--with-run`**. End-to-end runs serve real browsers and have side effects; that is a separate concern and the user's call.

## Usage

```
/testing-audit                                    # default: concise Top 5 + full report saved + ask about plan
/testing-audit --worktree                          # create an isolated Git worktree, then run the audit there
/testing-audit --learn                            # mid-level engineer teaching mode (detailed explanations + file/line examples)
/testing-audit --teach                            # alias for --learn
/testing-audit --with-run                         # static plus enrichment from Vitest/Jest coverage run
/testing-audit --threshold-priority-one-ratio=80  # override default 70 (percent)
/testing-audit --threshold-testid-ratio=5         # override default 10 (percent, ceiling)
/testing-audit --threshold-by-role-ratio=60       # override default 50 (percent of Priority 1)
/testing-audit --threshold-user-event-ratio=90    # override default 80 (percent)
/testing-audit --threshold-snapshot-lines=50      # override default 100 (lines, ceiling for partial)
```

**💡 Pro tip**: Add `--worktree` to run this audit in an isolated Git worktree.

The skill never accepts `--apply`. The implementation plan is descriptive Markdown.

**💡 Pro tip**: Run `/preflight --audit=testing` first to detect — and optionally install — the development dependency that makes `--with-run` useful (`vitest` or `jest`, with their coverage configuration in place). Skip if you already know the tooling is wired up.

## The opinionated baseline

A check resolves to one of four statuses:

- **present** — the invariant holds.
- **partial** — most signals resolve, with a small number of exceptions, or the codebase shows mixed adherence to a soft check.
- **missing** — a structural prerequisite is absent (no test runner installed, for example).
- **violation** — the audit identified concrete code that breaks the invariant.

Layer 0 is informational only and has no status.

### Layer 0 — Diagnostic snapshot (always written, no pass/fail)

- Detected test runner: Vitest (with version), Jest (with version), or none.
- Detected component testing library: `@testing-library/react`, `@testing-library/preact`, none.
- Detected end-to-end framework: Playwright, Cypress, none.
- Test file count and total test count (test count requires `--with-run`).
- **Query usage distribution** across the priority ladder: counts and percentages per tier, with `getByRole` broken out specifically.
- `userEvent` vs `fireEvent` usage ratio.
- `getByTestId` and `container.querySelector` usage counts and the top files for each.
- Coverage data when `--with-run`: line, statement, branch, and function coverage.
- Flaky-pattern signal counts: fixed `setTimeout`/`setInterval` waits in tests, ordering-dependent test patterns, shared mutable test state.
- Snapshot test count and average snapshot size in lines.
- Components without any test file mapping to them (graph-aware when Graphify is present; falls back to per-folder heuristics otherwise).

### Layer 1 — Test runner and tooling

| Check | Expectation | Violation signal |
| --- | --- | --- |
| Single test runner installed | Exactly one of Vitest or Jest in `devDependencies`. | Both present, with no clear migration. |
| `@testing-library/react` installed | When React is detected, the React Testing Library is in `devDependencies`. | Missing in a React project that ships tests. |
| `@testing-library/user-event` installed | Available for interaction simulation. The interaction-and-async layer prefers `userEvent` over `fireEvent`. | Missing. |
| `@testing-library/jest-dom` installed | Provides DOM-aware matchers (`toBeInTheDocument`, `toBeDisabled`, `toHaveClass`) so assertions can describe user-visible state rather than DOM-property internals. | Missing. |
| `eslint-plugin-testing-library` configured | The plugin is installed and enabled. (Overlap with `/linting-audit`; both surface so a single fix passes both.) | Plugin missing or not enabled. |
| `eslint-plugin-jest-dom` configured | The plugin is installed and enabled. (Overlap with `/linting-audit`.) | Plugin missing or not enabled. |
| jest-dom matchers loaded in setup file | The runner's setup file imports `@testing-library/jest-dom`. | No setup file, or setup file does not import jest-dom. |
| Coverage tool configured with thresholds | The test runner has a coverage configuration with explicit thresholds (line, statement, branch, function). | Coverage configured but thresholds absent, or no coverage configuration at all. |
| End-to-end framework present | Playwright or Cypress in `devDependencies`. Soft check — reported as `partial` for projects that legitimately may not need end-to-end tests. | Neither present in a user-facing application. |

### Layer 2 — Query priority and selector hygiene

This layer encodes the priority ladder and the most common selector-related pitfalls.

| Check | Expectation | Violation signal |
| --- | --- | --- |
| Query distribution favours Priority 1 | At least the threshold percentage (default 70%; tunable via `--threshold-priority-one-ratio`) of all `*By*` queries are Priority 1 queries. | Distribution below the threshold. |
| `getByTestId` usage is rare | At most the threshold percentage (default 10%; tunable via `--threshold-testid-ratio`) of all queries use `getByTestId`. | Distribution above the threshold. |
| No `container.querySelector` for finding elements | Tests use Testing Library queries, not `container.querySelector` or `document.querySelector`. | `querySelector` calls in tests for element lookup. |
| Queries via `screen`, not destructured | Queries come from `screen.getBy*`, not destructured from the `render` return value. `screen` works everywhere and keeps debugging tools (`screen.debug()`) consistent. | `const { getByRole } = render(...)` patterns. |
| `render` return named `view`, not `wrapper` | When the render return is captured, it is named `view` (or destructured for what's needed). The render return is not wrapping anything; the `wrapper` name is a holdover from older testing libraries. Soft check — reported as `partial`. | `const wrapper = render(...)`. |
| `*ByRole` is the dominant Priority 1 query | Of the Priority 1 queries, at least the threshold percentage (default 50%; tunable via `--threshold-by-role-ratio`) are `getByRole` (with the `name` option). Soft check — reported as `partial`. | Priority 1 use without meaningful `getByRole` adoption. |
| No redundant ARIA roles in test assertions | Tests do not assert on `role` attributes that are already implicit (``). The principle is the same one `/accessibility-audit` applies to source: do not pile on accessibility attributes that the semantic element already provides. | Test assertions matching `[role="button"]` on a ``. |

### Layer 3 — Interaction and async patterns

This layer encodes the well-known pitfalls in interaction style, async handling, and `waitFor` discipline.

| Check | Expectation | Violation signal |
| --- | --- | --- |
| `userEvent` preferred over `fireEvent` | At least the threshold percentage (default 80%; tunable via `--threshold-user-event-ratio`) of interaction calls use `userEvent` rather than `fireEvent`. `userEvent` simulates the full sequence of events a real user produces (focus, keydown, input, change), where `fireEvent` fires only one. | Ratio below the threshold. |
| `find*` used for elements not yet present | When waiting for an element to appear, tests use `findBy*`, not `waitFor(() => getBy*())`. `findBy*` already retries until a timeout and produces clearer error messages. | `waitFor` callbacks containing only a `getBy*` lookup. |
| `query*` only for absence assertions | `queryBy*` is used only with `not.toBeInTheDocument()` or analogous absence checks. `queryBy*` returns `null` instead of throwing, which is the only behaviour that makes "is this element absent?" assertable; using it for presence assertions silently skips the check. | `queryBy*` used in a positive-presence assertion. |
| `waitFor` callback contains a single assertion | Each `waitFor` call wraps exactly one `expect`. With several assertions in one callback, the first failure causes a re-run of all of them, slowing the suite and obscuring which one actually failed. | `waitFor` callbacks with multiple `expect` calls. |
| `waitFor` callback is not empty | `waitFor(() => {})` followed by an assertion outside is wrong; the assertion belongs inside the callback so `waitFor` knows what it is waiting for. | Empty `waitFor` callbacks. |
| No side effects in `waitFor` | The `waitFor` callback contains only assertions — no `fireEvent`, `userEvent`, or other state mutation. `waitFor` re-runs its callback until it succeeds, so any side effect inside fires repeatedly. | `fireEvent` or `userEvent` calls inside `waitFor`. |
| No unnecessary `act` wrapping | `render` and `fireEvent` calls are not wrapped in `act(...)`; both are already wrapped internally. Hand-rolled `act` only adds noise (and sometimes silences warnings that should have been visible). | `act(() => { render(...) })` or `act(() => { fireEvent.click(...) })` patterns. |
| No manual `cleanup()` calls | Tests do not import or call `cleanup` from `@testing-library/react`. Modern test runners auto-cleanup after each test. | `cleanup` imported or called. |
| Assertions are explicit | Tests do not rely on `getBy*` throwing as the assertion; they wrap with `expect(...).toBeInTheDocument()` (or analogous). The intent of the test should be readable at a glance. | `getBy*` calls appearing on a line by themselves with no `expect`. |

### Layer 4 — Test design and coverage

This layer encodes the testing philosophy stated above.

| Check | Expectation | Violation signal |
| --- | --- | --- |
| Tests describe user behaviour | Test names describe the user-visible behaviour (`it('disables submit while saving', ...)`), not the implementation (`it('calls saveMutation when isLoading is true', ...)`). Heuristic detection but a first-class principle of this audit; the implementation plan addresses drift here before any other layer-4 work. Soft check — reported as `partial` when adherence is mixed. | Test names that describe internal state changes rather than user-observable outcomes. |
| No assertions against hard-coded utility classes | Class assertions use semantic theme tokens (`bg-primary`, `text-brand`, `border-destructive`) — never raw utility classes (`bg-gray-100`, `text-slate-500`). Better still, assert on role, label, or text and skip the class assertion entirely. **Reported as `violation` when raw utility classes appear in `toHaveClass` assertions.** | `expect(...).toHaveClass('bg-gray-100')` (or any utility-class form: numeric Tailwind colour scales, raw spacing utilities like `p-4`, raw layout utilities like `flex`). The audit recognises a token via heuristic: a token has no numeric suffix and matches `[bg|text|border|ring|fill|strok

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [BenSheridanEdwards](https://github.com/BenSheridanEdwards)
- **Source:** [BenSheridanEdwards/ArchitectPlaybook](https://github.com/BenSheridanEdwards/ArchitectPlaybook)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-bensheridanedwards-architectplaybook-testing-audit
- Seller: https://agentstack.voostack.com/s/bensheridanedwards
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
