[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-react":16,"notes-page-react-15-engineering-and-architecture\u002F02-testing-strategy-rtl-e2e-testability":494,"i-ri:arrow-left-s-line":2504,"i-ri:menu-2-line":2506,"i-ri:arrow-right-s-line":2508,"i-ep:copy-document":2510},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38,42,46,50,54,58,62,66,70,74,78,82,86,90,94,98,102,106,110,114,118,122,126,130,134,138,142,146,150,154,158,162,166,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,254,258,262,266,270,274,278,282,286,290,294,298,302,306,310,314,318,322,326,330,334,338,342,346,350,354,358,362,366,370,374,378,382,386,390,394,398,402,406,410,414,418,422,426,430,434,438,442,446,450,454,458,462,466,470,474,478,482,486,490],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Freact\u002F01-basics\u002F01-how-to-read-this-repo","如何阅读本仓库（学习路径与章节依赖）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Freact\u002F01-basics\u002F02-render-vs-commit","React 渲染两阶段：Render vs Commit（总览）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Freact\u002F01-basics\u002F03-update-pipeline-birdview","从“更新”到“页面变更”：一次 setState 的全链路鸟瞰",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Freact\u002F01-basics","基础入门（建议先读）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F01-browser-multi-process","浏览器多进程架构",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F02-event-loop-overview","事件循环概述",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":43,"title":44,"meta":45},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F03-call-stack","函数调用栈",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":47,"title":48,"meta":49},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F04-threads","线程介绍",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":51,"title":52,"meta":53},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F05-refresh-rate-and-queues","刷新率与执行队列",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":55,"title":56,"meta":57},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F06-settimeout","setTimeout 原理详解",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":59,"title":60,"meta":61},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F07-promise","Promise 原理详解",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":63,"title":64,"meta":65},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F08-event-loop-in-practice","事件循环机制（综合）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":67,"title":68,"meta":69},"\u002Fnotes\u002Freact\u002F02-event-loop\u002F09-priority-queues","优先级队列",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":71,"title":72,"meta":73},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F01-concept","并发模式：概念",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":75,"title":76,"meta":77},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F02-demo","案例演示并发模式",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":79,"title":80,"meta":81},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F03-high-priority-preemption","高优先级插队",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":83,"title":84,"meta":85},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F04-preemption-vs-interruption","插队的影响、与中断的区别",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":87,"title":88,"meta":89},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F05-shouldYield","中断条件：shouldYield",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":91,"title":92,"meta":93},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F06-react-tasks-and-queues","React 中的任务与队列",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":95,"title":96,"meta":97},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F07-ensureRootIsScheduled","ensureRootIsScheduled",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":99,"title":100,"meta":101},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F08-batch-updates-merge","合并多个更新触发",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":103,"title":104,"meta":105},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F09-taskQueue-lifecycle","taskQueue 队列循环的启动与结束",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":107,"title":108,"meta":109},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F10-time-slicing","时间切片是如何实现的",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":111,"title":112,"meta":113},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F11-workLoop-full","workLoop 循环完整逻辑",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":115,"title":116,"meta":117},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F12-priority-design","React 中的优先级设计",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":119,"title":120,"meta":121},"\u002Fnotes\u002Freact\u002F03-concurrent-mode\u002F13-lane-model","lane 模型",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":123,"title":124,"meta":125},"\u002Fnotes\u002Freact\u002F04-fiber\u002F01-three-node-types","三种节点对象",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":127,"title":128,"meta":129},"\u002Fnotes\u002Freact\u002F04-fiber\u002F02-fiberRootNode","FiberRootNode",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":131,"title":132,"meta":133},"\u002Fnotes\u002Freact\u002F04-fiber\u002F03-fiber-tree","Fiber 树",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":135,"title":136,"meta":137},"\u002Fnotes\u002Freact\u002F04-fiber\u002F04-initial-mount-build","Fiber 树初始构建过程",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":139,"title":140,"meta":141},"\u002Fnotes\u002Freact\u002F04-fiber\u002F05-workLoopConcurrent","workLoopConcurrent",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":143,"title":144,"meta":145},"\u002Fnotes\u002Freact\u002F04-fiber\u002F06-stack-and-workInProgress","栈结构 与 workInProgress",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":147,"title":148,"meta":149},"\u002Fnotes\u002Freact\u002F04-fiber\u002F07-executionContext","执行上下文 executionContext",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":151,"title":152,"meta":153},"\u002Fnotes\u002Freact\u002F04-fiber\u002F08-update-lanes","update lanes",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":155,"title":156,"meta":157},"\u002Fnotes\u002Freact\u002F04-fiber\u002F09-render-lanes","render lanes",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":159,"title":160,"meta":161},"\u002Fnotes\u002Freact\u002F04-fiber\u002F10-fiber-lanes","fiber lanes",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":163,"title":164,"meta":165},"\u002Fnotes\u002Freact\u002F04-fiber\u002F11-flags-appendix","附录：flags",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":167,"title":168,"meta":169},"\u002Fnotes\u002Freact\u002F05-hooks\u002F01-background","hook 背景",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":171,"title":172,"meta":173},"\u002Fnotes\u002Freact\u002F05-hooks\u002F02-categories","hook 分类",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":175,"title":176,"meta":177},"\u002Fnotes\u002Freact\u002F05-hooks\u002F03-closures","hook 与闭包",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":179,"title":180,"meta":181},"\u002Fnotes\u002Freact\u002F05-hooks\u002F04-hooks-and-fiber","hook 与 fiber",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":183,"title":184,"meta":185},"\u002Fnotes\u002Freact\u002F05-hooks\u002F05-renderWithHooks","renderWithHooks",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":187,"title":188,"meta":189},"\u002Fnotes\u002Freact\u002F05-hooks\u002F06-init-state-hook","初始化 State Hook",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":191,"title":192,"meta":193},"\u002Fnotes\u002Freact\u002F05-hooks\u002F07-init-effect-hook","初始化 effect Hook",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":195,"title":196,"meta":197},"\u002Fnotes\u002Freact\u002F05-hooks\u002F08-circular-linked-list","环形链表详解",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":199,"title":200,"meta":201},"\u002Fnotes\u002Freact\u002F05-hooks\u002F09-useReducer-vs-useState","useReducer 与 useState 的区别",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":203,"title":204,"meta":205},"\u002Fnotes\u002Freact\u002F05-hooks\u002F10-update-linked-list","update 链表",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":207,"title":208,"meta":209},"\u002Fnotes\u002Freact\u002F05-hooks\u002F11-mount-state-updates","初始阶段的状态更新",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":211,"title":212,"meta":213},"\u002Fnotes\u002Freact\u002F05-hooks\u002F12-baseQueue-vs-pending","baseQueue 与 queue.pending",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":215,"title":216,"meta":217},"\u002Fnotes\u002Freact\u002F05-hooks\u002F13-dispatch-in-update","更新阶段的 dispatch",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":219,"title":220,"meta":221},"\u002Fnotes\u002Freact\u002F05-hooks\u002F14-commitRoot","commitRoot",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":223,"title":224,"meta":225},"\u002Fnotes\u002Freact\u002F06-diff\u002F01-update-overview","React 更新机制概述",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":227,"title":228,"meta":229},"\u002Fnotes\u002Freact\u002F06-diff\u002F02-diff-strategy","diff 策略",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":231,"title":232,"meta":233},"\u002Fnotes\u002Freact\u002F06-diff\u002F03-node-compare","节点比较方式",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":235,"title":236,"meta":237},"\u002Fnotes\u002Freact\u002F06-diff\u002F04-reconcileChildFibers","reconcileChildFibers",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":239,"title":240,"meta":241},"\u002Fnotes\u002Freact\u002F06-diff\u002F05-reconcileSingleElement","reconcileSingleElement",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":243,"title":244,"meta":245},"\u002Fnotes\u002Freact\u002F06-diff\u002F06-reconcileChildrenArray","reconcileChildrenArray",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":247,"title":248,"meta":249},"\u002Fnotes\u002Freact\u002F06-diff\u002F07-why-not-number-key","为什么不建议使用数字作为 key",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":251,"title":252,"meta":253},"\u002Fnotes\u002Freact\u002F07-event-system\u002F01-overview","事件系统概述",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":255,"title":256,"meta":257},"\u002Fnotes\u002Freact\u002F07-event-system\u002F02-event-plugin","事件插件",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":259,"title":260,"meta":261},"\u002Fnotes\u002Freact\u002F07-event-system\u002F03-event-pooling","事件池",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":263,"title":264,"meta":265},"\u002Fnotes\u002Freact\u002F07-event-system\u002F04-root-listeners","根节点事件绑定",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":267,"title":268,"meta":269},"\u002Fnotes\u002Freact\u002F07-event-system\u002F05-event-dispatch","事件触发",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":271,"title":272,"meta":273},"\u002Fnotes\u002Freact\u002F08-interview\u002F01-useEffect-vs-useLayoutEffect","useEffect 与 useLayoutEffect 的区别",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":275,"title":276,"meta":277},"\u002Fnotes\u002Freact\u002F08-interview\u002F02-why-no-conditional-hooks","为什么 hook 不能写在条件判断里",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":279,"title":280,"meta":281},"\u002Fnotes\u002Freact\u002F08-interview\u002F03-understanding-concurrency","聊聊在 React 中，你对并发模式的理解",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":283,"title":284,"meta":285},"\u002Fnotes\u002Freact\u002F08-interview\u002F04-update-mechanism","聊聊 React 的更新机制原理",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":287,"title":288,"meta":289},"\u002Fnotes\u002Freact\u002F08-interview\u002F05-setState-sync-or-async","setState 是同步还是异步的？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":291,"title":292,"meta":293},"\u002Fnotes\u002Freact\u002F08-interview\u002F06-useState-vs-useReducer","聊聊 useState 与 useReducer 的区别",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":295,"title":296,"meta":297},"\u002Fnotes\u002Freact\u002F08-interview\u002F07-closure-trap","聊聊闭包陷阱问题，以及如何解决",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":299,"title":300,"meta":301},"\u002Fnotes\u002Freact\u002F08-interview\u002F08-time-slicing-details","时间分片是如何实现的（任务拆分\u002F中断\u002F恢复）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":303,"title":304,"meta":305},"\u002Fnotes\u002Freact\u002F08-interview\u002F09-batched-updates-useState","聊聊 useState 的批处理更新机制",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":307,"title":308,"meta":309},"\u002Fnotes\u002Freact\u002F08-interview\u002F10-jsx-to-dom","JSX 是如何转变成为真实 DOM 的",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":311,"title":312,"meta":313},"\u002Fnotes\u002Freact\u002F08-interview\u002F11-useCallback-impl","useCallback 的实现原理是什么",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":315,"title":316,"meta":317},"\u002Fnotes\u002Freact\u002F08-interview\u002F12-scheduling-mechanism","详细聊聊 React 的调度机制",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":319,"title":320,"meta":321},"\u002Fnotes\u002Freact\u002F08-interview\u002F13-priority-and-lanes","聊聊 React 的优先级机制（Lane 模型）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":323,"title":324,"meta":325},"\u002Fnotes\u002Freact\u002F08-interview\u002F14-diff-algorithm","React 的 Diff 算法是什么样的",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":327,"title":328,"meta":329},"\u002Fnotes\u002Freact\u002F08-interview\u002F15-event-system","详细聊聊 React 的事件系统是如何实现的",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":331,"title":332,"meta":333},"\u002Fnotes\u002Freact\u002F08-interview\u002F16-portals-bubbling","React Portals 上的事件是否会冒泡到父组件，原理是什么",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":335,"title":336,"meta":337},"\u002Fnotes\u002Freact\u002F08-interview\u002F17-useEffect-impl","useEffect 的实现原理是什么",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":339,"title":340,"meta":341},"\u002Fnotes\u002Freact\u002F08-interview\u002F18-useContext-impl","请聊聊 useContext 的实现原理",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":343,"title":344,"meta":345},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice\u002F01-automatic-batching-and-flushsync","React 18 自动批处理（Automatic Batching）与 flushSync",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":347,"title":348,"meta":349},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice\u002F02-strictmode-and-idempotent-effects","React StrictMode：为什么会“双调用”？（源码\u002F机制视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":351,"title":352,"meta":353},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice\u002F03-transition-useTransition-useDeferredValue","Transition：startTransition \u002F useTransition \u002F useDeferredValue（机制视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":355,"title":356,"meta":357},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice\u002F04-suspense-loading-boundary-design","Suspense：加载态边界如何设计（机制\u002F架构视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":359,"title":360,"meta":361},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice\u002F05-error-boundary-reporting-and-recovery","Error Boundary：兜底、重试与上报（自研 vs Sentry）（机制\u002F架构视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":363,"title":364,"meta":365},"\u002Fnotes\u002Freact\u002F09-react-18-in-practice","React 18 实战（并发特性与默认行为）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":367,"title":368,"meta":369},"\u002Fnotes\u002Freact\u002F10-routing-and-navigation\u002F01-routing-and-url-state-mental-model","路由与 URL State：统一心智模型（架构师视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":371,"title":372,"meta":373},"\u002Fnotes\u002Freact\u002F10-routing-and-navigation\u002F02-tanstack-router-route-tree-search-loader-prefetch","TanStack Router（Vite \u002F SPA）：Route Tree \u002F Search \u002F Loader \u002F Prefetch（落地篇）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":375,"title":376,"meta":377},"\u002Fnotes\u002Freact\u002F10-routing-and-navigation\u002F03-next-app-router-segments-layout-loading-error-not-found-prefetch","Next.js App Router：Segments \u002F Layouts \u002F Loading-Error-NotFound \u002F Prefetch（落地篇）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":379,"title":380,"meta":381},"\u002Fnotes\u002Freact\u002F10-routing-and-navigation","路由与导航（TanStack Router \u002F Next App Router）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":383,"title":384,"meta":385},"\u002Fnotes\u002Freact\u002F11-data-layer\u002F01-state-layering-ui-server-url-global","状态分层：UI \u002F Server \u002F URL \u002F Global（架构师视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":387,"title":388,"meta":389},"\u002Fnotes\u002Freact\u002F11-data-layer\u002F02-tanstack-query-caching-and-invalidation","TanStack Query：缓存模型与失效策略（Caching & Invalidation）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":391,"title":392,"meta":393},"\u002Fnotes\u002Freact\u002F11-data-layer\u002F03-optimistic-updates-and-consistency","TanStack Query：乐观更新与一致性（Optimistic Updates & Consistency）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":395,"title":396,"meta":397},"\u002Fnotes\u002Freact\u002F11-data-layer\u002F04-form-architecture-react-hook-form-validation-performance-testability","表单架构：React Hook Form 的受控\u002F非受控、校验、性能与可测试性（架构师视角）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":399,"title":400,"meta":401},"\u002Fnotes\u002Freact\u002F11-data-layer","数据层（Server State \u002F 表单 \u002F 状态分层）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":403,"title":404,"meta":405},"\u002Fnotes\u002Freact\u002F12-rendering-and-delivery\u002F01-hydration-mismatch","Hydration mismatch（React \u002F Next.js）：成因、检测、排查清单与工程化规避",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":407,"title":408,"meta":409},"\u002Fnotes\u002Freact\u002F12-rendering-and-delivery\u002F01-nextjs-cache-revalidate-consistency-online-not-updating","Next.js 缓存\u002F重验证与一致性：避免「线上怎么不更新」事故（App Router）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":411,"title":412,"meta":413},"\u002Fnotes\u002Freact\u002F12-rendering-and-delivery\u002F02-csr-ssr-ssg-isr-how-to-choose","CSR vs SSR vs SSG vs ISR：怎么选？边界是什么？（体验 + 成本 + 一致性）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":415,"title":416,"meta":417},"\u002Fnotes\u002Freact\u002F12-rendering-and-delivery\u002F04-streaming-ssr-with-suspense","Streaming SSR + Suspense：提升前端感知性能的利器",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":419,"title":420,"meta":421},"\u002Fnotes\u002Freact\u002F12-rendering-and-delivery","渲染与交付（CSR\u002FSSR\u002FStreaming\u002FHydration\u002F缓存）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":423,"title":424,"meta":425},"\u002Fnotes\u002Freact\u002F13-production-readiness\u002F01-observability-logging-metrics-tracing","前端可观测性体系：日志、指标与链路追踪",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":427,"title":428,"meta":429},"\u002Fnotes\u002Freact\u002F13-production-readiness\u002F02-auth-and-web-security","鉴权与 Web 安全：OAuth, JWT, XSS, CSRF, CSP",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":431,"title":432,"meta":433},"\u002Fnotes\u002Freact\u002F13-production-readiness","生产可用性（错误\u002F可观测\u002F鉴权\u002F安全）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":435,"title":436,"meta":437},"\u002Fnotes\u002Freact\u002F14-performance\u002F01-performance-playbook-and-metrics","前端性能定位 Playbook：从指标到归因",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":439,"title":440,"meta":441},"\u002Fnotes\u002Freact\u002F14-performance\u002F02-rendering-optimization-memo-context-virtualization","React 渲染性能优化：Memo, Context, 与虚拟化",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":443,"title":444,"meta":445},"\u002Fnotes\u002Freact\u002F14-performance\u002F03-asset-and-bundle-optimization","资源与 Bundle 优化：代码分割、图片与 CDN",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":447,"title":448,"meta":449},"\u002Fnotes\u002Freact\u002F14-performance","性能体系（定位 → 优化 → 度量）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":451,"title":452,"meta":453},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture\u002F01-component-system-and-design-system","组件体系与设计系统：构建规模化前端的基石",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":455,"title":456,"meta":457},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture\u002F02-testing-strategy-rtl-e2e-testability","前端测试体系：RTL, E2E 与可测性设计",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":459,"title":460,"meta":461},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture\u002F03-monorepo-and-dependency-management","Monorepo 与依赖治理：pnpm 和 Turborepo",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":463,"title":464,"meta":465},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture\u002F04-cicd-and-quality-gates","CI\u002FCD 与质量门禁：构建可靠的自动化流程",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":467,"title":468,"meta":469},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture\u002F05-bff-and-api-design","BFF 与 API 设计：前端架构师的视角",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":471,"title":472,"meta":473},"\u002Fnotes\u002Freact\u002F15-engineering-and-architecture","工程与架构治理（规模化）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":475,"title":476,"meta":477},"\u002Fnotes\u002Freact\u002F16-advanced\u002F01-rsc-and-server-actions-deep-dive","RSC\u002FServer Actions 深水区：边界、约束与缓存一致性",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":479,"title":480,"meta":481},"\u002Fnotes\u002Freact\u002F16-advanced\u002F02-micro-frontend-governance","微前端治理：样式、路由与通信",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":483,"title":484,"meta":485},"\u002Fnotes\u002Freact\u002F16-advanced\u002F03-realtime-and-offline","实时与离线：WebSocket, PWA 与 Service Worker",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":487,"title":488,"meta":489},"\u002Fnotes\u002Freact\u002F16-advanced\u002F04-a11y-i18n-compliance","a11y, i18n 与合规：构建负责任的全球化应用",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":491,"title":492,"meta":493},"\u002Fnotes\u002Freact\u002F16-advanced","进阶专题（按业务需要选做）",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":495,"type":496,"sourcePath":497,"routePath":455,"path":455,"title":456,"updatedAt":21,"meta":498,"body":500,"toc":2492},"bm90ZTpub3Rlcy9yZWFjdC8xNS1lbmdpbmVlcmluZy1hbmQtYXJjaGl0ZWN0dXJlLzAyLXRlc3Rpbmctc3RyYXRlZ3ktcnRsLWUyZS10ZXN0YWJpbGl0eS5tZA","note","notes\u002Freact\u002F15-engineering-and-architecture\u002F02-testing-strategy-rtl-e2e-testability.md",{"id":495,"type":496,"sourcePath":497,"slug":499,"title":456,"updatedAt":21,"lastUpdated":21},"react\u002F15-engineering-and-architecture\u002F02-testing-strategy-rtl-e2e-testability",{"type":501,"children":502},"root",[503,511,527,531,538,543,556,682,692,695,701,711,718,774,780,1511,1524,1527,1533,1538,1645,1651,2156,2159,2165,2170,2407,2410,2416,2486],{"type":504,"tag":505,"props":506,"children":508},"element","h1",{"id":507},"前端测试体系rtl-e2e-与可测性设计",[509],{"type":510,"value":456},"text",{"type":504,"tag":512,"props":513,"children":514},"blockquote",{},[515],{"type":504,"tag":516,"props":517,"children":518},"p",{},[519,525],{"type":504,"tag":520,"props":521,"children":522},"strong",{},[523],{"type":510,"value":524},"一句话结论",{"type":510,"value":526},"：依据“测试金字塔”原则，建立一个以 React Testing Library (RTL) 进行单元\u002F集成测试为基础，辅以 Playwright\u002FCypress 进行关键流程 E2E 测试的自动化测试体系，并通过可测性设计从源头提升代码质量，是确保大型应用稳定可靠的工程实践。",{"type":504,"tag":528,"props":529,"children":530},"hr",{},[],{"type":504,"tag":532,"props":533,"children":535},"h2",{"id":534},"_1-测试金字塔-the-testing-pyramid",[536],{"type":510,"value":537},"1. 测试金字塔 (The Testing Pyramid)",{"type":504,"tag":516,"props":539,"children":540},{},[541],{"type":510,"value":542},"测试金字塔是一个经典的隐喻，它指导我们如何在不同类型的测试中分配投入。",{"type":504,"tag":544,"props":545,"children":549},"pre",{"className":546,"code":548,"language":510},[547],"language-text","      \u002F\\\\\n     \u002F  \\\\   E2E Tests (端到端测试)\n    \u002F----\\\\\n   \u002F      \\\\  Integration Tests (集成测试)\n  \u002F--------\\\\\n \u002F          \\\\ Unit Tests (单元测试)\n\u002F____________\\\\\n",[550],{"type":504,"tag":551,"props":552,"children":554},"code",{"__ignoreMap":553},"",[555],{"type":510,"value":548},{"type":504,"tag":557,"props":558,"children":559},"ul",{},[560,604,643],{"type":504,"tag":561,"props":562,"children":563},"li",{},[564,569,571],{"type":504,"tag":520,"props":565,"children":566},{},[567],{"type":510,"value":568},"单元测试 (Unit Tests)",{"type":510,"value":570},"：",{"type":504,"tag":557,"props":572,"children":573},{},[574,584,594],{"type":504,"tag":561,"props":575,"children":576},{},[577,582],{"type":504,"tag":520,"props":578,"children":579},{},[580],{"type":510,"value":581},"范围",{"type":510,"value":583},"：最小的可测试单元（单个函数、Hook、或简单组件）。",{"type":504,"tag":561,"props":585,"children":586},{},[587,592],{"type":504,"tag":520,"props":588,"children":589},{},[590],{"type":510,"value":591},"特点",{"type":510,"value":593},"：运行速度最快，数量最多，与实现细节紧密相关。",{"type":504,"tag":561,"props":595,"children":596},{},[597,602],{"type":504,"tag":520,"props":598,"children":599},{},[600],{"type":510,"value":601},"目标",{"type":510,"value":603},"：验证逻辑的正确性。",{"type":504,"tag":561,"props":605,"children":606},{},[607,612,613],{"type":504,"tag":520,"props":608,"children":609},{},[610],{"type":510,"value":611},"集成测试 (Integration Tests)",{"type":510,"value":570},{"type":504,"tag":557,"props":614,"children":615},{},[616,625,634],{"type":504,"tag":561,"props":617,"children":618},{},[619,623],{"type":504,"tag":520,"props":620,"children":621},{},[622],{"type":510,"value":581},{"type":510,"value":624},"：多个单元（组件）协同工作的场景。",{"type":504,"tag":561,"props":626,"children":627},{},[628,632],{"type":504,"tag":520,"props":629,"children":630},{},[631],{"type":510,"value":591},{"type":510,"value":633},"：速度和数量居中。",{"type":504,"tag":561,"props":635,"children":636},{},[637,641],{"type":504,"tag":520,"props":638,"children":639},{},[640],{"type":510,"value":601},{"type":510,"value":642},"：验证组件之间的交互是否符合预期。",{"type":504,"tag":561,"props":644,"children":645},{},[646,651,652],{"type":504,"tag":520,"props":647,"children":648},{},[649],{"type":510,"value":650},"端到端测试 (E2E Tests)",{"type":510,"value":570},{"type":504,"tag":557,"props":653,"children":654},{},[655,664,673],{"type":504,"tag":561,"props":656,"children":657},{},[658,662],{"type":504,"tag":520,"props":659,"children":660},{},[661],{"type":510,"value":581},{"type":510,"value":663},"：在真实浏览器环境中模拟完整的用户场景（如登录、购物、支付）。",{"type":504,"tag":561,"props":665,"children":666},{},[667,671],{"type":504,"tag":520,"props":668,"children":669},{},[670],{"type":510,"value":591},{"type":510,"value":672},"：运行速度最慢，最脆弱，成本最高，数量最少。",{"type":504,"tag":561,"props":674,"children":675},{},[676,680],{"type":504,"tag":520,"props":677,"children":678},{},[679],{"type":510,"value":601},{"type":510,"value":681},"：验证整个应用作为一个整体是否按预期工作，覆盖从前端到后端的完整流程。",{"type":504,"tag":516,"props":683,"children":684},{},[685,690],{"type":504,"tag":520,"props":686,"children":687},{},[688],{"type":510,"value":689},"核心思想",{"type":510,"value":691},"：将大部分精力投入到底层的单元和集成测试，因为它们运行快、反馈及时、定位问题准。只为最关键、最高价值的用户流程编写少量的 E2E 测试。",{"type":504,"tag":528,"props":693,"children":694},{},[],{"type":504,"tag":532,"props":696,"children":698},{"id":697},"_2-单元集成测试react-testing-library-rtl",[699],{"type":510,"value":700},"2. 单元\u002F集成测试：React Testing Library (RTL)",{"type":504,"tag":516,"props":702,"children":703},{},[704,706],{"type":510,"value":705},"RTL 是一个专注于测试 React 组件的库，它的核心哲学是：",{"type":504,"tag":520,"props":707,"children":708},{},[709],{"type":510,"value":710},"“测试你的软件，就像真实用户使用它一样。”",{"type":504,"tag":712,"props":713,"children":715},"h3",{"id":714},"a-核心原则",[716],{"type":510,"value":717},"A. 核心原则",{"type":504,"tag":557,"props":719,"children":720},{},[721,731,764],{"type":504,"tag":561,"props":722,"children":723},{},[724,729],{"type":504,"tag":520,"props":725,"children":726},{},[727],{"type":510,"value":728},"不关心实现细节",{"type":510,"value":730},"：避免测试组件的内部 state、私有方法或生命周期。而是通过与组件交互并断言结果来测试。",{"type":504,"tag":561,"props":732,"children":733},{},[734,739,741,747,749,755,756,762],{"type":504,"tag":520,"props":735,"children":736},{},[737],{"type":510,"value":738},"面向用户查询",{"type":510,"value":740},"：优先使用用户能感知到的方式来查找元素，如 ",{"type":504,"tag":551,"props":742,"children":744},{"className":743},[],[745],{"type":510,"value":746},"getByRole",{"type":510,"value":748},", ",{"type":504,"tag":551,"props":750,"children":752},{"className":751},[],[753],{"type":510,"value":754},"getByLabelText",{"type":510,"value":748},{"type":504,"tag":551,"props":757,"children":759},{"className":758},[],[760],{"type":510,"value":761},"getByText",{"type":510,"value":763},"。这使得测试对重构更具弹性。",{"type":504,"tag":561,"props":765,"children":766},{},[767,772],{"type":504,"tag":520,"props":768,"children":769},{},[770],{"type":510,"value":771},"可访问性驱动",{"type":510,"value":773},"：RTL 的查询方式天然地鼓励你编写更具可访问性 (A11y) 的代码。",{"type":504,"tag":712,"props":775,"children":777},{"id":776},"b-示例测试一个计数器组件",[778],{"type":510,"value":779},"B. 示例：测试一个计数器组件",{"type":504,"tag":544,"props":781,"children":785},{"className":782,"code":783,"language":784,"meta":553,"style":553},"language-jsx shiki shiki-themes github-light github-dark","\u002F\u002F Counter.js\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    \u003Cdiv>\n      \u003Cp>Current count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(count + 1)}>Increment\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F Counter.test.js\nimport { render, screen, fireEvent } from '@testing-library\u002Freact';\nimport { Counter } from '.\u002FCounter';\n\ntest('increments count when button is clicked', () => {\n  \u002F\u002F 1. Render: 渲染组件\n  render(\u003CCounter \u002F>);\n\n  \u002F\u002F 2. Find: 像用户一样找到元素\n  \u002F\u002F 找到一个角色为 \"button\" 且名字 (accessible name) 为 \"Increment\" 的元素\n  const incrementButton = screen.getByRole('button', { name: \u002Fincrement\u002Fi });\n  const countDisplay = screen.getByText(\u002Fcurrent count\u002Fi);\n\n  \u002F\u002F 3. Assert: 断言初始状态\n  expect(countDisplay).toHaveTextContent('Current count: 0');\n\n  \u002F\u002F 4. Act: 模拟用户交互\n  fireEvent.click(incrementButton);\n\n  \u002F\u002F 5. Assert: 断言交互后的结果\n  expect(countDisplay).toHaveTextContent('Current count: 1');\n});\n","jsx",[786],{"type":504,"tag":551,"props":787,"children":788},{"__ignoreMap":553},[789,801,833,843,868,927,941,961,987,1052,1069,1078,1087,1095,1104,1130,1156,1164,1196,1205,1229,1237,1246,1255,1321,1371,1379,1388,1420,1428,1437,1456,1464,1473,1502],{"type":504,"tag":790,"props":791,"children":794},"span",{"class":792,"line":793},"line",1,[795],{"type":504,"tag":790,"props":796,"children":798},{"style":797},"--shiki-default:#6A737D;--shiki-dark:#6A737D",[799],{"type":510,"value":800},"\u002F\u002F Counter.js\n",{"type":504,"tag":790,"props":802,"children":804},{"class":792,"line":803},2,[805,811,817,822,828],{"type":504,"tag":790,"props":806,"children":808},{"style":807},"--shiki-default:#D73A49;--shiki-dark:#F97583",[809],{"type":510,"value":810},"import",{"type":504,"tag":790,"props":812,"children":814},{"style":813},"--shiki-default:#24292E;--shiki-dark:#E1E4E8",[815],{"type":510,"value":816}," { useState } ",{"type":504,"tag":790,"props":818,"children":819},{"style":807},[820],{"type":510,"value":821},"from",{"type":504,"tag":790,"props":823,"children":825},{"style":824},"--shiki-default:#032F62;--shiki-dark:#9ECBFF",[826],{"type":510,"value":827}," 'react'",{"type":504,"tag":790,"props":829,"children":830},{"style":813},[831],{"type":510,"value":832},";\n",{"type":504,"tag":790,"props":834,"children":836},{"class":792,"line":835},3,[837],{"type":504,"tag":790,"props":838,"children":840},{"emptyLinePlaceholder":839},true,[841],{"type":510,"value":842},"\n",{"type":504,"tag":790,"props":844,"children":846},{"class":792,"line":845},4,[847,852,857,863],{"type":504,"tag":790,"props":848,"children":849},{"style":807},[850],{"type":510,"value":851},"export",{"type":504,"tag":790,"props":853,"children":854},{"style":807},[855],{"type":510,"value":856}," function",{"type":504,"tag":790,"props":858,"children":860},{"style":859},"--shiki-default:#6F42C1;--shiki-dark:#B392F0",[861],{"type":510,"value":862}," Counter",{"type":504,"tag":790,"props":864,"children":865},{"style":813},[866],{"type":510,"value":867},"() {\n",{"type":504,"tag":790,"props":869,"children":871},{"class":792,"line":870},5,[872,877,882,888,892,897,902,907,912,917,922],{"type":504,"tag":790,"props":873,"children":874},{"style":807},[875],{"type":510,"value":876},"  const",{"type":504,"tag":790,"props":878,"children":879},{"style":813},[880],{"type":510,"value":881}," [",{"type":504,"tag":790,"props":883,"children":885},{"style":884},"--shiki-default:#005CC5;--shiki-dark:#79B8FF",[886],{"type":510,"value":887},"count",{"type":504,"tag":790,"props":889,"children":890},{"style":813},[891],{"type":510,"value":748},{"type":504,"tag":790,"props":893,"children":894},{"style":884},[895],{"type":510,"value":896},"setCount",{"type":504,"tag":790,"props":898,"children":899},{"style":813},[900],{"type":510,"value":901},"] ",{"type":504,"tag":790,"props":903,"children":904},{"style":807},[905],{"type":510,"value":906},"=",{"type":504,"tag":790,"props":908,"children":909},{"style":859},[910],{"type":510,"value":911}," useState",{"type":504,"tag":790,"props":913,"children":914},{"style":813},[915],{"type":510,"value":916},"(",{"type":504,"tag":790,"props":918,"children":919},{"style":884},[920],{"type":510,"value":921},"0",{"type":504,"tag":790,"props":923,"children":924},{"style":813},[925],{"type":510,"value":926},");\n",{"type":504,"tag":790,"props":928,"children":930},{"class":792,"line":929},6,[931,936],{"type":504,"tag":790,"props":932,"children":933},{"style":807},[934],{"type":510,"value":935},"  return",{"type":504,"tag":790,"props":937,"children":938},{"style":813},[939],{"type":510,"value":940}," (\n",{"type":504,"tag":790,"props":942,"children":944},{"class":792,"line":943},7,[945,950,956],{"type":504,"tag":790,"props":946,"children":947},{"style":813},[948],{"type":510,"value":949},"    \u003C",{"type":504,"tag":790,"props":951,"children":953},{"style":952},"--shiki-default:#22863A;--shiki-dark:#85E89D",[954],{"type":510,"value":955},"div",{"type":504,"tag":790,"props":957,"children":958},{"style":813},[959],{"type":510,"value":960},">\n",{"type":504,"tag":790,"props":962,"children":964},{"class":792,"line":963},8,[965,970,974,979,983],{"type":504,"tag":790,"props":966,"children":967},{"style":813},[968],{"type":510,"value":969},"      \u003C",{"type":504,"tag":790,"props":971,"children":972},{"style":952},[973],{"type":510,"value":516},{"type":504,"tag":790,"props":975,"children":976},{"style":813},[977],{"type":510,"value":978},">Current count: {count}\u003C\u002F",{"type":504,"tag":790,"props":980,"children":981},{"style":952},[982],{"type":510,"value":516},{"type":504,"tag":790,"props":984,"children":985},{"style":813},[986],{"type":510,"value":960},{"type":504,"tag":790,"props":988,"children":990},{"class":792,"line":989},9,[991,995,1000,1005,1009,1014,1019,1024,1029,1034,1039,1044,1048],{"type":504,"tag":790,"props":992,"children":993},{"style":813},[994],{"type":510,"value":969},{"type":504,"tag":790,"props":996,"children":997},{"style":952},[998],{"type":510,"value":999},"button",{"type":504,"tag":790,"props":1001,"children":1002},{"style":859},[1003],{"type":510,"value":1004}," onClick",{"type":504,"tag":790,"props":1006,"children":1007},{"style":807},[1008],{"type":510,"value":906},{"type":504,"tag":790,"props":1010,"children":1011},{"style":813},[1012],{"type":510,"value":1013},"{() ",{"type":504,"tag":790,"props":1015,"children":1016},{"style":807},[1017],{"type":510,"value":1018},"=>",{"type":504,"tag":790,"props":1020,"children":1021},{"style":859},[1022],{"type":510,"value":1023}," setCount",{"type":504,"tag":790,"props":1025,"children":1026},{"style":813},[1027],{"type":510,"value":1028},"(count ",{"type":504,"tag":790,"props":1030,"children":1031},{"style":807},[1032],{"type":510,"value":1033},"+",{"type":504,"tag":790,"props":1035,"children":1036},{"style":884},[1037],{"type":510,"value":1038}," 1",{"type":504,"tag":790,"props":1040,"children":1041},{"style":813},[1042],{"type":510,"value":1043},")}>Increment\u003C\u002F",{"type":504,"tag":790,"props":1045,"children":1046},{"style":952},[1047],{"type":510,"value":999},{"type":504,"tag":790,"props":1049,"children":1050},{"style":813},[1051],{"type":510,"value":960},{"type":504,"tag":790,"props":1053,"children":1055},{"class":792,"line":1054},10,[1056,1061,1065],{"type":504,"tag":790,"props":1057,"children":1058},{"style":813},[1059],{"type":510,"value":1060},"    \u003C\u002F",{"type":504,"tag":790,"props":1062,"children":1063},{"style":952},[1064],{"type":510,"value":955},{"type":504,"tag":790,"props":1066,"children":1067},{"style":813},[1068],{"type":510,"value":960},{"type":504,"tag":790,"props":1070,"children":1072},{"class":792,"line":1071},11,[1073],{"type":504,"tag":790,"props":1074,"children":1075},{"style":813},[1076],{"type":510,"value":1077},"  );\n",{"type":504,"tag":790,"props":1079,"children":1081},{"class":792,"line":1080},12,[1082],{"type":504,"tag":790,"props":1083,"children":1084},{"style":813},[1085],{"type":510,"value":1086},"}\n",{"type":504,"tag":790,"props":1088,"children":1090},{"class":792,"line":1089},13,[1091],{"type":504,"tag":790,"props":1092,"children":1093},{"emptyLinePlaceholder":839},[1094],{"type":510,"value":842},{"type":504,"tag":790,"props":1096,"children":1098},{"class":792,"line":1097},14,[1099],{"type":504,"tag":790,"props":1100,"children":1101},{"style":797},[1102],{"type":510,"value":1103},"\u002F\u002F Counter.test.js\n",{"type":504,"tag":790,"props":1105,"children":1107},{"class":792,"line":1106},15,[1108,1112,1117,1121,1126],{"type":504,"tag":790,"props":1109,"children":1110},{"style":807},[1111],{"type":510,"value":810},{"type":504,"tag":790,"props":1113,"children":1114},{"style":813},[1115],{"type":510,"value":1116}," { render, screen, fireEvent } ",{"type":504,"tag":790,"props":1118,"children":1119},{"style":807},[1120],{"type":510,"value":821},{"type":504,"tag":790,"props":1122,"children":1123},{"style":824},[1124],{"type":510,"value":1125}," '@testing-library\u002Freact'",{"type":504,"tag":790,"props":1127,"children":1128},{"style":813},[1129],{"type":510,"value":832},{"type":504,"tag":790,"props":1131,"children":1133},{"class":792,"line":1132},16,[1134,1138,1143,1147,1152],{"type":504,"tag":790,"props":1135,"children":1136},{"style":807},[1137],{"type":510,"value":810},{"type":504,"tag":790,"props":1139,"children":1140},{"style":813},[1141],{"type":510,"value":1142}," { Counter } ",{"type":504,"tag":790,"props":1144,"children":1145},{"style":807},[1146],{"type":510,"value":821},{"type":504,"tag":790,"props":1148,"children":1149},{"style":824},[1150],{"type":510,"value":1151}," '.\u002FCounter'",{"type":504,"tag":790,"props":1153,"children":1154},{"style":813},[1155],{"type":510,"value":832},{"type":504,"tag":790,"props":1157,"children":1159},{"class":792,"line":1158},17,[1160],{"type":504,"tag":790,"props":1161,"children":1162},{"emptyLinePlaceholder":839},[1163],{"type":510,"value":842},{"type":504,"tag":790,"props":1165,"children":1167},{"class":792,"line":1166},18,[1168,1173,1177,1182,1187,1191],{"type":504,"tag":790,"props":1169,"children":1170},{"style":859},[1171],{"type":510,"value":1172},"test",{"type":504,"tag":790,"props":1174,"children":1175},{"style":813},[1176],{"type":510,"value":916},{"type":504,"tag":790,"props":1178,"children":1179},{"style":824},[1180],{"type":510,"value":1181},"'increments count when button is clicked'",{"type":504,"tag":790,"props":1183,"children":1184},{"style":813},[1185],{"type":510,"value":1186},", () ",{"type":504,"tag":790,"props":1188,"children":1189},{"style":807},[1190],{"type":510,"value":1018},{"type":504,"tag":790,"props":1192,"children":1193},{"style":813},[1194],{"type":510,"value":1195}," {\n",{"type":504,"tag":790,"props":1197,"children":1199},{"class":792,"line":1198},19,[1200],{"type":504,"tag":790,"props":1201,"children":1202},{"style":797},[1203],{"type":510,"value":1204},"  \u002F\u002F 1. Render: 渲染组件\n",{"type":504,"tag":790,"props":1206,"children":1208},{"class":792,"line":1207},20,[1209,1214,1219,1224],{"type":504,"tag":790,"props":1210,"children":1211},{"style":859},[1212],{"type":510,"value":1213},"  render",{"type":504,"tag":790,"props":1215,"children":1216},{"style":813},[1217],{"type":510,"value":1218},"(\u003C",{"type":504,"tag":790,"props":1220,"children":1221},{"style":884},[1222],{"type":510,"value":1223},"Counter",{"type":504,"tag":790,"props":1225,"children":1226},{"style":813},[1227],{"type":510,"value":1228}," \u002F>);\n",{"type":504,"tag":790,"props":1230,"children":1232},{"class":792,"line":1231},21,[1233],{"type":504,"tag":790,"props":1234,"children":1235},{"emptyLinePlaceholder":839},[1236],{"type":510,"value":842},{"type":504,"tag":790,"props":1238,"children":1240},{"class":792,"line":1239},22,[1241],{"type":504,"tag":790,"props":1242,"children":1243},{"style":797},[1244],{"type":510,"value":1245},"  \u002F\u002F 2. Find: 像用户一样找到元素\n",{"type":504,"tag":790,"props":1247,"children":1249},{"class":792,"line":1248},23,[1250],{"type":504,"tag":790,"props":1251,"children":1252},{"style":797},[1253],{"type":510,"value":1254},"  \u002F\u002F 找到一个角色为 \"button\" 且名字 (accessible name) 为 \"Increment\" 的元素\n",{"type":504,"tag":790,"props":1256,"children":1257},{"class":792,"line":5},[1258,1262,1267,1272,1277,1281,1285,1290,1295,1300,1306,1311,1316],{"type":504,"tag":790,"props":1259,"children":1260},{"style":807},[1261],{"type":510,"value":876},{"type":504,"tag":790,"props":1263,"children":1264},{"style":884},[1265],{"type":510,"value":1266}," incrementButton",{"type":504,"tag":790,"props":1268,"children":1269},{"style":807},[1270],{"type":510,"value":1271}," =",{"type":504,"tag":790,"props":1273,"children":1274},{"style":813},[1275],{"type":510,"value":1276}," screen.",{"type":504,"tag":790,"props":1278,"children":1279},{"style":859},[1280],{"type":510,"value":746},{"type":504,"tag":790,"props":1282,"children":1283},{"style":813},[1284],{"type":510,"value":916},{"type":504,"tag":790,"props":1286,"children":1287},{"style":824},[1288],{"type":510,"value":1289},"'button'",{"type":504,"tag":790,"props":1291,"children":1292},{"style":813},[1293],{"type":510,"value":1294},", { name:",{"type":504,"tag":790,"props":1296,"children":1297},{"style":824},[1298],{"type":510,"value":1299}," \u002F",{"type":504,"tag":790,"props":1301,"children":1303},{"style":1302},"--shiki-default:#032F62;--shiki-dark:#DBEDFF",[1304],{"type":510,"value":1305},"increment",{"type":504,"tag":790,"props":1307,"children":1308},{"style":824},[1309],{"type":510,"value":1310},"\u002F",{"type":504,"tag":790,"props":1312,"children":1313},{"style":807},[1314],{"type":510,"value":1315},"i",{"type":504,"tag":790,"props":1317,"children":1318},{"style":813},[1319],{"type":510,"value":1320}," });\n",{"type":504,"tag":790,"props":1322,"children":1324},{"class":792,"line":1323},25,[1325,1329,1334,1338,1342,1346,1350,1354,1359,1363,1367],{"type":504,"tag":790,"props":1326,"children":1327},{"style":807},[1328],{"type":510,"value":876},{"type":504,"tag":790,"props":1330,"children":1331},{"style":884},[1332],{"type":510,"value":1333}," countDisplay",{"type":504,"tag":790,"props":1335,"children":1336},{"style":807},[1337],{"type":510,"value":1271},{"type":504,"tag":790,"props":1339,"children":1340},{"style":813},[1341],{"type":510,"value":1276},{"type":504,"tag":790,"props":1343,"children":1344},{"style":859},[1345],{"type":510,"value":761},{"type":504,"tag":790,"props":1347,"children":1348},{"style":813},[1349],{"type":510,"value":916},{"type":504,"tag":790,"props":1351,"children":1352},{"style":824},[1353],{"type":510,"value":1310},{"type":504,"tag":790,"props":1355,"children":1356},{"style":1302},[1357],{"type":510,"value":1358},"current count",{"type":504,"tag":790,"props":1360,"children":1361},{"style":824},[1362],{"type":510,"value":1310},{"type":504,"tag":790,"props":1364,"children":1365},{"style":807},[1366],{"type":510,"value":1315},{"type":504,"tag":790,"props":1368,"children":1369},{"style":813},[1370],{"type":510,"value":926},{"type":504,"tag":790,"props":1372,"children":1374},{"class":792,"line":1373},26,[1375],{"type":504,"tag":790,"props":1376,"children":1377},{"emptyLinePlaceholder":839},[1378],{"type":510,"value":842},{"type":504,"tag":790,"props":1380,"children":1382},{"class":792,"line":1381},27,[1383],{"type":504,"tag":790,"props":1384,"children":1385},{"style":797},[1386],{"type":510,"value":1387},"  \u002F\u002F 3. Assert: 断言初始状态\n",{"type":504,"tag":790,"props":1389,"children":1391},{"class":792,"line":1390},28,[1392,1397,1402,1407,1411,1416],{"type":504,"tag":790,"props":1393,"children":1394},{"style":859},[1395],{"type":510,"value":1396},"  expect",{"type":504,"tag":790,"props":1398,"children":1399},{"style":813},[1400],{"type":510,"value":1401},"(countDisplay).",{"type":504,"tag":790,"props":1403,"children":1404},{"style":859},[1405],{"type":510,"value":1406},"toHaveTextContent",{"type":504,"tag":790,"props":1408,"children":1409},{"style":813},[1410],{"type":510,"value":916},{"type":504,"tag":790,"props":1412,"children":1413},{"style":824},[1414],{"type":510,"value":1415},"'Current count: 0'",{"type":504,"tag":790,"props":1417,"children":1418},{"style":813},[1419],{"type":510,"value":926},{"type":504,"tag":790,"props":1421,"children":1423},{"class":792,"line":1422},29,[1424],{"type":504,"tag":790,"props":1425,"children":1426},{"emptyLinePlaceholder":839},[1427],{"type":510,"value":842},{"type":504,"tag":790,"props":1429,"children":1431},{"class":792,"line":1430},30,[1432],{"type":504,"tag":790,"props":1433,"children":1434},{"style":797},[1435],{"type":510,"value":1436},"  \u002F\u002F 4. Act: 模拟用户交互\n",{"type":504,"tag":790,"props":1438,"children":1440},{"class":792,"line":1439},31,[1441,1446,1451],{"type":504,"tag":790,"props":1442,"children":1443},{"style":813},[1444],{"type":510,"value":1445},"  fireEvent.",{"type":504,"tag":790,"props":1447,"children":1448},{"style":859},[1449],{"type":510,"value":1450},"click",{"type":504,"tag":790,"props":1452,"children":1453},{"style":813},[1454],{"type":510,"value":1455},"(incrementButton);\n",{"type":504,"tag":790,"props":1457,"children":1459},{"class":792,"line":1458},32,[1460],{"type":504,"tag":790,"props":1461,"children":1462},{"emptyLinePlaceholder":839},[1463],{"type":510,"value":842},{"type":504,"tag":790,"props":1465,"children":1467},{"class":792,"line":1466},33,[1468],{"type":504,"tag":790,"props":1469,"children":1470},{"style":797},[1471],{"type":510,"value":1472},"  \u002F\u002F 5. Assert: 断言交互后的结果\n",{"type":504,"tag":790,"props":1474,"children":1476},{"class":792,"line":1475},34,[1477,1481,1485,1489,1493,1498],{"type":504,"tag":790,"props":1478,"children":1479},{"style":859},[1480],{"type":510,"value":1396},{"type":504,"tag":790,"props":1482,"children":1483},{"style":813},[1484],{"type":510,"value":1401},{"type":504,"tag":790,"props":1486,"children":1487},{"style":859},[1488],{"type":510,"value":1406},{"type":504,"tag":790,"props":1490,"children":1491},{"style":813},[1492],{"type":510,"value":916},{"type":504,"tag":790,"props":1494,"children":1495},{"style":824},[1496],{"type":510,"value":1497},"'Current count: 1'",{"type":504,"tag":790,"props":1499,"children":1500},{"style":813},[1501],{"type":510,"value":926},{"type":504,"tag":790,"props":1503,"children":1505},{"class":792,"line":1504},35,[1506],{"type":504,"tag":790,"props":1507,"children":1508},{"style":813},[1509],{"type":510,"value":1510},"});\n",{"type":504,"tag":516,"props":1512,"children":1513},{},[1514,1516,1522],{"type":510,"value":1515},"这个测试完美地体现了 RTL 的理念：它不知道 ",{"type":504,"tag":551,"props":1517,"children":1519},{"className":1518},[],[1520],{"type":510,"value":1521},"useState",{"type":510,"value":1523}," 的存在，只关心点击按钮后，屏幕上的文本是否如期更新。",{"type":504,"tag":528,"props":1525,"children":1526},{},[],{"type":504,"tag":532,"props":1528,"children":1530},{"id":1529},"_3-端到端测试-e2eplaywright-cypress",[1531],{"type":510,"value":1532},"3. 端到端测试 (E2E)：Playwright \u002F Cypress",{"type":504,"tag":516,"props":1534,"children":1535},{},[1536],{"type":510,"value":1537},"E2E 测试是质量保障的最后一道防线。",{"type":504,"tag":1539,"props":1540,"children":1541},"table",{},[1542,1567],{"type":504,"tag":1543,"props":1544,"children":1545},"thead",{},[1546],{"type":504,"tag":1547,"props":1548,"children":1549},"tr",{},[1550,1557,1562],{"type":504,"tag":1551,"props":1552,"children":1554},"th",{"align":1553},"left",[1555],{"type":510,"value":1556},"工具",{"type":504,"tag":1551,"props":1558,"children":1559},{"align":1553},[1560],{"type":510,"value":1561},"优点",{"type":504,"tag":1551,"props":1563,"children":1564},{"align":1553},[1565],{"type":510,"value":1566},"缺点",{"type":504,"tag":1568,"props":1569,"children":1570},"tbody",{},[1571,1612],{"type":504,"tag":1547,"props":1572,"children":1573},{},[1574,1583,1607],{"type":504,"tag":1575,"props":1576,"children":1577},"td",{"align":1553},[1578],{"type":504,"tag":520,"props":1579,"children":1580},{},[1581],{"type":510,"value":1582},"Playwright",{"type":504,"tag":1575,"props":1584,"children":1585},{"align":1553},[1586,1591,1593,1598,1600,1605],{"type":504,"tag":520,"props":1587,"children":1588},{},[1589],{"type":510,"value":1590},"跨浏览器",{"type":510,"value":1592},"：支持 Chromium, Firefox, WebKit。",{"type":504,"tag":520,"props":1594,"children":1595},{},[1596],{"type":510,"value":1597},"速度快",{"type":510,"value":1599},"：架构现代，执行效率高。",{"type":504,"tag":520,"props":1601,"children":1602},{},[1603],{"type":510,"value":1604},"功能强大",{"type":510,"value":1606},"：支持多 Tab、网络拦截、地理位置模拟等。",{"type":504,"tag":1575,"props":1608,"children":1609},{"align":1553},[1610],{"type":510,"value":1611},"生态相对 Cypress 略小。",{"type":504,"tag":1547,"props":1613,"children":1614},{},[1615,1623,1640],{"type":504,"tag":1575,"props":1616,"children":1617},{"align":1553},[1618],{"type":504,"tag":520,"props":1619,"children":1620},{},[1621],{"type":510,"value":1622},"Cypress",{"type":504,"tag":1575,"props":1624,"children":1625},{"align":1553},[1626,1631,1633,1638],{"type":504,"tag":520,"props":1627,"children":1628},{},[1629],{"type":510,"value":1630},"DX 极佳",{"type":510,"value":1632},"：拥有强大的图形化调试界面 (Time Travel)。",{"type":504,"tag":520,"props":1634,"children":1635},{},[1636],{"type":510,"value":1637},"社区庞大",{"type":510,"value":1639},"，文档和插件丰富。",{"type":504,"tag":1575,"props":1641,"children":1642},{"align":1553},[1643],{"type":510,"value":1644},"传统上只支持 Chromium-based 浏览器（新版已扩展支持）。架构原因，对多 Tab\u002Fiframe 支持不如 Playwright。",{"type":504,"tag":712,"props":1646,"children":1648},{"id":1647},"示例使用-playwright-测试登录流程",[1649],{"type":510,"value":1650},"示例：使用 Playwright 测试登录流程",{"type":504,"tag":544,"props":1652,"children":1656},{"className":1653,"code":1654,"language":1655,"meta":553,"style":553},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F login.spec.js\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest('successful login', async ({ page }) => {\n  \u002F\u002F 导航到登录页\n  await page.goto('\u002Flogin');\n\n  \u002F\u002F 像用户一样找到输入框和按钮\n  const emailInput = page.getByLabelText('Email');\n  const passwordInput = page.getByLabelText('Password');\n  const loginButton = page.getByRole('button', { name: \u002Flog in\u002Fi });\n\n  \u002F\u002F 填充表单\n  await emailInput.fill('user@example.com');\n  await passwordInput.fill('password123');\n\n  \u002F\u002F 点击登录\n  await loginButton.click();\n\n  \u002F\u002F 等待页面跳转并断言结果\n  await expect(page).toHaveURL('\u002Fdashboard');\n  await expect(page.getByText(\u002Fwelcome, user\u002Fi)).toBeVisible();\n});\n","javascript",[1657],{"type":504,"tag":551,"props":1658,"children":1659},{"__ignoreMap":553},[1660,1668,1693,1700,1749,1757,1788,1795,1803,1840,1877,1934,1941,1949,1979,2008,2015,2023,2044,2051,2059,2094,2149],{"type":504,"tag":790,"props":1661,"children":1662},{"class":792,"line":793},[1663],{"type":504,"tag":790,"props":1664,"children":1665},{"style":797},[1666],{"type":510,"value":1667},"\u002F\u002F login.spec.js\n",{"type":504,"tag":790,"props":1669,"children":1670},{"class":792,"line":803},[1671,1675,1680,1684,1689],{"type":504,"tag":790,"props":1672,"children":1673},{"style":807},[1674],{"type":510,"value":810},{"type":504,"tag":790,"props":1676,"children":1677},{"style":813},[1678],{"type":510,"value":1679}," { test, expect } ",{"type":504,"tag":790,"props":1681,"children":1682},{"style":807},[1683],{"type":510,"value":821},{"type":504,"tag":790,"props":1685,"children":1686},{"style":824},[1687],{"type":510,"value":1688}," '@playwright\u002Ftest'",{"type":504,"tag":790,"props":1690,"children":1691},{"style":813},[1692],{"type":510,"value":832},{"type":504,"tag":790,"props":1694,"children":1695},{"class":792,"line":835},[1696],{"type":504,"tag":790,"props":1697,"children":1698},{"emptyLinePlaceholder":839},[1699],{"type":510,"value":842},{"type":504,"tag":790,"props":1701,"children":1702},{"class":792,"line":845},[1703,1707,1711,1716,1720,1725,1730,1736,1741,1745],{"type":504,"tag":790,"props":1704,"children":1705},{"style":859},[1706],{"type":510,"value":1172},{"type":504,"tag":790,"props":1708,"children":1709},{"style":813},[1710],{"type":510,"value":916},{"type":504,"tag":790,"props":1712,"children":1713},{"style":824},[1714],{"type":510,"value":1715},"'successful login'",{"type":504,"tag":790,"props":1717,"children":1718},{"style":813},[1719],{"type":510,"value":748},{"type":504,"tag":790,"props":1721,"children":1722},{"style":807},[1723],{"type":510,"value":1724},"async",{"type":504,"tag":790,"props":1726,"children":1727},{"style":813},[1728],{"type":510,"value":1729}," ({ ",{"type":504,"tag":790,"props":1731,"children":1733},{"style":1732},"--shiki-default:#E36209;--shiki-dark:#FFAB70",[1734],{"type":510,"value":1735},"page",{"type":504,"tag":790,"props":1737,"children":1738},{"style":813},[1739],{"type":510,"value":1740}," }) ",{"type":504,"tag":790,"props":1742,"children":1743},{"style":807},[1744],{"type":510,"value":1018},{"type":504,"tag":790,"props":1746,"children":1747},{"style":813},[1748],{"type":510,"value":1195},{"type":504,"tag":790,"props":1750,"children":1751},{"class":792,"line":870},[1752],{"type":504,"tag":790,"props":1753,"children":1754},{"style":797},[1755],{"type":510,"value":1756},"  \u002F\u002F 导航到登录页\n",{"type":504,"tag":790,"props":1758,"children":1759},{"class":792,"line":929},[1760,1765,1770,1775,1779,1784],{"type":504,"tag":790,"props":1761,"children":1762},{"style":807},[1763],{"type":510,"value":1764},"  await",{"type":504,"tag":790,"props":1766,"children":1767},{"style":813},[1768],{"type":510,"value":1769}," page.",{"type":504,"tag":790,"props":1771,"children":1772},{"style":859},[1773],{"type":510,"value":1774},"goto",{"type":504,"tag":790,"props":1776,"children":1777},{"style":813},[1778],{"type":510,"value":916},{"type":504,"tag":790,"props":1780,"children":1781},{"style":824},[1782],{"type":510,"value":1783},"'\u002Flogin'",{"type":504,"tag":790,"props":1785,"children":1786},{"style":813},[1787],{"type":510,"value":926},{"type":504,"tag":790,"props":1789,"children":1790},{"class":792,"line":943},[1791],{"type":504,"tag":790,"props":1792,"children":1793},{"emptyLinePlaceholder":839},[1794],{"type":510,"value":842},{"type":504,"tag":790,"props":1796,"children":1797},{"class":792,"line":963},[1798],{"type":504,"tag":790,"props":1799,"children":1800},{"style":797},[1801],{"type":510,"value":1802},"  \u002F\u002F 像用户一样找到输入框和按钮\n",{"type":504,"tag":790,"props":1804,"children":1805},{"class":792,"line":989},[1806,1810,1815,1819,1823,1827,1831,1836],{"type":504,"tag":790,"props":1807,"children":1808},{"style":807},[1809],{"type":510,"value":876},{"type":504,"tag":790,"props":1811,"children":1812},{"style":884},[1813],{"type":510,"value":1814}," emailInput",{"type":504,"tag":790,"props":1816,"children":1817},{"style":807},[1818],{"type":510,"value":1271},{"type":504,"tag":790,"props":1820,"children":1821},{"style":813},[1822],{"type":510,"value":1769},{"type":504,"tag":790,"props":1824,"children":1825},{"style":859},[1826],{"type":510,"value":754},{"type":504,"tag":790,"props":1828,"children":1829},{"style":813},[1830],{"type":510,"value":916},{"type":504,"tag":790,"props":1832,"children":1833},{"style":824},[1834],{"type":510,"value":1835},"'Email'",{"type":504,"tag":790,"props":1837,"children":1838},{"style":813},[1839],{"type":510,"value":926},{"type":504,"tag":790,"props":1841,"children":1842},{"class":792,"line":1054},[1843,1847,1852,1856,1860,1864,1868,1873],{"type":504,"tag":790,"props":1844,"children":1845},{"style":807},[1846],{"type":510,"value":876},{"type":504,"tag":790,"props":1848,"children":1849},{"style":884},[1850],{"type":510,"value":1851}," passwordInput",{"type":504,"tag":790,"props":1853,"children":1854},{"style":807},[1855],{"type":510,"value":1271},{"type":504,"tag":790,"props":1857,"children":1858},{"style":813},[1859],{"type":510,"value":1769},{"type":504,"tag":790,"props":1861,"children":1862},{"style":859},[1863],{"type":510,"value":754},{"type":504,"tag":790,"props":1865,"children":1866},{"style":813},[1867],{"type":510,"value":916},{"type":504,"tag":790,"props":1869,"children":1870},{"style":824},[1871],{"type":510,"value":1872},"'Password'",{"type":504,"tag":790,"props":1874,"children":1875},{"style":813},[1876],{"type":510,"value":926},{"type":504,"tag":790,"props":1878,"children":1879},{"class":792,"line":1071},[1880,1884,1889,1893,1897,1901,1905,1909,1913,1917,1922,1926,1930],{"type":504,"tag":790,"props":1881,"children":1882},{"style":807},[1883],{"type":510,"value":876},{"type":504,"tag":790,"props":1885,"children":1886},{"style":884},[1887],{"type":510,"value":1888}," loginButton",{"type":504,"tag":790,"props":1890,"children":1891},{"style":807},[1892],{"type":510,"value":1271},{"type":504,"tag":790,"props":1894,"children":1895},{"style":813},[1896],{"type":510,"value":1769},{"type":504,"tag":790,"props":1898,"children":1899},{"style":859},[1900],{"type":510,"value":746},{"type":504,"tag":790,"props":1902,"children":1903},{"style":813},[1904],{"type":510,"value":916},{"type":504,"tag":790,"props":1906,"children":1907},{"style":824},[1908],{"type":510,"value":1289},{"type":504,"tag":790,"props":1910,"children":1911},{"style":813},[1912],{"type":510,"value":1294},{"type":504,"tag":790,"props":1914,"children":1915},{"style":824},[1916],{"type":510,"value":1299},{"type":504,"tag":790,"props":1918,"children":1919},{"style":1302},[1920],{"type":510,"value":1921},"log in",{"type":504,"tag":790,"props":1923,"children":1924},{"style":824},[1925],{"type":510,"value":1310},{"type":504,"tag":790,"props":1927,"children":1928},{"style":807},[1929],{"type":510,"value":1315},{"type":504,"tag":790,"props":1931,"children":1932},{"style":813},[1933],{"type":510,"value":1320},{"type":504,"tag":790,"props":1935,"children":1936},{"class":792,"line":1080},[1937],{"type":504,"tag":790,"props":1938,"children":1939},{"emptyLinePlaceholder":839},[1940],{"type":510,"value":842},{"type":504,"tag":790,"props":1942,"children":1943},{"class":792,"line":1089},[1944],{"type":504,"tag":790,"props":1945,"children":1946},{"style":797},[1947],{"type":510,"value":1948},"  \u002F\u002F 填充表单\n",{"type":504,"tag":790,"props":1950,"children":1951},{"class":792,"line":1097},[1952,1956,1961,1966,1970,1975],{"type":504,"tag":790,"props":1953,"children":1954},{"style":807},[1955],{"type":510,"value":1764},{"type":504,"tag":790,"props":1957,"children":1958},{"style":813},[1959],{"type":510,"value":1960}," emailInput.",{"type":504,"tag":790,"props":1962,"children":1963},{"style":859},[1964],{"type":510,"value":1965},"fill",{"type":504,"tag":790,"props":1967,"children":1968},{"style":813},[1969],{"type":510,"value":916},{"type":504,"tag":790,"props":1971,"children":1972},{"style":824},[1973],{"type":510,"value":1974},"'user@example.com'",{"type":504,"tag":790,"props":1976,"children":1977},{"style":813},[1978],{"type":510,"value":926},{"type":504,"tag":790,"props":1980,"children":1981},{"class":792,"line":1106},[1982,1986,1991,1995,1999,2004],{"type":504,"tag":790,"props":1983,"children":1984},{"style":807},[1985],{"type":510,"value":1764},{"type":504,"tag":790,"props":1987,"children":1988},{"style":813},[1989],{"type":510,"value":1990}," passwordInput.",{"type":504,"tag":790,"props":1992,"children":1993},{"style":859},[1994],{"type":510,"value":1965},{"type":504,"tag":790,"props":1996,"children":1997},{"style":813},[1998],{"type":510,"value":916},{"type":504,"tag":790,"props":2000,"children":2001},{"style":824},[2002],{"type":510,"value":2003},"'password123'",{"type":504,"tag":790,"props":2005,"children":2006},{"style":813},[2007],{"type":510,"value":926},{"type":504,"tag":790,"props":2009,"children":2010},{"class":792,"line":1132},[2011],{"type":504,"tag":790,"props":2012,"children":2013},{"emptyLinePlaceholder":839},[2014],{"type":510,"value":842},{"type":504,"tag":790,"props":2016,"children":2017},{"class":792,"line":1158},[2018],{"type":504,"tag":790,"props":2019,"children":2020},{"style":797},[2021],{"type":510,"value":2022},"  \u002F\u002F 点击登录\n",{"type":504,"tag":790,"props":2024,"children":2025},{"class":792,"line":1166},[2026,2030,2035,2039],{"type":504,"tag":790,"props":2027,"children":2028},{"style":807},[2029],{"type":510,"value":1764},{"type":504,"tag":790,"props":2031,"children":2032},{"style":813},[2033],{"type":510,"value":2034}," loginButton.",{"type":504,"tag":790,"props":2036,"children":2037},{"style":859},[2038],{"type":510,"value":1450},{"type":504,"tag":790,"props":2040,"children":2041},{"style":813},[2042],{"type":510,"value":2043},"();\n",{"type":504,"tag":790,"props":2045,"children":2046},{"class":792,"line":1198},[2047],{"type":504,"tag":790,"props":2048,"children":2049},{"emptyLinePlaceholder":839},[2050],{"type":510,"value":842},{"type":504,"tag":790,"props":2052,"children":2053},{"class":792,"line":1207},[2054],{"type":504,"tag":790,"props":2055,"children":2056},{"style":797},[2057],{"type":510,"value":2058},"  \u002F\u002F 等待页面跳转并断言结果\n",{"type":504,"tag":790,"props":2060,"children":2061},{"class":792,"line":1231},[2062,2066,2071,2076,2081,2085,2090],{"type":504,"tag":790,"props":2063,"children":2064},{"style":807},[2065],{"type":510,"value":1764},{"type":504,"tag":790,"props":2067,"children":2068},{"style":859},[2069],{"type":510,"value":2070}," expect",{"type":504,"tag":790,"props":2072,"children":2073},{"style":813},[2074],{"type":510,"value":2075},"(page).",{"type":504,"tag":790,"props":2077,"children":2078},{"style":859},[2079],{"type":510,"value":2080},"toHaveURL",{"type":504,"tag":790,"props":2082,"children":2083},{"style":813},[2084],{"type":510,"value":916},{"type":504,"tag":790,"props":2086,"children":2087},{"style":824},[2088],{"type":510,"value":2089},"'\u002Fdashboard'",{"type":504,"tag":790,"props":2091,"children":2092},{"style":813},[2093],{"type":510,"value":926},{"type":504,"tag":790,"props":2095,"children":2096},{"class":792,"line":1239},[2097,2101,2105,2110,2114,2118,2122,2127,2131,2135,2140,2145],{"type":504,"tag":790,"props":2098,"children":2099},{"style":807},[2100],{"type":510,"value":1764},{"type":504,"tag":790,"props":2102,"children":2103},{"style":859},[2104],{"type":510,"value":2070},{"type":504,"tag":790,"props":2106,"children":2107},{"style":813},[2108],{"type":510,"value":2109},"(page.",{"type":504,"tag":790,"props":2111,"children":2112},{"style":859},[2113],{"type":510,"value":761},{"type":504,"tag":790,"props":2115,"children":2116},{"style":813},[2117],{"type":510,"value":916},{"type":504,"tag":790,"props":2119,"children":2120},{"style":824},[2121],{"type":510,"value":1310},{"type":504,"tag":790,"props":2123,"children":2124},{"style":1302},[2125],{"type":510,"value":2126},"welcome, user",{"type":504,"tag":790,"props":2128,"children":2129},{"style":824},[2130],{"type":510,"value":1310},{"type":504,"tag":790,"props":2132,"children":2133},{"style":807},[2134],{"type":510,"value":1315},{"type":504,"tag":790,"props":2136,"children":2137},{"style":813},[2138],{"type":510,"value":2139},")).",{"type":504,"tag":790,"props":2141,"children":2142},{"style":859},[2143],{"type":510,"value":2144},"toBeVisible",{"type":504,"tag":790,"props":2146,"children":2147},{"style":813},[2148],{"type":510,"value":2043},{"type":504,"tag":790,"props":2150,"children":2151},{"class":792,"line":1248},[2152],{"type":504,"tag":790,"props":2153,"children":2154},{"style":813},[2155],{"type":510,"value":1510},{"type":504,"tag":528,"props":2157,"children":2158},{},[],{"type":504,"tag":532,"props":2160,"children":2162},{"id":2161},"_4-可测性设计-designing-for-testability",[2163],{"type":510,"value":2164},"4. 可测性设计 (Designing for Testability)",{"type":504,"tag":516,"props":2166,"children":2167},{},[2168],{"type":510,"value":2169},"好的测试不是在开发后“添加”的，而是在设计代码时就“内置”的。",{"type":504,"tag":2171,"props":2172,"children":2173},"ol",{},[2174,2255,2318,2352],{"type":504,"tag":561,"props":2175,"children":2176},{},[2177,2182,2183],{"type":504,"tag":520,"props":2178,"children":2179},{},[2180],{"type":510,"value":2181},"纯函数与副作用分离",{"type":510,"value":570},{"type":504,"tag":557,"props":2184,"children":2185},{},[2186,2191,2204,2222],{"type":504,"tag":561,"props":2187,"children":2188},{},[2189],{"type":510,"value":2190},"将复杂的业务逻辑抽离成纯函数（输入相同，输出必定相同）。纯函数极易测试。",{"type":504,"tag":561,"props":2192,"children":2193},{},[2194,2196,2202],{"type":510,"value":2195},"将与外部世界交互的副作用（API 请求、LocalStorage、",{"type":504,"tag":551,"props":2197,"children":2199},{"className":2198},[],[2200],{"type":510,"value":2201},"new Date()",{"type":510,"value":2203},"）隔离在特定的 Hooks 或模块中，并使用依赖注入。",{"type":504,"tag":561,"props":2205,"children":2206},{},[2207,2212,2214,2220],{"type":504,"tag":520,"props":2208,"children":2209},{},[2210],{"type":510,"value":2211},"反模式",{"type":510,"value":2213},"：组件内部混杂着复杂的计算和 ",{"type":504,"tag":551,"props":2215,"children":2217},{"className":2216},[],[2218],{"type":510,"value":2219},"fetch",{"type":510,"value":2221}," 调用。",{"type":504,"tag":561,"props":2223,"children":2224},{},[2225,2230,2232,2238,2240,2245,2247,2253],{"type":504,"tag":520,"props":2226,"children":2227},{},[2228],{"type":510,"value":2229},"好模式",{"type":510,"value":2231},"：创建一个 ",{"type":504,"tag":551,"props":2233,"children":2235},{"className":2234},[],[2236],{"type":510,"value":2237},"useUserData",{"type":510,"value":2239}," hook 负责 ",{"type":504,"tag":551,"props":2241,"children":2243},{"className":2242},[],[2244],{"type":510,"value":2219},{"type":510,"value":2246},"，组件只负责消费数据；复杂的计算逻辑放在 ",{"type":504,"tag":551,"props":2248,"children":2250},{"className":2249},[],[2251],{"type":510,"value":2252},"utils\u002Fcalculation.js",{"type":510,"value":2254}," 中。",{"type":504,"tag":561,"props":2256,"children":2257},{},[2258,2263,2264],{"type":504,"tag":520,"props":2259,"children":2260},{},[2261],{"type":510,"value":2262},"依赖注入 (Dependency Injection)",{"type":510,"value":570},{"type":504,"tag":557,"props":2265,"children":2266},{},[2267,2272,2286,2300],{"type":504,"tag":561,"props":2268,"children":2269},{},[2270],{"type":510,"value":2271},"不要在组件或函数内部硬编码外部依赖。通过 props 或参数将它们传入。",{"type":504,"tag":561,"props":2273,"children":2274},{},[2275,2279,2280],{"type":504,"tag":520,"props":2276,"children":2277},{},[2278],{"type":510,"value":2211},{"type":510,"value":570},{"type":504,"tag":551,"props":2281,"children":2283},{"className":2282},[],[2284],{"type":510,"value":2285},"const user = await api.fetchUser();",{"type":504,"tag":561,"props":2287,"children":2288},{},[2289,2293,2294],{"type":504,"tag":520,"props":2290,"children":2291},{},[2292],{"type":510,"value":2229},{"type":510,"value":570},{"type":504,"tag":551,"props":2295,"children":2297},{"className":2296},[],[2298],{"type":510,"value":2299},"function UserProfile({ fetchUser }) { const user = await fetchUser(); }",{"type":504,"tag":561,"props":2301,"children":2302},{},[2303,2308,2310,2316],{"type":504,"tag":520,"props":2304,"children":2305},{},[2306],{"type":510,"value":2307},"优势",{"type":510,"value":2309},"：在测试中，你可以轻松地传入一个 mock 的 ",{"type":504,"tag":551,"props":2311,"children":2313},{"className":2312},[],[2314],{"type":510,"value":2315},"fetchUser",{"type":510,"value":2317}," 函数，完全控制测试环境，使其快速、稳定、可预测。",{"type":504,"tag":561,"props":2319,"children":2320},{},[2321,2326,2327],{"type":504,"tag":520,"props":2322,"children":2323},{},[2324],{"type":510,"value":2325},"使用 Mock Service Worker (MSW)",{"type":510,"value":570},{"type":504,"tag":557,"props":2328,"children":2329},{},[2330,2342,2347],{"type":504,"tag":561,"props":2331,"children":2332},{},[2333,2335,2340],{"type":510,"value":2334},"MSW 是一个革命性的 API mock 工具。它通过在网络层面拦截请求来实现 mock，这意味着你的应用代码",{"type":504,"tag":520,"props":2336,"children":2337},{},[2338],{"type":510,"value":2339},"完全不需要任何修改",{"type":510,"value":2341},"。",{"type":504,"tag":561,"props":2343,"children":2344},{},[2345],{"type":510,"value":2346},"你的组件就像在和真实的后端 API 对话，但在测试环境中，所有请求都被 MSW 捕获并返回预设的 mock 响应。",{"type":504,"tag":561,"props":2348,"children":2349},{},[2350],{"type":510,"value":2351},"这使得编写集成测试变得异常简单和可靠。",{"type":504,"tag":561,"props":2353,"children":2354},{},[2355,2360,2361],{"type":504,"tag":520,"props":2356,"children":2357},{},[2358],{"type":510,"value":2359},"为测试添加定位符 (Test IDs)",{"type":510,"value":570},{"type":504,"tag":557,"props":2362,"children":2363},{},[2364,2369,2382,2391],{"type":504,"tag":561,"props":2365,"children":2366},{},[2367],{"type":510,"value":2368},"虽然 RTL 鼓励使用面向用户的查询，但在某些复杂或动态的 UI 中，这可能很困难。",{"type":504,"tag":561,"props":2370,"children":2371},{},[2372,2374,2380],{"type":510,"value":2373},"作为最后的备用方案，可以添加 ",{"type":504,"tag":551,"props":2375,"children":2377},{"className":2376},[],[2378],{"type":510,"value":2379},"data-testid",{"type":510,"value":2381}," 属性，专门用于测试定位。",{"type":504,"tag":561,"props":2383,"children":2384},{},[2385],{"type":504,"tag":551,"props":2386,"children":2388},{"className":2387},[],[2389],{"type":510,"value":2390},"screen.getByTestId('my-specific-element')",{"type":504,"tag":561,"props":2392,"children":2393},{},[2394,2399,2401,2406],{"type":504,"tag":520,"props":2395,"children":2396},{},[2397],{"type":510,"value":2398},"原则",{"type":510,"value":2400},"：优先使用用户可见的查询，仅在必要时使用 ",{"type":504,"tag":551,"props":2402,"children":2404},{"className":2403},[],[2405],{"type":510,"value":2379},{"type":510,"value":2341},{"type":504,"tag":528,"props":2408,"children":2409},{},[],{"type":504,"tag":532,"props":2411,"children":2413},{"id":2412},"_5-关联阅读",[2414],{"type":510,"value":2415},"5. 关联阅读",{"type":504,"tag":557,"props":2417,"children":2418},{},[2419,2438,2454,2470],{"type":504,"tag":561,"props":2420,"children":2421},{},[2422,2427,2429],{"type":504,"tag":520,"props":2423,"children":2424},{},[2425],{"type":510,"value":2426},"React Testing Library 官方文档",{"type":510,"value":2428},": ",{"type":504,"tag":2430,"props":2431,"children":2435},"a",{"href":2432,"rel":2433},"https:\u002F\u002Ftesting-library.com\u002Fdocs\u002Freact-testing-library\u002Fintro",[2434],"nofollow",[2436],{"type":510,"value":2437},"testing-library.com\u002Fdocs\u002Freact-testing-library\u002Fintro",{"type":504,"tag":561,"props":2439,"children":2440},{},[2441,2446,2447],{"type":504,"tag":520,"props":2442,"children":2443},{},[2444],{"type":510,"value":2445},"Playwright 官方文档",{"type":510,"value":2428},{"type":504,"tag":2430,"props":2448,"children":2451},{"href":2449,"rel":2450},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Fintro",[2434],[2452],{"type":510,"value":2453},"playwright.dev\u002Fdocs\u002Fintro",{"type":504,"tag":561,"props":2455,"children":2456},{},[2457,2462,2463],{"type":504,"tag":520,"props":2458,"children":2459},{},[2460],{"type":510,"value":2461},"Mock Service Worker (MSW) 官方文档",{"type":510,"value":2428},{"type":504,"tag":2430,"props":2464,"children":2467},{"href":2465,"rel":2466},"https:\u002F\u002Fmswjs.io\u002Fdocs\u002F",[2434],[2468],{"type":510,"value":2469},"mswjs.io",{"type":504,"tag":561,"props":2471,"children":2472},{},[2473,2478,2479],{"type":504,"tag":520,"props":2474,"children":2475},{},[2476],{"type":510,"value":2477},"Kent C. Dodds",{"type":510,"value":2428},{"type":504,"tag":2430,"props":2480,"children":2483},{"href":2481,"rel":2482},"https:\u002F\u002Fkentcdodds.com\u002Fblog\u002Ftesting-implementation-details",[2434],[2484],{"type":510,"value":2485},"Testing Implementation Details",{"type":504,"tag":2487,"props":2488,"children":2489},"style",{},[2490],{"type":510,"value":2491},"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);}",{"title":553,"searchDepth":803,"depth":803,"links":2493},[2494,2495,2499,2502,2503],{"id":534,"depth":803,"text":537},{"id":697,"depth":803,"text":700,"children":2496},[2497,2498],{"id":714,"depth":835,"text":717},{"id":776,"depth":835,"text":779},{"id":1529,"depth":803,"text":1532,"children":2500},[2501],{"id":1647,"depth":835,"text":1650},{"id":2161,"depth":803,"text":2164},{"id":2412,"depth":803,"text":2415},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":2505},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":2507},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":2509},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>",{"left":4,"top":4,"width":2511,"height":2511,"rotate":4,"vFlip":6,"hFlip":6,"body":2512},1024,"\u003Cpath fill=\"currentColor\" d=\"M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z\"\u002F>\u003Cpath fill=\"currentColor\" d=\"M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64\"\u002F>"]