Install
$ agentstack add skill-bensheridanedwards-architectplaybook-testing-audit ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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:
- 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.
- 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. - 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):
getByRole— the most reliable; queries elements exposed in the accessibility tree. Use with thenameoption:getByRole('button', { name: /submit/i }).getByLabelText— the right tool for form fields. Mirrors how users navigate forms.getByPlaceholderText— when a label isn't available. A placeholder is not a substitute for a label.getByText— for non-interactive elements; how users find content outside forms.getByDisplayValue— useful for navigating pages with pre-filled form values.
Priority 2 — Semantic queries (variable user experience):
getByAltText— for elements supportingalt(img, area, input, custom elements).getByTitle— least reliable in this tier; thetitleattribute is not consistently read by screen readers and is not visible by default for sighted users.
Priority 3 — Test IDs (last resort):
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
getByRolebroken out specifically. userEventvsfireEventusage ratio.getByTestIdandcontainer.querySelectorusage counts and the top files for each.- Coverage data when
--with-run: line, statement, branch, and function coverage. - Flaky-pattern signal counts: fixed
setTimeout/setIntervalwaits 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
- Source: BenSheridanEdwards/ArchitectPlaybook
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.