先读四个测试:它们到底要什么Read the four tests first: what exactly they ask for
四个测试全靠 data-testid 找元素。先抄一张 testid 表出来,再动手。All four tests find elements by data-testid. Copy out the list of testids first, then start writing.
这一页有什么On this page5
- 读出四个测试各自查的是什么Say what each of the four tests checks
- 抄出全部 13 个 data-testid,知道改名会红哪一片Copy out all 13 data-testid values, and know which tests fail if you rename one
- 说清为什么测试 4 是这道题真正的分水岭Explain why test 4 is the real dividing line in this task
- 知道 `vi.useFakeTimers()` 让 Loading 的 1 秒变成可控的Know that `vi.useFakeTimers()` puts the 1 second in Loading under your control
这道题的判分完全由 data-testid 驱动 —— 页面长得对但 testid 错一个,那一片全红。先读测试再写代码,能省掉一半的返工。测试 4「只留最新三条」是分水岭:slice 方向写反、忘了 reverse、或者原地改了 state,都会挂在这一条。Scoring here is driven entirely by data-testid. The page can look right, and one wrong testid still fails every check around it. Reading the tests before writing code saves half of the rework. Test 4, which keeps only the three newest rides, is the dividing line: a slice in the wrong direction, a missing reverse, or changing the state array in place all fail on that one test.
cab-booking-context/src/test/App.test.jsx四个测试,判分的全部依据Four tests, and the whole basis for the marks
cab-booking-context/src/test/App.test.jsxcab-booking-context/src/data/data.json三组六辆车,分组顺序来自这里的键顺序Six cars in three groups; the group order comes from the key order here
cab-booking-context/src/data/data.json四个测试各查什么What each of the four tests checks
读完这张表,你就知道要写哪些东西Read this table and you know everything you have to build
一句话:四个测试从「首页长对了吗」一路走到 「连订四辆车之后历史对吗」,刚好是一次完整的用户流程。
| # | 测试名 | 查什么 |
|---|---|---|
| 1 | renders the home page and empty ride history | 首页标题 + book-button + no-ride-title 的文字是No ride history yet. |
| 2 | shows grouped cab options with all required card fields | all-cabs-section 存在;三个 car-type-heading 的文字严格等于 ["Sedan", "SUV", "Luxury"];五种卡片字段各 6 个 |
| 3 | completes a booking and adds it to ride history | 选第一辆 → 出现 loading → 时间推进 1 秒 →confirm-message 是Ford Fusion is on the way and will arrive shortly.→ 点确认 → 历史里有 Ford Fusion 和 $20 |
| 4 | keeps only the newest three rides | 连订 4 辆,历史只剩 3 条,顺序是 Ford Explorer / Toyota Highlander / Honda Accord, 而且 Ford Fusion必须已经不在 DOM 里 |
测试 2 那个 toEqual 要特别注意:它比的是一个有序数组,不是「包含这三个」。 分组顺序错了就红。而顺序不是你决定的 —— 它来自 data.json 的键顺序, 你只要老实地 Object.keys(cabData).map(...) 就对了。
测试 4 是分水岭。它同时查三件事:只剩三条(数量)、最新在最上(顺序)、最旧那条真的不在 DOM 里(不是藏起来了)。 后面有一整节专门讲它。
In one line: the four tests walk from “does the home page look right” to “is the history correct after four bookings” — exactly one full user journey.
| # | Test | What it checks |
|---|---|---|
| 1 | renders the home page and empty ride history | The hero heading, book-button, and no-ride-title reading No ride history yet. |
| 2 | shows grouped cab options with all required card fields | all-cabs-section exists; the three car-type-heading texts are strictly equal to ["Sedan", "SUV", "Luxury"]; five card fields, 6 of each |
| 3 | completes a booking and adds it to ride history | Pick the first cab → loading appears → advance time by 1s → confirm-message reads Ford Fusion is on the way and will arrive shortly. → confirm → history holds Ford Fusion and $20 |
| 4 | keeps only the newest three rides | Book 4 cabs; history has exactly 3 entries, ordered Ford Explorer / Toyota Highlander / Honda Accord, and Ford Fusion must be gone from the DOM |
Watch that toEqual in test 2: it compares an ordered array, not “contains these three”. Wrong group order fails. And the order is not yours to choose — it comes from the key order in data.json, so plainly mapping Object.keys(cabData) gets it right.
Test 4 is where people fall. It checks three things at once: only three remain (count), newest on top (order), and the oldest is really out of the DOM (not merely hidden). A whole lesson below is about it.
cab-booking-context/src/test/App.test.jsx13 个 data-testid 就是契约The 13 data-testid values are the contract
改一个名字,红一片。所以先抄表Rename one and a whole group of tests fails. So copy the list out first
一句话:这道题的判分不看你的 class 名、 不看 DOM 结构、也不看样式,只看 13 个 data-testid 和它们里面的文字。
| testid | 在哪个组件 | 几个 |
|---|---|---|
book-button | Home | 1 |
no-ride-title | RideHistory(空的时候) | 0 或 1 |
history-cabs | RideHistory(每条一个) | 0–3 |
all-cabs-section | CabOptions | 1 |
car-type-heading | CabOptions(每组一个) | 3 |
cab-card-img / -name / -type / -price / -select-button | CabCard(每张卡五个) | 各 6 |
loading | Loading | 1 |
confirm-message / confirm-button | CabConfirmation | 各 1 |
两个容易忽略的细节:
no-ride-title和history-cabs是互斥的 —— 空的时候只有前者, 有记录的时候只有后者。所以 RideHistory 里必须是一个三元表达式, 不能两个都渲染。- 测试 4 最后一行是
queryByText(/Ford Fusion/)不能存在。注意它用的是queryBy而不是getBy——getBy找不到会抛错,queryBy找不到返回 null, 所以断言「不存在」时只能用queryBy。 这一点面试也会问。
In one line: the grader ignores your class names, your DOM shape and your styling. It looks at 13 data-testid hooks and the text inside them.
| testid | Component | How many |
|---|---|---|
book-button | Home | 1 |
no-ride-title | RideHistory (when empty) | 0 or 1 |
history-cabs | RideHistory (one per row) | 0–3 |
all-cabs-section | CabOptions | 1 |
car-type-heading | CabOptions (one per group) | 3 |
cab-card-img / -name / -type / -price / -select-button | CabCard (five per card) | 6 each |
loading | Loading | 1 |
confirm-message / confirm-button | CabConfirmation | 1 each |
Two details people miss:
no-ride-titleandhistory-cabsare mutually exclusive — only the first when empty, only the second once there are rides. So RideHistory needs a ternary; rendering both fails.- The last line of test 4 asserts that
queryByText(/Ford Fusion/)is not there. Note it usesqueryBy, notgetBy—getBythrows when nothing matches,queryByreturns null, so asserting absence only works withqueryBy. Interviewers ask about this too.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
no-ride-title, 有记录的时候只能出现 history-cabs。把三个空填上。When it is empty only no-ride-title may appear; when there are records only history-cabs may appear. Fill in the three blanks.初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
expect(rides).toHaveLength(3)。history-cabs 挂在 <ul> 上,getAllByTestId 只找到 1 个, 断言 3 直接失败。顺带另一个错:
key={ride.id} 在这里不安全 —— 同一辆车可以被订两次,id 会重复。 源项目用的是 `${ride.id}-${index}`。Test 4 is expect(rides).toHaveLength(3). Here history-cabs sits on the <ul>, so getAllByTestId finds only 1 element and the assertion for 3 fails.A second mistake in the same snippet:
key={ride.id} is not safe here — the same cab can be booked twice, so the id repeats. The source project uses `${ride.id}-${index}`.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- 四个测试是一次完整用户流程:首页 → 选车 → 加载 → 确认 → 历史。The four tests are one complete user journey: home page, pick a cab, loading, confirmation, history.
- 13 个 data-testid 是唯一契约;改名字或挂错层级都会红一片。The 13 data-testid values are the only contract; a renamed one or one on the wrong element fails a whole group of tests.
- 测试 2 的 toEqual 是有序断言,分组顺序来自 data.json 的键顺序,别自己排。The toEqual in test 2 is an ordered assertion. The group order comes from the key order in data.json, so do not sort them yourself.
- 断言「不存在」只能用 queryBy —— getBy 找不到会抛错。To assert that something is absent you have to use queryBy. getBy throws an error when it finds nothing.
- 原样跑是 0 个测试跑起来:把 CabContext.js 改名成 .jsx 才能开始。Run the project as it comes and 0 tests start: rename CabContext.js to .jsx before anything else.