The query decides how brittle the test is
getByRole("button", { name: "Add to the quote" }) is a sentence about the product. document.querySelector(".btn-primary") is a sentence about the stylesheet — and it goes red the day somebody renames a class, while missing the day the button stops being a button.
This is the same list as the Accessibility course, from the other end: a component that is hard to query by role is usually one that is hard to use with a screen reader.
| Find it by | Survives | Use |
|---|---|---|
| role and name | a redesign, a class rename, a framework change | first choice — it is what a user and a screen reader both see |
| label text | anything but a wording change | form fields |
| text | a wording change | static content |
| test id | everything | the escape hatch, when nothing above identifies it |
| CSS class | nothing | never — a class is styling, not identity |
Act like a user
Reaching into internal state tests the implementation, and it passes when the button that is supposed to trigger it has been disconnected. Dispatch the event the browser would dispatch, then assert on what appeared.
// what a user does
fireEvent.click(getByRole("button", { name: "Add to the quote" }));
fireEvent.input(getByLabelText("Quantity"), { target: { value: "3" } });
// what a user cannot do
component.setState({ added: true });
component.instance().handleAdd();
Assert what is visible
fireEvent.click(getByRole("button", { name: "Add to the quote" }));
expect(getByRole("status")).toHaveTextContent("1 door in the quote");
expect(getByRole("button", { name: "Remove" })).toBeInTheDocument();
A full-markup snapshot goes red on every whitespace change and green on every logic bug. Its failure message is a diff of HTML nobody reads, and the fix is "update the snapshot" — which is how a snapshot suite becomes a rubber stamp. Assert the handful of things that matter.
Waiting, in a DOM
fireEvent.click(getByRole("button", { name: "Load more" }));
// wrong: the fetch has not resolved yet
expect(getAllByRole("listitem")).toHaveLength(6);
// right: poll until it is true, or fail with a readable timeout
await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(6));
// and the negative case has to wait too, or it passes before anything happened
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
What a component test cannot tell you
It renders in a simulated DOM, not a browser: no real layout, no paint, no CSS in most setups, no scrolling, no actual focus ring. A component test proves the logic and the structure; it will never prove that the button is visible, reachable with a finger, or not covered by a banner. That is what the next lesson is for — and it is why a handful of real-browser tests earns its cost.