从零重写:空文件夹里做出来Rewrite it: build the whole app in an empty folder
这一节没有新知识。只有一个要求:不看答案,把整个应用写出来。There is nothing new to learn here. There is one requirement: write the whole app without looking at the answer.
这一页有什么On this page4
- 在空文件夹里搭出 Vite + React + Vitest 的测试环境Set up a Vite, React and Vitest test environment in an empty folder
- 凭四个测试的要求写出 Context 和六个组件Write the Context and the six components from what the four tests ask for
- 自己发现并修掉 .js / .jsx 那个坑Find and fix the .js and .jsx problem yourself
- 跑到 4 passed / 4 totalReach 4 passed / 4 total
真实考试就是这样:一个仓库、一份 README、一套测试,没有答案。前面三个部分你都是「跟着看」,这一节是「自己做」。做不出来不代表白学了 —— 卡在哪一步,那一步就是你真正的薄弱点。A real exam looks exactly like this: one repository, one README, one set of tests, and no answer. In the first three parts you were reading along. In this lesson you do it yourself. Not finishing does not mean the earlier work was wasted. Wherever you get stuck is your real weak point.
cab-booking-context/src/test/App.test.jsx唯一允许看的东西:四个测试The only thing you may look at: the four tests
cab-booking-context/src/test/App.test.jsxcab-booking-context/src/data/data.json数据可以照抄,那不是考点The data can be copied as it is; it is not what is being tested
cab-booking-context/src/data/data.json按什么顺序写What order to write it in
让测试一条一条变绿,而不是全写完再跑Make the tests pass one at a time, instead of writing everything and running them at the end
一句话:按测试的顺序写 —— 测试 1 绿了再写测试 2 需要的东西。
为什么不要「全写完再跑」:六个组件加一个 Context 一次写完, 第一次跑出来四条全红, 你不知道是哪一层的问题。 而一次只让一条变绿,红的原因永远只有一个。
推荐顺序(每一步都跑一次npx vitest run):
- 搭环境 + 跑空测试。
npm create vite@latest、装vitest/jsdom/@testing-library/react/@testing-library/jest-dom, 配好vite.config.mjs的test段和 setup 文件。把测试文件放进去,先确认它「能跑起来并且全红」 —— 这一步就已经比源项目的基线好了。 - Context + Provider。三件套写完,
index.jsx里包住<App />。文件名直接叫.jsx—— 你已经知道那个坑了。 - Home + RideHistory → 测试 1 变绿。空历史的分支先写,列表分支可以先留空。
- CabOptions + CabCard → 测试 2 变绿。
data.json照抄, 六张卡的五个 testid 对照表检查一遍。 - App 的状态机 + Loading + CabConfirmation → 测试 3 变绿。这一步最长,因为要串起四个页面。
- RideHistory 的
slice(-3).reverse()→ 测试 4 变绿。前面留空的列表分支现在补上。
卡住的时候:提示分四级,先自己想 15 分钟再看第一级。 看提示不丢人,但一上来就看提示,这一节就白做了 —— 你练的不是「照着提示写代码」, 是「没有提示时自己找路」。
In one line: write in the order of the tests — get test 1 green, then build what test 2 needs.
Why not “write it all, then run”: write six components and a Context in one go and the first run gives you four red tests with no idea which layer is at fault. Turning them green one at a time means there is only ever one reason for the red.
A good order (run npx vitest run after every step):
- Set up the project and run the empty test.
npm create vite@latest, installvitest/jsdom/@testing-library/react/@testing-library/jest-dom, then configure thetestsection ofvite.config.mjsand the setup file. Drop in the test file and confirm it runs and is all red — that alone already beats the source project’s baseline. - Context and Provider. Write the three parts and wrap
<App />inindex.jsx. Name the file.jsxfrom the start — you know about that trap now. - Home + RideHistory, until test 1 is green. Write the empty-history branch first; the list branch can stay empty for now.
- CabOptions + CabCard, until test 2 is green. Copy
data.jsonverbatim and check the five testids on each of the six cards against the table. - App’s state machine + Loading + CabConfirmation, until test 3 is green. This is the longest step, because it strings four pages together.
- RideHistory’s
slice(-3).reverse(), until test 4 is green. Fill in the list branch you left empty earlier.
When you get stuck: the hints come in four levels — think for 15 minutes before opening the first one. There is no shame in reading a hint, but opening one immediately wastes the whole exercise: what you are training is not “write code from a hint”, it is “find the way with no hint at all”.
动手做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.
- 首页:一个大标题「Book a Safe Ride with HackerRide」、一个 data-testid="book-button" 的按钮,下面是行程历史区Home page: a big heading "Book a Safe Ride with HackerRide", a button with data-testid="book-button", and the ride history area below it
- 行程历史:没有记录时显示 <p data-testid="no-ride-title">No ride history yet.</p>;有记录时每条一个 <li data-testid="history-cabs">,显示车名和 $价格Ride history: with no records show <p data-testid="no-ride-title">No ride history yet.</p>; with records show one <li data-testid="history-cabs"> per entry, holding the cab name and the $price
- 行程历史只显示最新三条,最新的排最上面The ride history shows the three newest entries only, newest at the top
- 点 book-button 进入选车页:容器 data-testid="all-cabs-section",按类型分三组,每组一个 <h3 data-testid="car-type-heading">,顺序必须是 Sedan / SUV / LuxuryPressing book-button opens the cab page: a container with data-testid="all-cabs-section", three groups by type, each with one <h3 data-testid="car-type-heading">, and the order has to be Sedan / SUV / Luxury
- 每辆车一张卡,五个 testid:cab-card-img / cab-card-name / cab-card-type / cab-card-price / cab-card-select-button。类型显示 "Type: X",价格显示 "Fare: $N"One card per cab, with five testids: cab-card-img / cab-card-name / cab-card-type / cab-card-price / cab-card-select-button. The type reads "Type: X" and the price reads "Fare: $N"
- 点某张卡的 Select:把这辆车记为当前预订、追加进历史,然后进入加载页 data-testid="loading"Pressing Select on a card: record that cab as the current booking, append it to the history, then go to the loading page with data-testid="loading"
- 加载页 1000ms 后自动进入确认页;确认页 data-testid="confirm-message" 显示「<车名> is on the way and will arrive shortly.」The loading page moves to the confirmation page after 1000ms; the confirmation page shows data-testid="confirm-message" reading "<cab name> is on the way and will arrive shortly."
- 确认页有 data-testid="confirm-button",点了回首页,此时历史里能看到刚才那辆车The confirmation page has data-testid="confirm-button"; pressing it returns to the home page, where the history now shows that cab
- 状态必须放在 Context 里:createContext + Provider + 自定义 hook(hook 里带「不在 Provider 内就抛错」的守卫),Provider 包在 App 外面The state has to live in a Context: createContext + Provider + a custom hook (the hook carries a guard that throws when it is used outside the Provider), and the Provider wraps App
- 数据用 data.json:三个类型各两辆车,Sedan 第一辆是 Ford Fusion / $20,SUV 两辆是 Toyota Highlander / Ford Explorer,Sedan 第二辆是 Honda AccordThe data comes from data.json: two cabs per type, the first Sedan is Ford Fusion / $20, the two SUVs are Toyota Highlander / Ford Explorer, and the second Sedan is Honda Accord
这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.
初学者常见的几种写法错误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.
而按测试顺序推进的话,每一步只有一个变量: 刚写完
RideHistory、测试 1 还是红, 那问题只可能在你刚写的那几行或 Provider 层级里。这不是「教学建议」,是真实考试里的时间管理。考场有计时,调试时间比写代码时间更容易失控。Four failing tests carry almost no information. A Provider on the wrong level, a misspelled testid, and a state machine that is not connected are three completely different causes that produce the same output.
If you work through the tests in order, each step has only one variable: you have just written
RideHistory and test 1 is still failing, so the problem can only be in the lines you just wrote or in the level of the Provider.This is not a teaching suggestion, it is time management in a real exam. The exam is timed, and debugging time gets out of control more easily than writing time.
换一道题也能用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.
- 按测试顺序写:测试 1 绿了再写测试 2 需要的东西,每步只有一个变量。Write in test order: make test 1 pass, then write what test 2 needs, so every step has only one variable.
- 第一步是「让测试能跑起来并且全红」—— 这就已经好过源项目的基线了。The first step is to get the tests running and all failing. That alone is better than the baseline of the source project.
- 带 JSX 的文件从一开始就叫 .jsx,别重复那个坑。Give every file that contains JSX the .jsx extension from the start, so the same problem does not come back.
- 两个最容易错的点:slice(-3).reverse() 的顺序、确认页的 ?.name。The two easiest things to get wrong: the order of slice(-3).reverse(), and ?.name on the confirmation page.
- 提示分四级,先自己想 15 分钟 —— 你练的是没提示时自己找路。The hints come in four levels. Think for 15 minutes on your own first, because what you are practising is finding the way without hints.