web-testing-react-testing-library-08ba36 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-testing-react-testing-library-08ba36 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Query through the accessibility tree (getByRolefirst,getByTestIdlast), drive interactions withuserEventrather thanfireEvent, and reach forfindBy*whenever the element arrives asynchronously.userEventhas been async andsetup()-first since v14, so every call is awaited. The library renders and interacts; the assertion API, mock functions and fake timers come from whatever test runner is executing the file.
Detailed Resources:
findBy*, waitFor, waitForElementToBeRemovedrenderHook, rerender, unmount, wrappers, hooks that throwwithin() for tables, cards, sections and modalsexamples/core.md and examples/user-events.md are the path.
renderHook exposes result.current;examples/hooks.md. Where the hook only makes sense inside its UI, test the component instead.
per test; examples/custom-render.md.
<critical_requirements>
Reach for queries in accessibility order — `getByRole`, then `getByLabelText`, then `getByText`, and `getByTestId` only when nothing else reaches the element. A query that has to fall back is telling you the element is unreachable to assistive technology too.
Call `userEvent.setup()` before the first interaction and await every method. Since v14 the methods return promises, so an un-awaited call lets assertions run against a DOM that has not finished updating.
*Use `findBy for anything that arrives asynchronously.** It retries and reports the DOM it gave up on, where waitFor wrapped around getBy*` reports only the last thrown error.
Query through `screen` rather than the object `render` returns. One import serves every query, and the destructured form drifts out of step as a test grows.
Assert on what the user can see or operate. Internal state, refs and instance fields change under a refactor that keeps the behaviour identical.
</critical_requirements>
Auto-detection: @testing-library/react, @testing-library/user-event, @testing-library/dom, render, screen, userEvent.setup, fireEvent, waitFor, waitForElementToBeRemoved, findByRole, getByRole, getByLabelText, queryByRole, renderHook, result.current, within, cleanup, prettyDOM, logRoles, logTestingPlaygroundURL, testIdAttribute, asyncUtilTimeout
Applies to:
Handled elsewhere:
describe/it, the assertion API, mock functions, fake timersand the setup file are the runner's; this skill only names the seam where an option must be handed the runner's timer-advance function
by whatever intercepts them
<philosophy>
The library's own guiding principle: the more a test resembles the way the software is used, the more confidence it gives. Three consequences follow.
Queries are an accessibility audit that happens to be a test. A component reachable by getByRole("button", { name: /submit/i }) is reachable by a screen reader for the same reason. When the only query that works is getByTestId, the test has just reported a defect in the UI rather than in itself.
Implementation details are whatever survives a refactor unchanged. State shape, hook internals, class names and DOM nesting are all free to move; rendered text, roles and accessible names are the contract.
The DOM is real. Components render into a real document, so integration bugs between a component and its children surface here rather than in production.
</philosophy>
<patterns>
Pick the query by how a user would find the element, not by what is convenient in the markup.
screen.getByRole("button", { name: /sign in/i }); // interactive elements
screen.getByLabelText(/password/i); // form fields, incl. password inputs (no textbox role)
screen.getByText(/welcome back/i); // non-interactive content
screen.getByDisplayValue("draft title"); // a field by its current value
screen.getByAltText(/profile photo/i); // images
screen.getByTestId(`product-${generatedId}`); // last resort: genuinely generated contentFull code: examples/core.md
fireEvent dispatches one DOM event. userEvent replays the sequence a real interaction produces — pointer, focus and keyboard events in order — which is where handler bugs actually live.
import userEvent from "@testing-library/user-event";
const user = userEvent.setup(); // before the first interaction
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.click(screen.getByRole("button", { name: /create account/i }));Reach for fireEvent only for events a user does not originate — resize, scroll, a synthetic event carrying properties you need to control.
Full code: examples/user-events.md
findBy* when the element appears; waitFor only when the thing you are waiting on is an assertion rather than an element; waitForElementToBeRemoved when it disappears.
expect(await screen.findByText(/results/i)).toBeInTheDocument();
await waitFor(() => {
expect(handler).toHaveBeenCalledWith("react"); // one assertion, no side effects
});
await waitForElementToBeRemoved(() => screen.queryByRole("progressbar"));waitFor polls until its callback stops throwing, so a second assertion inside it pays the full retry cost before the first failure is reported, and a side effect inside it runs on every poll.
Full code: examples/async-testing.md
renderHook mounts a hook without a component. result.current is re-read on every access, so hold the access rather than the value.
const { result, rerender, unmount } = renderHook(
({ key }) => useLocalStorage(key, "initial"),
{ initialProps: { key: "draft" } },
);
act(() => {
result.current[1]("updated");
});
expect(result.current[0]).toBe("updated");Prefer a component test where the hook is inseparable from its UI; renderHook earns its place for hooks you publish, or hooks with edge cases a component would obscure.
Full code: examples/hooks.md
Wrap the app's providers once and re-export, so no test repeats the nesting.
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: AllProviders, ...options });
}
export * from "@testing-library/react";
export { customRender as render }; // shadow the default exportWiden the options type to accept per-test initial state — an auth state, a starting route — so a test states its precondition instead of arranging it.
Full code: examples/custom-render.md
Beyond finding elements, the accessibility tree is where keyboard order, expanded/selected state and announcements are asserted.
await user.tab();
expect(screen.getByRole("combobox")).toHaveFocus();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: /section 1/i })).toHaveAttribute(
"aria-expanded",
"true",
);
expect(await screen.findByRole("alert")).toHaveTextContent(
/email is required/i,
);Full code: examples/accessibility.md
within bounds a query to a container, which is what makes repeated structures testable at all.
const bobRow = screen.getAllByRole("row")[2];
await user.click(within(bobRow).getByRole("button", { name: /edit/i }));
const billing = screen.getByRole("region", { name: /billing address/i });
await user.type(
within(billing).getByLabelText(/street address/i),
"123 Billing St",
);Full code: examples/scoped-queries.md
Three options change behaviour rather than tune it, and all three are set at setup().
const user = userEvent.setup({
advanceTimers: advanceTimersBy, // your runner's timer-advance fn; without it, fake timers hang
delay: null, // skip the inter-event wait
pointerEventsCheck: 0, // interact with an element whose CSS sets pointer-events: none
});Full code: examples/configuration.md
</patterns>
<red_flags>
Breaks at runtime:
userEvent call without await — assertions run against the pre-interaction DOM, and the test passes or fails by timingadvanceTimers to setup() — userEvent's internal delay never resolves and the test hangs until the runner's timeoutuserEvent.click(...) directly instead of on a setup() session — the direct API is deprecated and carries none of the session's pointer or keyboard stategetBy* for an element that arrives asynchronously — throws immediately rather than waitingwaitFor callback — the callback runs on every poll, so the effect fires repeatedlySurprising behaviour:
container.querySelector(".btn-primary") returns null rather than throwing, so the failure surfaces one line later at the assertion instead of at the query — and it matches on the class names and nesting a refactor is free to change, which is the whole thing the query hierarchy exists to avoidqueryBy* returns null instead of throwing, which is what makes it right for absence assertions and wrong for everything elsefindBy* defaults to a 1000ms timeout, and its third argument — not its second — is where that is raisedresult.current is re-read each time it is touched, so a value captured into a local goes stale after the next actrender, fireEvent and userEvent already wrap in act; wrapping them again is noise that also hides genuine act warningsafterEach(cleanup) is redundantwaitFor pay the full retry window before the first failure surfaceswaitFor(() => {}) waits for the next poll tick and nothing else — a sleep wearing a matcher's clothesscreen.debug() truncates at 7000 characters, which is why a debugged form often appears to end mid-element</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.