[{"data":1,"prerenderedAt":4006},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-testing-custom-hooks-guide":3},{"id":4,"title":5,"body":6,"description":3991,"difficulty":3992,"extension":3993,"framework":3994,"frameworkSlug":3995,"meta":3996,"navigation":92,"order":96,"path":3997,"qaPath":3998,"seo":3999,"stem":4000,"subtopic":4001,"topic":4002,"topicSlug":4003,"updated":4004,"__hash__":4005},"blog\u002Fblog\u002Freact-testing-custom-hooks-guide.md","Testing React Custom Hooks — Complete Guide",{"type":7,"value":8,"toc":3968},"minimark",[9,14,18,26,32,45,225,236,241,252,361,368,377,529,532,576,583,587,594,1054,1064,1068,1078,1495,1498,1506,1515,1819,1823,1826,2271,2274,2281,2284,2554,2557,2669,2673,2832,2836,2842,3130,3134,3139,3289,3294,3298,3305,3481,3484,3499,3503,3506,3512,3523,3637,3642,3656,3778,3781,3785,3795,3893,3897,3900,3951,3964],[10,11,13],"h2",{"id":12},"why-custom-hooks-need-their-own-tests","Why custom hooks need their own tests",[15,16,17],"p",{},"When you extract logic into a custom hook, you've created a reusable unit with\nits own API contract: the arguments it accepts, the values it returns, and the\nside effects it produces. That contract deserves tests independent of any\nparticular UI that uses the hook.",[15,19,20,21,25],{},"Without direct hook tests you have two bad options: write one massive component\ntest that exercises every edge case through the UI (slow, hard to read), or\nskip edge cases and only test the golden path (dangerous). ",[22,23,24],"code",{},"renderHook"," gives\nyou a third option: test the hook directly, at the granularity it deserves.",[10,27,29,31],{"id":28},"renderhook-the-core-api",[22,30,24],{}," — the core API",[15,33,34,36,37,40,41,44],{},[22,35,24],{}," from ",[22,38,39],{},"@testing-library\u002Freact"," mounts a minimal wrapper component\nthat calls the hook under test and exposes its return value through a ",[22,42,43],{},"result","\nobject.",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-js shiki shiki-themes github-light github-dark","import { renderHook } from '@testing-library\u002Freact'\nimport { useCounter } from '.\u002FuseCounter'\n\ntest('initializes with given count', () => {\n  const { result } = renderHook(() => useCounter(10))\n\n  expect(result.current.count).toBe(10)\n  expect(typeof result.current.increment).toBe('function')\n  expect(typeof result.current.decrement).toBe('function')\n})\n","js","",[22,53,54,74,87,94,116,153,158,177,199,219],{"__ignoreMap":51},[55,56,59,63,67,70],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"szBVR","import",[55,64,66],{"class":65},"sVt8B"," { renderHook } ",[55,68,69],{"class":61},"from",[55,71,73],{"class":72},"sZZnC"," '@testing-library\u002Freact'\n",[55,75,77,79,82,84],{"class":57,"line":76},2,[55,78,62],{"class":61},[55,80,81],{"class":65}," { useCounter } ",[55,83,69],{"class":61},[55,85,86],{"class":72}," '.\u002FuseCounter'\n",[55,88,90],{"class":57,"line":89},3,[55,91,93],{"emptyLinePlaceholder":92},true,"\n",[55,95,97,101,104,107,110,113],{"class":57,"line":96},4,[55,98,100],{"class":99},"sScJk","test",[55,102,103],{"class":65},"(",[55,105,106],{"class":72},"'initializes with given count'",[55,108,109],{"class":65},", () ",[55,111,112],{"class":61},"=>",[55,114,115],{"class":65}," {\n",[55,117,119,122,125,128,131,134,137,140,142,145,147,150],{"class":57,"line":118},5,[55,120,121],{"class":61},"  const",[55,123,124],{"class":65}," { ",[55,126,43],{"class":127},"sj4cs",[55,129,130],{"class":65}," } ",[55,132,133],{"class":61},"=",[55,135,136],{"class":99}," renderHook",[55,138,139],{"class":65},"(() ",[55,141,112],{"class":61},[55,143,144],{"class":99}," useCounter",[55,146,103],{"class":65},[55,148,149],{"class":127},"10",[55,151,152],{"class":65},"))\n",[55,154,156],{"class":57,"line":155},6,[55,157,93],{"emptyLinePlaceholder":92},[55,159,161,164,167,170,172,174],{"class":57,"line":160},7,[55,162,163],{"class":99},"  expect",[55,165,166],{"class":65},"(result.current.count).",[55,168,169],{"class":99},"toBe",[55,171,103],{"class":65},[55,173,149],{"class":127},[55,175,176],{"class":65},")\n",[55,178,180,182,184,187,190,192,194,197],{"class":57,"line":179},8,[55,181,163],{"class":99},[55,183,103],{"class":65},[55,185,186],{"class":61},"typeof",[55,188,189],{"class":65}," result.current.increment).",[55,191,169],{"class":99},[55,193,103],{"class":65},[55,195,196],{"class":72},"'function'",[55,198,176],{"class":65},[55,200,202,204,206,208,211,213,215,217],{"class":57,"line":201},9,[55,203,163],{"class":99},[55,205,103],{"class":65},[55,207,186],{"class":61},[55,209,210],{"class":65}," result.current.decrement).",[55,212,169],{"class":99},[55,214,103],{"class":65},[55,216,196],{"class":72},[55,218,176],{"class":65},[55,220,222],{"class":57,"line":221},10,[55,223,224],{"class":65},"})\n",[15,226,227,230,231,235],{},[22,228,229],{},"result.current"," always holds the ",[232,233,234],"em",{},"latest"," return value from the hook. It\nupdates whenever the hook causes a state change.",[237,238,240],"h3",{"id":239},"testing-with-initial-props-and-prop-changes","Testing with initial props and prop changes",[15,242,243,244,247,248,251],{},"Use ",[22,245,246],{},"initialProps"," to pass constructor arguments, and ",[22,249,250],{},"rerender"," to test how\nthe hook responds when its arguments change:",[46,253,255],{"className":48,"code":254,"language":50,"meta":51,"style":51},"const { result, rerender } = renderHook(\n  ({ step }) => useCounter(0, step),\n  { initialProps: { step: 1 } }\n)\n\nexpect(result.current.count).toBe(0)\n\nrerender({ step: 5 })\n\u002F\u002F Hook now has a different step value\n",[22,256,257,280,304,315,319,323,338,342,355],{"__ignoreMap":51},[55,258,259,262,264,266,269,271,273,275,277],{"class":57,"line":58},[55,260,261],{"class":61},"const",[55,263,124],{"class":65},[55,265,43],{"class":127},[55,267,268],{"class":65},", ",[55,270,250],{"class":127},[55,272,130],{"class":65},[55,274,133],{"class":61},[55,276,136],{"class":99},[55,278,279],{"class":65},"(\n",[55,281,282,285,289,292,294,296,298,301],{"class":57,"line":76},[55,283,284],{"class":65},"  ({ ",[55,286,288],{"class":287},"s4XuR","step",[55,290,291],{"class":65}," }) ",[55,293,112],{"class":61},[55,295,144],{"class":99},[55,297,103],{"class":65},[55,299,300],{"class":127},"0",[55,302,303],{"class":65},", step),\n",[55,305,306,309,312],{"class":57,"line":89},[55,307,308],{"class":65},"  { initialProps: { step: ",[55,310,311],{"class":127},"1",[55,313,314],{"class":65}," } }\n",[55,316,317],{"class":57,"line":96},[55,318,176],{"class":65},[55,320,321],{"class":57,"line":118},[55,322,93],{"emptyLinePlaceholder":92},[55,324,325,328,330,332,334,336],{"class":57,"line":155},[55,326,327],{"class":99},"expect",[55,329,166],{"class":65},[55,331,169],{"class":99},[55,333,103],{"class":65},[55,335,300],{"class":127},[55,337,176],{"class":65},[55,339,340],{"class":57,"line":160},[55,341,93],{"emptyLinePlaceholder":92},[55,343,344,346,349,352],{"class":57,"line":179},[55,345,250],{"class":99},[55,347,348],{"class":65},"({ step: ",[55,350,351],{"class":127},"5",[55,353,354],{"class":65}," })\n",[55,356,357],{"class":57,"line":201},[55,358,360],{"class":359},"sJ8bj","\u002F\u002F Hook now has a different step value\n",[10,362,364,367],{"id":363},"act-the-state-update-wrapper",[22,365,366],{},"act()"," — the state-update wrapper",[15,369,370,371,373,374,376],{},"Any call that causes a state update inside ",[22,372,24],{}," must be wrapped in\n",[22,375,366],{},". This tells React to flush the update before assertions run.",[46,378,380],{"className":48,"code":379,"language":50,"meta":51,"style":51},"import { renderHook, act } from '@testing-library\u002Freact'\nimport { useToggle } from '.\u002FuseToggle'\n\ntest('toggles boolean state', () => {\n  const { result } = renderHook(() => useToggle(false))\n\n  expect(result.current.isOn).toBe(false)\n\n  act(() => {\n    result.current.toggle()\n  })\n\n  expect(result.current.isOn).toBe(true)\n})\n",[22,381,382,393,405,409,424,452,456,471,475,486,497,503,508,524],{"__ignoreMap":51},[55,383,384,386,389,391],{"class":57,"line":58},[55,385,62],{"class":61},[55,387,388],{"class":65}," { renderHook, act } ",[55,390,69],{"class":61},[55,392,73],{"class":72},[55,394,395,397,400,402],{"class":57,"line":76},[55,396,62],{"class":61},[55,398,399],{"class":65}," { useToggle } ",[55,401,69],{"class":61},[55,403,404],{"class":72}," '.\u002FuseToggle'\n",[55,406,407],{"class":57,"line":89},[55,408,93],{"emptyLinePlaceholder":92},[55,410,411,413,415,418,420,422],{"class":57,"line":96},[55,412,100],{"class":99},[55,414,103],{"class":65},[55,416,417],{"class":72},"'toggles boolean state'",[55,419,109],{"class":65},[55,421,112],{"class":61},[55,423,115],{"class":65},[55,425,426,428,430,432,434,436,438,440,442,445,447,450],{"class":57,"line":118},[55,427,121],{"class":61},[55,429,124],{"class":65},[55,431,43],{"class":127},[55,433,130],{"class":65},[55,435,133],{"class":61},[55,437,136],{"class":99},[55,439,139],{"class":65},[55,441,112],{"class":61},[55,443,444],{"class":99}," useToggle",[55,446,103],{"class":65},[55,448,449],{"class":127},"false",[55,451,152],{"class":65},[55,453,454],{"class":57,"line":155},[55,455,93],{"emptyLinePlaceholder":92},[55,457,458,460,463,465,467,469],{"class":57,"line":160},[55,459,163],{"class":99},[55,461,462],{"class":65},"(result.current.isOn).",[55,464,169],{"class":99},[55,466,103],{"class":65},[55,468,449],{"class":127},[55,470,176],{"class":65},[55,472,473],{"class":57,"line":179},[55,474,93],{"emptyLinePlaceholder":92},[55,476,477,480,482,484],{"class":57,"line":201},[55,478,479],{"class":99},"  act",[55,481,139],{"class":65},[55,483,112],{"class":61},[55,485,115],{"class":65},[55,487,488,491,494],{"class":57,"line":221},[55,489,490],{"class":65},"    result.current.",[55,492,493],{"class":99},"toggle",[55,495,496],{"class":65},"()\n",[55,498,500],{"class":57,"line":499},11,[55,501,502],{"class":65},"  })\n",[55,504,506],{"class":57,"line":505},12,[55,507,93],{"emptyLinePlaceholder":92},[55,509,511,513,515,517,519,522],{"class":57,"line":510},13,[55,512,163],{"class":99},[55,514,462],{"class":65},[55,516,169],{"class":99},[55,518,103],{"class":65},[55,520,521],{"class":127},"true",[55,523,176],{"class":65},[55,525,527],{"class":57,"line":526},14,[55,528,224],{"class":65},[15,530,531],{},"For async state updates:",[46,533,535],{"className":48,"code":534,"language":50,"meta":51,"style":51},"await act(async () => {\n  result.current.fetchData()\n})\n\u002F\u002F All state updates from the async operation are flushed\n",[22,536,537,557,567,571],{"__ignoreMap":51},[55,538,539,542,545,547,550,553,555],{"class":57,"line":58},[55,540,541],{"class":61},"await",[55,543,544],{"class":99}," act",[55,546,103],{"class":65},[55,548,549],{"class":61},"async",[55,551,552],{"class":65}," () ",[55,554,112],{"class":61},[55,556,115],{"class":65},[55,558,559,562,565],{"class":57,"line":76},[55,560,561],{"class":65},"  result.current.",[55,563,564],{"class":99},"fetchData",[55,566,496],{"class":65},[55,568,569],{"class":57,"line":89},[55,570,224],{"class":65},[55,572,573],{"class":57,"line":96},[55,574,575],{"class":359},"\u002F\u002F All state updates from the async operation are flushed\n",[15,577,578,579,582],{},"You'll see the \"not wrapped in act\" warning whenever a state update escapes\ntest control. The fix is always to find the trigger and wrap it in ",[22,580,581],{},"act",".",[10,584,586],{"id":585},"testing-async-hooks","Testing async hooks",[15,588,589,590,593],{},"Async hooks (those that fetch data or run async operations) use the same\nMSW + ",[22,591,592],{},"waitFor"," pattern as component tests:",[46,595,597],{"className":48,"code":596,"language":50,"meta":51,"style":51},"import { renderHook, waitFor } from '@testing-library\u002Freact'\nimport { server } from '..\u002Fmocks\u002Fserver'\nimport { http, HttpResponse } from 'msw'\nimport { useFetchUser } from '.\u002FuseFetchUser'\n\ntest('loading → data transition', async () => {\n  server.use(\n    http.get('\u002Fapi\u002Fusers\u002F1', () =>\n      HttpResponse.json({ id: 1, name: 'Alice', role: 'admin' })\n    )\n  )\n\n  const { result } = renderHook(() => useFetchUser(1))\n\n  \u002F\u002F Immediately after mount — loading\n  expect(result.current.loading).toBe(true)\n  expect(result.current.user).toBeNull()\n\n  \u002F\u002F Wait for the async update\n  await waitFor(() => expect(result.current.loading).toBe(false))\n\n  expect(result.current.user).toEqual({ id: 1, name: 'Alice', role: 'admin' })\n  expect(result.current.error).toBeNull()\n})\n\ntest('loading → error transition', 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",[22,598,599,610,622,634,646,650,669,679,697,724,729,734,738,765,769,775,791,804,809,815,841,846,870,882,887,892,912,921,956,961,966,993,998,1021,1026,1038,1049],{"__ignoreMap":51},[55,600,601,603,606,608],{"class":57,"line":58},[55,602,62],{"class":61},[55,604,605],{"class":65}," { renderHook, waitFor } ",[55,607,69],{"class":61},[55,609,73],{"class":72},[55,611,612,614,617,619],{"class":57,"line":76},[55,613,62],{"class":61},[55,615,616],{"class":65}," { server } ",[55,618,69],{"class":61},[55,620,621],{"class":72}," '..\u002Fmocks\u002Fserver'\n",[55,623,624,626,629,631],{"class":57,"line":89},[55,625,62],{"class":61},[55,627,628],{"class":65}," { http, HttpResponse } ",[55,630,69],{"class":61},[55,632,633],{"class":72}," 'msw'\n",[55,635,636,638,641,643],{"class":57,"line":96},[55,637,62],{"class":61},[55,639,640],{"class":65}," { useFetchUser } ",[55,642,69],{"class":61},[55,644,645],{"class":72}," '.\u002FuseFetchUser'\n",[55,647,648],{"class":57,"line":118},[55,649,93],{"emptyLinePlaceholder":92},[55,651,652,654,656,659,661,663,665,667],{"class":57,"line":155},[55,653,100],{"class":99},[55,655,103],{"class":65},[55,657,658],{"class":72},"'loading → data transition'",[55,660,268],{"class":65},[55,662,549],{"class":61},[55,664,552],{"class":65},[55,666,112],{"class":61},[55,668,115],{"class":65},[55,670,671,674,677],{"class":57,"line":160},[55,672,673],{"class":65},"  server.",[55,675,676],{"class":99},"use",[55,678,279],{"class":65},[55,680,681,684,687,689,692,694],{"class":57,"line":179},[55,682,683],{"class":65},"    http.",[55,685,686],{"class":99},"get",[55,688,103],{"class":65},[55,690,691],{"class":72},"'\u002Fapi\u002Fusers\u002F1'",[55,693,109],{"class":65},[55,695,696],{"class":61},"=>\n",[55,698,699,702,705,708,710,713,716,719,722],{"class":57,"line":201},[55,700,701],{"class":65},"      HttpResponse.",[55,703,704],{"class":99},"json",[55,706,707],{"class":65},"({ id: ",[55,709,311],{"class":127},[55,711,712],{"class":65},", name: ",[55,714,715],{"class":72},"'Alice'",[55,717,718],{"class":65},", role: ",[55,720,721],{"class":72},"'admin'",[55,723,354],{"class":65},[55,725,726],{"class":57,"line":221},[55,727,728],{"class":65},"    )\n",[55,730,731],{"class":57,"line":499},[55,732,733],{"class":65},"  )\n",[55,735,736],{"class":57,"line":505},[55,737,93],{"emptyLinePlaceholder":92},[55,739,740,742,744,746,748,750,752,754,756,759,761,763],{"class":57,"line":510},[55,741,121],{"class":61},[55,743,124],{"class":65},[55,745,43],{"class":127},[55,747,130],{"class":65},[55,749,133],{"class":61},[55,751,136],{"class":99},[55,753,139],{"class":65},[55,755,112],{"class":61},[55,757,758],{"class":99}," useFetchUser",[55,760,103],{"class":65},[55,762,311],{"class":127},[55,764,152],{"class":65},[55,766,767],{"class":57,"line":526},[55,768,93],{"emptyLinePlaceholder":92},[55,770,772],{"class":57,"line":771},15,[55,773,774],{"class":359},"  \u002F\u002F Immediately after mount — loading\n",[55,776,778,780,783,785,787,789],{"class":57,"line":777},16,[55,779,163],{"class":99},[55,781,782],{"class":65},"(result.current.loading).",[55,784,169],{"class":99},[55,786,103],{"class":65},[55,788,521],{"class":127},[55,790,176],{"class":65},[55,792,794,796,799,802],{"class":57,"line":793},17,[55,795,163],{"class":99},[55,797,798],{"class":65},"(result.current.user).",[55,800,801],{"class":99},"toBeNull",[55,803,496],{"class":65},[55,805,807],{"class":57,"line":806},18,[55,808,93],{"emptyLinePlaceholder":92},[55,810,812],{"class":57,"line":811},19,[55,813,814],{"class":359},"  \u002F\u002F Wait for the async update\n",[55,816,818,821,824,826,828,831,833,835,837,839],{"class":57,"line":817},20,[55,819,820],{"class":61},"  await",[55,822,823],{"class":99}," waitFor",[55,825,139],{"class":65},[55,827,112],{"class":61},[55,829,830],{"class":99}," expect",[55,832,782],{"class":65},[55,834,169],{"class":99},[55,836,103],{"class":65},[55,838,449],{"class":127},[55,840,152],{"class":65},[55,842,844],{"class":57,"line":843},21,[55,845,93],{"emptyLinePlaceholder":92},[55,847,849,851,853,856,858,860,862,864,866,868],{"class":57,"line":848},22,[55,850,163],{"class":99},[55,852,798],{"class":65},[55,854,855],{"class":99},"toEqual",[55,857,707],{"class":65},[55,859,311],{"class":127},[55,861,712],{"class":65},[55,863,715],{"class":72},[55,865,718],{"class":65},[55,867,721],{"class":72},[55,869,354],{"class":65},[55,871,873,875,878,880],{"class":57,"line":872},23,[55,874,163],{"class":99},[55,876,877],{"class":65},"(result.current.error).",[55,879,801],{"class":99},[55,881,496],{"class":65},[55,883,885],{"class":57,"line":884},24,[55,886,224],{"class":65},[55,888,890],{"class":57,"line":889},25,[55,891,93],{"emptyLinePlaceholder":92},[55,893,895,897,899,902,904,906,908,910],{"class":57,"line":894},26,[55,896,100],{"class":99},[55,898,103],{"class":65},[55,900,901],{"class":72},"'loading → error transition'",[55,903,268],{"class":65},[55,905,549],{"class":61},[55,907,552],{"class":65},[55,909,112],{"class":61},[55,911,115],{"class":65},[55,913,915,917,919],{"class":57,"line":914},27,[55,916,673],{"class":65},[55,918,676],{"class":99},[55,920,279],{"class":65},[55,922,924,926,928,930,932,934,936,939,942,944,947,950,953],{"class":57,"line":923},28,[55,925,683],{"class":65},[55,927,686],{"class":99},[55,929,103],{"class":65},[55,931,691],{"class":72},[55,933,109],{"class":65},[55,935,112],{"class":61},[55,937,938],{"class":61}," new",[55,940,941],{"class":99}," HttpResponse",[55,943,103],{"class":65},[55,945,946],{"class":127},"null",[55,948,949],{"class":65},", { status: ",[55,951,952],{"class":127},"500",[55,954,955],{"class":65}," }))\n",[55,957,959],{"class":57,"line":958},29,[55,960,733],{"class":65},[55,962,964],{"class":57,"line":963},30,[55,965,93],{"emptyLinePlaceholder":92},[55,967,969,971,973,975,977,979,981,983,985,987,989,991],{"class":57,"line":968},31,[55,970,121],{"class":61},[55,972,124],{"class":65},[55,974,43],{"class":127},[55,976,130],{"class":65},[55,978,133],{"class":61},[55,980,136],{"class":99},[55,982,139],{"class":65},[55,984,112],{"class":61},[55,986,758],{"class":99},[55,988,103],{"class":65},[55,990,311],{"class":127},[55,992,152],{"class":65},[55,994,996],{"class":57,"line":995},32,[55,997,93],{"emptyLinePlaceholder":92},[55,999,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019],{"class":57,"line":1000},33,[55,1002,820],{"class":61},[55,1004,823],{"class":99},[55,1006,139],{"class":65},[55,1008,112],{"class":61},[55,1010,830],{"class":99},[55,1012,782],{"class":65},[55,1014,169],{"class":99},[55,1016,103],{"class":65},[55,1018,449],{"class":127},[55,1020,152],{"class":65},[55,1022,1024],{"class":57,"line":1023},34,[55,1025,93],{"emptyLinePlaceholder":92},[55,1027,1029,1031,1033,1036],{"class":57,"line":1028},35,[55,1030,163],{"class":99},[55,1032,877],{"class":65},[55,1034,1035],{"class":99},"toBeTruthy",[55,1037,496],{"class":65},[55,1039,1041,1043,1045,1047],{"class":57,"line":1040},36,[55,1042,163],{"class":99},[55,1044,798],{"class":65},[55,1046,801],{"class":99},[55,1048,496],{"class":65},[55,1050,1052],{"class":57,"line":1051},37,[55,1053,224],{"class":65},[15,1055,243,1056,1059,1060,1063],{},[22,1057,1058],{},"waitFor(() => expect(...))"," rather than ",[22,1061,1062],{},"findBy*"," — there's no DOM\nto find; you're waiting for hook state values to change.",[10,1065,1067],{"id":1066},"testing-context-dependent-hooks","Testing context-dependent hooks",[15,1069,1070,1071,1074,1075,1077],{},"Pass a ",[22,1072,1073],{},"wrapper"," option to ",[22,1076,24],{}," to provide the required context:",[46,1079,1081],{"className":48,"code":1080,"language":50,"meta":51,"style":51},"import { renderHook } from '@testing-library\u002Freact'\nimport { AuthProvider } from '.\u002FAuthContext'\nimport { useAuth } from '.\u002FuseAuth'\n\nfunction createWrapper(user) {\n  return function Wrapper({ children }) {\n    return \u003CAuthProvider initialUser={user}>{children}\u003C\u002FAuthProvider>\n  }\n}\n\ntest('returns authenticated user from context', () => {\n  const { result } = renderHook(() => useAuth(), {\n    wrapper: createWrapper({ id: 1, name: 'Alice', role: 'admin' }),\n  })\n\n  expect(result.current.user.name).toBe('Alice')\n  expect(result.current.isAuthenticated).toBe(true)\n})\n\ntest('returns null when no user', () => {\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 outside provider', () => {\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n  expect(() => renderHook(() => useAuth())).toThrow(\u002Fmust be used within AuthProvider\u002Fi)\n  consoleSpy.mockRestore()\n})\n",[22,1082,1083,1093,1105,1117,1121,1137,1157,1181,1186,1191,1195,1210,1234,1257,1261,1265,1280,1295,1299,1303,1318,1340,1353,1357,1361,1371,1385,1389,1393,1408,1443,1481,1491],{"__ignoreMap":51},[55,1084,1085,1087,1089,1091],{"class":57,"line":58},[55,1086,62],{"class":61},[55,1088,66],{"class":65},[55,1090,69],{"class":61},[55,1092,73],{"class":72},[55,1094,1095,1097,1100,1102],{"class":57,"line":76},[55,1096,62],{"class":61},[55,1098,1099],{"class":65}," { AuthProvider } ",[55,1101,69],{"class":61},[55,1103,1104],{"class":72}," '.\u002FAuthContext'\n",[55,1106,1107,1109,1112,1114],{"class":57,"line":89},[55,1108,62],{"class":61},[55,1110,1111],{"class":65}," { useAuth } ",[55,1113,69],{"class":61},[55,1115,1116],{"class":72}," '.\u002FuseAuth'\n",[55,1118,1119],{"class":57,"line":96},[55,1120,93],{"emptyLinePlaceholder":92},[55,1122,1123,1126,1129,1131,1134],{"class":57,"line":118},[55,1124,1125],{"class":61},"function",[55,1127,1128],{"class":99}," createWrapper",[55,1130,103],{"class":65},[55,1132,1133],{"class":287},"user",[55,1135,1136],{"class":65},") {\n",[55,1138,1139,1142,1145,1148,1151,1154],{"class":57,"line":155},[55,1140,1141],{"class":61},"  return",[55,1143,1144],{"class":61}," function",[55,1146,1147],{"class":99}," Wrapper",[55,1149,1150],{"class":65},"({ ",[55,1152,1153],{"class":287},"children",[55,1155,1156],{"class":65}," }) {\n",[55,1158,1159,1162,1165,1168,1171,1173,1176,1178],{"class":57,"line":160},[55,1160,1161],{"class":61},"    return",[55,1163,1164],{"class":65}," \u003C",[55,1166,1167],{"class":127},"AuthProvider",[55,1169,1170],{"class":99}," initialUser",[55,1172,133],{"class":61},[55,1174,1175],{"class":65},"{user}>{children}\u003C\u002F",[55,1177,1167],{"class":127},[55,1179,1180],{"class":65},">\n",[55,1182,1183],{"class":57,"line":179},[55,1184,1185],{"class":65},"  }\n",[55,1187,1188],{"class":57,"line":201},[55,1189,1190],{"class":65},"}\n",[55,1192,1193],{"class":57,"line":221},[55,1194,93],{"emptyLinePlaceholder":92},[55,1196,1197,1199,1201,1204,1206,1208],{"class":57,"line":499},[55,1198,100],{"class":99},[55,1200,103],{"class":65},[55,1202,1203],{"class":72},"'returns authenticated user from context'",[55,1205,109],{"class":65},[55,1207,112],{"class":61},[55,1209,115],{"class":65},[55,1211,1212,1214,1216,1218,1220,1222,1224,1226,1228,1231],{"class":57,"line":505},[55,1213,121],{"class":61},[55,1215,124],{"class":65},[55,1217,43],{"class":127},[55,1219,130],{"class":65},[55,1221,133],{"class":61},[55,1223,136],{"class":99},[55,1225,139],{"class":65},[55,1227,112],{"class":61},[55,1229,1230],{"class":99}," useAuth",[55,1232,1233],{"class":65},"(), {\n",[55,1235,1236,1239,1242,1244,1246,1248,1250,1252,1254],{"class":57,"line":510},[55,1237,1238],{"class":65},"    wrapper: ",[55,1240,1241],{"class":99},"createWrapper",[55,1243,707],{"class":65},[55,1245,311],{"class":127},[55,1247,712],{"class":65},[55,1249,715],{"class":72},[55,1251,718],{"class":65},[55,1253,721],{"class":72},[55,1255,1256],{"class":65}," }),\n",[55,1258,1259],{"class":57,"line":526},[55,1260,502],{"class":65},[55,1262,1263],{"class":57,"line":771},[55,1264,93],{"emptyLinePlaceholder":92},[55,1266,1267,1269,1272,1274,1276,1278],{"class":57,"line":777},[55,1268,163],{"class":99},[55,1270,1271],{"class":65},"(result.current.user.name).",[55,1273,169],{"class":99},[55,1275,103],{"class":65},[55,1277,715],{"class":72},[55,1279,176],{"class":65},[55,1281,1282,1284,1287,1289,1291,1293],{"class":57,"line":793},[55,1283,163],{"class":99},[55,1285,1286],{"class":65},"(result.current.isAuthenticated).",[55,1288,169],{"class":99},[55,1290,103],{"class":65},[55,1292,521],{"class":127},[55,1294,176],{"class":65},[55,1296,1297],{"class":57,"line":806},[55,1298,224],{"class":65},[55,1300,1301],{"class":57,"line":811},[55,1302,93],{"emptyLinePlaceholder":92},[55,1304,1305,1307,1309,1312,1314,1316],{"class":57,"line":817},[55,1306,100],{"class":99},[55,1308,103],{"class":65},[55,1310,1311],{"class":72},"'returns null when no user'",[55,1313,109],{"class":65},[55,1315,112],{"class":61},[55,1317,115],{"class":65},[55,1319,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338],{"class":57,"line":843},[55,1321,121],{"class":61},[55,1323,124],{"class":65},[55,1325,43],{"class":127},[55,1327,130],{"class":65},[55,1329,133],{"class":61},[55,1331,136],{"class":99},[55,1333,139],{"class":65},[55,1335,112],{"class":61},[55,1337,1230],{"class":99},[55,1339,1233],{"class":65},[55,1341,1342,1344,1346,1348,1350],{"class":57,"line":848},[55,1343,1238],{"class":65},[55,1345,1241],{"class":99},[55,1347,103],{"class":65},[55,1349,946],{"class":127},[55,1351,1352],{"class":65},"),\n",[55,1354,1355],{"class":57,"line":872},[55,1356,502],{"class":65},[55,1358,1359],{"class":57,"line":884},[55,1360,93],{"emptyLinePlaceholder":92},[55,1362,1363,1365,1367,1369],{"class":57,"line":889},[55,1364,163],{"class":99},[55,1366,798],{"class":65},[55,1368,801],{"class":99},[55,1370,496],{"class":65},[55,1372,1373,1375,1377,1379,1381,1383],{"class":57,"line":894},[55,1374,163],{"class":99},[55,1376,1286],{"class":65},[55,1378,169],{"class":99},[55,1380,103],{"class":65},[55,1382,449],{"class":127},[55,1384,176],{"class":65},[55,1386,1387],{"class":57,"line":914},[55,1388,224],{"class":65},[55,1390,1391],{"class":57,"line":923},[55,1392,93],{"emptyLinePlaceholder":92},[55,1394,1395,1397,1399,1402,1404,1406],{"class":57,"line":958},[55,1396,100],{"class":99},[55,1398,103],{"class":65},[55,1400,1401],{"class":72},"'throws outside provider'",[55,1403,109],{"class":65},[55,1405,112],{"class":61},[55,1407,115],{"class":65},[55,1409,1410,1412,1415,1418,1421,1424,1427,1430,1433,1436,1438,1440],{"class":57,"line":963},[55,1411,121],{"class":61},[55,1413,1414],{"class":127}," consoleSpy",[55,1416,1417],{"class":61}," =",[55,1419,1420],{"class":65}," vi.",[55,1422,1423],{"class":99},"spyOn",[55,1425,1426],{"class":65},"(console, ",[55,1428,1429],{"class":72},"'error'",[55,1431,1432],{"class":65},").",[55,1434,1435],{"class":99},"mockImplementation",[55,1437,139],{"class":65},[55,1439,112],{"class":61},[55,1441,1442],{"class":65}," {})\n",[55,1444,1445,1447,1449,1451,1453,1455,1457,1459,1462,1465,1467,1470,1474,1476,1479],{"class":57,"line":968},[55,1446,163],{"class":99},[55,1448,139],{"class":65},[55,1450,112],{"class":61},[55,1452,136],{"class":99},[55,1454,139],{"class":65},[55,1456,112],{"class":61},[55,1458,1230],{"class":99},[55,1460,1461],{"class":65},"())).",[55,1463,1464],{"class":99},"toThrow",[55,1466,103],{"class":65},[55,1468,1469],{"class":72},"\u002F",[55,1471,1473],{"class":1472},"sA_wV","must be used within AuthProvider",[55,1475,1469],{"class":72},[55,1477,1478],{"class":61},"i",[55,1480,176],{"class":65},[55,1482,1483,1486,1489],{"class":57,"line":995},[55,1484,1485],{"class":65},"  consoleSpy.",[55,1487,1488],{"class":99},"mockRestore",[55,1490,496],{"class":65},[55,1492,1493],{"class":57,"line":1000},[55,1494,224],{"class":65},[15,1496,1497],{},"Always test the \"outside provider\" case — it verifies your guard condition\ngives developers a clear message in development.",[10,1499,1501,1502,1505],{"id":1500},"testing-useeffect-side-effects","Testing ",[22,1503,1504],{},"useEffect"," side effects",[15,1507,1508,1509,1511,1512,1514],{},"Effects run synchronously during ",[22,1510,24],{}," (RTL wraps it in ",[22,1513,581],{},"\nautomatically). Test initial effects, update-triggered effects, and cleanup.",[46,1516,1518],{"className":48,"code":1517,"language":50,"meta":51,"style":51},"import { renderHook } from '@testing-library\u002Freact'\nimport { useDocumentTitle } from '.\u002FuseDocumentTitle'\n\ntest('sets document title on mount', () => {\n  renderHook(() => useDocumentTitle('My Dashboard'))\n  expect(document.title).toBe('My Dashboard')\n})\n\ntest('updates title when value prop changes', () => {\n  const { rerender } = renderHook(\n    ({ title }) => useDocumentTitle(title),\n    { initialProps: { title: 'Page One' } }\n  )\n\n  expect(document.title).toBe('Page One')\n\n  rerender({ title: 'Page Two' })\n  expect(document.title).toBe('Page Two')\n})\n\ntest('restores original title on unmount', () => {\n  document.title = 'App'\n  const { unmount } = renderHook(() => useDocumentTitle('Temporary'))\n  expect(document.title).toBe('Temporary')\n  unmount()\n  expect(document.title).toBe('App')  \u002F\u002F restored by cleanup return\n})\n",[22,1519,1520,1530,1542,1546,1561,1580,1595,1599,1603,1618,1634,1651,1661,1665,1669,1683,1687,1700,1714,1718,1722,1737,1747,1775,1789,1796,1815],{"__ignoreMap":51},[55,1521,1522,1524,1526,1528],{"class":57,"line":58},[55,1523,62],{"class":61},[55,1525,66],{"class":65},[55,1527,69],{"class":61},[55,1529,73],{"class":72},[55,1531,1532,1534,1537,1539],{"class":57,"line":76},[55,1533,62],{"class":61},[55,1535,1536],{"class":65}," { useDocumentTitle } ",[55,1538,69],{"class":61},[55,1540,1541],{"class":72}," '.\u002FuseDocumentTitle'\n",[55,1543,1544],{"class":57,"line":89},[55,1545,93],{"emptyLinePlaceholder":92},[55,1547,1548,1550,1552,1555,1557,1559],{"class":57,"line":96},[55,1549,100],{"class":99},[55,1551,103],{"class":65},[55,1553,1554],{"class":72},"'sets document title on mount'",[55,1556,109],{"class":65},[55,1558,112],{"class":61},[55,1560,115],{"class":65},[55,1562,1563,1566,1568,1570,1573,1575,1578],{"class":57,"line":118},[55,1564,1565],{"class":99},"  renderHook",[55,1567,139],{"class":65},[55,1569,112],{"class":61},[55,1571,1572],{"class":99}," useDocumentTitle",[55,1574,103],{"class":65},[55,1576,1577],{"class":72},"'My Dashboard'",[55,1579,152],{"class":65},[55,1581,1582,1584,1587,1589,1591,1593],{"class":57,"line":155},[55,1583,163],{"class":99},[55,1585,1586],{"class":65},"(document.title).",[55,1588,169],{"class":99},[55,1590,103],{"class":65},[55,1592,1577],{"class":72},[55,1594,176],{"class":65},[55,1596,1597],{"class":57,"line":160},[55,1598,224],{"class":65},[55,1600,1601],{"class":57,"line":179},[55,1602,93],{"emptyLinePlaceholder":92},[55,1604,1605,1607,1609,1612,1614,1616],{"class":57,"line":201},[55,1606,100],{"class":99},[55,1608,103],{"class":65},[55,1610,1611],{"class":72},"'updates title when value prop changes'",[55,1613,109],{"class":65},[55,1615,112],{"class":61},[55,1617,115],{"class":65},[55,1619,1620,1622,1624,1626,1628,1630,1632],{"class":57,"line":221},[55,1621,121],{"class":61},[55,1623,124],{"class":65},[55,1625,250],{"class":127},[55,1627,130],{"class":65},[55,1629,133],{"class":61},[55,1631,136],{"class":99},[55,1633,279],{"class":65},[55,1635,1636,1639,1642,1644,1646,1648],{"class":57,"line":499},[55,1637,1638],{"class":65},"    ({ ",[55,1640,1641],{"class":287},"title",[55,1643,291],{"class":65},[55,1645,112],{"class":61},[55,1647,1572],{"class":99},[55,1649,1650],{"class":65},"(title),\n",[55,1652,1653,1656,1659],{"class":57,"line":505},[55,1654,1655],{"class":65},"    { initialProps: { title: ",[55,1657,1658],{"class":72},"'Page One'",[55,1660,314],{"class":65},[55,1662,1663],{"class":57,"line":510},[55,1664,733],{"class":65},[55,1666,1667],{"class":57,"line":526},[55,1668,93],{"emptyLinePlaceholder":92},[55,1670,1671,1673,1675,1677,1679,1681],{"class":57,"line":771},[55,1672,163],{"class":99},[55,1674,1586],{"class":65},[55,1676,169],{"class":99},[55,1678,103],{"class":65},[55,1680,1658],{"class":72},[55,1682,176],{"class":65},[55,1684,1685],{"class":57,"line":777},[55,1686,93],{"emptyLinePlaceholder":92},[55,1688,1689,1692,1695,1698],{"class":57,"line":793},[55,1690,1691],{"class":99},"  rerender",[55,1693,1694],{"class":65},"({ title: ",[55,1696,1697],{"class":72},"'Page Two'",[55,1699,354],{"class":65},[55,1701,1702,1704,1706,1708,1710,1712],{"class":57,"line":806},[55,1703,163],{"class":99},[55,1705,1586],{"class":65},[55,1707,169],{"class":99},[55,1709,103],{"class":65},[55,1711,1697],{"class":72},[55,1713,176],{"class":65},[55,1715,1716],{"class":57,"line":811},[55,1717,224],{"class":65},[55,1719,1720],{"class":57,"line":817},[55,1721,93],{"emptyLinePlaceholder":92},[55,1723,1724,1726,1728,1731,1733,1735],{"class":57,"line":843},[55,1725,100],{"class":99},[55,1727,103],{"class":65},[55,1729,1730],{"class":72},"'restores original title on unmount'",[55,1732,109],{"class":65},[55,1734,112],{"class":61},[55,1736,115],{"class":65},[55,1738,1739,1742,1744],{"class":57,"line":848},[55,1740,1741],{"class":65},"  document.title ",[55,1743,133],{"class":61},[55,1745,1746],{"class":72}," 'App'\n",[55,1748,1749,1751,1753,1756,1758,1760,1762,1764,1766,1768,1770,1773],{"class":57,"line":872},[55,1750,121],{"class":61},[55,1752,124],{"class":65},[55,1754,1755],{"class":127},"unmount",[55,1757,130],{"class":65},[55,1759,133],{"class":61},[55,1761,136],{"class":99},[55,1763,139],{"class":65},[55,1765,112],{"class":61},[55,1767,1572],{"class":99},[55,1769,103],{"class":65},[55,1771,1772],{"class":72},"'Temporary'",[55,1774,152],{"class":65},[55,1776,1777,1779,1781,1783,1785,1787],{"class":57,"line":884},[55,1778,163],{"class":99},[55,1780,1586],{"class":65},[55,1782,169],{"class":99},[55,1784,103],{"class":65},[55,1786,1772],{"class":72},[55,1788,176],{"class":65},[55,1790,1791,1794],{"class":57,"line":889},[55,1792,1793],{"class":99},"  unmount",[55,1795,496],{"class":65},[55,1797,1798,1800,1802,1804,1806,1809,1812],{"class":57,"line":894},[55,1799,163],{"class":99},[55,1801,1586],{"class":65},[55,1803,169],{"class":99},[55,1805,103],{"class":65},[55,1807,1808],{"class":72},"'App'",[55,1810,1811],{"class":65},")  ",[55,1813,1814],{"class":359},"\u002F\u002F restored by cleanup return\n",[55,1816,1817],{"class":57,"line":914},[55,1818,224],{"class":65},[10,1820,1822],{"id":1821},"testing-timer-hooks","Testing timer hooks",[15,1824,1825],{},"Fake timers make timer-based hooks testable without real waiting:",[46,1827,1829],{"className":48,"code":1828,"language":50,"meta":51,"style":51},"import { 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('initial', 300))\n    expect(result.current).toBe('initial')\n  })\n\n  test('delays update until timeout elapses', () => {\n    const { result, rerender } = renderHook(\n      ({ value }) => useDebounce(value, 300),\n      { initialProps: { value: 'first' } }\n    )\n\n    rerender({ value: 'second' })\n\n    \u002F\u002F Before timeout\n    act(() => vi.advanceTimersByTime(200))\n    expect(result.current).toBe('first')\n\n    \u002F\u002F After timeout\n    act(() => vi.advanceTimersByTime(100))\n    expect(result.current).toBe('second')\n  })\n\n  test('clears pending timer on unmount', () => {\n    const clearSpy = vi.spyOn(global, 'clearTimeout')\n    const { unmount } = renderHook(() => useDebounce('test', 300))\n    unmount()\n    expect(clearSpy).toHaveBeenCalled()\n  })\n})\n",[22,1830,1831,1841,1853,1865,1869,1885,1902,1918,1922,1938,1972,1988,1992,1996,2011,2031,2052,2062,2066,2070,2083,2087,2092,2113,2127,2131,2136,2155,2169,2173,2177,2192,2213,2244,2251,2263,2267],{"__ignoreMap":51},[55,1832,1833,1835,1837,1839],{"class":57,"line":58},[55,1834,62],{"class":61},[55,1836,388],{"class":65},[55,1838,69],{"class":61},[55,1840,73],{"class":72},[55,1842,1843,1845,1848,1850],{"class":57,"line":76},[55,1844,62],{"class":61},[55,1846,1847],{"class":65}," { vi } ",[55,1849,69],{"class":61},[55,1851,1852],{"class":72}," 'vitest'\n",[55,1854,1855,1857,1860,1862],{"class":57,"line":89},[55,1856,62],{"class":61},[55,1858,1859],{"class":65}," { useDebounce } ",[55,1861,69],{"class":61},[55,1863,1864],{"class":72}," '.\u002FuseDebounce'\n",[55,1866,1867],{"class":57,"line":96},[55,1868,93],{"emptyLinePlaceholder":92},[55,1870,1871,1874,1876,1879,1881,1883],{"class":57,"line":118},[55,1872,1873],{"class":99},"describe",[55,1875,103],{"class":65},[55,1877,1878],{"class":72},"'useDebounce'",[55,1880,109],{"class":65},[55,1882,112],{"class":61},[55,1884,115],{"class":65},[55,1886,1887,1890,1892,1894,1896,1899],{"class":57,"line":155},[55,1888,1889],{"class":99},"  beforeEach",[55,1891,139],{"class":65},[55,1893,112],{"class":61},[55,1895,1420],{"class":65},[55,1897,1898],{"class":99},"useFakeTimers",[55,1900,1901],{"class":65},"())\n",[55,1903,1904,1907,1909,1911,1913,1916],{"class":57,"line":160},[55,1905,1906],{"class":99},"  afterEach",[55,1908,139],{"class":65},[55,1910,112],{"class":61},[55,1912,1420],{"class":65},[55,1914,1915],{"class":99},"useRealTimers",[55,1917,1901],{"class":65},[55,1919,1920],{"class":57,"line":179},[55,1921,93],{"emptyLinePlaceholder":92},[55,1923,1924,1927,1929,1932,1934,1936],{"class":57,"line":201},[55,1925,1926],{"class":99},"  test",[55,1928,103],{"class":65},[55,1930,1931],{"class":72},"'returns initial value immediately'",[55,1933,109],{"class":65},[55,1935,112],{"class":61},[55,1937,115],{"class":65},[55,1939,1940,1943,1945,1947,1949,1951,1953,1955,1957,1960,1962,1965,1967,1970],{"class":57,"line":221},[55,1941,1942],{"class":61},"    const",[55,1944,124],{"class":65},[55,1946,43],{"class":127},[55,1948,130],{"class":65},[55,1950,133],{"class":61},[55,1952,136],{"class":99},[55,1954,139],{"class":65},[55,1956,112],{"class":61},[55,1958,1959],{"class":99}," useDebounce",[55,1961,103],{"class":65},[55,1963,1964],{"class":72},"'initial'",[55,1966,268],{"class":65},[55,1968,1969],{"class":127},"300",[55,1971,152],{"class":65},[55,1973,1974,1977,1980,1982,1984,1986],{"class":57,"line":499},[55,1975,1976],{"class":99},"    expect",[55,1978,1979],{"class":65},"(result.current).",[55,1981,169],{"class":99},[55,1983,103],{"class":65},[55,1985,1964],{"class":72},[55,1987,176],{"class":65},[55,1989,1990],{"class":57,"line":505},[55,1991,502],{"class":65},[55,1993,1994],{"class":57,"line":510},[55,1995,93],{"emptyLinePlaceholder":92},[55,1997,1998,2000,2002,2005,2007,2009],{"class":57,"line":526},[55,1999,1926],{"class":99},[55,2001,103],{"class":65},[55,2003,2004],{"class":72},"'delays update until timeout elapses'",[55,2006,109],{"class":65},[55,2008,112],{"class":61},[55,2010,115],{"class":65},[55,2012,2013,2015,2017,2019,2021,2023,2025,2027,2029],{"class":57,"line":771},[55,2014,1942],{"class":61},[55,2016,124],{"class":65},[55,2018,43],{"class":127},[55,2020,268],{"class":65},[55,2022,250],{"class":127},[55,2024,130],{"class":65},[55,2026,133],{"class":61},[55,2028,136],{"class":99},[55,2030,279],{"class":65},[55,2032,2033,2036,2039,2041,2043,2045,2048,2050],{"class":57,"line":777},[55,2034,2035],{"class":65},"      ({ ",[55,2037,2038],{"class":287},"value",[55,2040,291],{"class":65},[55,2042,112],{"class":61},[55,2044,1959],{"class":99},[55,2046,2047],{"class":65},"(value, ",[55,2049,1969],{"class":127},[55,2051,1352],{"class":65},[55,2053,2054,2057,2060],{"class":57,"line":793},[55,2055,2056],{"class":65},"      { initialProps: { value: ",[55,2058,2059],{"class":72},"'first'",[55,2061,314],{"class":65},[55,2063,2064],{"class":57,"line":806},[55,2065,728],{"class":65},[55,2067,2068],{"class":57,"line":811},[55,2069,93],{"emptyLinePlaceholder":92},[55,2071,2072,2075,2078,2081],{"class":57,"line":817},[55,2073,2074],{"class":99},"    rerender",[55,2076,2077],{"class":65},"({ value: ",[55,2079,2080],{"class":72},"'second'",[55,2082,354],{"class":65},[55,2084,2085],{"class":57,"line":843},[55,2086,93],{"emptyLinePlaceholder":92},[55,2088,2089],{"class":57,"line":848},[55,2090,2091],{"class":359},"    \u002F\u002F Before timeout\n",[55,2093,2094,2097,2099,2101,2103,2106,2108,2111],{"class":57,"line":872},[55,2095,2096],{"class":99},"    act",[55,2098,139],{"class":65},[55,2100,112],{"class":61},[55,2102,1420],{"class":65},[55,2104,2105],{"class":99},"advanceTimersByTime",[55,2107,103],{"class":65},[55,2109,2110],{"class":127},"200",[55,2112,152],{"class":65},[55,2114,2115,2117,2119,2121,2123,2125],{"class":57,"line":884},[55,2116,1976],{"class":99},[55,2118,1979],{"class":65},[55,2120,169],{"class":99},[55,2122,103],{"class":65},[55,2124,2059],{"class":72},[55,2126,176],{"class":65},[55,2128,2129],{"class":57,"line":889},[55,2130,93],{"emptyLinePlaceholder":92},[55,2132,2133],{"class":57,"line":894},[55,2134,2135],{"class":359},"    \u002F\u002F After timeout\n",[55,2137,2138,2140,2142,2144,2146,2148,2150,2153],{"class":57,"line":914},[55,2139,2096],{"class":99},[55,2141,139],{"class":65},[55,2143,112],{"class":61},[55,2145,1420],{"class":65},[55,2147,2105],{"class":99},[55,2149,103],{"class":65},[55,2151,2152],{"class":127},"100",[55,2154,152],{"class":65},[55,2156,2157,2159,2161,2163,2165,2167],{"class":57,"line":923},[55,2158,1976],{"class":99},[55,2160,1979],{"class":65},[55,2162,169],{"class":99},[55,2164,103],{"class":65},[55,2166,2080],{"class":72},[55,2168,176],{"class":65},[55,2170,2171],{"class":57,"line":958},[55,2172,502],{"class":65},[55,2174,2175],{"class":57,"line":963},[55,2176,93],{"emptyLinePlaceholder":92},[55,2178,2179,2181,2183,2186,2188,2190],{"class":57,"line":968},[55,2180,1926],{"class":99},[55,2182,103],{"class":65},[55,2184,2185],{"class":72},"'clears pending timer on unmount'",[55,2187,109],{"class":65},[55,2189,112],{"class":61},[55,2191,115],{"class":65},[55,2193,2194,2196,2199,2201,2203,2205,2208,2211],{"class":57,"line":995},[55,2195,1942],{"class":61},[55,2197,2198],{"class":127}," clearSpy",[55,2200,1417],{"class":61},[55,2202,1420],{"class":65},[55,2204,1423],{"class":99},[55,2206,2207],{"class":65},"(global, ",[55,2209,2210],{"class":72},"'clearTimeout'",[55,2212,176],{"class":65},[55,2214,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233,2235,2238,2240,2242],{"class":57,"line":1000},[55,2216,1942],{"class":61},[55,2218,124],{"class":65},[55,2220,1755],{"class":127},[55,2222,130],{"class":65},[55,2224,133],{"class":61},[55,2226,136],{"class":99},[55,2228,139],{"class":65},[55,2230,112],{"class":61},[55,2232,1959],{"class":99},[55,2234,103],{"class":65},[55,2236,2237],{"class":72},"'test'",[55,2239,268],{"class":65},[55,2241,1969],{"class":127},[55,2243,152],{"class":65},[55,2245,2246,2249],{"class":57,"line":1023},[55,2247,2248],{"class":99},"    unmount",[55,2250,496],{"class":65},[55,2252,2253,2255,2258,2261],{"class":57,"line":1028},[55,2254,1976],{"class":99},[55,2256,2257],{"class":65},"(clearSpy).",[55,2259,2260],{"class":99},"toHaveBeenCalled",[55,2262,496],{"class":65},[55,2264,2265],{"class":57,"line":1040},[55,2266,502],{"class":65},[55,2268,2269],{"class":57,"line":1051},[55,2270,224],{"class":65},[15,2272,2273],{},"Three tests for every timer hook: initial value, debounce behavior, and cleanup.",[10,2275,1501,2277,2280],{"id":2276},"testing-usereducer-based-hooks",[22,2278,2279],{},"useReducer","-based hooks",[15,2282,2283],{},"Test through the hook's public action API, not by inspecting the reducer:",[46,2285,2287],{"className":48,"code":2286,"language":50,"meta":51,"style":51},"import { renderHook, act } from '@testing-library\u002Freact'\nimport { useShoppingCart } from '.\u002FuseShoppingCart'\n\ntest('add and remove items', () => {\n  const { result } = renderHook(() => useShoppingCart())\n\n  expect(result.current.items).toHaveLength(0)\n\n  act(() => {\n    result.current.addItem({ id: 1, name: 'Widget', price: 9.99 })\n  })\n  act(() => {\n    result.current.addItem({ id: 2, name: 'Gadget', price: 24.99 })\n  })\n\n  expect(result.current.items).toHaveLength(2)\n  expect(result.current.total).toBeCloseTo(34.98)\n\n  act(() => {\n    result.current.removeItem(1)\n  })\n\n  expect(result.current.items).toHaveLength(1)\n  expect(result.current.total).toBeCloseTo(24.99)\n})\n",[22,2288,2289,2299,2311,2315,2330,2353,2357,2373,2377,2387,2411,2415,2425,2448,2452,2456,2470,2487,2491,2501,2514,2518,2522,2536,2550],{"__ignoreMap":51},[55,2290,2291,2293,2295,2297],{"class":57,"line":58},[55,2292,62],{"class":61},[55,2294,388],{"class":65},[55,2296,69],{"class":61},[55,2298,73],{"class":72},[55,2300,2301,2303,2306,2308],{"class":57,"line":76},[55,2302,62],{"class":61},[55,2304,2305],{"class":65}," { useShoppingCart } ",[55,2307,69],{"class":61},[55,2309,2310],{"class":72}," '.\u002FuseShoppingCart'\n",[55,2312,2313],{"class":57,"line":89},[55,2314,93],{"emptyLinePlaceholder":92},[55,2316,2317,2319,2321,2324,2326,2328],{"class":57,"line":96},[55,2318,100],{"class":99},[55,2320,103],{"class":65},[55,2322,2323],{"class":72},"'add and remove items'",[55,2325,109],{"class":65},[55,2327,112],{"class":61},[55,2329,115],{"class":65},[55,2331,2332,2334,2336,2338,2340,2342,2344,2346,2348,2351],{"class":57,"line":118},[55,2333,121],{"class":61},[55,2335,124],{"class":65},[55,2337,43],{"class":127},[55,2339,130],{"class":65},[55,2341,133],{"class":61},[55,2343,136],{"class":99},[55,2345,139],{"class":65},[55,2347,112],{"class":61},[55,2349,2350],{"class":99}," useShoppingCart",[55,2352,1901],{"class":65},[55,2354,2355],{"class":57,"line":155},[55,2356,93],{"emptyLinePlaceholder":92},[55,2358,2359,2361,2364,2367,2369,2371],{"class":57,"line":160},[55,2360,163],{"class":99},[55,2362,2363],{"class":65},"(result.current.items).",[55,2365,2366],{"class":99},"toHaveLength",[55,2368,103],{"class":65},[55,2370,300],{"class":127},[55,2372,176],{"class":65},[55,2374,2375],{"class":57,"line":179},[55,2376,93],{"emptyLinePlaceholder":92},[55,2378,2379,2381,2383,2385],{"class":57,"line":201},[55,2380,479],{"class":99},[55,2382,139],{"class":65},[55,2384,112],{"class":61},[55,2386,115],{"class":65},[55,2388,2389,2391,2394,2396,2398,2400,2403,2406,2409],{"class":57,"line":221},[55,2390,490],{"class":65},[55,2392,2393],{"class":99},"addItem",[55,2395,707],{"class":65},[55,2397,311],{"class":127},[55,2399,712],{"class":65},[55,2401,2402],{"class":72},"'Widget'",[55,2404,2405],{"class":65},", price: ",[55,2407,2408],{"class":127},"9.99",[55,2410,354],{"class":65},[55,2412,2413],{"class":57,"line":499},[55,2414,502],{"class":65},[55,2416,2417,2419,2421,2423],{"class":57,"line":505},[55,2418,479],{"class":99},[55,2420,139],{"class":65},[55,2422,112],{"class":61},[55,2424,115],{"class":65},[55,2426,2427,2429,2431,2433,2436,2438,2441,2443,2446],{"class":57,"line":510},[55,2428,490],{"class":65},[55,2430,2393],{"class":99},[55,2432,707],{"class":65},[55,2434,2435],{"class":127},"2",[55,2437,712],{"class":65},[55,2439,2440],{"class":72},"'Gadget'",[55,2442,2405],{"class":65},[55,2444,2445],{"class":127},"24.99",[55,2447,354],{"class":65},[55,2449,2450],{"class":57,"line":526},[55,2451,502],{"class":65},[55,2453,2454],{"class":57,"line":771},[55,2455,93],{"emptyLinePlaceholder":92},[55,2457,2458,2460,2462,2464,2466,2468],{"class":57,"line":777},[55,2459,163],{"class":99},[55,2461,2363],{"class":65},[55,2463,2366],{"class":99},[55,2465,103],{"class":65},[55,2467,2435],{"class":127},[55,2469,176],{"class":65},[55,2471,2472,2474,2477,2480,2482,2485],{"class":57,"line":793},[55,2473,163],{"class":99},[55,2475,2476],{"class":65},"(result.current.total).",[55,2478,2479],{"class":99},"toBeCloseTo",[55,2481,103],{"class":65},[55,2483,2484],{"class":127},"34.98",[55,2486,176],{"class":65},[55,2488,2489],{"class":57,"line":806},[55,2490,93],{"emptyLinePlaceholder":92},[55,2492,2493,2495,2497,2499],{"class":57,"line":811},[55,2494,479],{"class":99},[55,2496,139],{"class":65},[55,2498,112],{"class":61},[55,2500,115],{"class":65},[55,2502,2503,2505,2508,2510,2512],{"class":57,"line":817},[55,2504,490],{"class":65},[55,2506,2507],{"class":99},"removeItem",[55,2509,103],{"class":65},[55,2511,311],{"class":127},[55,2513,176],{"class":65},[55,2515,2516],{"class":57,"line":843},[55,2517,502],{"class":65},[55,2519,2520],{"class":57,"line":848},[55,2521,93],{"emptyLinePlaceholder":92},[55,2523,2524,2526,2528,2530,2532,2534],{"class":57,"line":872},[55,2525,163],{"class":99},[55,2527,2363],{"class":65},[55,2529,2366],{"class":99},[55,2531,103],{"class":65},[55,2533,311],{"class":127},[55,2535,176],{"class":65},[55,2537,2538,2540,2542,2544,2546,2548],{"class":57,"line":884},[55,2539,163],{"class":99},[55,2541,2476],{"class":65},[55,2543,2479],{"class":99},[55,2545,103],{"class":65},[55,2547,2445],{"class":127},[55,2549,176],{"class":65},[55,2551,2552],{"class":57,"line":889},[55,2553,224],{"class":65},[15,2555,2556],{},"Separately unit-test the reducer as a pure function:",[46,2558,2560],{"className":48,"code":2559,"language":50,"meta":51,"style":51},"import { cartReducer } from '.\u002FcartReducer'\n\ntest('ADD_ITEM increments quantity for existing item', () => {\n  const state = { items: [{ id: 1, qty: 1, price: 10 }] }\n  const result = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 1 } })\n  expect(result.items[0].qty).toBe(2)\n})\n",[22,2561,2562,2574,2578,2593,2619,2645,2665],{"__ignoreMap":51},[55,2563,2564,2566,2569,2571],{"class":57,"line":58},[55,2565,62],{"class":61},[55,2567,2568],{"class":65}," { cartReducer } ",[55,2570,69],{"class":61},[55,2572,2573],{"class":72}," '.\u002FcartReducer'\n",[55,2575,2576],{"class":57,"line":76},[55,2577,93],{"emptyLinePlaceholder":92},[55,2579,2580,2582,2584,2587,2589,2591],{"class":57,"line":89},[55,2581,100],{"class":99},[55,2583,103],{"class":65},[55,2585,2586],{"class":72},"'ADD_ITEM increments quantity for existing item'",[55,2588,109],{"class":65},[55,2590,112],{"class":61},[55,2592,115],{"class":65},[55,2594,2595,2597,2600,2602,2605,2607,2610,2612,2614,2616],{"class":57,"line":96},[55,2596,121],{"class":61},[55,2598,2599],{"class":127}," state",[55,2601,1417],{"class":61},[55,2603,2604],{"class":65}," { items: [{ id: ",[55,2606,311],{"class":127},[55,2608,2609],{"class":65},", qty: ",[55,2611,311],{"class":127},[55,2613,2405],{"class":65},[55,2615,149],{"class":127},[55,2617,2618],{"class":65}," }] }\n",[55,2620,2621,2623,2626,2628,2631,2634,2637,2640,2642],{"class":57,"line":118},[55,2622,121],{"class":61},[55,2624,2625],{"class":127}," result",[55,2627,1417],{"class":61},[55,2629,2630],{"class":99}," cartReducer",[55,2632,2633],{"class":65},"(state, { type: ",[55,2635,2636],{"class":72},"'ADD_ITEM'",[55,2638,2639],{"class":65},", payload: { id: ",[55,2641,311],{"class":127},[55,2643,2644],{"class":65}," } })\n",[55,2646,2647,2649,2652,2654,2657,2659,2661,2663],{"class":57,"line":155},[55,2648,163],{"class":99},[55,2650,2651],{"class":65},"(result.items[",[55,2653,300],{"class":127},[55,2655,2656],{"class":65},"].qty).",[55,2658,169],{"class":99},[55,2660,103],{"class":65},[55,2662,2435],{"class":127},[55,2664,176],{"class":65},[55,2666,2667],{"class":57,"line":160},[55,2668,224],{"class":65},[10,2670,2672],{"id":2671},"testing-hooks-with-arguments-prop-changes","Testing hooks with arguments (prop changes)",[46,2674,2676],{"className":48,"code":2675,"language":50,"meta":51,"style":51},"import { renderHook } from '@testing-library\u002Freact'\nimport { usePagination } from '.\u002FusePagination'\n\ntest('recalculates on pageSize change', () => {\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: 25 })\n  expect(result.current.totalPages).toBe(4)\n})\n",[22,2677,2678,2688,2700,2704,2719,2739,2765,2774,2778,2782,2797,2801,2813,2828],{"__ignoreMap":51},[55,2679,2680,2682,2684,2686],{"class":57,"line":58},[55,2681,62],{"class":61},[55,2683,66],{"class":65},[55,2685,69],{"class":61},[55,2687,73],{"class":72},[55,2689,2690,2692,2695,2697],{"class":57,"line":76},[55,2691,62],{"class":61},[55,2693,2694],{"class":65}," { usePagination } ",[55,2696,69],{"class":61},[55,2698,2699],{"class":72}," '.\u002FusePagination'\n",[55,2701,2702],{"class":57,"line":89},[55,2703,93],{"emptyLinePlaceholder":92},[55,2705,2706,2708,2710,2713,2715,2717],{"class":57,"line":96},[55,2707,100],{"class":99},[55,2709,103],{"class":65},[55,2711,2712],{"class":72},"'recalculates on pageSize change'",[55,2714,109],{"class":65},[55,2716,112],{"class":61},[55,2718,115],{"class":65},[55,2720,2721,2723,2725,2727,2729,2731,2733,2735,2737],{"class":57,"line":118},[55,2722,121],{"class":61},[55,2724,124],{"class":65},[55,2726,43],{"class":127},[55,2728,268],{"class":65},[55,2730,250],{"class":127},[55,2732,130],{"class":65},[55,2734,133],{"class":61},[55,2736,136],{"class":99},[55,2738,279],{"class":65},[55,2740,2741,2743,2746,2748,2750,2753,2756,2758,2761,2763],{"class":57,"line":155},[55,2742,1638],{"class":65},[55,2744,2745],{"class":287},"pageSize",[55,2747,291],{"class":65},[55,2749,112],{"class":61},[55,2751,2752],{"class":99}," usePagination",[55,2754,2755],{"class":65},"({ page: ",[55,2757,311],{"class":127},[55,2759,2760],{"class":65},", pageSize, total: ",[55,2762,2152],{"class":127},[55,2764,1256],{"class":65},[55,2766,2767,2770,2772],{"class":57,"line":160},[55,2768,2769],{"class":65},"    { initialProps: { pageSize: ",[55,2771,149],{"class":127},[55,2773,314],{"class":65},[55,2775,2776],{"class":57,"line":179},[55,2777,733],{"class":65},[55,2779,2780],{"class":57,"line":201},[55,2781,93],{"emptyLinePlaceholder":92},[55,2783,2784,2786,2789,2791,2793,2795],{"class":57,"line":221},[55,2785,163],{"class":99},[55,2787,2788],{"class":65},"(result.current.totalPages).",[55,2790,169],{"class":99},[55,2792,103],{"class":65},[55,2794,149],{"class":127},[55,2796,176],{"class":65},[55,2798,2799],{"class":57,"line":499},[55,2800,93],{"emptyLinePlaceholder":92},[55,2802,2803,2805,2808,2811],{"class":57,"line":505},[55,2804,1691],{"class":99},[55,2806,2807],{"class":65},"({ pageSize: ",[55,2809,2810],{"class":127},"25",[55,2812,354],{"class":65},[55,2814,2815,2817,2819,2821,2823,2826],{"class":57,"line":510},[55,2816,163],{"class":99},[55,2818,2788],{"class":65},[55,2820,169],{"class":99},[55,2822,103],{"class":65},[55,2824,2825],{"class":127},"4",[55,2827,176],{"class":65},[55,2829,2830],{"class":57,"line":526},[55,2831,224],{"class":65},[10,2833,2835],{"id":2834},"mocking-hook-dependencies","Mocking hook dependencies",[15,2837,243,2838,2841],{},[22,2839,2840],{},"vi.mock()"," to replace modules the hook depends on:",[46,2843,2845],{"className":48,"code":2844,"language":50,"meta":51,"style":51},"import { vi } from 'vitest'\nimport * as analytics from '..\u002Fanalytics'\nimport { useProductView } from '.\u002FuseProductView'\n\nvi.mock('..\u002Fanalytics')\nconst mockedAnalytics = vi.mocked(analytics)\n\nbeforeEach(() => mockedAnalytics.trackEvent.mockImplementation(() => {}))\nafterEach(() => vi.clearAllMocks())\n\ntest('tracks view event on mount', () => {\n  renderHook(() => useProductView({ id: 42, name: 'Widget' }))\n  expect(mockedAnalytics.trackEvent).toHaveBeenCalledWith('product_view', {\n    product_id: 42,\n    product_name: 'Widget',\n  })\n})\n\ntest('tracks only once regardless of re-renders', () => {\n  const { rerender } = renderHook(() => useProductView({ id: 42, name: 'Widget' }))\n  rerender()\n  rerender()\n  expect(mockedAnalytics.trackEvent).toHaveBeenCalledTimes(1)\n})\n",[22,2846,2847,2857,2875,2887,2891,2906,2923,2927,2948,2964,2968,2983,3005,3023,3033,3042,3046,3050,3054,3069,3099,3105,3111,3126],{"__ignoreMap":51},[55,2848,2849,2851,2853,2855],{"class":57,"line":58},[55,2850,62],{"class":61},[55,2852,1847],{"class":65},[55,2854,69],{"class":61},[55,2856,1852],{"class":72},[55,2858,2859,2861,2864,2867,2870,2872],{"class":57,"line":76},[55,2860,62],{"class":61},[55,2862,2863],{"class":127}," *",[55,2865,2866],{"class":61}," as",[55,2868,2869],{"class":65}," analytics ",[55,2871,69],{"class":61},[55,2873,2874],{"class":72}," '..\u002Fanalytics'\n",[55,2876,2877,2879,2882,2884],{"class":57,"line":89},[55,2878,62],{"class":61},[55,2880,2881],{"class":65}," { useProductView } ",[55,2883,69],{"class":61},[55,2885,2886],{"class":72}," '.\u002FuseProductView'\n",[55,2888,2889],{"class":57,"line":96},[55,2890,93],{"emptyLinePlaceholder":92},[55,2892,2893,2896,2899,2901,2904],{"class":57,"line":118},[55,2894,2895],{"class":65},"vi.",[55,2897,2898],{"class":99},"mock",[55,2900,103],{"class":65},[55,2902,2903],{"class":72},"'..\u002Fanalytics'",[55,2905,176],{"class":65},[55,2907,2908,2910,2913,2915,2917,2920],{"class":57,"line":155},[55,2909,261],{"class":61},[55,2911,2912],{"class":127}," mockedAnalytics",[55,2914,1417],{"class":61},[55,2916,1420],{"class":65},[55,2918,2919],{"class":99},"mocked",[55,2921,2922],{"class":65},"(analytics)\n",[55,2924,2925],{"class":57,"line":160},[55,2926,93],{"emptyLinePlaceholder":92},[55,2928,2929,2932,2934,2936,2939,2941,2943,2945],{"class":57,"line":179},[55,2930,2931],{"class":99},"beforeEach",[55,2933,139],{"class":65},[55,2935,112],{"class":61},[55,2937,2938],{"class":65}," mockedAnalytics.trackEvent.",[55,2940,1435],{"class":99},[55,2942,139],{"class":65},[55,2944,112],{"class":61},[55,2946,2947],{"class":65}," {}))\n",[55,2949,2950,2953,2955,2957,2959,2962],{"class":57,"line":201},[55,2951,2952],{"class":99},"afterEach",[55,2954,139],{"class":65},[55,2956,112],{"class":61},[55,2958,1420],{"class":65},[55,2960,2961],{"class":99},"clearAllMocks",[55,2963,1901],{"class":65},[55,2965,2966],{"class":57,"line":221},[55,2967,93],{"emptyLinePlaceholder":92},[55,2969,2970,2972,2974,2977,2979,2981],{"class":57,"line":499},[55,2971,100],{"class":99},[55,2973,103],{"class":65},[55,2975,2976],{"class":72},"'tracks view event on mount'",[55,2978,109],{"class":65},[55,2980,112],{"class":61},[55,2982,115],{"class":65},[55,2984,2985,2987,2989,2991,2994,2996,2999,3001,3003],{"class":57,"line":505},[55,2986,1565],{"class":99},[55,2988,139],{"class":65},[55,2990,112],{"class":61},[55,2992,2993],{"class":99}," useProductView",[55,2995,707],{"class":65},[55,2997,2998],{"class":127},"42",[55,3000,712],{"class":65},[55,3002,2402],{"class":72},[55,3004,955],{"class":65},[55,3006,3007,3009,3012,3015,3017,3020],{"class":57,"line":510},[55,3008,163],{"class":99},[55,3010,3011],{"class":65},"(mockedAnalytics.trackEvent).",[55,3013,3014],{"class":99},"toHaveBeenCalledWith",[55,3016,103],{"class":65},[55,3018,3019],{"class":72},"'product_view'",[55,3021,3022],{"class":65},", {\n",[55,3024,3025,3028,3030],{"class":57,"line":526},[55,3026,3027],{"class":65},"    product_id: ",[55,3029,2998],{"class":127},[55,3031,3032],{"class":65},",\n",[55,3034,3035,3038,3040],{"class":57,"line":771},[55,3036,3037],{"class":65},"    product_name: ",[55,3039,2402],{"class":72},[55,3041,3032],{"class":65},[55,3043,3044],{"class":57,"line":777},[55,3045,502],{"class":65},[55,3047,3048],{"class":57,"line":793},[55,3049,224],{"class":65},[55,3051,3052],{"class":57,"line":806},[55,3053,93],{"emptyLinePlaceholder":92},[55,3055,3056,3058,3060,3063,3065,3067],{"class":57,"line":811},[55,3057,100],{"class":99},[55,3059,103],{"class":65},[55,3061,3062],{"class":72},"'tracks only once regardless of re-renders'",[55,3064,109],{"class":65},[55,3066,112],{"class":61},[55,3068,115],{"class":65},[55,3070,3071,3073,3075,3077,3079,3081,3083,3085,3087,3089,3091,3093,3095,3097],{"class":57,"line":817},[55,3072,121],{"class":61},[55,3074,124],{"class":65},[55,3076,250],{"class":127},[55,3078,130],{"class":65},[55,3080,133],{"class":61},[55,3082,136],{"class":99},[55,3084,139],{"class":65},[55,3086,112],{"class":61},[55,3088,2993],{"class":99},[55,3090,707],{"class":65},[55,3092,2998],{"class":127},[55,3094,712],{"class":65},[55,3096,2402],{"class":72},[55,3098,955],{"class":65},[55,3100,3101,3103],{"class":57,"line":843},[55,3102,1691],{"class":99},[55,3104,496],{"class":65},[55,3106,3107,3109],{"class":57,"line":848},[55,3108,1691],{"class":99},[55,3110,496],{"class":65},[55,3112,3113,3115,3117,3120,3122,3124],{"class":57,"line":872},[55,3114,163],{"class":99},[55,3116,3011],{"class":65},[55,3118,3119],{"class":99},"toHaveBeenCalledTimes",[55,3121,103],{"class":65},[55,3123,311],{"class":127},[55,3125,176],{"class":65},[55,3127,3128],{"class":57,"line":884},[55,3129,224],{"class":65},[10,3131,3133],{"id":3132},"testing-hooks-that-use-refs","Testing hooks that use refs",[15,3135,3136,3138],{},[22,3137,24],{}," doesn't produce DOM elements — wrap the hook in a component\nwhen the hook needs a real DOM node attached to a ref:",[46,3140,3142],{"className":48,"code":3141,"language":50,"meta":51,"style":51},"test('useAutoFocus focuses the element on mount', () => {\n  function Fixture() {\n    const ref = React.useRef(null)\n    useAutoFocus(ref)\n    return \u003Cinput ref={ref} aria-label=\"Auto-focused\" \u002F>\n  }\n\n  render(\u003CFixture \u002F>)\n  expect(screen.getByRole('textbox', { name: \u002Fauto-focused\u002Fi })).toHaveFocus()\n})\n",[22,3143,3144,3159,3170,3191,3199,3227,3231,3235,3249,3285],{"__ignoreMap":51},[55,3145,3146,3148,3150,3153,3155,3157],{"class":57,"line":58},[55,3147,100],{"class":99},[55,3149,103],{"class":65},[55,3151,3152],{"class":72},"'useAutoFocus focuses the element on mount'",[55,3154,109],{"class":65},[55,3156,112],{"class":61},[55,3158,115],{"class":65},[55,3160,3161,3164,3167],{"class":57,"line":76},[55,3162,3163],{"class":61},"  function",[55,3165,3166],{"class":99}," Fixture",[55,3168,3169],{"class":65},"() {\n",[55,3171,3172,3174,3177,3179,3182,3185,3187,3189],{"class":57,"line":89},[55,3173,1942],{"class":61},[55,3175,3176],{"class":127}," ref",[55,3178,1417],{"class":61},[55,3180,3181],{"class":65}," React.",[55,3183,3184],{"class":99},"useRef",[55,3186,103],{"class":65},[55,3188,946],{"class":127},[55,3190,176],{"class":65},[55,3192,3193,3196],{"class":57,"line":96},[55,3194,3195],{"class":99},"    useAutoFocus",[55,3197,3198],{"class":65},"(ref)\n",[55,3200,3201,3203,3205,3209,3211,3213,3216,3219,3221,3224],{"class":57,"line":118},[55,3202,1161],{"class":61},[55,3204,1164],{"class":65},[55,3206,3208],{"class":3207},"s9eBZ","input",[55,3210,3176],{"class":99},[55,3212,133],{"class":61},[55,3214,3215],{"class":65},"{ref} ",[55,3217,3218],{"class":99},"aria-label",[55,3220,133],{"class":61},[55,3222,3223],{"class":72},"\"Auto-focused\"",[55,3225,3226],{"class":65}," \u002F>\n",[55,3228,3229],{"class":57,"line":155},[55,3230,1185],{"class":65},[55,3232,3233],{"class":57,"line":160},[55,3234,93],{"emptyLinePlaceholder":92},[55,3236,3237,3240,3243,3246],{"class":57,"line":179},[55,3238,3239],{"class":99},"  render",[55,3241,3242],{"class":65},"(\u003C",[55,3244,3245],{"class":127},"Fixture",[55,3247,3248],{"class":65}," \u002F>)\n",[55,3250,3251,3253,3256,3259,3261,3264,3267,3270,3273,3275,3277,3280,3283],{"class":57,"line":201},[55,3252,163],{"class":99},[55,3254,3255],{"class":65},"(screen.",[55,3257,3258],{"class":99},"getByRole",[55,3260,103],{"class":65},[55,3262,3263],{"class":72},"'textbox'",[55,3265,3266],{"class":65},", { name:",[55,3268,3269],{"class":72}," \u002F",[55,3271,3272],{"class":1472},"auto-focused",[55,3274,1469],{"class":72},[55,3276,1478],{"class":61},[55,3278,3279],{"class":65}," })).",[55,3281,3282],{"class":99},"toHaveFocus",[55,3284,496],{"class":65},[55,3286,3287],{"class":57,"line":221},[55,3288,224],{"class":65},[15,3290,3291,3292,582],{},"This is the natural break point: hooks that need the DOM are best tested\nthrough a minimal component fixture, not ",[22,3293,24],{},[10,3295,3297],{"id":3296},"testing-hook-cleanup","Testing hook cleanup",[15,3299,3300,3301,3304],{},"Every hook that registers listeners, starts timers, or opens subscriptions\nmust clean up on unmount. Verify it with ",[22,3302,3303],{},"unmount()",":",[46,3306,3308],{"className":48,"code":3307,"language":50,"meta":51,"style":51},"test('useWindowResize removes 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  const [, addedHandler] = addSpy.mock.calls[0]  \u002F\u002F capture the handler\n  unmount()\n\n  expect(removeSpy).toHaveBeenCalledWith('resize', addedHandler)\n\n  addSpy.mockRestore()\n  removeSpy.mockRestore()\n})\n",[22,3309,3310,3325,3347,3367,3371,3398,3402,3428,3434,3438,3455,3459,3468,3477],{"__ignoreMap":51},[55,3311,3312,3314,3316,3319,3321,3323],{"class":57,"line":58},[55,3313,100],{"class":99},[55,3315,103],{"class":65},[55,3317,3318],{"class":72},"'useWindowResize removes listener on unmount'",[55,3320,109],{"class":65},[55,3322,112],{"class":61},[55,3324,115],{"class":65},[55,3326,3327,3329,3332,3335,3337,3339,3342,3345],{"class":57,"line":76},[55,3328,121],{"class":61},[55,3330,3331],{"class":127}," addSpy",[55,3333,3334],{"class":61},"    =",[55,3336,1420],{"class":65},[55,3338,1423],{"class":99},[55,3340,3341],{"class":65},"(window, ",[55,3343,3344],{"class":72},"'addEventListener'",[55,3346,176],{"class":65},[55,3348,3349,3351,3354,3356,3358,3360,3362,3365],{"class":57,"line":89},[55,3350,121],{"class":61},[55,3352,3353],{"class":127}," removeSpy",[55,3355,1417],{"class":61},[55,3357,1420],{"class":65},[55,3359,1423],{"class":99},[55,3361,3341],{"class":65},[55,3363,3364],{"class":72},"'removeEventListener'",[55,3366,176],{"class":65},[55,3368,3369],{"class":57,"line":96},[55,3370,93],{"emptyLinePlaceholder":92},[55,3372,3373,3375,3377,3379,3381,3383,3385,3387,3389,3392,3394,3396],{"class":57,"line":118},[55,3374,121],{"class":61},[55,3376,124],{"class":65},[55,3378,1755],{"class":127},[55,3380,130],{"class":65},[55,3382,133],{"class":61},[55,3384,136],{"class":99},[55,3386,139],{"class":65},[55,3388,112],{"class":61},[55,3390,3391],{"class":99}," useWindowResize",[55,3393,139],{"class":65},[55,3395,112],{"class":61},[55,3397,2947],{"class":65},[55,3399,3400],{"class":57,"line":155},[55,3401,93],{"emptyLinePlaceholder":92},[55,3403,3404,3406,3409,3412,3415,3417,3420,3422,3425],{"class":57,"line":160},[55,3405,121],{"class":61},[55,3407,3408],{"class":65}," [, ",[55,3410,3411],{"class":127},"addedHandler",[55,3413,3414],{"class":65},"] ",[55,3416,133],{"class":61},[55,3418,3419],{"class":65}," addSpy.mock.calls[",[55,3421,300],{"class":127},[55,3423,3424],{"class":65},"]  ",[55,3426,3427],{"class":359},"\u002F\u002F capture the handler\n",[55,3429,3430,3432],{"class":57,"line":179},[55,3431,1793],{"class":99},[55,3433,496],{"class":65},[55,3435,3436],{"class":57,"line":201},[55,3437,93],{"emptyLinePlaceholder":92},[55,3439,3440,3442,3445,3447,3449,3452],{"class":57,"line":221},[55,3441,163],{"class":99},[55,3443,3444],{"class":65},"(removeSpy).",[55,3446,3014],{"class":99},[55,3448,103],{"class":65},[55,3450,3451],{"class":72},"'resize'",[55,3453,3454],{"class":65},", addedHandler)\n",[55,3456,3457],{"class":57,"line":499},[55,3458,93],{"emptyLinePlaceholder":92},[55,3460,3461,3464,3466],{"class":57,"line":505},[55,3462,3463],{"class":65},"  addSpy.",[55,3465,1488],{"class":99},[55,3467,496],{"class":65},[55,3469,3470,3473,3475],{"class":57,"line":510},[55,3471,3472],{"class":65},"  removeSpy.",[55,3474,1488],{"class":99},[55,3476,496],{"class":65},[55,3478,3479],{"class":57,"line":526},[55,3480,224],{"class":65},[15,3482,3483],{},"Cleanup test matters because leaked listeners cause:",[3485,3486,3487,3491,3494],"ul",{},[3488,3489,3490],"li",{},"Memory leaks in production.",[3488,3492,3493],{},"\"Can't perform state update on unmounted component\" warnings.",[3488,3495,3496,3498],{},[22,3497,366],{}," warnings in subsequent tests.",[10,3500,3502],{"id":3501},"when-to-test-hooks-directly-vs-through-components","When to test hooks directly vs through components",[15,3504,3505],{},"This question comes up in nearly every testing interview. The answer is not\nblack-and-white:",[15,3507,3508],{},[3509,3510,3511],"strong",{},"Test through the component when:",[3485,3513,3514,3517,3520],{},[3488,3515,3516],{},"The hook is only used in one place.",[3488,3518,3519],{},"The hook's behavior is completely visible through the UI.",[3488,3521,3522],{},"The hook is simple (trivial state, no complex async logic).",[46,3524,3526],{"className":48,"code":3525,"language":50,"meta":51,"style":51},"\u002F\u002F useSearch is only used in SearchPage — test through SearchPage\ntest('search results update as user types', async () => {\n  const user = userEvent.setup()\n  render(\u003CSearchPage \u002F>)\n  await user.type(screen.getByRole('textbox'), 'react')\n  expect(await screen.findByText('React Hooks')).toBeInTheDocument()\n})\n",[22,3527,3528,3533,3552,3569,3580,3606,3633],{"__ignoreMap":51},[55,3529,3530],{"class":57,"line":58},[55,3531,3532],{"class":359},"\u002F\u002F useSearch is only used in SearchPage — test through SearchPage\n",[55,3534,3535,3537,3539,3542,3544,3546,3548,3550],{"class":57,"line":76},[55,3536,100],{"class":99},[55,3538,103],{"class":65},[55,3540,3541],{"class":72},"'search results update as user types'",[55,3543,268],{"class":65},[55,3545,549],{"class":61},[55,3547,552],{"class":65},[55,3549,112],{"class":61},[55,3551,115],{"class":65},[55,3553,3554,3556,3559,3561,3564,3567],{"class":57,"line":89},[55,3555,121],{"class":61},[55,3557,3558],{"class":127}," user",[55,3560,1417],{"class":61},[55,3562,3563],{"class":65}," userEvent.",[55,3565,3566],{"class":99},"setup",[55,3568,496],{"class":65},[55,3570,3571,3573,3575,3578],{"class":57,"line":96},[55,3572,3239],{"class":99},[55,3574,3242],{"class":65},[55,3576,3577],{"class":127},"SearchPage",[55,3579,3248],{"class":65},[55,3581,3582,3584,3587,3590,3592,3594,3596,3598,3601,3604],{"class":57,"line":118},[55,3583,820],{"class":61},[55,3585,3586],{"class":65}," user.",[55,3588,3589],{"class":99},"type",[55,3591,3255],{"class":65},[55,3593,3258],{"class":99},[55,3595,103],{"class":65},[55,3597,3263],{"class":72},[55,3599,3600],{"class":65},"), ",[55,3602,3603],{"class":72},"'react'",[55,3605,176],{"class":65},[55,3607,3608,3610,3612,3614,3617,3620,3622,3625,3628,3631],{"class":57,"line":155},[55,3609,163],{"class":99},[55,3611,103],{"class":65},[55,3613,541],{"class":61},[55,3615,3616],{"class":65}," screen.",[55,3618,3619],{"class":99},"findByText",[55,3621,103],{"class":65},[55,3623,3624],{"class":72},"'React Hooks'",[55,3626,3627],{"class":65},")).",[55,3629,3630],{"class":99},"toBeInTheDocument",[55,3632,496],{"class":65},[55,3634,3635],{"class":57,"line":160},[55,3636,224],{"class":65},[15,3638,3639],{},[3509,3640,3641],{},"Test the hook directly when:",[3485,3643,3644,3647,3650,3653],{},[3488,3645,3646],{},"The hook is reused across multiple components.",[3488,3648,3649],{},"The hook manages complex state transitions (async, state machines, caching).",[3488,3651,3652],{},"The hook has edge cases that are impractical to exercise through UI.",[3488,3654,3655],{},"You want to document the hook's API contract explicitly.",[46,3657,3659],{"className":48,"code":3658,"language":50,"meta":51,"style":51},"\u002F\u002F useInfiniteScroll is used in 5 components — test directly\ntest('loads next page when fetchNextPage is called', async () => {\n  const { result } = renderHook(() => useInfiniteScroll(fetchPage))\n  await waitFor(() => expect(result.current.pages).toHaveLength(1))\n\n  act(() => result.current.fetchNextPage())\n  await waitFor(() => expect(result.current.pages).toHaveLength(2))\n})\n",[22,3660,3661,3666,3685,3709,3732,3736,3752,3774],{"__ignoreMap":51},[55,3662,3663],{"class":57,"line":58},[55,3664,3665],{"class":359},"\u002F\u002F useInfiniteScroll is used in 5 components — test directly\n",[55,3667,3668,3670,3672,3675,3677,3679,3681,3683],{"class":57,"line":76},[55,3669,100],{"class":99},[55,3671,103],{"class":65},[55,3673,3674],{"class":72},"'loads next page when fetchNextPage is called'",[55,3676,268],{"class":65},[55,3678,549],{"class":61},[55,3680,552],{"class":65},[55,3682,112],{"class":61},[55,3684,115],{"class":65},[55,3686,3687,3689,3691,3693,3695,3697,3699,3701,3703,3706],{"class":57,"line":89},[55,3688,121],{"class":61},[55,3690,124],{"class":65},[55,3692,43],{"class":127},[55,3694,130],{"class":65},[55,3696,133],{"class":61},[55,3698,136],{"class":99},[55,3700,139],{"class":65},[55,3702,112],{"class":61},[55,3704,3705],{"class":99}," useInfiniteScroll",[55,3707,3708],{"class":65},"(fetchPage))\n",[55,3710,3711,3713,3715,3717,3719,3721,3724,3726,3728,3730],{"class":57,"line":96},[55,3712,820],{"class":61},[55,3714,823],{"class":99},[55,3716,139],{"class":65},[55,3718,112],{"class":61},[55,3720,830],{"class":99},[55,3722,3723],{"class":65},"(result.current.pages).",[55,3725,2366],{"class":99},[55,3727,103],{"class":65},[55,3729,311],{"class":127},[55,3731,152],{"class":65},[55,3733,3734],{"class":57,"line":118},[55,3735,93],{"emptyLinePlaceholder":92},[55,3737,3738,3740,3742,3744,3747,3750],{"class":57,"line":155},[55,3739,479],{"class":99},[55,3741,139],{"class":65},[55,3743,112],{"class":61},[55,3745,3746],{"class":65}," result.current.",[55,3748,3749],{"class":99},"fetchNextPage",[55,3751,1901],{"class":65},[55,3753,3754,3756,3758,3760,3762,3764,3766,3768,3770,3772],{"class":57,"line":160},[55,3755,820],{"class":61},[55,3757,823],{"class":99},[55,3759,139],{"class":65},[55,3761,112],{"class":61},[55,3763,830],{"class":99},[55,3765,3723],{"class":65},[55,3767,2366],{"class":99},[55,3769,103],{"class":65},[55,3771,2435],{"class":127},[55,3773,152],{"class":65},[55,3775,3776],{"class":57,"line":179},[55,3777,224],{"class":65},[15,3779,3780],{},"The practical middle ground: one component test per major use case, plus\ndirect hook tests for edge cases and the API contract.",[10,3782,3784],{"id":3783},"testing-hooks-that-throw","Testing hooks that throw",[15,3786,3787,3788,3791,3792,3304],{},"Suppress ",[22,3789,3790],{},"console.error"," and wrap the call in ",[22,3793,3794],{},"expect(...).toThrow()",[46,3796,3798],{"className":48,"code":3797,"language":50,"meta":51,"style":51},"test('throws when used outside provider', () => {\n  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n  expect(() => renderHook(() => useTheme())).toThrow(\n    'useTheme must be used inside ThemeProvider'\n  )\n\n  consoleSpy.mockRestore()\n})\n",[22,3799,3800,3815,3841,3845,3868,3873,3877,3881,3889],{"__ignoreMap":51},[55,3801,3802,3804,3806,3809,3811,3813],{"class":57,"line":58},[55,3803,100],{"class":99},[55,3805,103],{"class":65},[55,3807,3808],{"class":72},"'throws when used outside provider'",[55,3810,109],{"class":65},[55,3812,112],{"class":61},[55,3814,115],{"class":65},[55,3816,3817,3819,3821,3823,3825,3827,3829,3831,3833,3835,3837,3839],{"class":57,"line":76},[55,3818,121],{"class":61},[55,3820,1414],{"class":127},[55,3822,1417],{"class":61},[55,3824,1420],{"class":65},[55,3826,1423],{"class":99},[55,3828,1426],{"class":65},[55,3830,1429],{"class":72},[55,3832,1432],{"class":65},[55,3834,1435],{"class":99},[55,3836,139],{"class":65},[55,3838,112],{"class":61},[55,3840,1442],{"class":65},[55,3842,3843],{"class":57,"line":89},[55,3844,93],{"emptyLinePlaceholder":92},[55,3846,3847,3849,3851,3853,3855,3857,3859,3862,3864,3866],{"class":57,"line":96},[55,3848,163],{"class":99},[55,3850,139],{"class":65},[55,3852,112],{"class":61},[55,3854,136],{"class":99},[55,3856,139],{"class":65},[55,3858,112],{"class":61},[55,3860,3861],{"class":99}," useTheme",[55,3863,1461],{"class":65},[55,3865,1464],{"class":99},[55,3867,279],{"class":65},[55,3869,3870],{"class":57,"line":118},[55,3871,3872],{"class":72},"    'useTheme must be used inside ThemeProvider'\n",[55,3874,3875],{"class":57,"line":155},[55,3876,733],{"class":65},[55,3878,3879],{"class":57,"line":160},[55,3880,93],{"emptyLinePlaceholder":92},[55,3882,3883,3885,3887],{"class":57,"line":179},[55,3884,1485],{"class":65},[55,3886,1488],{"class":99},[55,3888,496],{"class":65},[55,3890,3891],{"class":57,"line":201},[55,3892,224],{"class":65},[10,3894,3896],{"id":3895},"interview-checklist","Interview checklist",[15,3898,3899],{},"Make sure you can explain and demonstrate:",[3485,3901,3902,3911,3917,3922,3930,3936,3942,3948],{},[3488,3903,3904,3905,3907,3908,3910],{},"How ",[22,3906,24],{}," works and what ",[22,3909,229],{}," holds.",[3488,3912,3913,3914,3916],{},"Why ",[22,3915,366],{}," is required and how to apply it to sync vs async updates.",[3488,3918,3919,3920,582],{},"How to test async hooks with MSW and ",[22,3921,592],{},[3488,3923,3924,3925,3927,3928,582],{},"How to provide context providers to ",[22,3926,24],{}," via ",[22,3929,1073],{},[3488,3931,3932,3933,3935],{},"How to test ",[22,3934,1504],{}," effects at mount, update, and unmount.",[3488,3937,3938,3939,582],{},"How to control timers in hook tests with ",[22,3940,3941],{},"vi.useFakeTimers",[3488,3943,3944,3945,3947],{},"How to verify cleanup with ",[22,3946,3303],{}," and event listener spies.",[3488,3949,3950],{},"When to test a hook directly vs through the component that uses it.",[15,3952,3953,3956,3957,3960,3961,3963],{},[3509,3954,3955],{},"Rule of thumb:"," Test hooks through their public return value, not their\ninternals. If you find yourself mocking ",[22,3958,3959],{},"useState"," or ",[22,3962,2279],{},", you're\ntesting implementation — step back and test what callers see instead.",[3965,3966,3967],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":51,"searchDepth":76,"depth":76,"links":3969},[3970,3971,3975,3977,3978,3979,3981,3982,3984,3985,3986,3987,3988,3989,3990],{"id":12,"depth":76,"text":13},{"id":28,"depth":76,"text":3972,"children":3973},"renderHook — the core API",[3974],{"id":239,"depth":89,"text":240},{"id":363,"depth":76,"text":3976},"act() — the state-update wrapper",{"id":585,"depth":76,"text":586},{"id":1066,"depth":76,"text":1067},{"id":1500,"depth":76,"text":3980},"Testing useEffect side effects",{"id":1821,"depth":76,"text":1822},{"id":2276,"depth":76,"text":3983},"Testing useReducer-based hooks",{"id":2671,"depth":76,"text":2672},{"id":2834,"depth":76,"text":2835},{"id":3132,"depth":76,"text":3133},{"id":3296,"depth":76,"text":3297},{"id":3501,"depth":76,"text":3502},{"id":3783,"depth":76,"text":3784},{"id":3895,"depth":76,"text":3896},"Learn how to test React custom hooks with renderHook — async hooks, context- dependent hooks, timer hooks, useReducer hooks, cleanup verification, and when to test hooks directly versus through components.","hard","md","React","react",{},"\u002Fblog\u002Freact-testing-custom-hooks-guide","\u002Freact\u002Ftesting\u002Ftesting-custom-hooks",{"title":5,"description":3991},"blog\u002Freact-testing-custom-hooks-guide","Testing Custom Hooks","Testing","testing","2026-06-24","6GvXH3HsR1xuQe7EKDifcj7-JdmQsrXaiHhWf6NG_tk",1782244083318]