[{"data":1,"prerenderedAt":324},["ShallowReactive",2],{"topic-react-testing":3},{"framework":4,"topic":15,"subtopics":24},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":8,"stem":12,"tier":13,"__hash__":14},"frameworks\u002Fframeworks\u002Freact.yml","React interview questions on hooks, components, state and rendering — the most-requested front-end framework in technical interviews.","yml","react",{},"React",2,"frameworks\u002Freact",1,"RA6wemU0xFHrA1ZKCABP1J7MireA3Bt_FCJMOuMgsm0",{"id":16,"description":17,"extension":7,"frameworkSlug":8,"meta":18,"name":19,"order":20,"slug":21,"stem":22,"__hash__":23},"topics\u002Ftopics\u002Freact-testing.yml","React Testing Library, component interaction testing, mocking async operations, and testing custom hooks — everything you need to write confident, maintainable tests for React applications.",{},"Testing",8,"testing","topics\u002Freact-testing","3ziCNjEbki2ecb-F-Es314NCtEkSgqlzjnjqmkmE0eM",[25,114,189,257],{"id":26,"title":27,"body":28,"description":32,"difficulty":34,"extension":35,"framework":10,"frameworkSlug":8,"meta":36,"navigation":37,"order":13,"path":38,"questions":39,"questionsCount":106,"related":107,"seo":108,"seoDescription":109,"stem":110,"subtopic":111,"topic":19,"topicSlug":21,"updated":112,"__hash__":113},"qa\u002Freact\u002Ftesting\u002Frtl-basics.md","Rtl Basics",{"type":29,"value":30,"toc":31},"minimark",[],{"title":32,"searchDepth":11,"depth":11,"links":33},"",[],"medium","md",{},true,"\u002Freact\u002Ftesting\u002Frtl-basics",[40,45,49,53,57,61,65,69,73,77,81,85,89,93,97,102],{"id":41,"difficulty":42,"q":43,"a":44},"rtl-philosophy","easy","What is React Testing Library and what is its core testing philosophy?","**React Testing Library (RTL)** is a lightweight testing utility built on\ntop of `@testing-library\u002Fdom`. Its core philosophy comes from a single\nguiding principle:\n\n> *\"The more your tests resemble the way your software is used, the more\n> confidence they can give you.\"* — Kent C. Dodds\n\nThis means RTL deliberately does **not** expose component internals\n(state, instance methods, refs). Instead, every query and assertion goes\nthrough the same DOM the user sees.\n\n```js\n\u002F\u002F ❌ Enzyme-style (tests implementation details)\nwrapper.state('isOpen')          \u002F\u002F couples test to internal state name\nwrapper.instance().handleClick() \u002F\u002F calls method directly, bypassing UI\n\n\u002F\u002F ✅ RTL-style (tests user-visible behavior)\nexpect(screen.queryByRole('dialog')).not.toBeInTheDocument()\nawait userEvent.click(screen.getByRole('button', { name: \u002Fopen\u002Fi }))\nexpect(screen.getByRole('dialog')).toBeInTheDocument()\n```\n\nThe practical consequences:\n- Queries find elements by **role, label, text, or test-id** — things real\n  users and assistive technology rely on.\n- Interactions go through `userEvent` or `fireEvent`, which dispatch real\n  DOM events rather than calling handlers directly.\n- If you can't test it without accessing internals, the API is probably\n  missing something worth exposing.\n\n**Rule of thumb:** If your test would still pass after a complete internal\nrefactor (renaming state, changing the component hierarchy), it's a good\nRTL test.\n",{"id":46,"difficulty":42,"q":47,"a":48},"render-api","What does `render()` do and what does it return?","`render()` mounts a React component into a detached `\u003Cdiv>` that is\nappended to `document.body`, runs all effects and state updates, then\nreturns a bag of query helpers bound to that container.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\n\nconst { getByText, queryByRole, findByLabelText, container, unmount, rerender } =\n  render(\u003CLoginForm \u002F>)\n\n\u002F\u002F Most common keys:\n\u002F\u002F container   — the raw DOM node wrapping the rendered output\n\u002F\u002F unmount()   — tear down the component (called automatically after each test)\n\u002F\u002F rerender()  — re-render with new props\n\u002F\u002F All getBy*\u002FqueryBy*\u002FfindBy* helpers (scoped to this render)\n```\n\nIn practice the bound helpers from the return value are rarely used\ndirectly. The **`screen`** object exports the same queries but always\noperates on `document.body`, which works even if multiple renders exist\nin one test.\n\n```js\nrender(\u003CLoginForm \u002F>)\n\u002F\u002F Prefer screen.* over the returned object\nconst button = screen.getByRole('button', { name: \u002Fsign in\u002Fi })\n```\n\nAfter each test, `@testing-library\u002Freact` calls `cleanup()` automatically\n(via an `afterEach` it registers), which unmounts the component and removes\nthe container from `document.body`.\n\n**Rule of thumb:** Always use `screen.*` for queries and keep the return\nvalue only when you need `rerender`, `unmount`, or `container`.\n",{"id":50,"difficulty":34,"q":51,"a":52},"query-variants","What are the three query variants — `getBy`, `queryBy`, `findBy` — and when should you use each?","RTL provides three prefixes for every query, each with different behavior\non missing elements and async awareness.\n\n| Variant | Not found | Multiple found | Async? |\n|---------|-----------|----------------|--------|\n| `getBy` | throws | throws | No |\n| `queryBy` | returns `null` | throws | No |\n| `findBy` | rejects (Promise) | rejects | Yes (retries until timeout) |\n\n```js\n\u002F\u002F getBy — use when element MUST exist right now\nconst heading = screen.getByRole('heading', { name: \u002Fdashboard\u002Fi })\n\n\u002F\u002F queryBy — use to assert element is ABSENT (toBeNull \u002F not.toBeInTheDocument)\nexpect(screen.queryByText('Error')).not.toBeInTheDocument()\n\n\u002F\u002F findBy — use when element appears AFTER an async operation\nconst item = await screen.findByText('Data loaded')  \u002F\u002F polls until found or timeout\n```\n\nAll three have `All` variants (`getAllBy`, `queryAllBy`, `findAllBy`) that\nreturn arrays and only throw when *no* elements are found (for `getAllBy`\nand `findAllBy`).\n\nCommon mistakes:\n- Using `getBy` to assert absence — it throws before you can check.\n- Forgetting `await` on `findBy` — returns a Promise, not the element.\n- Using `findBy` when the element is already there — wastes the polling\n  overhead.\n\n**Rule of thumb:** Get → exists now. Query → might not exist. Find → will\nexist soon.\n",{"id":54,"difficulty":42,"q":55,"a":56},"screen-object","What is `screen` and why is it preferred over the queries returned by `render()`?","`screen` is a re-export from `@testing-library\u002Fdom` that exposes the same\nfull query set but always targets `document.body` rather than a scoped\ncontainer.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\n\nrender(\u003CApp \u002F>)\n\n\u002F\u002F Both work, but screen.* is preferred\nconst { getByRole } = render(\u003CApp \u002F>)   \u002F\u002F ❌ scoped to this render's container\nscreen.getByRole('navigation')           \u002F\u002F ✅ searches all of document.body\n```\n\nAdvantages of `screen`:\n1. **No destructuring** — one import, always available.\n2. **Works across renders** — if a component portals into `document.body`\n   outside the container, `screen` still finds it.\n3. **`screen.debug()`** — prints the current DOM to the console for\n   quick inspection without needing the return value.\n\n```js\nscreen.debug()                     \u002F\u002F prints full document.body\nscreen.debug(screen.getByRole('dialog'))  \u002F\u002F prints just the dialog node\n```\n\n**Rule of thumb:** Always import and use `screen`; only keep the\n`render()` return value when you need `rerender`, `unmount`, or the raw\n`container` node.\n",{"id":58,"difficulty":34,"q":59,"a":60},"query-priority","In what priority order should you choose RTL queries, and why?","RTL's official priority list (from the docs) guides you toward queries\nthat reflect how real users and accessibility tools discover elements:\n\n1. **By role** (`getByRole`) — matches ARIA roles; works for buttons,\n   headings, links, inputs, dialogs, etc. Preferred for almost everything.\n2. **By label** (`getByLabelText`) — finds the form control associated\n   with a `\u003Clabel>`. Best for inputs, selects, textareas.\n3. **By placeholder** (`getByPlaceholderText`) — fallback when there is\n   no visible label.\n4. **By text** (`getByText`) — finds nodes by their text content.\n   Good for paragraphs, list items, non-interactive elements.\n5. **By display value** (`getByDisplayValue`) — current value of an\n   input\u002Fselect\u002Ftextarea.\n6. **By alt text** (`getByAltText`) — for images.\n7. **By title** (`getByTitle`) — `title` attribute.\n8. **By test id** (`getByTestId`) — only when nothing above is practical;\n   requires adding `data-testid` attributes to the DOM.\n\n```js\n\u002F\u002F ✅ Tier 1 — most accessible, most like how a user would find it\nscreen.getByRole('button', { name: \u002Fsubmit\u002Fi })\nscreen.getByRole('textbox', { name: \u002Femail\u002Fi })\n\n\u002F\u002F ✅ Tier 2 — great for forms with proper labels\nscreen.getByLabelText(\u002Fpassword\u002Fi)\n\n\u002F\u002F ⚠️ Last resort — opaque, requires markup change\nscreen.getByTestId('submit-btn')\n```\n\nThe deeper reason: queries that would break if you changed a class name\nor state field are brittle. Queries that break only when the visible UI\nchanges are meaningful.\n\n**Rule of thumb:** Reach for `getByRole` first; add `aria-label` or\n`aria-labelledby` to make otherwise unlabelled elements queryable before\nfalling back to `data-testid`.\n",{"id":62,"difficulty":34,"q":63,"a":64},"user-event-vs-fire-event","What is the difference between `userEvent` and `fireEvent`?","Both simulate user interactions, but at very different levels of fidelity.\n\n**`fireEvent`** dispatches a single synthetic DOM event and returns\nimmediately. It is synchronous and low-level.\n\n**`userEvent`** (from `@testing-library\u002Fuser-event`) simulates the *full\nsequence* of events a real user triggers: pointer down, pointer up, focus,\nkeydown, input, change, click, blur, etc. Since v14 it is async.\n\n```js\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { fireEvent, render, screen } from '@testing-library\u002Freact'\n\nrender(\u003CSearchBox \u002F>)\n\n\u002F\u002F fireEvent — fires one 'change' event, skips pointer\u002Fkeyboard events\nfireEvent.change(screen.getByRole('textbox'), { target: { value: 'react' } })\n\n\u002F\u002F userEvent — types character by character, fires keydown\u002Fkeypress\u002Finput\u002Fkeyup per char\nconst user = userEvent.setup()\nawait user.type(screen.getByRole('textbox'), 'react')\n```\n\nWhen to use each:\n- **userEvent** for anything the user actually does: typing, clicking,\n  selecting, hovering, tabbing. It is the right default.\n- **fireEvent** when you need to simulate an event that userEvent does not\n  support, or in unit tests where you want a specific event without the\n  full browser sequence.\n\nSetup pattern (v14+):\n```js\n\u002F\u002F Call setup once per test, not inside loops\nconst user = userEvent.setup()\nawait user.click(button)\nawait user.keyboard('{Enter}')\n```\n\n**Rule of thumb:** Default to `userEvent`; use `fireEvent` only when\n`userEvent` doesn't cover the event you need.\n",{"id":66,"difficulty":34,"q":67,"a":68},"waitfor","What is `waitFor` and when should you use it?","`waitFor` repeatedly executes a callback until it stops throwing (or the\ntimeout expires). It is used when an assertion depends on something that\nhasn't happened yet — a state update, an effect, or an async API call.\n\n```js\nimport { render, screen, waitFor } from '@testing-library\u002Freact'\n\ntest('shows success message after save', async () => {\n  render(\u003CSaveButton \u002F>)\n  await userEvent.setup().click(screen.getByRole('button', { name: \u002Fsave\u002Fi }))\n\n  \u002F\u002F The success message appears after a state update triggered by an API call\n  await waitFor(() => {\n    expect(screen.getByText('Saved!')).toBeInTheDocument()\n  })\n})\n```\n\nKey options:\n```js\nawait waitFor(callback, {\n  timeout: 3000,   \u002F\u002F default 1000 ms\n  interval: 50,    \u002F\u002F polling interval, default 50 ms\n})\n```\n\n`waitFor` vs `findBy*`:\n- `findBy*` is syntactic sugar for `waitFor(() => getBy*(...))` — use it\n  when you're waiting for a single element to appear.\n- `waitFor` is for more complex assertions (multiple elements, negative\n  assertions, or checking a value, not just presence).\n\nCommon mistake — putting multiple assertions in one `waitFor`:\n```js\n\u002F\u002F ❌ If the first passes then the second fails, waitFor retries the whole block\nawait waitFor(() => {\n  expect(a).toBeInTheDocument()\n  expect(b).toBeInTheDocument()  \u002F\u002F a keeps re-asserting, masking failures\n})\n\n\u002F\u002F ✅ Wait for the first, then assert synchronously\nawait screen.findByText('A')\nexpect(screen.getByText('B')).toBeInTheDocument()\n```\n\n**Rule of thumb:** Use `findBy*` for single async element appearances;\nuse `waitFor` for complex multi-assertion async scenarios.\n",{"id":70,"difficulty":42,"q":71,"a":72},"jest-dom-matchers","What does `@testing-library\u002Fjest-dom` provide and what are the most useful matchers?","`@testing-library\u002Fjest-dom` extends Jest\u002FVitest's `expect` with custom\nDOM matchers that produce clearer error messages than raw Jest assertions.\n\nSetup (once in your test setup file):\n```js\n\u002F\u002F vitest.config.ts \u002F jest.setup.ts\nimport '@testing-library\u002Fjest-dom'\n\u002F\u002F or\nimport '@testing-library\u002Fjest-dom\u002Fvitest'\n```\n\nMost useful matchers:\n\n```js\n\u002F\u002F Presence\nexpect(el).toBeInTheDocument()\nexpect(el).not.toBeInTheDocument()\n\n\u002F\u002F Visibility\nexpect(el).toBeVisible()           \u002F\u002F not hidden via CSS\nexpect(el).toBeEnabled()           \u002F\u002F not disabled\nexpect(el).toBeDisabled()\n\n\u002F\u002F Content\nexpect(el).toHaveTextContent('Hello')\nexpect(el).toHaveTextContent(\u002Fhello\u002Fi)\n\n\u002F\u002F Form state\nexpect(checkbox).toBeChecked()\nexpect(input).toHaveValue('react')\nexpect(select).toHaveDisplayValue('Option A')\n\n\u002F\u002F Attributes & classes\nexpect(el).toHaveAttribute('href', '\u002Fhome')\nexpect(el).toHaveClass('active')\n\n\u002F\u002F Focus\nexpect(input).toHaveFocus()\n```\n\nWithout jest-dom you'd write `expect(el).not.toBeNull()` instead of\n`expect(el).toBeInTheDocument()` — the latter produces a far more\ndescriptive failure message.\n\n**Rule of thumb:** Always import `@testing-library\u002Fjest-dom` in your\nglobal setup file so every test file gets the matchers automatically.\n",{"id":74,"difficulty":34,"q":75,"a":76},"within","What is `within()` and when is it useful?","`within()` scopes all queries to a specific DOM node, letting you select\nelements inside a particular region when multiple similar elements exist\non the page.\n\n```js\nimport { render, screen, within } from '@testing-library\u002Freact'\n\nrender(\n  \u003Cul>\n    \u003Cli>\n      \u003Cspan>Alice\u003C\u002Fspan>\n      \u003Cbutton>Delete\u003C\u002Fbutton>\n    \u003C\u002Fli>\n    \u003Cli>\n      \u003Cspan>Bob\u003C\u002Fspan>\n      \u003Cbutton>Delete\u003C\u002Fbutton>\n    \u003C\u002Fli>\n  \u003C\u002Ful>\n)\n\n\u002F\u002F Without within — ambiguous: two \"Delete\" buttons exist\n\u002F\u002F screen.getByRole('button', { name: \u002Fdelete\u002Fi }) — throws (multiple found)\n\n\u002F\u002F With within — unambiguous\nconst aliceRow = screen.getByText('Alice').closest('li')\nwithin(aliceRow).getByRole('button', { name: \u002Fdelete\u002Fi }).click()\n```\n\nCommon use cases:\n- Tables with per-row actions\n- Lists where each item has the same repeated controls\n- Modals\u002Fdialogs — scope queries to just the dialog content\n\n```js\nconst dialog = screen.getByRole('dialog')\nexpect(within(dialog).getByRole('heading')).toHaveTextContent('Confirm')\nawait userEvent.setup().click(within(dialog).getByRole('button', { name: \u002Fcancel\u002Fi }))\n```\n\n**Rule of thumb:** When `getBy*` throws because multiple matching elements\nexist, use `within()` to narrow the search to the relevant container.\n",{"id":78,"difficulty":34,"q":79,"a":80},"async-testing-patterns","How do you test a component that fetches data on mount?","The standard pattern: render the component, assert the loading state,\nthen await the resolved state.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { rest } from 'msw'        \u002F\u002F Mock Service Worker (preferred)\nimport { setupServer } from 'msw\u002Fnode'\n\nconst server = setupServer(\n  rest.get('\u002Fapi\u002Fusers', (req, res, ctx) =>\n    res(ctx.json([{ id: 1, name: 'Alice' }]))\n  )\n)\n\nbeforeAll(() => server.listen())\nafterEach(() => server.resetHandlers())\nafterAll(() => server.close())\n\ntest('shows users after fetch', async () => {\n  render(\u003CUserList \u002F>)\n\n  \u002F\u002F Assert loading state first (optional but good practice)\n  expect(screen.getByText(\u002Floading\u002Fi)).toBeInTheDocument()\n\n  \u002F\u002F Wait for data to appear\n  expect(await screen.findByText('Alice')).toBeInTheDocument()\n\n  \u002F\u002F Assert loading indicator is gone\n  expect(screen.queryByText(\u002Floading\u002Fi)).not.toBeInTheDocument()\n})\n\ntest('shows error on fetch failure', async () => {\n  server.use(\n    rest.get('\u002Fapi\u002Fusers', (req, res, ctx) => res(ctx.status(500)))\n  )\n  render(\u003CUserList \u002F>)\n  expect(await screen.findByText(\u002Fsomething went wrong\u002Fi)).toBeInTheDocument()\n})\n```\n\nWithout MSW you can mock `fetch` directly:\n```js\nglobal.fetch = vi.fn().mockResolvedValue({\n  ok: true,\n  json: async () => [{ id: 1, name: 'Alice' }],\n})\n```\n\n**Rule of thumb:** Prefer MSW for HTTP mocking — it intercepts at the\nnetwork level, so the same handlers work in both tests and the browser\nduring development.\n",{"id":82,"difficulty":42,"q":83,"a":84},"setup-vitest","How do you set up React Testing Library with Vitest?","Four steps:\n\n**1. Install dependencies**\n```bash\nnpm install -D vitest @vitest\u002Fui jsdom \\\n  @testing-library\u002Freact @testing-library\u002Fuser-event \\\n  @testing-library\u002Fjest-dom\n```\n\n**2. Configure Vitest (`vitest.config.ts`)**\n```ts\nimport { defineConfig } from 'vitest\u002Fconfig'\nimport react from '@vitejs\u002Fplugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: 'jsdom',          \u002F\u002F simulate the browser DOM\n    setupFiles: ['.\u002Fsrc\u002Ftest\u002Fsetup.ts'],\n  },\n})\n```\n\n**3. Create setup file (`src\u002Ftest\u002Fsetup.ts`)**\n```ts\nimport '@testing-library\u002Fjest-dom\u002Fvitest'   \u002F\u002F custom matchers\nimport { cleanup } from '@testing-library\u002Freact'\nimport { afterEach } from 'vitest'\n\nafterEach(() => cleanup())  \u002F\u002F auto-cleanup (RTL registers this automatically, but explicit is safer)\n```\n\n**4. Write a test**\n```ts\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { describe, it, expect } from 'vitest'\nimport Counter from '.\u002FCounter'\n\ndescribe('Counter', () => {\n  it('increments on click', async () => {\n    render(\u003CCounter \u002F>)\n    await userEvent.setup().click(screen.getByRole('button', { name: \u002Fincrement\u002Fi }))\n    expect(screen.getByText('1')).toBeInTheDocument()\n  })\n})\n```\n\n**Rule of thumb:** Set `environment: 'jsdom'` and import `jest-dom` in\nyour setup file — those two steps cover 90% of RTL configuration.\n",{"id":86,"difficulty":34,"q":87,"a":88},"rerender","How do you test a component that should update when its props change?","Use the `rerender` function returned by `render()`. It re-renders the same\ncomponent with new props without unmounting\u002Fremounting.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\n\nfunction Greeting({ name }) {\n  return \u003Ch1>Hello, {name}!\u003C\u002Fh1>\n}\n\ntest('updates when name prop changes', () => {\n  const { rerender } = render(\u003CGreeting name=\"Alice\" \u002F>)\n  expect(screen.getByRole('heading')).toHaveTextContent('Hello, Alice!')\n\n  rerender(\u003CGreeting name=\"Bob\" \u002F>)\n  expect(screen.getByRole('heading')).toHaveTextContent('Hello, Bob!')\n})\n```\n\n`rerender` is also useful for testing prop-driven animations or conditional\nrendering toggled from outside:\n\n```js\ntest('hides content when visible prop is false', () => {\n  const { rerender } = render(\u003CDrawer visible={true}>\u003Cp>Content\u003C\u002Fp>\u003C\u002FDrawer>)\n  expect(screen.getByText('Content')).toBeVisible()\n\n  rerender(\u003CDrawer visible={false}>\u003Cp>Content\u003C\u002Fp>\u003C\u002FDrawer>)\n  expect(screen.queryByText('Content')).not.toBeInTheDocument()\n})\n```\n\n**Rule of thumb:** Use `rerender` to test prop changes; use `userEvent`\nto test changes driven by user interaction.\n",{"id":90,"difficulty":34,"q":91,"a":92},"wrapping-providers","How do you render a component that requires context providers or a router?","Pass a `wrapper` option to `render()` that wraps the component under test\nwith any necessary providers.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { MemoryRouter } from 'react-router-dom'\nimport { ThemeProvider } from '.\u002FThemeContext'\n\n\u002F\u002F Inline wrapper\nrender(\u003CNavBar \u002F>, {\n  wrapper: ({ children }) => (\n    \u003CMemoryRouter initialEntries={['\u002Fdashboard']}>\n      \u003CThemeProvider theme=\"dark\">\n        {children}\n      \u003C\u002FThemeProvider>\n    \u003C\u002FMemoryRouter>\n  ),\n})\n```\n\nFor reuse across many tests, create a custom `render` helper:\n\n```js\n\u002F\u002F test\u002Futils.tsx\nimport { render } from '@testing-library\u002Freact'\nimport { MemoryRouter } from 'react-router-dom'\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query'\n\nfunction AllProviders({ children }) {\n  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })\n  return (\n    \u003CQueryClientProvider client={client}>\n      \u003CMemoryRouter>{children}\u003C\u002FMemoryRouter>\n    \u003C\u002FQueryClientProvider>\n  )\n}\n\nconst customRender = (ui, options) =>\n  render(ui, { wrapper: AllProviders, ...options })\n\nexport * from '@testing-library\u002Freact'  \u002F\u002F re-export everything\nexport { customRender as render }        \u002F\u002F override render\n```\n\nThen import from `test\u002Futils` instead of `@testing-library\u002Freact` in\nevery test file.\n\n**Rule of thumb:** Create a single `customRender` helper with all app-wide\nproviders so tests don't repeat provider boilerplate.\n",{"id":94,"difficulty":42,"q":95,"a":96},"debug-failing-tests","What tools does RTL provide to debug failing tests?","RTL offers several built-in debugging tools:\n\n**`screen.debug()`** — prints the current DOM state to the console:\n```js\nrender(\u003CComplexForm \u002F>)\nscreen.debug()                          \u002F\u002F prints all of document.body\nscreen.debug(screen.getByRole('form'))  \u002F\u002F prints just that subtree\n```\n\n**`screen.logTestingPlaygroundURL()`** — generates a URL to the\nTesting Playground website pre-loaded with the current DOM, so you can\ninteractively explore which query to use:\n```js\nscreen.logTestingPlaygroundURL()\n\u002F\u002F Logs: https:\u002F\u002Ftesting-playground.com\u002F#markup=...\n```\n\n**`prettyDOM()`** — programmatic version of debug, returns a string:\n```js\nimport { prettyDOM } from '@testing-library\u002Freact'\nconsole.log(prettyDOM(document.body))\nconsole.log(prettyDOM(someElement, 5000))  \u002F\u002F second arg: max length\n```\n\n**Query suggestion in error messages** — when a query fails, RTL v13+\nsuggests alternative queries that would have matched:\n```\nTestingLibraryElementError: Unable to find an accessible element with the\nrole \"button\" and name \"Submit\"\n\nHere are the accessible roles:\n  button:\n    Name \"submit\":\n    \u003Cbutton>submit\u003C\u002Fbutton>  ← hint: case-sensitive name option used\n```\n\n**Rule of thumb:** When a test fails with \"Unable to find element,\" call\n`screen.debug()` immediately to see the actual DOM at that point.\n",{"id":98,"difficulty":99,"q":100,"a":101},"custom-queries","hard","Can you create custom RTL queries, and when would you need one?","Yes. Custom queries follow the same `getBy`\u002F`queryBy`\u002F`findBy` contract\nand are useful when none of the built-in queries fit your domain — for\nexample, querying by a data attribute specific to your component library.\n\n```js\nimport { buildQueries, queryHelpers } from '@testing-library\u002Freact'\n\n\u002F\u002F Step 1 — the raw queryAll function\nconst queryAllByDataCy = (container, id) =>\n  queryHelpers.queryAllByAttribute('data-cy', container, id)\n\n\u002F\u002F Step 2 — build the full query family from queryAll\nconst [\n  queryByDataCy,\n  getAllByDataCy,\n  getByDataCy,\n  findAllByDataCy,\n  findByDataCy,\n] = buildQueries(\n  queryAllByDataCy,\n  (c, id) => `Found multiple elements with data-cy=\"${id}\"`,\n  (c, id) => `Unable to find element with data-cy=\"${id}\"`\n)\n\nexport { queryByDataCy, getAllByDataCy, getByDataCy, findAllByDataCy, findByDataCy }\n```\n\nWire them into a custom render:\n```js\nconst customRender = (ui, options) =>\n  render(ui, {\n    queries: { ...queries, getByDataCy, findByDataCy },\n    ...options,\n  })\n```\n\nIn practice, the need for custom queries is rare. Common triggers:\n- Your component library uses a non-standard attribute as the accessible\n  identifier and you can't change it.\n- You need to query by a complex compound condition.\n\n**Rule of thumb:** Exhaust all built-in queries (especially `getByRole`\nwith `name` and `within`) before writing custom ones — they're extra\nmaintenance overhead.\n",{"id":103,"difficulty":34,"q":104,"a":105},"render-hook","When should you use `renderHook` instead of rendering a component?","`renderHook` is for testing **custom hooks in isolation** — when the hook\nlogic is complex enough to warrant direct tests but you don't want to\ncreate a throwaway wrapper component manually.\n\n```js\nimport { renderHook, act } from '@testing-library\u002Freact'\nimport { useCounter } from '.\u002FuseCounter'\n\ntest('increments the counter', () => {\n  const { result } = renderHook(() => useCounter(0))\n\n  \u002F\u002F result.current holds the hook's return value\n  expect(result.current.count).toBe(0)\n\n  act(() => {\n    result.current.increment()\n  })\n\n  expect(result.current.count).toBe(1)\n})\n```\n\nIt also accepts a `wrapper` for context:\n```js\nconst { result } = renderHook(() => useAuth(), {\n  wrapper: ({ children }) => \u003CAuthProvider>{children}\u003C\u002FAuthProvider>,\n})\n```\n\nWhen **not** to use `renderHook`:\n- When the hook is trivial and already covered by component tests.\n- When you'd rather test the hook through the component that uses it\n  (integration test) — usually the better approach.\n\n**Rule of thumb:** Use `renderHook` when a custom hook encapsulates\nmeaningful logic (state machines, async flows, caching) that deserves\ndirect tests beyond what component-level tests catch.\n",16,null,{"description":32},"React Testing Library interview questions — render, screen, queries, userEvent, waitFor, jest-dom matchers, async testing, and the RTL testing philosophy.","react\u002Ftesting\u002Frtl-basics","RTL Basics","2026-06-24","FQwmpxfpp_kNFH3s3_yIPSe9cgpVhuzelX5XtQbSoOE",{"id":115,"title":116,"body":117,"description":32,"difficulty":34,"extension":35,"framework":10,"frameworkSlug":8,"meta":121,"navigation":37,"order":11,"path":122,"questions":123,"questionsCount":184,"related":107,"seo":185,"seoDescription":186,"stem":187,"subtopic":116,"topic":19,"topicSlug":21,"updated":112,"__hash__":188},"qa\u002Freact\u002Ftesting\u002Fcomponent-interaction-testing.md","Component Interaction Testing",{"type":29,"value":118,"toc":119},[],{"title":32,"searchDepth":11,"depth":11,"links":120},[],{},"\u002Freact\u002Ftesting\u002Fcomponent-interaction-testing",[124,128,132,136,140,144,148,152,156,160,164,168,172,176,180],{"id":125,"difficulty":42,"q":126,"a":127},"user-click-test","How do you test a button click with React Testing Library?","Use `userEvent.click()` from `@testing-library\u002Fuser-event`. It fires the\nfull sequence of pointer and mouse events a real click produces.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\nfunction Counter() {\n  const [count, setCount] = React.useState(0)\n  return (\n    \u003C>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>Increment\u003C\u002Fbutton>\n    \u003C\u002F>\n  )\n}\n\ntest('increments count on click', async () => {\n  const user = userEvent.setup()    \u002F\u002F setup once per test\n  render(\u003CCounter \u002F>)\n\n  expect(screen.getByText('Count: 0')).toBeInTheDocument()\n\n  await user.click(screen.getByRole('button', { name: \u002Fincrement\u002Fi }))\n\n  expect(screen.getByText('Count: 1')).toBeInTheDocument()\n})\n```\n\nKey points:\n- `userEvent.setup()` returns a user instance; create it once per test\n  outside any loops to keep fake timers consistent.\n- `await` every `user.*` call — they're async since user-event v14.\n- Query by role + name so the test doubles as an accessibility check.\n\n**Rule of thumb:** `await user.click()` → assert. One interaction, one\nassertion block. Don't chain multiple un-awaited clicks.\n",{"id":129,"difficulty":34,"q":130,"a":131},"form-submission-test","How do you test form submission?","The most reliable approach is to fill in all fields via `userEvent.type`,\nthen submit via `userEvent.click` on the submit button (or\n`userEvent.keyboard('{Enter}')` if that's how users submit).\n\n```js\nimport { render, screen, waitFor } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\ntest('submits registration form', async () => {\n  const handleSubmit = vi.fn()\n  const user = userEvent.setup()\n\n  render(\u003CRegistrationForm onSubmit={handleSubmit} \u002F>)\n\n  await user.type(screen.getByLabelText(\u002Femail\u002Fi), 'user@example.com')\n  await user.type(screen.getByLabelText(\u002Fpassword\u002Fi), 'secret123')\n  await user.click(screen.getByRole('button', { name: \u002Fregister\u002Fi }))\n\n  await waitFor(() => {\n    expect(handleSubmit).toHaveBeenCalledWith({\n      email: 'user@example.com',\n      password: 'secret123',\n    })\n  })\n})\n```\n\nFor native form submission (no JS handler), you can also listen for the\n`submit` event on the form element:\n```js\nconst handleSubmit = vi.fn(e => e.preventDefault())\nrender(\u003Cform onSubmit={handleSubmit}>\u003Cbutton type=\"submit\">Go\u003C\u002Fbutton>\u003C\u002Fform>)\nawait user.click(screen.getByRole('button', { name: \u002Fgo\u002Fi }))\nexpect(handleSubmit).toHaveBeenCalled()\n```\n\n**Rule of thumb:** Test the *outcome* of submission (callback called,\nsuccess message shown, navigation triggered) rather than testing the\nform's internal state.\n",{"id":133,"difficulty":42,"q":134,"a":135},"controlled-input-test","How do you test a controlled input that updates as the user types?","`userEvent.type()` fires a keydown\u002Fkeypress\u002Finput\u002Fkeyup sequence for\neach character, causing controlled inputs to update through the standard\nReact synthetic event flow.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\nfunction SearchBox() {\n  const [query, setQuery] = React.useState('')\n  return (\n    \u003Cinput\n      aria-label=\"Search\"\n      value={query}\n      onChange={e => setQuery(e.target.value)}\n    \u002F>\n  )\n}\n\ntest('updates value as user types', async () => {\n  const user = userEvent.setup()\n  render(\u003CSearchBox \u002F>)\n\n  const input = screen.getByRole('textbox', { name: \u002Fsearch\u002Fi })\n  await user.type(input, 'react hooks')\n\n  expect(input).toHaveValue('react hooks')\n})\n```\n\nTo clear an existing value before typing, use `user.clear()`:\n```js\nawait user.clear(input)\nawait user.type(input, 'new value')\n```\n\nTo type into a `\u003Cselect>`:\n```js\nawait user.selectOptions(screen.getByRole('combobox'), 'Option B')\nexpect(screen.getByRole('combobox')).toHaveDisplayValue('Option B')\n```\n\n**Rule of thumb:** Prefer `user.type()` over manually setting\n`.value` + dispatching a change event — it tests the full keyboard\ninteraction path.\n",{"id":137,"difficulty":42,"q":138,"a":139},"conditional-rendering-test","How do you test conditional rendering?","Assert presence with `getByRole`\u002F`getByText` and absence with\n`queryBy*` returning `null` or `not.toBeInTheDocument()`.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\nfunction Alert({ type, message }) {\n  if (!message) return null\n  return \u003Cdiv role=\"alert\" className={`alert-${type}`}>{message}\u003C\u002Fdiv>\n}\n\ntest('renders nothing when message is empty', () => {\n  render(\u003CAlert type=\"error\" message=\"\" \u002F>)\n  expect(screen.queryByRole('alert')).not.toBeInTheDocument()\n})\n\ntest('renders error message', () => {\n  render(\u003CAlert type=\"error\" message=\"Something failed\" \u002F>)\n  expect(screen.getByRole('alert')).toHaveTextContent('Something failed')\n})\n```\n\nTesting a toggle:\n```js\ntest('shows\u002Fhides details on button click', async () => {\n  const user = userEvent.setup()\n  render(\u003CExpandableSection title=\"Details\" content=\"Hidden text\" \u002F>)\n\n  expect(screen.queryByText('Hidden text')).not.toBeInTheDocument()\n\n  await user.click(screen.getByRole('button', { name: \u002Fdetails\u002Fi }))\n  expect(screen.getByText('Hidden text')).toBeInTheDocument()\n\n  await user.click(screen.getByRole('button', { name: \u002Fdetails\u002Fi }))\n  expect(screen.queryByText('Hidden text')).not.toBeInTheDocument()\n})\n```\n\n**Rule of thumb:** Always use `queryBy*` (not `getBy*`) when asserting\nan element is absent — `getBy*` throws before your assertion runs.\n",{"id":141,"difficulty":34,"q":142,"a":143},"context-provider-test","How do you test a component that consumes React context?","Wrap the component with the real provider (not a mock) in the `wrapper`\noption or a custom render helper.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { ThemeProvider } from '.\u002FThemeContext'\nimport ThemeToggle from '.\u002FThemeToggle'\n\nfunction renderWithTheme(ui, { theme = 'light' } = {}) {\n  return render(ui, {\n    wrapper: ({ children }) => (\n      \u003CThemeProvider initialTheme={theme}>{children}\u003C\u002FThemeProvider>\n    ),\n  })\n}\n\ntest('switches to dark mode', async () => {\n  const user = userEvent.setup()\n  renderWithTheme(\u003CThemeToggle \u002F>)\n\n  expect(document.body).toHaveClass('light')\n  await user.click(screen.getByRole('button', { name: \u002Fdark mode\u002Fi }))\n  expect(document.body).toHaveClass('dark')\n})\n\ntest('reads initial theme from context', () => {\n  renderWithTheme(\u003CThemeToggle \u002F>, { theme: 'dark' })\n  expect(screen.getByRole('button', { name: \u002Flight mode\u002Fi })).toBeInTheDocument()\n})\n```\n\nAvoid mocking the context value directly (`vi.mock`) unless the context\ncomes from a third-party library you can't control. Testing through the\nreal provider catches integration bugs between the provider and consumer.\n\n**Rule of thumb:** Use the real provider with controlled initial values;\nmock only external context sources (auth libraries, feature-flag SDKs).\n",{"id":145,"difficulty":34,"q":146,"a":147},"router-testing","How do you test components that use React Router hooks like `useNavigate` or `useParams`?","Wrap with `MemoryRouter` (or `createMemoryRouter` + `RouterProvider` for\nv6 data routers) so the component has a router context without a real\nbrowser history.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { MemoryRouter, Route, Routes } from 'react-router-dom'\n\n\u002F\u002F Testing useParams\ntest('renders product with correct id', () => {\n  render(\n    \u003CMemoryRouter initialEntries={['\u002Fproducts\u002F42']}>\n      \u003CRoutes>\n        \u003CRoute path=\"\u002Fproducts\u002F:id\" element={\u003CProductPage \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002FMemoryRouter>\n  )\n  expect(screen.getByText('Product 42')).toBeInTheDocument()\n})\n\n\u002F\u002F Testing useNavigate\ntest('navigates to home after logout', async () => {\n  const user = userEvent.setup()\n  render(\n    \u003CMemoryRouter initialEntries={['\u002Fdashboard']}>\n      \u003CRoutes>\n        \u003CRoute path=\"\u002Fdashboard\" element={\u003CDashboard \u002F>} \u002F>\n        \u003CRoute path=\"\u002F\" element={\u003Cp>Home\u003C\u002Fp>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002FMemoryRouter>\n  )\n\n  await user.click(screen.getByRole('button', { name: \u002Flogout\u002Fi }))\n  expect(screen.getByText('Home')).toBeInTheDocument()\n})\n```\n\nFor components using the newer data router APIs, use\n`createMemoryRouter` + `RouterProvider`:\n```js\nimport { createMemoryRouter, RouterProvider } from 'react-router-dom'\n\nconst router = createMemoryRouter(routes, { initialEntries: ['\u002Fdashboard'] })\nrender(\u003CRouterProvider router={router} \u002F>)\n```\n\n**Rule of thumb:** `MemoryRouter` for simple path\u002Fparam tests;\n`createMemoryRouter` when you need loaders, actions, or data-router\nfeatures.\n",{"id":149,"difficulty":42,"q":150,"a":151},"list-rendering-test","How do you test that a list renders the correct number of items?","Query all list items with `getAllBy*` or `queryAllBy*` and check the\narray length, then assert individual items by their content.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\n\nconst todos = [\n  { id: 1, text: 'Buy milk' },\n  { id: 2, text: 'Walk dog' },\n  { id: 3, text: 'Read book' },\n]\n\ntest('renders correct number of todo items', () => {\n  render(\u003CTodoList todos={todos} \u002F>)\n\n  const items = screen.getAllByRole('listitem')\n  expect(items).toHaveLength(3)\n})\n\ntest('renders each todo text', () => {\n  render(\u003CTodoList todos={todos} \u002F>)\n\n  expect(screen.getByText('Buy milk')).toBeInTheDocument()\n  expect(screen.getByText('Walk dog')).toBeInTheDocument()\n  expect(screen.getByText('Read book')).toBeInTheDocument()\n})\n\ntest('shows empty state when no todos', () => {\n  render(\u003CTodoList todos={[]} \u002F>)\n  expect(screen.getByText(\u002Fno todos yet\u002Fi)).toBeInTheDocument()\n  expect(screen.queryAllByRole('listitem')).toHaveLength(0)\n})\n```\n\n**Rule of thumb:** Test the count and a sample of items; don't assert\nevery item in a large list — that couples tests to data and makes them\nbrittle.\n",{"id":153,"difficulty":34,"q":154,"a":155},"modal-portal-test","How do you test a component that renders into a portal (`ReactDOM.createPortal`)?","RTL's `screen` queries target `document.body`, which includes portal\ncontent — no special setup needed.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport Modal from '.\u002FModal'\n\n\u002F\u002F Modal renders into document.body via createPortal\ntest('opens and closes modal', async () => {\n  const user = userEvent.setup()\n  render(\u003CModal trigger={\u003Cbutton>Open\u003C\u002Fbutton>}>Dialog content\u003C\u002FModal>)\n\n  \u002F\u002F Before open — modal not in DOM\n  expect(screen.queryByRole('dialog')).not.toBeInTheDocument()\n\n  \u002F\u002F Open the modal\n  await user.click(screen.getByRole('button', { name: \u002Fopen\u002Fi }))\n  expect(screen.getByRole('dialog')).toBeInTheDocument()\n  expect(screen.getByText('Dialog content')).toBeInTheDocument()\n\n  \u002F\u002F Close with Escape\n  await user.keyboard('{Escape}')\n  expect(screen.queryByRole('dialog')).not.toBeInTheDocument()\n})\n```\n\nIf the portal target is a custom element (not `document.body`), you may\nneed to create it in setup:\n```js\nbeforeEach(() => {\n  const el = document.createElement('div')\n  el.setAttribute('id', 'modal-root')\n  document.body.appendChild(el)\n})\nafterEach(() => {\n  document.getElementById('modal-root')?.remove()\n})\n```\n\n**Rule of thumb:** Test portals through `screen` like any other\nelement — their implementation detail (portal vs inline) is irrelevant\nto user-facing behavior.\n",{"id":157,"difficulty":34,"q":158,"a":159},"keyboard-navigation-test","How do you test keyboard navigation and focus management?","Use `userEvent.keyboard()` or `userEvent.tab()` to simulate keyboard\ninput and check focus with `toHaveFocus()`.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\ntest('traps focus inside modal', async () => {\n  const user = userEvent.setup()\n  render(\u003CModal open>\u003Cinput aria-label=\"Name\" \u002F>\u003Cbutton>Close\u003C\u002Fbutton>\u003C\u002FModal>)\n\n  const nameInput = screen.getByRole('textbox', { name: \u002Fname\u002Fi })\n  const closeButton = screen.getByRole('button', { name: \u002Fclose\u002Fi })\n\n  \u002F\u002F Focus starts on first focusable element\n  expect(nameInput).toHaveFocus()\n\n  \u002F\u002F Tab moves to next element\n  await user.tab()\n  expect(closeButton).toHaveFocus()\n\n  \u002F\u002F Tab wraps back to first element (focus trap)\n  await user.tab()\n  expect(nameInput).toHaveFocus()\n})\n\ntest('closes dropdown on Escape', async () => {\n  const user = userEvent.setup()\n  render(\u003CDropdown label=\"Options\">\u003Cli>Item 1\u003C\u002Fli>\u003C\u002FDropdown>)\n\n  await user.click(screen.getByRole('button', { name: \u002Foptions\u002Fi }))\n  expect(screen.getByRole('listbox')).toBeInTheDocument()\n\n  await user.keyboard('{Escape}')\n  expect(screen.queryByRole('listbox')).not.toBeInTheDocument()\n})\n```\n\nUseful keyboard shortcuts for `user.keyboard()`:\n```\n'{Tab}'    '{Shift>}{Tab}{\u002FShift}'    '{Enter}'\n'{Space}'  '{Escape}'                  '{ArrowDown}'\n```\n\n**Rule of thumb:** Always test keyboard navigation for interactive\ncomponents (modals, dropdowns, menus) — it's both a UX and accessibility\nrequirement.\n",{"id":161,"difficulty":99,"q":162,"a":163},"error-boundary-test","How do you test error boundaries?","Render a child component that throws inside an error boundary, and assert\nthe fallback UI. Since React logs errors to `console.error`, suppress\nthat to keep test output clean.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport ErrorBoundary from '.\u002FErrorBoundary'\n\nfunction ThrowOnMount() {\n  throw new Error('Test error')\n}\n\ntest('renders fallback when child throws', () => {\n  \u002F\u002F Suppress React's error output in test logs\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  render(\n    \u003CErrorBoundary fallback={\u003Cp>Something went wrong\u003C\u002Fp>}>\n      \u003CThrowOnMount \u002F>\n    \u003C\u002FErrorBoundary>\n  )\n\n  expect(screen.getByText('Something went wrong')).toBeInTheDocument()\n  consoleSpy.mockRestore()\n})\n\ntest('renders children when no error', () => {\n  render(\n    \u003CErrorBoundary fallback={\u003Cp>Something went wrong\u003C\u002Fp>}>\n      \u003Cp>All good\u003C\u002Fp>\n    \u003C\u002FErrorBoundary>\n  )\n  expect(screen.getByText('All good')).toBeInTheDocument()\n  expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument()\n})\n```\n\nFor error boundaries with a \"retry\" button, update the child after\nerror to test recovery:\n```js\ntest('recovers after reset', async () => {\n  const user = userEvent.setup()\n  let shouldThrow = true\n  function MaybeThrow() {\n    if (shouldThrow) throw new Error('oops')\n    return \u003Cp>Recovered\u003C\u002Fp>\n  }\n  \u002F\u002F ... render, suppress, click retry, update shouldThrow, assert\n})\n```\n\n**Rule of thumb:** Always mock `console.error` in error boundary tests —\nRTL and React both log thrown errors, cluttering test output without it.\n",{"id":165,"difficulty":99,"q":166,"a":167},"integration-test-pattern","How do you write an integration test covering multiple components together?","Render the top-level component that composes the components you want to\ntest together, provide any required context or mocks at the boundary,\nand simulate the full user flow from start to finish.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { MemoryRouter } from 'react-router-dom'\nimport { rest } from 'msw'\nimport { setupServer } from 'msw\u002Fnode'\nimport App from '.\u002FApp'\n\nconst server = setupServer(\n  rest.post('\u002Fapi\u002Flogin', (req, res, ctx) =>\n    res(ctx.json({ token: 'abc123', user: { name: 'Alice' } }))\n  )\n)\n\nbeforeAll(() => server.listen())\nafterEach(() => server.resetHandlers())\nafterAll(() => server.close())\n\ntest('full login flow: form → API → dashboard', async () => {\n  const user = userEvent.setup()\n\n  render(\n    \u003CMemoryRouter initialEntries={['\u002Flogin']}>\n      \u003CApp \u002F>\n    \u003C\u002FMemoryRouter>\n  )\n\n  \u002F\u002F Fill login form\n  await user.type(screen.getByLabelText(\u002Femail\u002Fi), 'alice@example.com')\n  await user.type(screen.getByLabelText(\u002Fpassword\u002Fi), 'secret')\n  await user.click(screen.getByRole('button', { name: \u002Fsign in\u002Fi }))\n\n  \u002F\u002F After successful login, should show dashboard\n  expect(await screen.findByText('Welcome, Alice')).toBeInTheDocument()\n  expect(screen.queryByRole('form')).not.toBeInTheDocument()\n})\n```\n\nIntegration tests give the highest confidence but are slower and harder\nto debug. Strategy:\n- Unit tests for complex logic (reducers, utilities).\n- Component tests for individual component behavior.\n- Integration tests for critical user flows (login, checkout, onboarding).\n\n**Rule of thumb:** Write integration tests for flows that cross component\nboundaries and involve real user journeys, not for individual component\nrendering.\n",{"id":169,"difficulty":42,"q":170,"a":171},"spy-prop-function","How do you test that a callback prop is called with the right arguments?","Pass a `vi.fn()` (or `jest.fn()`) as the prop and assert on it after\nthe interaction.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { vi, expect } from 'vitest'\nimport ProductCard from '.\u002FProductCard'\n\ntest('calls onAddToCart with product id when button clicked', async () => {\n  const onAddToCart = vi.fn()\n  const user = userEvent.setup()\n\n  render(\u003CProductCard id={42} name=\"Widget\" onAddToCart={onAddToCart} \u002F>)\n\n  await user.click(screen.getByRole('button', { name: \u002Fadd to cart\u002Fi }))\n\n  expect(onAddToCart).toHaveBeenCalledTimes(1)\n  expect(onAddToCart).toHaveBeenCalledWith(42)\n})\n```\n\nFor multiple interactions:\n```js\nawait user.click(incrementBtn)\nawait user.click(incrementBtn)\nexpect(onChange).toHaveBeenCalledTimes(2)\nexpect(onChange).toHaveBeenNthCalledWith(1, 1)\nexpect(onChange).toHaveBeenNthCalledWith(2, 2)\n```\n\n**Rule of thumb:** Use `vi.fn()` for callback props; use `toHaveBeenCalledWith`\nrather than checking internal state — you're testing the contract between\nparent and child.\n",{"id":173,"difficulty":42,"q":174,"a":175},"select-checkbox-test","How do you test checkboxes, radio buttons, and select elements?","RTL has dedicated `userEvent` methods for each form element type.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\nconst user = userEvent.setup()\n\n\u002F\u002F Checkbox\ntest('toggles checkbox', async () => {\n  render(\u003Clabel>\u003Cinput type=\"checkbox\" \u002F>Accept terms\u003C\u002Flabel>)\n  const checkbox = screen.getByRole('checkbox', { name: \u002Faccept terms\u002Fi })\n\n  expect(checkbox).not.toBeChecked()\n  await user.click(checkbox)\n  expect(checkbox).toBeChecked()\n  await user.click(checkbox)\n  expect(checkbox).not.toBeChecked()\n})\n\n\u002F\u002F Radio buttons\ntest('selects radio option', async () => {\n  render(\n    \u003Cfieldset>\n      \u003Clegend>Size\u003C\u002Flegend>\n      \u003Clabel>\u003Cinput type=\"radio\" name=\"size\" value=\"S\" \u002F>Small\u003C\u002Flabel>\n      \u003Clabel>\u003Cinput type=\"radio\" name=\"size\" value=\"L\" \u002F>Large\u003C\u002Flabel>\n    \u003C\u002Ffieldset>\n  )\n  await user.click(screen.getByRole('radio', { name: \u002Fsmall\u002Fi }))\n  expect(screen.getByRole('radio', { name: \u002Fsmall\u002Fi })).toBeChecked()\n  expect(screen.getByRole('radio', { name: \u002Flarge\u002Fi })).not.toBeChecked()\n})\n\n\u002F\u002F Select\ntest('selects option from dropdown', async () => {\n  render(\n    \u003Cselect aria-label=\"Country\">\n      \u003Coption value=\"\">--\u003C\u002Foption>\n      \u003Coption value=\"us\">US\u003C\u002Foption>\n      \u003Coption value=\"ca\">Canada\u003C\u002Foption>\n    \u003C\u002Fselect>\n  )\n  await user.selectOptions(screen.getByRole('combobox', { name: \u002Fcountry\u002Fi }), 'us')\n  expect(screen.getByRole('combobox', { name: \u002Fcountry\u002Fi })).toHaveDisplayValue('US')\n})\n```\n\n**Rule of thumb:** Always query form elements by their accessible role +\nname (derived from `\u003Clabel>`) — this forces you to write accessible markup.\n",{"id":177,"difficulty":42,"q":178,"a":179},"disabled-element-test","How do you test that a button or input is disabled, and that it can't be interacted with?","Assert the `disabled` state with `toBeDisabled()` and verify that\nclicking a disabled button does not invoke callbacks.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\n\ntest('submit button is disabled while loading', () => {\n  render(\u003CSubmitButton loading={true} \u002F>)\n  expect(screen.getByRole('button', { name: \u002Fsubmit\u002Fi })).toBeDisabled()\n})\n\ntest('disabled button does not call onClick', async () => {\n  const onClick = vi.fn()\n  const user = userEvent.setup()\n\n  render(\u003Cbutton disabled onClick={onClick}>Click me\u003C\u002Fbutton>)\n\n  await user.click(screen.getByRole('button'))\n  \u002F\u002F userEvent respects the disabled attribute and does not fire the click\n  expect(onClick).not.toHaveBeenCalled()\n})\n\ntest('submit button becomes enabled after required fields filled', async () => {\n  const user = userEvent.setup()\n  render(\u003CForm \u002F>)\n\n  const button = screen.getByRole('button', { name: \u002Fsubmit\u002Fi })\n  expect(button).toBeDisabled()\n\n  await user.type(screen.getByLabelText(\u002Femail\u002Fi), 'user@test.com')\n  expect(button).toBeEnabled()\n})\n```\n\n**Rule of thumb:** `toBeDisabled()` and `toBeEnabled()` from\n`@testing-library\u002Fjest-dom` are clearer than `toHaveAttribute('disabled')`.\n",{"id":181,"difficulty":42,"q":182,"a":183},"snapshot-testing","What is snapshot testing and when should (and shouldn't) you use it with RTL?","Snapshot testing captures the rendered output as a string on first run and\ncompares future runs against that snapshot. Vitest\u002FJest provide this via\n`toMatchSnapshot()` or `toMatchInlineSnapshot()`.\n\n```js\nimport { render } from '@testing-library\u002Freact'\n\ntest('renders badge correctly', () => {\n  const { container } = render(\u003CBadge count={5} \u002F>)\n  expect(container.firstChild).toMatchSnapshot()\n})\n```\n\nRTL's stance: **use sparingly**. Snapshots are good for:\n- Preventing unintentional markup changes in leaf UI components.\n- Locking down third-party rendered output you don't control.\n\nSnapshots are bad for:\n- Testing behavior — they catch \"it changed\", not \"it's wrong\".\n- Large components — snapshots become huge and are mindlessly updated.\n- Fast-moving codebases — you spend more time updating snapshots than\n  fixing real bugs.\n\nPrefer inline snapshots when you do use them:\n```js\nexpect(container.firstChild).toMatchInlineSnapshot(`\n  \u003Cspan class=\"badge badge--blue\">5\u003C\u002Fspan>\n`)\n```\n\n**Rule of thumb:** Write behavioral assertions (text content, role,\nvisibility) instead of snapshots. Use snapshots only for small, stable,\npurely presentational components.\n",15,{"description":32},"React Testing Library interview questions on component interaction — user events, form testing, conditional rendering, context, portals, keyboard navigation, and integration testing patterns.","react\u002Ftesting\u002Fcomponent-interaction-testing","-AUbvLNtLgCYWx1KFc_HmPgK-UBn7Rgfrv2MfaNc8PY",{"id":190,"title":191,"body":192,"description":32,"difficulty":99,"extension":35,"framework":10,"frameworkSlug":8,"meta":196,"navigation":37,"order":197,"path":198,"questions":199,"questionsCount":252,"related":107,"seo":253,"seoDescription":254,"stem":255,"subtopic":191,"topic":19,"topicSlug":21,"updated":112,"__hash__":256},"qa\u002Freact\u002Ftesting\u002Fmocking-async.md","Mocking Async",{"type":29,"value":193,"toc":194},[],{"title":32,"searchDepth":11,"depth":11,"links":195},[],{},3,"\u002Freact\u002Ftesting\u002Fmocking-async",[200,204,208,212,216,220,224,228,232,236,240,244,248],{"id":201,"difficulty":34,"q":202,"a":203},"mock-fetch","How do you mock `fetch` in React component tests?","The simplest approach is to replace `global.fetch` with a spy before\neach test and restore it after.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport UserProfile from '.\u002FUserProfile'\n\nbeforeEach(() => {\n  global.fetch = vi.fn()\n})\n\nafterEach(() => {\n  vi.restoreAllMocks()\n})\n\ntest('displays user data after fetch', async () => {\n  global.fetch.mockResolvedValue({\n    ok: true,\n    json: async () => ({ id: 1, name: 'Alice', role: 'admin' }),\n  })\n\n  render(\u003CUserProfile userId={1} \u002F>)\n\n  expect(await screen.findByText('Alice')).toBeInTheDocument()\n  expect(screen.getByText('admin')).toBeInTheDocument()\n  expect(global.fetch).toHaveBeenCalledWith('\u002Fapi\u002Fusers\u002F1')\n})\n\ntest('shows error when fetch fails', async () => {\n  global.fetch.mockResolvedValue({ ok: false, status: 404 })\n\n  render(\u003CUserProfile userId={999} \u002F>)\n\n  expect(await screen.findByText(\u002Fuser not found\u002Fi)).toBeInTheDocument()\n})\n```\n\nThe downside: you're asserting the URL and response shape in every test.\nThis couples tests to the HTTP layer. For larger projects, prefer MSW\n(Mock Service Worker) which intercepts at the network level.\n\n**Rule of thumb:** Use `global.fetch` mocking for quick unit tests;\nswitch to MSW when you have more than a handful of API calls to test.\n",{"id":205,"difficulty":34,"q":206,"a":207},"msw-setup","What is Mock Service Worker (MSW) and how do you set it up with RTL?","**MSW** intercepts outgoing HTTP requests at the service-worker level\n(browser) or the Node.js `http` module level (tests), letting you define\nhandlers that return fake responses — without modifying `fetch` or\n`axios` in your source code.\n\nSetup for Vitest\u002FNode:\n\n**1. Install**\n```bash\nnpm install -D msw\n```\n\n**2. Define handlers (`src\u002Fmocks\u002Fhandlers.ts`)**\n```ts\nimport { http, HttpResponse } from 'msw'\n\nexport const handlers = [\n  http.get('\u002Fapi\u002Fusers\u002F:id', ({ params }) =>\n    HttpResponse.json({ id: params.id, name: 'Alice' })\n  ),\n  http.post('\u002Fapi\u002Flogin', async ({ request }) => {\n    const body = await request.json()\n    if (body.password === 'secret') {\n      return HttpResponse.json({ token: 'abc123' })\n    }\n    return new HttpResponse(null, { status: 401 })\n  }),\n]\n```\n\n**3. Create the server (`src\u002Fmocks\u002Fserver.ts`)**\n```ts\nimport { setupServer } from 'msw\u002Fnode'\nimport { handlers } from '.\u002Fhandlers'\n\nexport const server = setupServer(...handlers)\n```\n\n**4. Wire into test setup**\n```ts\n\u002F\u002F src\u002Ftest\u002Fsetup.ts\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { beforeAll, afterEach, afterAll } from 'vitest'\n\nbeforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\nafterEach(() => server.resetHandlers())   \u002F\u002F clean up per-test overrides\nafterAll(() => server.close())\n```\n\nNow every test automatically intercepts matching requests without any\nper-test configuration.\n\n**Rule of thumb:** Set `onUnhandledRequest: 'error'` — it forces you to\nhandle every request explicitly, catching forgotten mocks before they\nsilently return `undefined`.\n",{"id":209,"difficulty":42,"q":210,"a":211},"loading-state-test","How do you test loading states in a component that fetches data?","Assert the loading indicator is present *before* the data arrives, then\nassert it's gone once the data loads.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport UserList from '.\u002FUserList'\n\ntest('shows skeleton while loading, then data', async () => {\n  \u002F\u002F Default handler from server setup will respond eventually\n  render(\u003CUserList \u002F>)\n\n  \u002F\u002F Loading state should appear synchronously\n  expect(screen.getByRole('status', { name: \u002Floading\u002Fi }))\n    .toBeInTheDocument()\n  \u002F\u002F or: expect(screen.getByTestId('skeleton')).toBeInTheDocument()\n\n  \u002F\u002F Data appears after fetch completes\n  expect(await screen.findByText('Alice')).toBeInTheDocument()\n\n  \u002F\u002F Loading indicator is gone\n  expect(screen.queryByRole('status', { name: \u002Floading\u002Fi }))\n    .not.toBeInTheDocument()\n})\n```\n\nFor a slow network simulation, delay the MSW response:\n```js\nimport { delay } from 'msw'\n\nserver.use(\n  http.get('\u002Fapi\u002Fusers', async () => {\n    await delay(500)           \u002F\u002F artificial delay\n    return HttpResponse.json([{ id: 1, name: 'Alice' }])\n  })\n)\n```\n\n**Rule of thumb:** Always assert both the loading state AND the resolved\nstate in the same test — it proves the loading indicator appears and\ndisappears correctly.\n",{"id":213,"difficulty":42,"q":214,"a":215},"error-state-test","How do you test error states from failed API calls?","Override the MSW handler for the specific test to return an error\nresponse, then assert the error UI.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport UserList from '.\u002FUserList'\n\ntest('shows error message on 500', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers', () => new HttpResponse(null, { status: 500 }))\n  )\n  render(\u003CUserList \u002F>)\n  expect(await screen.findByRole('alert')).toHaveTextContent(\u002Fsomething went wrong\u002Fi)\n})\n\ntest('shows not-found message on 404', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers\u002F999', () => new HttpResponse(null, { status: 404 }))\n  )\n  render(\u003CUserProfile userId={999} \u002F>)\n  expect(await screen.findByText(\u002Fuser not found\u002Fi)).toBeInTheDocument()\n})\n\ntest('shows network error message', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers', () => HttpResponse.error())  \u002F\u002F simulates network failure\n  )\n  render(\u003CUserList \u002F>)\n  expect(await screen.findByText(\u002Fnetwork error\u002Fi)).toBeInTheDocument()\n})\n```\n\n`server.resetHandlers()` in `afterEach` ensures these one-off overrides\ndon't bleed into other tests.\n\n**Rule of thumb:** Test at least three HTTP outcomes: success, known error\n(400\u002F404), and unexpected server error (500). Each maps to a different\nuser-facing message.\n",{"id":217,"difficulty":99,"q":218,"a":219},"fake-timers","How do you test components that use `setTimeout` or `setInterval`?","Use Vitest's (or Jest's) fake timer API to control time without actually\nwaiting.\n\n```js\nimport { render, screen, act } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\n\nfunction Countdown({ seconds }) {\n  const [remaining, setRemaining] = React.useState(seconds)\n  React.useEffect(() => {\n    const id = setInterval(() => setRemaining(r => r - 1), 1000)\n    return () => clearInterval(id)\n  }, [])\n  return \u003Cp>{remaining}s remaining\u003C\u002Fp>\n}\n\ndescribe('Countdown', () => {\n  beforeEach(() => vi.useFakeTimers())\n  afterEach(() => vi.useRealTimers())\n\n  test('decrements every second', () => {\n    render(\u003CCountdown seconds={5} \u002F>)\n    expect(screen.getByText('5s remaining')).toBeInTheDocument()\n\n    act(() => vi.advanceTimersByTime(1000))\n    expect(screen.getByText('4s remaining')).toBeInTheDocument()\n\n    act(() => vi.advanceTimersByTime(3000))\n    expect(screen.getByText('1s remaining')).toBeInTheDocument()\n  })\n\n  test('cleans up interval on unmount', () => {\n    const clearIntervalSpy = vi.spyOn(global, 'clearInterval')\n    const { unmount } = render(\u003CCountdown seconds={3} \u002F>)\n    unmount()\n    expect(clearIntervalSpy).toHaveBeenCalled()\n  })\n})\n```\n\nWrapping `vi.advanceTimersByTime` in `act()` flushes any resulting React\nstate updates synchronously.\n\n**Rule of thumb:** Always call `vi.useRealTimers()` in `afterEach` —\nfake timers left running can cause cascading failures in later tests.\n",{"id":221,"difficulty":34,"q":222,"a":223},"module-mocking","How do you mock an entire module (e.g., an API client) in Vitest?","Use `vi.mock()` at the top of the test file (Vitest hoists it before\nimports). Then configure specific implementations with `vi.mocked()`.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport * as api from '..\u002Fapi\u002FuserApi'   \u002F\u002F named imports\nimport UserProfile from '.\u002FUserProfile'\n\nvi.mock('..\u002Fapi\u002FuserApi')   \u002F\u002F auto-mocks all exports to vi.fn()\n\nconst mockedApi = vi.mocked(api)\n\nbeforeEach(() => {\n  mockedApi.fetchUser.mockResolvedValue({ id: 1, name: 'Alice' })\n})\n\nafterEach(() => vi.clearAllMocks())\n\ntest('fetches and displays user', async () => {\n  render(\u003CUserProfile userId={1} \u002F>)\n  expect(await screen.findByText('Alice')).toBeInTheDocument()\n  expect(mockedApi.fetchUser).toHaveBeenCalledWith(1)\n})\n\ntest('shows error when fetch throws', async () => {\n  mockedApi.fetchUser.mockRejectedValue(new Error('Network error'))\n  render(\u003CUserProfile userId={1} \u002F>)\n  expect(await screen.findByText(\u002Fnetwork error\u002Fi)).toBeInTheDocument()\n})\n```\n\nFor a partial mock (keep some real implementations):\n```js\nvi.mock('..\u002Futils\u002Fdate', async (importOriginal) => {\n  const real = await importOriginal()\n  return { ...real, formatDate: vi.fn().mockReturnValue('Jan 1') }\n})\n```\n\n**Rule of thumb:** Prefer MSW for HTTP mocking; use `vi.mock()` for\nnon-HTTP modules (analytics SDKs, third-party clients, browser APIs).\n",{"id":225,"difficulty":99,"q":226,"a":227},"react-query-testing","How do you test components that use React Query (`useQuery`, `useMutation`)?","Create a fresh `QueryClient` per test with `retry: false` (so failed\nqueries don't retry in tests), wrap the component in a\n`QueryClientProvider`, and let MSW handle the HTTP responses.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport UserList from '.\u002FUserList'\n\nfunction createTestClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: { retry: false },    \u002F\u002F don't retry on error in tests\n      mutations: { retry: false },\n    },\n  })\n}\n\nfunction renderWithQuery(ui) {\n  const client = createTestClient()\n  return render(\n    \u003CQueryClientProvider client={client}>{ui}\u003C\u002FQueryClientProvider>\n  )\n}\n\ntest('renders user list from query', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers', () =>\n      HttpResponse.json([{ id: 1, name: 'Alice' }])\n    )\n  )\n  renderWithQuery(\u003CUserList \u002F>)\n  expect(await screen.findByText('Alice')).toBeInTheDocument()\n})\n\ntest('mutation updates UI after success', async () => {\n  const user = userEvent.setup()\n  server.use(\n    http.get('\u002Fapi\u002Ftodos', () => HttpResponse.json([])),\n    http.post('\u002Fapi\u002Ftodos', () => HttpResponse.json({ id: 1, text: 'New todo' }))\n  )\n  renderWithQuery(\u003CTodoApp \u002F>)\n  await user.type(screen.getByRole('textbox'), 'New todo')\n  await user.click(screen.getByRole('button', { name: \u002Fadd\u002Fi }))\n  expect(await screen.findByText('New todo')).toBeInTheDocument()\n})\n```\n\n**Rule of thumb:** Always use a fresh `QueryClient` per test — a shared\nclient caches query results across tests and causes false passes or\nunexplained failures.\n",{"id":229,"difficulty":99,"q":230,"a":231},"debounce-throttle-test","How do you test debounced or throttled functions inside components?","Use fake timers to control the debounce delay without real waiting.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { vi } from 'vitest'\nimport SearchBox from '.\u002FSearchBox'  \u002F\u002F internally debounces onSearch by 300ms\n\ndescribe('SearchBox debounce', () => {\n  beforeEach(() => vi.useFakeTimers())\n  afterEach(() => vi.useRealTimers())\n\n  test('calls onSearch only after debounce delay', async () => {\n    const onSearch = vi.fn()\n    const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })\n    render(\u003CSearchBox onSearch={onSearch} \u002F>)\n\n    await user.type(screen.getByRole('textbox'), 'react')\n\n    \u002F\u002F Before debounce fires — should not have been called\n    expect(onSearch).not.toHaveBeenCalled()\n\n    \u002F\u002F Advance time past debounce window\n    vi.runAllTimers()\n\n    expect(onSearch).toHaveBeenCalledTimes(1)\n    expect(onSearch).toHaveBeenCalledWith('react')\n  })\n\n  test('debounces rapid typing to a single call', async () => {\n    const onSearch = vi.fn()\n    const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })\n    render(\u003CSearchBox onSearch={onSearch} \u002F>)\n\n    await user.type(screen.getByRole('textbox'), 'react')\n    await user.clear(screen.getByRole('textbox'))\n    await user.type(screen.getByRole('textbox'), 'hooks')\n\n    vi.runAllTimers()\n\n    \u002F\u002F Only the final value triggers the callback\n    expect(onSearch).toHaveBeenCalledTimes(1)\n    expect(onSearch).toHaveBeenCalledWith('hooks')\n  })\n})\n```\n\nKey: pass `{ advanceTimers: vi.advanceTimersByTime }` to `userEvent.setup()`\nso userEvent's internal delays use fake timers too.\n\n**Rule of thumb:** Always pair fake timers with `userEvent.setup({ advanceTimers })`\n— without it, userEvent's own async delays break fake-timer tests.\n",{"id":233,"difficulty":34,"q":234,"a":235},"localstorage-mock","How do you mock `localStorage` in tests?","The jsdom environment (used by Vitest\u002FJest with RTL) provides a real\n`localStorage` implementation, but it persists between tests unless\ncleared. The cleanest approach is to clear it after each test.\n\n```js\nafterEach(() => localStorage.clear())\n\ntest('persists theme preference to localStorage', async () => {\n  const user = userEvent.setup()\n  render(\u003CThemeToggle \u002F>)\n\n  await user.click(screen.getByRole('button', { name: \u002Fdark mode\u002Fi }))\n\n  expect(localStorage.getItem('theme')).toBe('dark')\n})\n\ntest('reads theme from localStorage on mount', () => {\n  localStorage.setItem('theme', 'dark')\n  render(\u003CThemeToggle \u002F>)\n  expect(screen.getByRole('button', { name: \u002Flight mode\u002Fi })).toBeInTheDocument()\n})\n```\n\nIf you need to test a case where `localStorage` is unavailable (private\nbrowsing), mock it with a spy:\n```js\nvi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {\n  throw new DOMException('QuotaExceededError')\n})\n```\n\n**Rule of thumb:** Call `localStorage.clear()` in `afterEach`, not\n`beforeEach` — clearing before masks cleanup bugs from prior tests.\n",{"id":237,"difficulty":34,"q":238,"a":239},"async-act-warning","What does the \"act() warning\" mean and how do you fix it?","The `act()` warning appears when a state update happens *outside* of a\nReact `act()` call, meaning React's test utilities weren't notified that\nstate was about to change.\n\n```\nWarning: An update to Counter inside a test was not wrapped in act(...)\n```\n\n**Common causes and fixes:**\n\n**1. Async state update not awaited:**\n```js\n\u002F\u002F ❌ State update from async callback is not awaited\nfireEvent.click(button)\n\u002F\u002F The click triggers a fetch that updates state after the test ends\n\n\u002F\u002F ✅ Use findBy* which internally wraps in act\nawait screen.findByText('Loaded')\n```\n\n**2. Missing `await` on userEvent:**\n```js\n\u002F\u002F ❌\nuser.click(button)   \u002F\u002F forgot await\n\n\u002F\u002F ✅\nawait user.click(button)\n```\n\n**3. `useEffect` with a timer not cleaned up:**\n```js\n\u002F\u002F ❌ Timer fires after test cleanup\nReact.useEffect(() => {\n  setTimeout(() => setState('done'), 500)\n}, [])\n\n\u002F\u002F ✅ Cancel the timer in cleanup\nReact.useEffect(() => {\n  const id = setTimeout(() => setState('done'), 500)\n  return () => clearTimeout(id)\n}, [])\n```\n\n**4. Manually wrapping needed:**\n```js\nact(() => {\n  \u002F\u002F trigger some event that causes synchronous state updates\n  eventEmitter.emit('update', newData)\n})\n```\n\n**Rule of thumb:** The warning is always a signal that a state update\nescaped test control — find and `await` the async operation that triggered\nit.\n",{"id":241,"difficulty":42,"q":242,"a":243},"spy-console","How do you test that a component logs errors or warnings without polluting test output?","Spy on `console.error` or `console.warn` with `vi.spyOn()`, suppress the\noutput by replacing the implementation, assert the call, then restore.\n\n```js\nimport { vi } from 'vitest'\n\ntest('logs validation error for invalid prop', () => {\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  render(\u003CDatePicker value=\"not-a-date\" \u002F>)\n\n  expect(consoleSpy).toHaveBeenCalledWith(\n    expect.stringContaining('Invalid date format')\n  )\n\n  consoleSpy.mockRestore()\n})\n```\n\nFor error boundaries that log the error:\n```js\ntest('error boundary shows fallback and logs', () => {\n  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  render(\n    \u003CErrorBoundary>\n      \u003CThrowingComponent \u002F>\n    \u003C\u002FErrorBoundary>\n  )\n\n  expect(screen.getByText(\u002Fsomething went wrong\u002Fi)).toBeInTheDocument()\n  expect(errorSpy).toHaveBeenCalled()\n\n  errorSpy.mockRestore()\n})\n```\n\nIf you want to assert `console.error` is NOT called (no unexpected\nerrors):\n```js\nconst consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\nrender(\u003CCleanComponent \u002F>)\nexpect(consoleSpy).not.toHaveBeenCalled()\nconsoleSpy.mockRestore()\n```\n\n**Rule of thumb:** Always `mockRestore()` after spy assertions — leaving\nthe spy in place will swallow real errors in subsequent tests.\n",{"id":245,"difficulty":99,"q":246,"a":247},"polling-test","How do you test a component that polls an API at a regular interval?","Combine fake timers with MSW to control both the clock and the server\nresponses. Advance time in steps and assert updated data between steps.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport { act } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport LivePriceWidget from '.\u002FLivePriceWidget'  \u002F\u002F polls \u002Fapi\u002Fprice every 5s\n\ndescribe('LivePriceWidget', () => {\n  beforeEach(() => vi.useFakeTimers())\n  afterEach(() => {\n    vi.useRealTimers()\n    server.resetHandlers()\n  })\n\n  test('updates price after each poll interval', async () => {\n    let price = 100\n\n    server.use(\n      http.get('\u002Fapi\u002Fprice', () => HttpResponse.json({ value: price }))\n    )\n\n    render(\u003CLivePriceWidget \u002F>)\n    expect(await screen.findByText('$100')).toBeInTheDocument()\n\n    \u002F\u002F Simulate a price change on the server\n    price = 150\n    await act(async () => {\n      vi.advanceTimersByTime(5000)  \u002F\u002F trigger next poll\n    })\n\n    expect(await screen.findByText('$150')).toBeInTheDocument()\n  })\n})\n```\n\nThe `await act(async () => { ... })` pattern flushes both the timer\ncallback and the resulting async state update.\n\n**Rule of thumb:** For polling tests, wrap `vi.advanceTimersByTime`\nin `await act(async () => { ... })` to ensure React processes both the\ntimer expiry and the subsequent promise resolution.\n",{"id":249,"difficulty":99,"q":250,"a":251},"race-condition-test","How do you test that a component handles race conditions — e.g., only showing the result of the latest request?","Use MSW handlers with controlled response ordering (via `delay`) to\nsimulate an older request resolving after a newer one.\n\n```js\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse, delay } from 'msw'\nimport SearchResults from '.\u002FSearchResults'\n\ntest('shows result of latest search, ignoring stale responses', async () => {\n  let requestCount = 0\n\n  server.use(\n    http.get('\u002Fapi\u002Fsearch', async ({ request }) => {\n      const q = new URL(request.url).searchParams.get('q')\n      requestCount++\n\n      if (requestCount === 1) {\n        \u002F\u002F First request (stale) — slow response\n        await delay(500)\n        return HttpResponse.json({ query: q, results: ['Stale result'] })\n      }\n      \u002F\u002F Second request (latest) — fast response\n      return HttpResponse.json({ query: q, results: ['Fresh result'] })\n    })\n  )\n\n  const user = userEvent.setup()\n  render(\u003CSearchResults \u002F>)\n\n  \u002F\u002F Type first query, then quickly change it\n  await user.type(screen.getByRole('textbox'), 'slow')\n  await user.clear(screen.getByRole('textbox'))\n  await user.type(screen.getByRole('textbox'), 'fast')\n\n  \u002F\u002F Should show fresh result only\n  expect(await screen.findByText('Fresh result')).toBeInTheDocument()\n  expect(screen.queryByText('Stale result')).not.toBeInTheDocument()\n})\n```\n\nThis test verifies the component cancels or ignores stale requests\n(typically via `AbortController` or a flag in `useEffect`).\n\n**Rule of thumb:** Test race conditions by controlling response timing\nin MSW handlers; if the test passes even without abort logic, the\ntiming simulation isn't strict enough.\n",13,{"description":32},"React async testing interview questions — mocking fetch, MSW setup, testing loading and error states, fake timers, vi.mock, React Query testing, debounce\u002Fthrottle, and localStorage mocking.","react\u002Ftesting\u002Fmocking-async","4qnBXsczPivanIvp--w3X85AcXTAYLr32BCFDvmr2qo",{"id":258,"title":259,"body":260,"description":32,"difficulty":99,"extension":35,"framework":10,"frameworkSlug":8,"meta":264,"navigation":37,"order":265,"path":266,"questions":267,"questionsCount":252,"related":107,"seo":320,"seoDescription":321,"stem":322,"subtopic":259,"topic":19,"topicSlug":21,"updated":112,"__hash__":323},"qa\u002Freact\u002Ftesting\u002Ftesting-custom-hooks.md","Testing Custom Hooks",{"type":29,"value":261,"toc":262},[],{"title":32,"searchDepth":11,"depth":11,"links":263},[],{},4,"\u002Freact\u002Ftesting\u002Ftesting-custom-hooks",[268,272,276,280,284,288,292,296,300,304,308,312,316],{"id":269,"difficulty":34,"q":270,"a":271},"render-hook-intro","What is `renderHook` and how does it work?","`renderHook` from `@testing-library\u002Freact` mounts a minimal wrapper\ncomponent that calls the hook under test and exposes its return value\nthrough a `result` object. It is the standard way to test hooks in\nisolation without building a throwaway UI component.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { useCounter } from '.\u002FuseCounter'\n\ntest('initializes with given value', () => {\n  const { result } = renderHook(() => useCounter(10))\n\n  \u002F\u002F result.current holds whatever the hook returns\n  expect(result.current.count).toBe(10)\n  expect(typeof result.current.increment).toBe('function')\n})\n```\n\nThe wrapper component re-renders each time the hook causes a state\nupdate, and `result.current` always reflects the *latest* return value.\n\nInternally, `renderHook(callback)` is roughly:\n```js\nfunction TestHook() {\n  result.current = callback()  \u002F\u002F captures hook return value\n  return null\n}\nrender(\u003CTestHook \u002F>)\n```\n\nUpdating arguments on re-render:\n```js\nconst { result, rerender } = renderHook(\n  ({ step }) => useCounter(0, step),\n  { initialProps: { step: 1 } }\n)\nrerender({ step: 5 })   \u002F\u002F hook re-executes with new props\n```\n\n**Rule of thumb:** Use `renderHook` when the hook encapsulates logic\nyou want to verify independently; use component tests when the\nhook is simple and fully covered by the component's test.\n",{"id":273,"difficulty":34,"q":274,"a":275},"act-in-hook-tests","Why do you need to wrap hook state updates in `act()`?","React batches state updates and applies them asynchronously in its\nfiber scheduler. `act()` tells React's test utilities: \"all state changes\ninside this block should be flushed before assertions run.\" Without it\nyou may assert before React has committed the updated state.\n\n```js\nimport { renderHook, act } from '@testing-library\u002Freact'\nimport { useToggle } from '.\u002FuseToggle'\n\ntest('toggles between true and false', () => {\n  const { result } = renderHook(() => useToggle(false))\n\n  expect(result.current.isOn).toBe(false)\n\n  \u002F\u002F Wrap the state-updating call in act()\n  act(() => {\n    result.current.toggle()\n  })\n\n  expect(result.current.isOn).toBe(true)\n\n  act(() => {\n    result.current.toggle()\n  })\n\n  expect(result.current.isOn).toBe(false)\n})\n```\n\nFor async updates (hooks that call `setState` inside a Promise or timer):\n```js\nawait act(async () => {\n  result.current.fetchData()\n})\n\u002F\u002F Safe to assert here — all async state changes have been flushed\n```\n\nRTL's `userEvent` helpers wrap their actions in `act` automatically, which\nis one reason to prefer `userEvent` over manual `act` calls in component\ntests. For hook tests you usually call `act` directly.\n\n**Rule of thumb:** Any call inside `renderHook` that triggers a state\nupdate must be wrapped in `act()`. If you see the \"not wrapped in act\"\nwarning, find the state update and wrap the trigger.\n",{"id":277,"difficulty":99,"q":278,"a":279},"async-hook-test","How do you test a custom hook that fetches data asynchronously?","Combine `renderHook`, `waitFor`, and a mock (MSW or `vi.fn()`) to assert\non loading, success, and error states.\n\n```js\nimport { renderHook, waitFor } from '@testing-library\u002Freact'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport { useFetchUser } from '.\u002FuseFetchUser'\n\n\u002F\u002F useFetchUser: returns { user, loading, error }\n\ntest('transitions loading → data', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers\u002F1', () =>\n      HttpResponse.json({ id: 1, name: 'Alice' })\n    )\n  )\n\n  const { result } = renderHook(() => useFetchUser(1))\n\n  \u002F\u002F Loading state immediately after mount\n  expect(result.current.loading).toBe(true)\n  expect(result.current.user).toBeNull()\n\n  \u002F\u002F Wait until loading is false\n  await waitFor(() => expect(result.current.loading).toBe(false))\n\n  expect(result.current.user).toEqual({ id: 1, name: 'Alice' })\n  expect(result.current.error).toBeNull()\n})\n\ntest('sets error on fetch failure', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers\u002F1', () => new HttpResponse(null, { status: 500 }))\n  )\n\n  const { result } = renderHook(() => useFetchUser(1))\n\n  await waitFor(() => expect(result.current.loading).toBe(false))\n\n  expect(result.current.error).toBeTruthy()\n  expect(result.current.user).toBeNull()\n})\n```\n\n**Rule of thumb:** Use `waitFor(() => expect(...))` rather than\n`findBy*` in hook tests because hooks return values, not DOM elements.\n",{"id":281,"difficulty":34,"q":282,"a":283},"context-hook-test","How do you test a hook that depends on React context?","Pass a `wrapper` option to `renderHook` that wraps the hook in the\nrequired provider, the same way you'd wrap a component.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { AuthProvider } from '.\u002FAuthContext'\nimport { useAuth } from '.\u002FuseAuth'\n\nfunction createWrapper(user) {\n  return function AuthWrapper({ children }) {\n    return \u003CAuthProvider initialUser={user}>{children}\u003C\u002FAuthProvider>\n  }\n}\n\ntest('returns current user from context', () => {\n  const { result } = renderHook(() => useAuth(), {\n    wrapper: createWrapper({ id: 1, name: 'Alice', role: 'admin' }),\n  })\n\n  expect(result.current.user).toEqual({ id: 1, name: 'Alice', role: 'admin' })\n  expect(result.current.isAuthenticated).toBe(true)\n})\n\ntest('returns null user when not authenticated', () => {\n  const { result } = renderHook(() => useAuth(), {\n    wrapper: createWrapper(null),\n  })\n\n  expect(result.current.user).toBeNull()\n  expect(result.current.isAuthenticated).toBe(false)\n})\n\ntest('throws when used outside AuthProvider', () => {\n  \u002F\u002F No wrapper — hook should throw\n  expect(() => renderHook(() => useAuth())).toThrow(\u002Fmust be used within AuthProvider\u002Fi)\n})\n```\n\n**Rule of thumb:** Always test the \"missing provider\" error path for\nhooks that call `useContext` — it verifies your guard condition and\nproduces a clear error message in production.\n",{"id":285,"difficulty":34,"q":286,"a":287},"useeffect-hook-test","How do you test the side effects of a custom hook that uses `useEffect`?","Wrap the side-effectful action in `act()` and assert the result after\n`act` resolves. For side effects that happen on mount, they run\nsynchronously during `renderHook` (within RTL's own `act` call).\n\n```js\nimport { renderHook, act } from '@testing-library\u002Freact'\nimport { useDocumentTitle } from '.\u002FuseDocumentTitle'\n\n\u002F\u002F useDocumentTitle: sets document.title to the given string\ntest('sets document title on mount', () => {\n  renderHook(() => useDocumentTitle('My Page'))\n  expect(document.title).toBe('My Page')\n})\n\ntest('updates document title when value changes', () => {\n  const { rerender } = renderHook(\n    ({ title }) => useDocumentTitle(title),\n    { initialProps: { title: 'Page 1' } }\n  )\n\n  expect(document.title).toBe('Page 1')\n\n  rerender({ title: 'Page 2' })\n  expect(document.title).toBe('Page 2')\n})\n\ntest('restores title on unmount', () => {\n  document.title = 'Original'\n  const { unmount } = renderHook(() => useDocumentTitle('Temp'))\n  expect(document.title).toBe('Temp')\n  unmount()\n  expect(document.title).toBe('Original')\n})\n```\n\nFor async effects (data fetching on mount), use `waitFor` as shown in\nthe `useFetchUser` example.\n\n**Rule of thumb:** Test three lifecycle moments: initial mount effect,\nupdate-triggered effect, and cleanup on unmount.\n",{"id":289,"difficulty":99,"q":290,"a":291},"timer-hook-test","How do you test a custom hook that internally uses `setTimeout` or `setInterval`?","Use `vi.useFakeTimers()` and advance time inside `act()`.\n\n```js\nimport { renderHook, act } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport { useDebounce } from '.\u002FuseDebounce'\n\ndescribe('useDebounce', () => {\n  beforeEach(() => vi.useFakeTimers())\n  afterEach(() => vi.useRealTimers())\n\n  test('returns initial value immediately', () => {\n    const { result } = renderHook(() => useDebounce('hello', 300))\n    expect(result.current).toBe('hello')\n  })\n\n  test('does not update before delay', () => {\n    const { result, rerender } = renderHook(\n      ({ value }) => useDebounce(value, 300),\n      { initialProps: { value: 'hello' } }\n    )\n\n    rerender({ value: 'world' })\n    act(() => vi.advanceTimersByTime(100))\n    expect(result.current).toBe('hello')   \u002F\u002F not yet updated\n  })\n\n  test('updates after debounce delay', () => {\n    const { result, rerender } = renderHook(\n      ({ value }) => useDebounce(value, 300),\n      { initialProps: { value: 'hello' } }\n    )\n\n    rerender({ value: 'world' })\n    act(() => vi.advanceTimersByTime(300))\n    expect(result.current).toBe('world')\n  })\n\n  test('clears pending timer on unmount', () => {\n    const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')\n    const { unmount } = renderHook(() => useDebounce('test', 300))\n    unmount()\n    expect(clearTimeoutSpy).toHaveBeenCalled()\n  })\n})\n```\n\n**Rule of thumb:** Always test the cleanup path (`unmount`) for timer\nhooks — leaking timers is a common source of \"act warning\" failures in\nsubsequent tests.\n",{"id":293,"difficulty":34,"q":294,"a":295},"reducer-hook-test","How do you test a hook built on `useReducer`?","Test through the hook's public interface (the actions\u002Fdispatch wrappers\nit returns), not by inspecting the reducer directly. The reducer itself\ncan have its own pure unit tests.\n\n```js\nimport { renderHook, act } from '@testing-library\u002Freact'\nimport { useShoppingCart } from '.\u002FuseShoppingCart'\n\ntest('adds and removes items', () => {\n  const { result } = renderHook(() => useShoppingCart())\n\n  expect(result.current.items).toHaveLength(0)\n  expect(result.current.total).toBe(0)\n\n  act(() => {\n    result.current.addItem({ id: 1, name: 'Widget', price: 9.99 })\n  })\n  expect(result.current.items).toHaveLength(1)\n  expect(result.current.total).toBeCloseTo(9.99)\n\n  act(() => {\n    result.current.addItem({ id: 2, name: 'Gadget', price: 24.99 })\n  })\n  expect(result.current.total).toBeCloseTo(34.98)\n\n  act(() => {\n    result.current.removeItem(1)\n  })\n  expect(result.current.items).toHaveLength(1)\n  expect(result.current.total).toBeCloseTo(24.99)\n})\n\ntest('clears cart', () => {\n  const { result } = renderHook(() => useShoppingCart())\n  act(() => {\n    result.current.addItem({ id: 1, name: 'Widget', price: 9.99 })\n    result.current.clearCart()\n  })\n  expect(result.current.items).toHaveLength(0)\n})\n```\n\nSeparately, test the reducer as a pure function:\n```js\nimport { cartReducer } from '.\u002FcartReducer'\n\ntest('ADD_ITEM increases quantity for existing item', () => {\n  const state = { items: [{ id: 1, qty: 1, price: 10 }] }\n  const next = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 1 } })\n  expect(next.items[0].qty).toBe(2)\n})\n```\n\n**Rule of thumb:** Test hooks through their action API (what the caller\nsees); test reducers as pure functions (same input → same output).\n",{"id":297,"difficulty":42,"q":298,"a":299},"hook-args-test","How do you test a hook that accepts arguments, and test how it behaves when those arguments change?","Pass `initialProps` to `renderHook` and call `rerender` with new props\nto simulate prop changes.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { usePagination } from '.\u002FusePagination'\n\ntest('initializes with given page and pageSize', () => {\n  const { result } = renderHook(\n    ({ page, pageSize }) => usePagination({ page, pageSize, total: 100 }),\n    { initialProps: { page: 1, pageSize: 10 } }\n  )\n\n  expect(result.current.currentPage).toBe(1)\n  expect(result.current.totalPages).toBe(10)\n  expect(result.current.startIndex).toBe(0)\n  expect(result.current.endIndex).toBe(9)\n})\n\ntest('recalculates when pageSize changes', () => {\n  const { result, rerender } = renderHook(\n    ({ pageSize }) => usePagination({ page: 1, pageSize, total: 100 }),\n    { initialProps: { pageSize: 10 } }\n  )\n\n  expect(result.current.totalPages).toBe(10)\n\n  rerender({ pageSize: 20 })\n  expect(result.current.totalPages).toBe(5)\n})\n\ntest('clamps page when it exceeds new totalPages', () => {\n  const { result, rerender } = renderHook(\n    ({ total }) => usePagination({ page: 5, pageSize: 10, total }),\n    { initialProps: { total: 100 } }\n  )\n\n  expect(result.current.currentPage).toBe(5)\n\n  rerender({ total: 30 })    \u002F\u002F now only 3 pages\n  expect(result.current.currentPage).toBe(3)  \u002F\u002F clamped\n})\n```\n\n**Rule of thumb:** Use `initialProps` + `rerender` to test argument\nchanges; this is the hook equivalent of testing prop changes via\n`render` + `rerender`.\n",{"id":301,"difficulty":34,"q":302,"a":303},"hook-vs-component-test","When should you test a custom hook directly with `renderHook` versus testing it through the component that uses it?","Both are valid — the choice depends on the complexity and reuse of the\nhook.\n\n**Test through the component when:**\n- The hook is tightly coupled to one component and used nowhere else.\n- The hook's behavior is fully visible through the component's UI.\n- The hook is simple (one or two state variables).\n\n```js\n\u002F\u002F useExpanded is used only in Accordion — test through Accordion\ntest('expands panel on click', async () => {\n  const user = userEvent.setup()\n  render(\u003CAccordion panels={[{ title: 'Q1', body: 'A1' }]} \u002F>)\n  expect(screen.queryByText('A1')).not.toBeInTheDocument()\n  await user.click(screen.getByRole('button', { name: \u002FQ1\u002Fi }))\n  expect(screen.getByText('A1')).toBeInTheDocument()\n})\n```\n\n**Test the hook directly when:**\n- The hook encapsulates complex logic used by multiple components.\n- The hook has many edge cases that would be tedious to exercise via UI.\n- The hook manages async state (loading\u002Ferror\u002Fdata transitions).\n- You want to document the hook's API contract separately.\n\n```js\n\u002F\u002F useInfiniteScroll is reused across 4 components — test directly\ntest('loads next page when sentinel is visible', async () => {\n  const { result } = renderHook(() => useInfiniteScroll(fetchPage))\n  \u002F\u002F ... assert loading, page count, hasMore\n})\n```\n\nA practical middle ground: write one integration test per major use-case\nat the component level, then add direct hook tests for tricky edge cases.\n\n**Rule of thumb:** Start with component tests. Add direct hook tests\nwhen the component test becomes unwieldy or when the hook is shared.\n",{"id":305,"difficulty":99,"q":306,"a":307},"mock-hook-dependency","How do you mock a module dependency used inside a custom hook?","Use `vi.mock()` to replace the dependency before the hook (and any\ncomponent using it) imports it.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport * as analyticsModule from '..\u002Futils\u002Fanalytics'\nimport { useProductView } from '.\u002FuseProductView'\n\nvi.mock('..\u002Futils\u002Fanalytics')\n\nconst mockedAnalytics = vi.mocked(analyticsModule)\n\nbeforeEach(() => {\n  mockedAnalytics.trackEvent.mockImplementation(() => {})\n})\n\nafterEach(() => vi.clearAllMocks())\n\ntest('tracks product view event on mount', () => {\n  renderHook(() => useProductView({ id: 42, name: 'Widget' }))\n\n  expect(mockedAnalytics.trackEvent).toHaveBeenCalledWith('product_view', {\n    product_id: 42,\n    product_name: 'Widget',\n  })\n})\n\ntest('tracks only once even if hook re-renders', () => {\n  const { rerender } = renderHook(() => useProductView({ id: 42, name: 'Widget' }))\n  rerender()\n  rerender()\n  expect(mockedAnalytics.trackEvent).toHaveBeenCalledTimes(1)\n})\n```\n\nFor mocking a hook that's used inside another hook (hook composition):\n```js\nvi.mock('.\u002FuseAuth', () => ({\n  useAuth: () => ({ user: { id: 1 }, isAuthenticated: true }),\n}))\n```\n\n**Rule of thumb:** Mock at the module boundary, not deep inside the\nimplementation. If you're mocking an internal helper, the hook's\nresponsibility boundary may be too large.\n",{"id":309,"difficulty":99,"q":310,"a":311},"ref-hook-test","How do you test a custom hook that uses `useRef` to interact with DOM elements?","`renderHook` by itself doesn't produce DOM elements — you need a component\nto attach refs to real DOM nodes. Use a custom wrapper component.\n\n```js\nimport { render, screen, act } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { useAutoFocus } from '.\u002FuseAutoFocus'\n\n\u002F\u002F useAutoFocus: takes a ref and calls .focus() on mount\n\ntest('focuses element on mount', () => {\n  function TestComponent() {\n    const ref = React.useRef(null)\n    useAutoFocus(ref)\n    return \u003Cinput ref={ref} aria-label=\"Auto-focused input\" \u002F>\n  }\n\n  render(\u003CTestComponent \u002F>)\n  expect(screen.getByRole('textbox', { name: \u002Fauto-focused\u002Fi })).toHaveFocus()\n})\n```\n\nFor hooks that expose a ref (e.g., `useScrollLock`):\n```js\nfunction TestComponent() {\n  const containerRef = React.useRef(null)\n  const { lock, unlock } = useScrollLock(containerRef)\n  return (\n    \u003Cdiv ref={containerRef} style={{ overflow: 'auto' }}>\n      \u003Cbutton onClick={lock}>Lock\u003C\u002Fbutton>\n      \u003Cbutton onClick={unlock}>Unlock\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  )\n}\n\ntest('sets overflow hidden on lock', async () => {\n  const user = userEvent.setup()\n  render(\u003CTestComponent \u002F>)\n  await user.click(screen.getByRole('button', { name: \u002Flock\u002Fi }))\n  \u002F\u002F Assert DOM side-effect\n  expect(screen.getByRole('button', { name: \u002Flock\u002Fi }).closest('div'))\n    .toHaveStyle({ overflow: 'hidden' })\n})\n```\n\n**Rule of thumb:** When a hook needs a real DOM node (for refs,\nresize observers, intersection observers), write a small wrapper\ncomponent in the test file rather than forcing `renderHook` to do it.\n",{"id":313,"difficulty":34,"q":314,"a":315},"hook-cleanup-test","How do you verify that a custom hook properly cleans up its effects on unmount?","Call `unmount()` from the `renderHook` result, then assert that side\neffects have been reversed — event listeners removed, timers cleared,\nsubscriptions cancelled.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport { useWindowResize } from '.\u002FuseWindowResize'\n\ntest('removes event listener on unmount', () => {\n  const addSpy = vi.spyOn(window, 'addEventListener')\n  const removeSpy = vi.spyOn(window, 'removeEventListener')\n\n  const { unmount } = renderHook(() => useWindowResize(() => {}))\n\n  expect(addSpy).toHaveBeenCalledWith('resize', expect.any(Function))\n\n  unmount()\n\n  \u002F\u002F Same handler that was added should be removed\n  expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function))\n\n  addSpy.mockRestore()\n  removeSpy.mockRestore()\n})\n\ntest('cancels pending subscription on unmount', () => {\n  const unsubscribe = vi.fn()\n  const subscribe = vi.fn().mockReturnValue(unsubscribe)\n\n  const { unmount } = renderHook(() => useStoreSubscription(subscribe))\n\n  expect(subscribe).toHaveBeenCalledTimes(1)\n  unmount()\n  expect(unsubscribe).toHaveBeenCalledTimes(1)\n})\n```\n\nCleanup tests matter because leaking subscriptions or listeners cause:\n- Memory leaks in production.\n- \"Can't perform state update on unmounted component\" warnings.\n- State updates firing after the component is gone (act warnings in tests).\n\n**Rule of thumb:** Write an unmount cleanup test for every hook that\nregisters event listeners, starts timers, or opens subscriptions.\n",{"id":317,"difficulty":34,"q":318,"a":319},"hook-error-test","How do you test that a hook throws an error under invalid conditions?","Wrap the `renderHook` call in a `try\u002Fcatch` or use\n`expect(() => ...).toThrow()`. Because React catches errors during\nrender and logs them, suppress `console.error` too.\n\n```js\nimport { renderHook } from '@testing-library\u002Freact'\nimport { vi } from 'vitest'\nimport { useRequiredContext } from '.\u002FuseRequiredContext'\n\ntest('throws when used outside provider', () => {\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  expect(() => renderHook(() => useRequiredContext())).toThrow(\n    'useRequiredContext must be used inside RequiredContextProvider'\n  )\n\n  consoleSpy.mockRestore()\n})\n```\n\nFor hooks that validate their arguments:\n```js\ntest('throws for negative page size', () => {\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  expect(() =>\n    renderHook(() => usePagination({ page: 1, pageSize: -1, total: 100 }))\n  ).toThrow('pageSize must be positive')\n\n  consoleSpy.mockRestore()\n})\n```\n\nNote: React 18 with concurrent mode may not throw immediately on the\nfirst render — if you find that `expect(...).toThrow()` doesn't catch\nthe error, use an Error Boundary wrapper instead.\n\n**Rule of thumb:** Test the error guard condition (missing provider,\ninvalid args) so users get a clear message in development. Suppress\n`console.error` in these tests to keep output clean.\n",{"description":32},"Testing custom React hooks interview questions — renderHook, act, async hooks, context-dependent hooks, hook cleanup, refs, when to test hooks directly vs through components, and mocking hook dependencies.","react\u002Ftesting\u002Ftesting-custom-hooks","e-_K5wqYhGwL-YnIKLEUSaPIEC5MDbvOACpbC3E5Qr4",1782244096734]