DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 5 个练习(共 148 个)。Showing 5 of 148.
来自From 从零重写:空文件夹到 4 个测试全过Write it again yourself: from an empty folder to 4 passing tests · React 考试React exam
L4从零重写Rebuild from scratch从零重建 Q1 · Notes ManagerRebuild Q1 · Notes Manager

空目录开始,建出一个 React + TypeScript + Vite 项目, 实现 Notes Manager 的增删改,让下面那四个测试全过。不要打开 react-notes-app 参考。

Starting from an empty directory, build a React + TypeScript + Vite project. Implement add, delete and edit in Notes Manager, and make all four tests below pass. Do not open react-notes-app to look.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 页面上方是表单:Title 输入框、Content 文本域、一个提交按钮The form sits at the top of the page: a Title input, a Content textarea, and one submit button
  • 页面下方是表格:表头 Title / Content / Edit / Delete,每条笔记一行The table sits below: the header is Title / Content / Edit / Delete, with one row per note
  • 两个输入框都必须是受控的(value + onChange)Both inputs must be controlled (value + onChange)
  • 标题或内容为空(含只有空格)时,提交按钮 disabledWhen the title or the content is empty (including only spaces), the submit button is disabled
  • Task 1 Add:提交后新笔记出现在表格末尾,原有的都还在Task 1 Add: after submit the new note appears at the end of the table, and every existing note is still there
  • Task 2 Delete:点某行的 Delete,该行按 id 被移除(同名笔记只删对的那条)Task 2 Delete: clicking Delete on a row removes that row by id (with notes of the same name, only the right one goes)
  • Task 3 Edit:点某行的 Edit → 内容回填进表单、按钮文字变成 UpdateTask 3 Edit: clicking Edit on a row fills its content back into the form, and the button text becomes Update
  • Task 3 提交后:该笔记在原位置被更新(顺序不变),然后退出编辑模式(表单清空、按钮回到 Add)Task 3 after submit: the note is updated in place (the order does not change), and edit mode ends (the form clears, the button goes back to Add)
  • 必须带上这些 data-testid:note-manager / note-form / form-input / form-textarea / form-submit-button / notes-listThese data-testid values are required: note-manager / note-form / form-input / form-textarea / form-submit-button / notes-list
  • 行内按钮的文字必须正好是 Edit 和 DeleteThe text on the row buttons must be exactly Edit and Delete
  • Note 的类型是 { id: number; title: string; content: string }The type of Note is { id: number; title: string; content: string }
你需要自己建的文件Files you create yourself
文件清单File list
package.json自己写 scripts 与依赖(react / react-dom / vite / @vitejs/plugin-react / typescript / vitest / jsdom / @testing-library/*)You write the scripts and dependencies (react / react-dom / vite / @vitejs/plugin-react / typescript / vitest / jsdom / @testing-library/*)
index.html一个 <div id="root"> 加一行 module scriptOne <div id="root"> plus one module script line
tsconfig.jsonstrict、jsx: react-jsx、moduleResolution: bundlerstrict, jsx: react-jsx, moduleResolution: bundler
vite.config.tsReact 插件 + 内联 vitest 配置(environment: jsdom、globals、setupFiles)The React plugin plus an inline vitest config (environment: jsdom, globals, setupFiles)
vitest.setup.tsimport "@testing-library/jest-dom"
src/main.tsxcreateRoot().render(<App />)
src/App.tsx渲染顶层组件Renders the top-level component
src/types/Note.tsNote 类型The Note type
src/components/NoteManager/index.tsx★ 状态所有者:notes + noteToEdit + 三个 handler★ The state owner: notes + noteToEdit + three handlers
src/components/NoteForm/index.tsx★ 受控表单、编辑回填、Add/Update 切换、提交时 id 的取舍★ The controlled form, filling values back for an edit, switching Add and Update, and choosing the id on submit
src/components/NoteTable/index.tsx表格骨架 + map + notes-list 的 testidThe table skeleton, the map, and the notes-list data-testid
src/components/NoteItem/index.tsx单行 + Edit / Delete 按钮One row plus the Edit and Delete buttons
src/NoteManager.test.tsx把四个测试抄进来当判卷器(见下方参考答案区)Copy the four tests in and let them grade you (see the reference answer area below)
写完后在本机这样验证Verify it locally like this
npm install
装完依赖,node_modules 与 package-lock.json 出现The dependencies install, and node_modules and package-lock.json appear
npm run dev
打开提示的 localhost 地址,能看到表单和空表格Open the localhost address it prints, and you see the form and an empty table
npx vitest run
Test Files 1 passed (1) / Tests 4 passed (4)
npm run dev
手动验证三件事:① 加三条同名笔记,删中间那条,只消失一条 ② 编辑中间那条,它还在第二行 ③ 更新完按钮回到 Add、表单清空Check three things by hand: ① add three notes with the same name, delete the middle one, and only one disappears ② edit the middle one and it is still on the second row ③ after the update the button goes back to Add and the form clears
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。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.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 从零重写:空文件夹到 4 个测试全过Write it again yourself: from an empty folder to 4 passing tests · React 考试React exam
L4从零重写Rebuild from scratch从零重建 Q2 · 并发任务调度器Rebuild Q2 · the concurrent task runner

只给类型定义和三条要求。自己写出 runTasks, 并自己写一个验证台来证明它对。

You get only the type definitions and three requirements. Write runTasks yourself, and write your own check harness to show that it is right.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • runTasks(tasks, limit) 接收一个「函数数组」,每个函数被调用后返回 PromiserunTasks(tasks, limit) takes an array of functions, and each function returns a Promise when it is called
  • 同一时刻最多 limit 个任务在运行;某个结束后立刻启动下一个At most limit tasks run at the same time; as soon as one finishes, start the next
  • 任何任务失败都不能让 runTasks 抛错No failing task may make runTasks throw
  • 返回数组顺序必须与 tasks 一致The order of the returned array must match tasks
  • 成功写 { status: "fulfilled", value },失败写 { status: "rejected", reason }On success write { status: "fulfilled", value }; on failure write { status: "rejected", reason }
  • 自己写一个 demo:6 个任务(其中至少 1 个 reject)、limit = 2,打印实时并发数与最终结果Write your own demo: 6 tasks (at least 1 of which rejects), limit = 2, printing how many run at each moment and the final results
你需要自己建的文件Files you create yourself
文件清单File list
package.json装 tsx 和 typescript,加一条跑 demo 的 scriptInstall tsx and typescript, and add one script that runs the demo
tsconfig.jsonstrict: true 就够了strict: true is enough
q2/taskRunner.ts★ Task / SettledResult 类型 + runTasks 实现★ The Task / SettledResult types plus the runTasks implementation
q2/demo.ts★ 自己写验证台:一个 running 计数器 + 6 个任务 + 打印★ Write the check harness yourself: one running counter, 6 tasks, and the printing
写完后在本机这样验证Verify it locally like this
npm install
装好 tsx 和 typescripttsx and typescript are installed
npm run q2
输出里 running now 从不超过 2;最终 6 条结果顺序与输入一致;reject 的那条是 { status: 'rejected', reason: Error }running now never goes above 2 in the output; the final 6 results are in the same order as the input; the rejected one is { status: 'rejected', reason: Error }
npx tsc --noEmit
没有类型错误No type errors
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。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.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing · Federation 考试Federation exam
L4从零重写Rebuild from scratch从零重建 Task 1 · Orders subgraphRebuild Task 1 · the Orders subgraph

空目录开始,搭出一个 Apollo Federation subgraph, 实现四个 resolver 加一个 mutation,让 10 个测试全过, 并且 _service_entities 都能正常工作。不要打开源项目的 orderResolvers.js。

Starting from an empty directory, build an Apollo Federation subgraph. Write four resolvers plus one mutation, get all 10 tests passing, and make both _service and _entities work. Do not open orderResolvers.js from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 用 @apollo/server + @apollo/subgraph 起一个 subgraph,监听 4000Start a subgraph with @apollo/server + @apollo/subgraph, listening on 4000
  • schema 从 .graphql 文件读入,用 buildSubgraphSchema 组装Read the schema from a .graphql file and assemble it with buildSubgraphSchema
  • 每个请求构造 context:三个数据源、两个 DataLoader、一个 correlationIdBuild the context per request: three data sources, two DataLoaders, one correlationId
  • correlationId 优先取请求头 x-correlation-id,没有就生成Take correlationId from the x-correlation-id request header, and generate one when it is absent
  • 实现 User.__resolveReference:把 representation 变成本地对象Write User.__resolveReference: turn the representation into a local object
  • 实现 User.orders:按 user.id 取订单,[Order!]! 所以绝不返回 nullWrite User.orders: read orders by user.id; the type is [Order!]!, so never return null
  • 实现 Order.shippingInfo:必须走 DataLoader 防 N+1;可空,找不到返回 nullWrite Order.shippingInfo: it must go through the DataLoader to prevent N+1; it is nullable, so return null when nothing is found
  • 实现 Query.order:走 DataLoader;找不到抛带 ORDER_NOT_FOUND 的 GraphQLErrorWrite Query.order: go through the DataLoader; when nothing is found, throw a GraphQLError carrying ORDER_NOT_FOUND
  • 实现 Query.orders:校验 userId;[Order!]! 所以兜底 []Write Query.orders: validate userId; the type is [Order!]!, so fall back to []
  • 实现 Mutation.createOrder:先查商品价格补全 items,再创建;校验失败抛 INVALID_INPUTWrite Mutation.createOrder: look up product prices to complete items first, then create; throw INVALID_INPUT when validation fails
  • 两个 DataLoader 的 batch 函数:返回数组的长度与顺序必须和 keys 一一对应The batch function of both DataLoaders: the array it returns must match keys in both length and order
  • 所有 resolver 都用 try/catch,catch 第一行放行已有的 GraphQLErrorWrap every resolver in try/catch, and let an existing GraphQLError pass through on the first line of catch
  • 所有日志和错误 extensions 里带上 correlationIdCarry correlationId in every log line and in the extensions of every error
你需要自己建的文件Files you create yourself
文件清单File list
package.json自己写:type: module、start / test script(test 要带 NODE_OPTIONS=--experimental-vm-modules)、依赖 @apollo/server @apollo/subgraph graphql graphql-tag dataloader,devDep jest @jest/globals,以及内嵌 jest 配置You write it: type: module, the start / test scripts (test needs NODE_OPTIONS=--experimental-vm-modules), the dependencies @apollo/server @apollo/subgraph graphql graphql-tag dataloader, the devDependencies jest @jest/globals, and an inline jest config
src/schema.graphql★ 抄源项目的(这是题目):User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input★ Copy it from the source project (this is the question): the User entity + Order/OrderItem/ShippingInfo + enum + Query/Mutation + input
src/dataSources/orderDataSource.js★ 抄源项目的(这是题目):三个 mock 数据源类。注意 OrderDataSource 只有 getOrder / getOrdersByUserId / createOrder★ Copy it from the source project (this is the question): three mock data source classes. Note that OrderDataSource has only getOrder / getOrdersByUserId / createOrder
src/index.js★ 自己写:读 schema、buildSubgraphSchema、ApolloServer + formatError、startStandaloneServer、每请求造 context★ You write it: read the schema, buildSubgraphSchema, ApolloServer + formatError, startStandaloneServer, and build the context per request
src/resolvers/orderResolvers.js★★ 自己写:两个 loader 工厂 + resolvers(User / Order / Query / Mutation)+ ErrorCodes★★ You write it: two loader factories + the resolvers (User / Order / Query / Mutation) + ErrorCodes
__tests__/resolvers.test.js★ 抄源项目的(这是判卷器):10 个测试,beforeEach 里重建 dataSources 与 loaders★ Copy it from the source project (this is what grades you): 10 tests, with dataSources and loaders rebuilt in beforeEach
verify-schema.mjs★ 自己写:进程内查 _service、普通查询、_entities、mutation★ You write it: query _service in process, then a normal query, then _entities, then the mutation
写完后在本机这样验证Verify it locally like this
npm install
依赖装好,出现 node_modules 与 package-lock.jsonThe dependencies install, and node_modules and package-lock.json appear
npm start
打印 Subgraph ready at http://0.0.0.0:4000/It prints Subgraph ready at http://0.0.0.0:4000/
npm test
Tests: 10 passed, 10 total
node verify-schema.mjs
SDL 出得来且含 @key;orders + shippingInfo 有值;order-999 返回 ORDER_NOT_FOUND;_entities 能拿到 orders;createOrder 的 items[0].price 有值且 totalAmount > 0;空 items 返回 INVALID_INPUTThe SDL comes out and contains @key; orders + shippingInfo have values; order-999 returns ORDER_NOT_FOUND; _entities can read orders; items[0].price from createOrder has a value and totalAmount > 0; empty items returns INVALID_INPUT
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。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.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing · Federation 考试Federation exam
L4从零重写Rebuild from scratch从零重建 Task 2 · Spring Boot 控制器Rebuild Task 2 · the Spring Boot controller

给你 OrderService 的方法签名和五个测试。 自己搭一个 Spring Boot 项目,写出六个端点。不要打开源项目的 OrderController.java。

You are given the method signatures of OrderService and five tests. Set up a Spring Boot project yourself and write six endpoints. Do not open OrderController.java from the source project.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • Spring Boot 3.3 + Java 17,依赖 web / validation / actuator / testSpring Boot 3.3 + Java 17, with the web / validation / actuator / test dependencies
  • 一个 @RestController,构造器注入 OrderServiceOne @RestController, with OrderService injected through the constructor
  • GET /api/orders:?userId= 传了就按用户过滤,没传返回全部;200GET /api/orders: filter by user when ?userId= is given, return everything when it is not; 200
  • GET /api/orders/{id}:200;找不到时由全局异常处理器给出 404(控制器不要 catch)GET /api/orders/{id}: 200; when nothing is found, the global exception handler answers 404 (do not catch it in the controller)
  • GET /api/orders/user/{userId}:200GET /api/orders/user/{userId}: 200
  • POST /api/orders:@Valid 校验请求体;成功返回 201 CreatedPOST /api/orders: validate the request body with @Valid; on success return 201 Created
  • PATCH /api/orders/{id}/status:body 是 {"status":"..."};转成 OrderStatus;缺失或非法值返回 400;成功 200PATCH /api/orders/{id}/status: the body is {"status":"..."}; convert it to OrderStatus; a missing or invalid value returns 400; on success 200
  • DELETE /api/orders/{id}:204 No ContentDELETE /api/orders/{id}: 204 No Content
  • 六个端点都用 SLF4J 打日志,并带上 MDC 里的 correlationIdAll six endpoints log through SLF4J and carry the correlationId from MDC
  • 自己写一个 CorrelationIdFilter:读 X-Correlation-ID 头,没有就生成 UUID,放进 MDC,finally 里清理Write your own CorrelationIdFilter: read the X-Correlation-ID header, generate a UUID when it is absent, put it in MDC, and clear it in finally
  • 自己写 GlobalExceptionHandler:EntityNotFoundException → 404,MethodArgumentNotValidException → 400Write your own GlobalExceptionHandler: EntityNotFoundException → 404, MethodArgumentNotValidException → 400
你需要自己建的文件Files you create yourself
文件清单File list
pom.xmlparent 用 spring-boot-starter-parent 3.3.2,java.version 17,四个依赖 + spring-boot-maven-pluginThe parent is spring-boot-starter-parent 3.3.2, java.version is 17, four dependencies + spring-boot-maven-plugin
src/main/resources/application.propertiesserver.port=8080 就够(顺便按书面题的结论收紧 actuator)server.port=8080 is enough (and tighten actuator while you are here, following the written question)
src/main/java/.../OrderServiceApplication.java@SpringBootApplication + main
src/main/java/.../model/Order.java、OrderItem.java、OrderStatus.java★ 抄源项目的(这是题目)★ Copy it from the source project (this is the question)
src/main/java/.../dto/CreateOrderRequest.java、OrderItemRequest.java★ 抄源项目的:带 @NotBlank / @NotEmpty / @Min / @Valid★ Copy it from the source project: it carries @NotBlank / @NotEmpty / @Min / @Valid
src/main/java/.../repository/OrderRepository.java、InMemoryOrderRepository.java★ 抄源项目的:接口 + 内存实现(含一条种子数据)★ Copy it from the source project: the interface + an in-memory implementation (with one seed record)
src/main/java/.../service/OrderService.java★ 抄源项目的(这是题目):六个方法,三个会抛 EntityNotFoundException★ Copy it from the source project (this is the question): six methods, three of which throw EntityNotFoundException
src/main/java/.../exception/EntityNotFoundException.java、GlobalExceptionHandler.java★ 自己写:两个 @ExceptionHandler★ You write it: two @ExceptionHandler methods
src/main/java/.../config/CorrelationIdFilter.java★ 自己写:OncePerRequestFilter + MDC★ You write it: OncePerRequestFilter + MDC
src/main/java/.../controller/OrderController.java★★ 自己写:六个端点★★ You write it: six endpoints
src/test/java/.../OrderControllerTest.java★ 抄源项目的(这是判卷器):@WebMvcTest + @MockBean + 五个测试★ Copy it from the source project (this is what grades you): @WebMvcTest + @MockBean + five tests
写完后在本机这样验证Verify it locally like this
mvn test
Tests run: 5, Failures: 0, Errors: 0 — BUILD SUCCESS
mvn spring-boot:run
服务起在 8080,日志里能看到 Started OrderServiceApplicationThe service starts on 8080, and the log shows Started OrderServiceApplication
curl -i -s localhost:8080/api/orders/999
404 + {"timestamp":...,"status":404,"message":"Order not found with id: 999"}
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"123","items":[{"productId":"prod-789","quantity":2}]}'
201 Created + 订单 JSON(totalAmount 应为 299.98)201 Created + the order JSON (totalAmount should be 299.98)
curl -i -s -X POST localhost:8080/api/orders -H 'Content-Type: application/json' -d '{"userId":"","items":[]}'
400 Bad Request(Bean Validation 生效)400 Bad Request (Bean Validation is working)
curl -i -s -X PATCH localhost:8080/api/orders/1/status -H 'Content-Type: application/json' -d '{"status":"FLYING"}'
400 Bad Request(不是 500)400 Bad Request (not 500)
curl -i -s -X DELETE localhost:8080/api/orders/1
204 No Content,body 为空204 No Content, with an empty body
curl -i -s -H 'X-Correlation-ID: my-trace-1' localhost:8080/api/orders
响应头里有同一个 X-Correlation-ID;服务端日志里也是它The response header carries the same X-Correlation-ID, and so does the server log
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。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.

Tick this once you have written it locally and the verification commands give the expected output.
来自From 从零重写:空文件夹里做出来Rewrite it: build the whole app in an empty folder · Cab BookingCab Booking
L4从零重写Rebuild from scratch空文件夹里做出整个 Cab BookingBuild the whole of Cab Booking from an empty folder
只给需求和文件清单。不给任何代码。卡住了按四级提示走,答案在最后一道门后面。You get the requirements and the file list only. No code at all. If you get stuck, work through the four levels of hints. The answer sits behind the last door.
需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 首页:一个大标题「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
你需要自己建的文件Files you create yourself
文件清单File list
package.jsonvite + react + vitest + jsdom + @testing-library/react + @testing-library/jest-dom
vite.config.mjsplugins: [react()],test 段配 environment: "jsdom" / globals / setupFilesplugins: [react()], and a test section with environment: "jsdom" / globals / setupFiles
index.html一个 <div id="root">one <div id="root">
src/index.jsxcreateRoot,用 <CabProvider> 包住 <App />createRoot, with <CabProvider> wrapping <App />
src/App.jsxcurrentPage 状态机 + handleSelectCabthe currentPage state machine + handleSelectCab
src/context/CabContext.jsx三件套。注意扩展名 —— 里面有 JSXthe three parts. Watch the extension — this file holds JSX
src/components/AppHeader.jsx只显示标题,没有 testidshows the title only, no testid
src/components/Home/Home.jsxhero + book-button + <RideHistory />
src/components/Home/RideHistory.jsx空状态 / 最新三条倒序the empty state / the three newest in reverse
src/components/CabOptions/CabOptions.jsxObject.keys 分组grouping with Object.keys
src/components/CabOptions/CabCard.jsx五个 testidthe five testids
src/components/Loading/Loading.jsxsetTimeout 1000 + clearTimeout
src/components/CabConfirmation/CabConfirmation.jsx?.name + confirm-button
src/data/data.json三组六辆车,键顺序 Sedan → SUV → Luxurysix cabs in three groups, with the key order Sedan → SUV → Luxury
src/test/setup.jsimport "@testing-library/jest-dom"
src/test/App.test.jsx把源项目那四个测试原样放进来 —— 这是你的判分依据copy the source project's four tests in unchanged — this is what grades you
写完后在本机这样验证Verify it locally like this
npm install
装完无报错。React 18/19 都可以,测试用的 API 没差别It finishes with no errors. React 18 or 19 both work; the APIs the tests use are the same
npx vitest run
刚放进测试文件时应该是 4 failed / 4 total,报错都是 Unable to find an element by: [data-testid=...]。全部做完是 Test Files 1 passed / Tests 4 passed (4)Right after you drop the test file in it should be 4 failed / 4 total, all reporting Unable to find an element by: [data-testid=...]. When everything is done it is Test Files 1 passed / Tests 4 passed (4)
npx vitest run 2>&1 | grep -c 'no tests'
0。如果不是 0,说明你也踩了 .js 里写 JSX 那个坑 —— 把带 JSX 的文件改名成 .jsx0. Anything else means you hit the JSX-in-a-.js-file problem too — rename the files that hold JSX to .jsx
npm run dev
浏览器里手动走一遍:首页 → 选车 → 加载 1 秒 → 确认 → 回首页看到历史。连订四辆,历史应该只有三条且最新在最上Walk through it by hand in the browser: home page → pick a cab → 1 second of loading → confirm → back to the home page with the history there. Book four in a row and the history should hold three, newest at the top
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。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.

Tick this once you have written it locally and the verification commands give the expected output.