DrillLab

模拟考 A · Support Ticket BoardMock exam A · Support Ticket Board

IT 支持工单看板。可以新建工单(标题 + 优先级)、关闭工单、改派(编辑)工单,还能按优先级筛选。业务场景换了,数据结构多了一个枚举字段和一个筛选状态,但底层要你会的东西和 Notes Manager 一模一样。An IT support ticket board. You can create a ticket (title plus priority), close one, reassign (edit) one, and filter by priority. The business setting is different and the data has one extra enum field and one filter state, but what you need to know underneath is the same as in Notes Manager.

DrillLab 自出Written by DrillLab建议 75 分钟~75 min5 个任务5 tasks满分 100100 points total
这套题在考什么What this paper tests

与真实 Q1 完全相同的考点:受控输入、列表渲染与 key、useState 的三种不可变更新(增 / 删 / 就地替换)、useEffect 同步外部 prop、派生数据、状态提升、双模式按钮与 id 复用。额外增加一个「筛选」考点 —— 这是这类题最常见的变式方向。Exactly the same points as the real Q1: controlled inputs, list rendering and key, the three ways useState updates without changing the original (add, remove, replace in place), useEffect syncing an outside prop, derived data, lifting state up, and a two-mode button reusing an id. One point is added: filtering, which is the most common way this kind of task is varied.

任务与评分标准Tasks and rubric
TASK 1Task 1 · Create20 分20 pts
  • 填写标题、选择优先级,点 Create 后新工单出现在列表末尾Fill in a title, pick a priority, and after Create the new ticket appears at the end of the list
  • 标题为空(含只有空格)时 Create 按钮必须 disabledWhile the title is empty (spaces only counts as empty) the Create button must be disabled
  • 优先级下拉默认选中 mediumThe priority dropdown starts on medium
  • 提交后清空表单(标题清空,优先级回到 medium)The form clears after submitting: the title empties and the priority goes back to medium
8 分8 pts两个表单控件都是受控的(value + onChange)Both form controls are controlled, with value and onChange
6 分6 pts用不可变更新追加到末尾,没有 pushAppends without changing the original array; no push
4 分4 ptsdisabled 是派生数据,不是额外的 statedisabled is derived, not a second piece of state
2 分2 pts提交后正确重置表单The form resets correctly after submitting
TASK 2Task 2 · Close15 分15 pts
  • 点某一行的 Close,该工单按 id 从列表移除Clicking Close on a row removes that ticket from the list by id
  • 两条标题相同的工单,只移除被点的那一条With two tickets that share a title, only the one that was clicked is removed
8 分8 pts用 filter 并按 id 比较Uses filter and compares by id
4 分4 pts没有修改原数组(无 splice)The original array is untouched; no splice
3 分3 pts回调只接收 id,没有多余参数The callback takes only the id, with no extra arguments
TASK 3Task 3 · Reassign(就地编辑)Task 3 · Reassign (edit in place)28 分28 pts
  • 点某一行的 Reassign:标题和优先级回填进表单,按钮文字变成 SaveClicking Reassign on a row puts its title and priority back into the form, and the button now reads Save
  • 改完提交:该工单在原位置被更新,顺序不变Submitting the change updates that ticket in place; the order does not change
  • 提交后退出改派模式:表单清空、按钮回到 CreateSubmitting leaves reassign mode: the form clears and the button goes back to Create
  • 改派时必须复用原 idReassigning has to reuse the original id
10 分10 pts用 map 就地替换,顺序不变(不是先删再加)Replaces in place with map so the order holds; not a remove followed by an add
6 分6 ptsuseEffect 依赖数组只放 ticketToEditThe useEffect dependency array holds only ticketToEdit
5 分5 pts提交时复用旧 idThe original id is reused on submit
4 分4 pts提交后把 ticketToEdit 设回 nullticketToEdit is set back to null after submitting
3 分3 pts按钮文字 Save / Create 严格一致The button text is exactly Save or Create, with nothing in between
TASK 4Task 4 · Filter by priority(新增考点)Task 4 · Filter by priority (a point the real Q1 does not have)20 分20 pts
  • 顶部有一个筛选下拉(data-testid="filter-priority"),选项:all / low / medium / highThere is a filter dropdown at the top (data-testid="filter-priority") with the options all / low / medium / high
  • 选中某个优先级后,列表只显示该优先级的工单Picking a priority shows only the tickets with that priority
  • 筛选不能影响底层数据 —— 切回 all 应该恢复全部Filtering must not touch the underlying data — switching back to all brings everything back
  • 筛选结果必须是派生数据,不许再开一个 state 存筛选后的数组The filtered result has to be derived data; do not add a second piece of state holding the filtered array
8 分8 pts只用一个 state 存筛选条件One piece of state holds the filter
8 分8 pts可见列表是派生出来的(不是第二份 state)The visible list is derived, not a second copy in state
4 分4 pts筛选状态下的删除/编辑依然按 id 正确作用于原数据With a filter on, delete and edit still act on the right item by id
TASK 5Task 5 · 工程质量Task 5 · Engineering quality17 分17 pts
  • 列表用 map 渲染,key 用 ticket.idThe list is rendered with map, using ticket.id as the key
  • 所有 data-testid 与需求一致Every data-testid matches the requirements
  • npx tsc --noEmit 无错误npx tsc --noEmit reports no errors
  • 五个测试全过All five tests pass
5 分5 ptskey 用了稳定 id,不是 indexThe key is a stable id, not the index
5 分5 pts类型检查通过,Priority 用了联合类型Type checking passes, and Priority is a union type
7 分7 pts五个测试全过All five tests pass

这一页故意不给运行环境There is deliberately no runner on this page

不是做不到。是这一档的意义就在于什么都不给。Coding 题里那 11 道浏览器沙箱把文件、依赖、 测试全备好了 —— 你只要写函数体。真实考试不是这样:你会拿到一个空文件夹 或者一份跑不起来的脚手架,自己 npm install、自己读报错、 自己决定文件放哪。Not because we cannot. Because getting nothing is the point of this tier. The 11 browser sandboxes under Coding hand you files, deps and tests. A real assessment does not: you get an empty folder or a scaffold that does not build, and you install, read the errors and lay out the files yourself.

所以下面给足了三样东西:能直接抄的命令完整的文件树起始态和做对之后各该看到什么(都是实测数字)。搭不起来不该是这道题的难点。So below you get three things: commands you can paste, the full file tree, and what the tests actually print before and after (both measured). Getting set up should not be the hard part.

去哪跑:本机 + VS Code 最接近真实考试,要先装好 Node。 装不了 Node 就用 StackBlitz,选 Vite + React + TS 模板。代码会上传到 StackBlitz 的服务器上。Where: local VS Code is closest to the real thing; install Node first. No Node? Use StackBlitz with the Vite + React + TS template. Your code is uploaded to StackBlitz.

从零起一个项目Bootstrap the project
npm create vite@latest ticket-board -- --template react-ts
选 react-ts 模板。别选 JS 模板 —— 这道题的考点之一是读类型Pick the react-ts template, not the JS one — reading the types is part of what this tests.
cd ticket-board && npm install
装 React 本体Installs React itself.
npm i -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom
判卷器要用的。@testing-library/dom 必须显式装 —— 它是 RTL 的 peerDependency,不会被自动带进来What the grader needs. @testing-library/dom must be listed explicitly — it is a peerDependency of RTL and does not come along on its own.
文件清单(目录可以不完全一样,接口要对得上)Files (your layout can differ; the interfaces have to match)高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
vite.config.ts加一段 test 配置:environment 用 jsdom、globals 开、setupFiles 指向下面那个文件Add a test block: environment jsdom, globals on, setupFiles pointing at the file below.
vitest.setup.ts一行:import "@testing-library/jest-dom";One line: import "@testing-library/jest-dom";
src/types/Ticket.tsPriority 与 Ticket 类型(starter 里给了)The Priority and Ticket types (given in the starter).
src/components/TicketBoard/index.tsxTODO 1–7:三个 state、三个 handler、派生的 visibleTicketsTODO 1–7: three pieces of state, three handlers, and a derived visibleTickets.
src/components/TicketForm/index.tsxTODO 8–12:受控输入、双模式按钮、回填TODO 8–12: controlled inputs, a dual-mode button, and refilling on edit.
src/components/TicketList/index.tsxTODO 13:列表与两个按钮TODO 13: the list and its two buttons.
src/TicketBoard.test.tsx判卷器,五个测试。原样抄下面那份,不要改它The grader, five tests. Copy it verbatim and do not edit it.
你该看到什么What you should see
一行还没写的时候Before you write anythingTests: 5 failed, 5 total —— 第一条报 Unable to find an element by: [data-testid="ticket-subject"]Tests: 5 failed, 5 total — the first reports Unable to find an element by: [data-testid="ticket-subject"]看不到这个就是环境没搭对,先解决它再动手。If you do not see this, the setup is wrong. Fix that first.
全做对之后When you are doneTest Files 1 passed (1) / Tests 5 passed (5)Test Files 1 passed (1) / Tests 5 passed (5)这个数字是参考解法在本机实测出来的,不是估的。Measured from the reference solution on a real machine.
验收命令 —— 逐条跑Acceptance commands
npm install
依赖装好
npx vitest run
Tests 5 passed (5)
npm run dev
手动验证:① 建三条同名工单,关闭中间那条,只消失一条 ② 改派中间那条,位置不变 ③ 筛选到 high 之后再切回 all,数据完整
Starter 代码Starter code
TypeScriptstarter 代码骨架(13 个 TODO)The starter skeleton (13 TODOs)已跑通Verified
1// src/types/Ticket.ts
2export type Priority = "low" | "medium" | "high";
3
4export type Ticket = {
5 id: number;
6 subject: string;
7 priority: Priority;
8};
9
10// ────────────────────────────────────────────────────
11// src/components/TicketBoard/index.tsx
12import { useState } from "react";
13import type { Priority, Ticket } from "../../types/Ticket";
14import TicketForm from "../TicketForm";
15import TicketList from "../TicketList";
16
17const TicketBoard: React.FC = () => {
18 // TODO 1: tickets 列表
19 // TODO 2: 正在改派的工单(没有时为 null)
20 // TODO 3: 优先级筛选条件("all" | Priority)
21
22 // TODO 4: handleSubmitTicket —— 新建 或 就地更新
23 // TODO 5: handleClose —— 按 id 移除
24 // TODO 6: handleReassign —— 进入改派模式
25
26 // TODO 7: visibleTickets —— 派生数据,别做成 state
27
28 return (
29 <div data-testid="ticket-board">
30 {/* 筛选下拉:data-testid="filter-priority",选项 all / low / medium / high */}
31 {/* <TicketForm ... /> */}
32 {/* <TicketList tickets={visibleTickets} ... /> */}
33 </div>
34 );
35};
36
37export default TicketBoard;
38
39// ────────────────────────────────────────────────────
40// src/components/TicketForm/index.tsx
41interface TicketFormProps {
42 onSubmit: (ticket: Ticket) => void;
43 ticketToEdit: Ticket | null;
44}
45
46// TODO 8: 受控的 subject 输入框(data-testid="ticket-subject")
47// TODO 9: 受控的 priority 下拉(data-testid="ticket-priority",默认 medium)
48// TODO 10: 提交按钮(data-testid="ticket-submit")
49// subject 为空(含只有空格)时 disabled
50// 改派模式下文字是 "Save",否则是 "Create"
51// TODO 11: ticketToEdit 变化时回填 / 清空
52// TODO 12: 提交时 id 的取舍
53
54// ────────────────────────────────────────────────────
55// src/components/TicketList/index.tsx
56// TODO 13: <ul data-testid="ticket-list">,每条一个 <li>
57// 显示 subject 和 priority
58// 两个按钮,文字必须正好是 "Reassign" 和 "Close"
这是 DrillLab 自出的模拟题,不是源项目内容。把它拆成对应文件后自己实现。注意 data-testid 全部被测试依赖,不能改。DrillLab wrote this mock task; it does not come from the source project. Split it into the matching files and write the implementation yourself. Every data-testid is used by a test, so do not rename them.
测试(你的判卷器)Tests — your own grader
TSXsrc/TicketBoard.test.tsx(判卷器,五个测试)src/TicketBoard.test.tsx (the marker, five tests)已跑通Verified
1import { render, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import TicketBoard from "./components/TicketBoard";
4
5test("creates a ticket", async () => {
6 render(<TicketBoard />);
7 await userEvent.type(screen.getByTestId("ticket-subject"), "打印机坏了");
8 await userEvent.selectOptions(screen.getByTestId("ticket-priority"), "high");
9 await userEvent.click(screen.getByTestId("ticket-submit"));
10
11 expect(screen.getByTestId("ticket-list")).toHaveTextContent("打印机坏了");
12});
13
14test("submit disabled when subject empty", () => {
15 render(<TicketBoard />);
16 expect(screen.getByTestId("ticket-submit")).toBeDisabled();
17});
18
19test("closes a ticket by id", async () => {
20 render(<TicketBoard />);
21 await userEvent.type(screen.getByTestId("ticket-subject"), "网络中断");
22 await userEvent.click(screen.getByTestId("ticket-submit"));
23 await userEvent.click(screen.getByRole("button", { name: "Close" }));
24
25 expect(screen.getByTestId("ticket-list")).not.toHaveTextContent("网络中断");
26});
27
28test("reassigns a ticket in place", async () => {
29 render(<TicketBoard />);
30 await userEvent.type(screen.getByTestId("ticket-subject"), "旧标题");
31 await userEvent.click(screen.getByTestId("ticket-submit"));
32
33 await userEvent.click(screen.getByRole("button", { name: "Reassign" }));
34 expect(screen.getByTestId("ticket-submit")).toHaveTextContent("Save");
35
36 const subject = screen.getByTestId("ticket-subject");
37 await userEvent.clear(subject);
38 await userEvent.type(subject, "新标题");
39 await userEvent.click(screen.getByTestId("ticket-submit"));
40
41 expect(screen.getByTestId("ticket-list")).toHaveTextContent("新标题");
42 expect(screen.getByTestId("ticket-list")).not.toHaveTextContent("旧标题");
43});
44
45test("filters by priority", async () => {
46 render(<TicketBoard />);
47 await userEvent.type(screen.getByTestId("ticket-subject"), "低优先级问题");
48 await userEvent.selectOptions(screen.getByTestId("ticket-priority"), "low");
49 await userEvent.click(screen.getByTestId("ticket-submit"));
50
51 await userEvent.type(screen.getByTestId("ticket-subject"), "高优先级问题");
52 await userEvent.selectOptions(screen.getByTestId("ticket-priority"), "high");
53 await userEvent.click(screen.getByTestId("ticket-submit"));
54
55 await userEvent.selectOptions(screen.getByTestId("filter-priority"), "high");
56
57 expect(screen.getByTestId("ticket-list")).toHaveTextContent("高优先级问题");
58 expect(screen.getByTestId("ticket-list")).not.toHaveTextContent("低优先级问题");
59});
第五个测试是真实 Q1 没有的 —— 它查筛选。注意 userEvent.selectOptions 用来操作 <select>,这是本模拟题新引入的 API。The fifth test is not part of the real Q1. It checks filtering. Note userEvent.selectOptions, the API for driving a <select>; this mock task is where it first appears.
做完之后按 rubric 给自己打个分,记在这里。When you are done, score yourself against the rubric and record it here.

讲解里会直接说出每一处陷阱在哪。请确认你已经在本机把这套题做完、跑过测试、按 rubric 自评过,再打开。The walkthrough names every trap outright. Only open it once you have finished the paper locally, run the tests, and scored yourself against the rubric.