1import { act, render, renderHook, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import { afterEach, beforeEach, expect, test, vi } from "vitest";
4import Dropdown from "./components/Dropdown";
5import Tabs from "./components/Tabs";
6import StarRating from "./components/StarRating";
7import Player from "./components/Player";
8import Kanban, { moveCard } from "./components/Kanban";
9import { useLocalStorage } from "./hooks/useLocalStorage";
10import type { Board } from "./types/Card";
11
12/* ---------------- #366 Dropdown ---------------- */
13
14const OPTS = [
15 { id: "a", label: "苹果" },
16 { id: "b", label: "香蕉" },
17];
18
19test("[366] 点触发器展开、选中后收起并显示选中项", async () => {
20 render(<Dropdown options={OPTS} />);
21 expect(screen.queryByTestId("dropdown-list")).toBeNull();
22
23 await userEvent.click(screen.getByTestId("dropdown-trigger"));
24 expect(screen.getByTestId("dropdown-list")).toBeInTheDocument();
25 expect(screen.getByTestId("dropdown-trigger")).toHaveAttribute("aria-expanded", "true");
26
27 await userEvent.click(screen.getByTestId("option-b"));
28 expect(screen.queryByTestId("dropdown-list")).toBeNull();
29 expect(screen.getByTestId("dropdown-trigger")).toHaveTextContent("香蕉");
30});
31
32test("[366] 点外面会关掉,点自己里面不会", async () => {
33 render(
34 <div>
35 <Dropdown options={OPTS} />
36 <button data-testid="outside">外面</button>
37 </div>,
38 );
39 await userEvent.click(screen.getByTestId("dropdown-trigger"));
40
41 // 点自己内部:不该关
42 await userEvent.click(screen.getByTestId("dropdown-list"));
43 expect(screen.getByTestId("dropdown-list")).toBeInTheDocument();
44
45 // 点外面:该关
46 await userEvent.click(screen.getByTestId("outside"));
47 expect(screen.queryByTestId("dropdown-list")).toBeNull();
48});
49
50test("[366] 按 Escape 关闭", async () => {
51 render(<Dropdown options={OPTS} />);
52 await userEvent.click(screen.getByTestId("dropdown-trigger"));
53 await userEvent.keyboard("{Escape}");
54 expect(screen.queryByTestId("dropdown-list")).toBeNull();
55});
56
57test("[366] 卸载后解绑了 document 监听器(清理函数生效)", async () => {
58 const add = vi.spyOn(document, "addEventListener");
59 const remove = vi.spyOn(document, "removeEventListener");
60
61 const { unmount } = render(<Dropdown options={OPTS} />);
62 await userEvent.click(screen.getByTestId("dropdown-trigger")); // 展开才会绑
63 const added = add.mock.calls.filter(([t]) => t === "mousedown" || t === "keydown").length;
64 expect(added).toBe(2);
65
66 unmount();
67 const removed = remove.mock.calls.filter(([t]) => t === "mousedown" || t === "keydown").length;
68 expect(removed).toBe(2); // 少了清理函数这里会是 0
69
70 add.mockRestore();
71 remove.mockRestore();
72});
73
74/* ---------------- #367 Tabs ---------------- */
75
76const TABS = [
77 { id: "one", label: "第一", content: <p>内容一</p> },
78 { id: "two", label: "第二", content: <p>内容二</p> },
79 { id: "three", label: "第三", content: <p>内容三</p> },
80];
81
82test("[367] 默认激活第一个,只渲染激活的面板", () => {
83 render(<Tabs tabs={TABS} />);
84 expect(screen.getByTestId("tab-one")).toHaveAttribute("aria-selected", "true");
85 expect(screen.getByTestId("panel")).toHaveTextContent("内容一");
86 expect(screen.queryByText("内容二")).toBeNull();
87});
88
89test("[367] 点第二个切换,aria-selected 跟着走", async () => {
90 render(<Tabs tabs={TABS} />);
91 await userEvent.click(screen.getByTestId("tab-two"));
92
93 expect(screen.getByTestId("panel")).toHaveTextContent("内容二");
94 expect(screen.getByTestId("tab-two")).toHaveAttribute("aria-selected", "true");
95 expect(screen.getByTestId("tab-one")).toHaveAttribute("aria-selected", "false");
96});
97
98test("[367] initialId 能指定初始激活项", () => {
99 render(<Tabs tabs={TABS} initialId="three" />);
100 expect(screen.getByTestId("panel")).toHaveTextContent("内容三");
101});
102
103/* ---------------- #368 StarRating ---------------- */
104
105test("[368] 点第三颗得 3 分,前三颗填充", async () => {
106 render(<StarRating />);
107 await userEvent.click(screen.getByTestId("star-3"));
108
109 expect(screen.getByTestId("stars-value")).toHaveTextContent("3");
110 expect(screen.getByTestId("star-3")).toHaveAttribute("data-filled", "true");
111 expect(screen.getByTestId("star-4")).toHaveAttribute("data-filled", "false");
112});
113
114test("[368] hover 时预览、移出后回到已选值", async () => {
115 render(<StarRating />);
116 await userEvent.click(screen.getByTestId("star-2"));
117
118 await userEvent.hover(screen.getByTestId("star-5"));
119 expect(screen.getByTestId("star-5")).toHaveAttribute("data-filled", "true"); // 预览
120
121 await userEvent.unhover(screen.getByTestId("star-5"));
122 // 注意 unhover 只离开了那颗星,要真正离开整个容器
123 await userEvent.pointer({ target: document.body });
124 expect(screen.getByTestId("stars")).toHaveAttribute("data-value", "2"); // 已选值没变
125});
126
127test("[368] 再点同一颗清零", async () => {
128 render(<StarRating />);
129 await userEvent.click(screen.getByTestId("star-4"));
130 await userEvent.click(screen.getByTestId("star-4"));
131 expect(screen.getByTestId("stars-value")).toHaveTextContent("0");
132});
133
134test("[368] 受控模式下自己不改值,只调 onChange", async () => {
135 const onChange = vi.fn();
136 render(<StarRating value={1} onChange={onChange} />);
137 await userEvent.click(screen.getByTestId("star-5"));
138
139 expect(onChange).toHaveBeenCalledWith(5);
140 expect(screen.getByTestId("stars")).toHaveAttribute("data-value", "1"); // 仍是父级给的 1
141});
142
143/* ---------------- #373 Player(useRef 操作 DOM) ---------------- */
144
145let play: ReturnType<typeof vi.spyOn>;
146let pause: ReturnType<typeof vi.spyOn>;
147
148beforeEach(() => {
149 // jsdom 没实现媒体播放,play() 会抛 Not implemented —— 所以要 stub
150 play = vi
151 .spyOn(HTMLMediaElement.prototype, "play")
152 .mockImplementation(() => Promise.resolve());
153 pause = vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
154});
155
156afterEach(() => {
157 play.mockRestore();
158 pause.mockRestore();
159});
160
161test("[373] 点 Play 调 audio.play(),再点调 pause()", async () => {
162 render(<Player src="/a.mp3" />);
163 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
164
165 await userEvent.click(screen.getByTestId("toggle"));
166 expect(play).toHaveBeenCalledTimes(1);
167 expect(screen.getByTestId("toggle")).toHaveTextContent("Pause");
168
169 await userEvent.click(screen.getByTestId("toggle"));
170 expect(pause).toHaveBeenCalledTimes(1);
171 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
172});
173
174test("[373] Stop 把 currentTime 归零并停下", async () => {
175 render(<Player src="/a.mp3" />);
176 const audio = screen.getByTestId("audio") as HTMLAudioElement;
177
178 await userEvent.click(screen.getByTestId("toggle"));
179 audio.currentTime = 30;
180 await userEvent.click(screen.getByTestId("stop"));
181
182 expect(audio.currentTime).toBe(0); // 直接改的是 DOM 属性
183 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
184});
185
186/* ---------------- #375 自定义 hook ---------------- */
187
188test("[375] useLocalStorage 首次用默认值,并写进 localStorage", () => {
189 localStorage.clear();
190 const { result } = renderHook(() => useLocalStorage("k", { n: 1 }));
191
192 expect(result.current[0]).toEqual({ n: 1 });
193 expect(JSON.parse(localStorage.getItem("k")!)).toEqual({ n: 1 });
194});
195
196test("[375] 已有值时读出来,而不是用默认值", () => {
197 localStorage.setItem("k", JSON.stringify("存过的"));
198 const { result } = renderHook(() => useLocalStorage("k", "默认"));
199 expect(result.current[0]).toBe("存过的");
200});
201
202test("[375] setValue 会同步写回", () => {
203 localStorage.clear();
204 const { result } = renderHook(() => useLocalStorage("k", 0));
205
206 act(() => result.current[1](42));
207 expect(result.current[0]).toBe(42);
208 expect(localStorage.getItem("k")).toBe("42");
209});
210
211test("[375] 脏数据不会让组件炸,退回默认值", () => {
212 localStorage.setItem("k", "{不是合法 JSON");
213 const { result } = renderHook(() => useLocalStorage("k", "兜底"));
214 expect(result.current[0]).toBe("兜底");
215});
216
217test("[375] 两个组件各调一次 = 两份独立状态(复用逻辑不复用状态)", () => {
218 localStorage.clear();
219 const a = renderHook(() => useLocalStorage("shared", 0));
220 const b = renderHook(() => useLocalStorage("shared", 0));
221
222 act(() => a.result.current[1](5));
223 expect(a.result.current[0]).toBe(5);
224 expect(b.result.current[0]).toBe(0); // b 不会跟着变
225});
226
227/* ---------------- #378 Kanban ---------------- */
228
229const board = (): Board => ({
230 todo: [{ id: 1, title: "写文档" }, { id: 2, title: "改 bug" }],
231 doing: [{ id: 3, title: "评审" }],
232 done: [],
233});
234
235function deepFreeze<T>(o: T): T {
236 Object.freeze(o);
237 Object.values(o as Record<string, unknown>).forEach((v) => {
238 if (v && typeof v === "object" && !Object.isFrozen(v)) deepFreeze(v);
239 });
240 return o;
241}
242
243test("[378] moveCard 把卡移到目标列,且不改原 board", () => {
244 const original = deepFreeze(board());
245 const next = moveCard(original, "todo", "doing", 1);
246
247 expect(next.todo.map((c) => c.id)).toEqual([2]);
248 expect(next.doing.map((c) => c.id)).toEqual([3, 1]);
249 // 原对象一个字节都没动
250 expect(original.todo.map((c) => c.id)).toEqual([1, 2]);
251 expect(original.doing.map((c) => c.id)).toEqual([3]);
252});
253
254test("[378] 没动或找不到卡时原样返回同一个引用", () => {
255 const b = board();
256 expect(moveCard(b, "todo", "todo", 1)).toBe(b);
257 expect(moveCard(b, "todo", "done", 999)).toBe(b);
258});
259
260test("[378] 没被碰到的列复用原数组引用(只重建改动的部分)", () => {
261 const b = board();
262 const next = moveCard(b, "todo", "doing", 1);
263 expect(next.done).toBe(b.done); // done 没动,引用不变
264 expect(next.todo).not.toBe(b.todo); // 动过的必须是新数组
265});
266
267test("[378] 点右移按钮,卡片换列且计数跟着变", async () => {
268 render(<Kanban initial={board()} />);
269 expect(screen.getByTestId("count-todo")).toHaveTextContent("2");
270 expect(screen.getByTestId("card-1")).toHaveAttribute("data-col", "todo");
271
272 await userEvent.click(screen.getByLabelText("把 写文档 右移"));
273
274 expect(screen.getByTestId("card-1")).toHaveAttribute("data-col", "doing");
275 expect(screen.getByTestId("count-todo")).toHaveTextContent("1");
276 expect(screen.getByTestId("count-doing")).toHaveTextContent("2");
277});
278
279test("[378] 第一列没有左移按钮,最后一列没有右移按钮", () => {
280 render(<Kanban initial={board()} />);
281 expect(screen.queryByLabelText("把 写文档 左移")).toBeNull(); // todo 是第一列
282 expect(screen.getByLabelText("把 评审 右移")).toBeInTheDocument();
283});
284
285test("[378] 新增的卡进 todo 列", async () => {
286 render(<Kanban initial={board()} />);
287 await userEvent.type(screen.getByTestId("card-input"), "新任务");
288 await userEvent.click(screen.getByTestId("card-submit"));
289
290 expect(screen.getByTestId("col-todo")).toHaveTextContent("新任务");
291 expect(screen.getByTestId("count-todo")).toHaveTextContent("3");
292});
1import { act, render, renderHook, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import { afterEach, beforeEach, expect, test, vi } from "vitest";
4import Dropdown from "./components/Dropdown";
5import Tabs from "./components/Tabs";
6import StarRating from "./components/StarRating";
7import Player from "./components/Player";
8import Kanban, { moveCard } from "./components/Kanban";
9import { useLocalStorage } from "./hooks/useLocalStorage";
10import type { Board } from "./types/Card";
11
12/* ---------------- #366 Dropdown ---------------- */
13
14const OPTS = [
15 { id: "a", label: "苹果" },
16 { id: "b", label: "香蕉" },
17];
18
19test("[366] the trigger opens it; picking an option closes it and shows the choice", async () => {
20 render(<Dropdown options={OPTS} />);
21 expect(screen.queryByTestId("dropdown-list")).toBeNull();
22
23 await userEvent.click(screen.getByTestId("dropdown-trigger"));
24 expect(screen.getByTestId("dropdown-list")).toBeInTheDocument();
25 expect(screen.getByTestId("dropdown-trigger")).toHaveAttribute("aria-expanded", "true");
26
27 await userEvent.click(screen.getByTestId("option-b"));
28 expect(screen.queryByTestId("dropdown-list")).toBeNull();
29 expect(screen.getByTestId("dropdown-trigger")).toHaveTextContent("香蕉");
30});
31
32test("[366] a click outside closes it, a click inside does not", async () => {
33 render(
34 <div>
35 <Dropdown options={OPTS} />
36 <button data-testid="outside">外面</button>
37 </div>,
38 );
39 await userEvent.click(screen.getByTestId("dropdown-trigger"));
40
41 // Click inside itself: should not close
42 await userEvent.click(screen.getByTestId("dropdown-list"));
43 expect(screen.getByTestId("dropdown-list")).toBeInTheDocument();
44
45 // Click outside: should close
46 await userEvent.click(screen.getByTestId("outside"));
47 expect(screen.queryByTestId("dropdown-list")).toBeNull();
48});
49
50test("[366] Escape closes it", async () => {
51 render(<Dropdown options={OPTS} />);
52 await userEvent.click(screen.getByTestId("dropdown-trigger"));
53 await userEvent.keyboard("{Escape}");
54 expect(screen.queryByTestId("dropdown-list")).toBeNull();
55});
56
57test("[366] the document listeners are removed after unmount (the cleanup works)", async () => {
58 const add = vi.spyOn(document, "addEventListener");
59 const remove = vi.spyOn(document, "removeEventListener");
60
61 const { unmount } = render(<Dropdown options={OPTS} />);
62 await userEvent.click(screen.getByTestId("dropdown-trigger")); // it only binds once open
63 const added = add.mock.calls.filter(([t]) => t === "mousedown" || t === "keydown").length;
64 expect(added).toBe(2);
65
66 unmount();
67 const removed = remove.mock.calls.filter(([t]) => t === "mousedown" || t === "keydown").length;
68 expect(removed).toBe(2); // without the cleanup function this would be 0
69
70 add.mockRestore();
71 remove.mockRestore();
72});
73
74/* ---------------- #367 Tabs ---------------- */
75
76const TABS = [
77 { id: "one", label: "第一", content: <p>内容一</p> },
78 { id: "two", label: "第二", content: <p>内容二</p> },
79 { id: "three", label: "第三", content: <p>内容三</p> },
80];
81
82test("[367] the first tab is active by default, and only the active panel renders", () => {
83 render(<Tabs tabs={TABS} />);
84 expect(screen.getByTestId("tab-one")).toHaveAttribute("aria-selected", "true");
85 expect(screen.getByTestId("panel")).toHaveTextContent("内容一");
86 expect(screen.queryByText("内容二")).toBeNull();
87});
88
89test("[367] clicking the second one switches, and aria-selected follows", async () => {
90 render(<Tabs tabs={TABS} />);
91 await userEvent.click(screen.getByTestId("tab-two"));
92
93 expect(screen.getByTestId("panel")).toHaveTextContent("内容二");
94 expect(screen.getByTestId("tab-two")).toHaveAttribute("aria-selected", "true");
95 expect(screen.getByTestId("tab-one")).toHaveAttribute("aria-selected", "false");
96});
97
98test("[367] initialId sets which tab starts active", () => {
99 render(<Tabs tabs={TABS} initialId="three" />);
100 expect(screen.getByTestId("panel")).toHaveTextContent("内容三");
101});
102
103/* ---------------- #368 StarRating ---------------- */
104
105test("[368] clicking the third star scores 3, and the first three fill in", async () => {
106 render(<StarRating />);
107 await userEvent.click(screen.getByTestId("star-3"));
108
109 expect(screen.getByTestId("stars-value")).toHaveTextContent("3");
110 expect(screen.getByTestId("star-3")).toHaveAttribute("data-filled", "true");
111 expect(screen.getByTestId("star-4")).toHaveAttribute("data-filled", "false");
112});
113
114test("[368] hover previews, and moving out returns to the picked value", async () => {
115 render(<StarRating />);
116 await userEvent.click(screen.getByTestId("star-2"));
117
118 await userEvent.hover(screen.getByTestId("star-5"));
119 expect(screen.getByTestId("star-5")).toHaveAttribute("data-filled", "true"); // the preview
120
121 await userEvent.unhover(screen.getByTestId("star-5"));
122 // note that unhover only left that one star; you have to leave the whole container
123 await userEvent.pointer({ target: document.body });
124 expect(screen.getByTestId("stars")).toHaveAttribute("data-value", "2"); // the picked value did not change
125});
126
127test("[368] clicking the same star again resets to zero", async () => {
128 render(<StarRating />);
129 await userEvent.click(screen.getByTestId("star-4"));
130 await userEvent.click(screen.getByTestId("star-4"));
131 expect(screen.getByTestId("stars-value")).toHaveTextContent("0");
132});
133
134test("[368] in controlled mode it does not change its own value, it only calls onChange", async () => {
135 const onChange = vi.fn();
136 render(<StarRating value={1} onChange={onChange} />);
137 await userEvent.click(screen.getByTestId("star-5"));
138
139 expect(onChange).toHaveBeenCalledWith(5);
140 expect(screen.getByTestId("stars")).toHaveAttribute("data-value", "1"); // still the 1 the parent passed in
141});
142
143/* ---------------- #373 Player (useRef driving the DOM) ---------------- */
144
145let play: ReturnType<typeof vi.spyOn>;
146let pause: ReturnType<typeof vi.spyOn>;
147
148beforeEach(() => {
149 // jsdom does not implement media playback; play() throws Not implemented, so it needs a stub
150 play = vi
151 .spyOn(HTMLMediaElement.prototype, "play")
152 .mockImplementation(() => Promise.resolve());
153 pause = vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
154});
155
156afterEach(() => {
157 play.mockRestore();
158 pause.mockRestore();
159});
160
161test("[373] clicking Play calls audio.play(), clicking again calls pause()", async () => {
162 render(<Player src="/a.mp3" />);
163 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
164
165 await userEvent.click(screen.getByTestId("toggle"));
166 expect(play).toHaveBeenCalledTimes(1);
167 expect(screen.getByTestId("toggle")).toHaveTextContent("Pause");
168
169 await userEvent.click(screen.getByTestId("toggle"));
170 expect(pause).toHaveBeenCalledTimes(1);
171 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
172});
173
174test("[373] Stop sets currentTime back to zero and halts playback", async () => {
175 render(<Player src="/a.mp3" />);
176 const audio = screen.getByTestId("audio") as HTMLAudioElement;
177
178 await userEvent.click(screen.getByTestId("toggle"));
179 audio.currentTime = 30;
180 await userEvent.click(screen.getByTestId("stop"));
181
182 expect(audio.currentTime).toBe(0); // what changed is the DOM property itself
183 expect(screen.getByTestId("toggle")).toHaveTextContent("Play");
184});
185
186/* ---------------- #375 a custom hook ---------------- */
187
188test("[375] useLocalStorage uses the default the first time, and writes it to localStorage", () => {
189 localStorage.clear();
190 const { result } = renderHook(() => useLocalStorage("k", { n: 1 }));
191
192 expect(result.current[0]).toEqual({ n: 1 });
193 expect(JSON.parse(localStorage.getItem("k")!)).toEqual({ n: 1 });
194});
195
196test("[375] an existing value is read back instead of using the default", () => {
197 localStorage.setItem("k", JSON.stringify("存过的"));
198 const { result } = renderHook(() => useLocalStorage("k", "默认"));
199 expect(result.current[0]).toBe("存过的");
200});
201
202test("[375] setValue writes back in step", () => {
203 localStorage.clear();
204 const { result } = renderHook(() => useLocalStorage("k", 0));
205
206 act(() => result.current[1](42));
207 expect(result.current[0]).toBe(42);
208 expect(localStorage.getItem("k")).toBe("42");
209});
210
211test("[375] bad data does not break the component; it falls back to the default", () => {
212 localStorage.setItem("k", "{不是合法 JSON");
213 const { result } = renderHook(() => useLocalStorage("k", "兜底"));
214 expect(result.current[0]).toBe("兜底");
215});
216
217test("[375] two components each call it once = two independent states (shared logic, not shared state)", () => {
218 localStorage.clear();
219 const a = renderHook(() => useLocalStorage("shared", 0));
220 const b = renderHook(() => useLocalStorage("shared", 0));
221
222 act(() => a.result.current[1](5));
223 expect(a.result.current[0]).toBe(5);
224 expect(b.result.current[0]).toBe(0); // b does not follow along
225});
226
227/* ---------------- #378 Kanban ---------------- */
228
229const board = (): Board => ({
230 todo: [{ id: 1, title: "写文档" }, { id: 2, title: "改 bug" }],
231 doing: [{ id: 3, title: "评审" }],
232 done: [],
233});
234
235function deepFreeze<T>(o: T): T {
236 Object.freeze(o);
237 Object.values(o as Record<string, unknown>).forEach((v) => {
238 if (v && typeof v === "object" && !Object.isFrozen(v)) deepFreeze(v);
239 });
240 return o;
241}
242
243test("[378] moveCard moves the card to the target column and does not change the original board", () => {
244 const original = deepFreeze(board());
245 const next = moveCard(original, "todo", "doing", 1);
246
247 expect(next.todo.map((c) => c.id)).toEqual([2]);
248 expect(next.doing.map((c) => c.id)).toEqual([3, 1]);
249 // the original object was not touched at all
250 expect(original.todo.map((c) => c.id)).toEqual([1, 2]);
251 expect(original.doing.map((c) => c.id)).toEqual([3]);
252});
253
254test("[378] a no-op move, or a card that is not found, returns the very same reference", () => {
255 const b = board();
256 expect(moveCard(b, "todo", "todo", 1)).toBe(b);
257 expect(moveCard(b, "todo", "done", 999)).toBe(b);
258});
259
260test("[378] untouched columns keep the original array reference (only the changed parts are rebuilt)", () => {
261 const b = board();
262 const next = moveCard(b, "todo", "doing", 1);
263 expect(next.done).toBe(b.done); // done did not move, so the reference is unchanged
264 expect(next.todo).not.toBe(b.todo); // what moved has to be a new array
265});
266
267test("[378] clicking move-right changes the card's column and the counts follow", async () => {
268 render(<Kanban initial={board()} />);
269 expect(screen.getByTestId("count-todo")).toHaveTextContent("2");
270 expect(screen.getByTestId("card-1")).toHaveAttribute("data-col", "todo");
271
272 await userEvent.click(screen.getByLabelText("把 写文档 右移"));
273
274 expect(screen.getByTestId("card-1")).toHaveAttribute("data-col", "doing");
275 expect(screen.getByTestId("count-todo")).toHaveTextContent("1");
276 expect(screen.getByTestId("count-doing")).toHaveTextContent("2");
277});
278
279test("[378] the first column has no move-left button, the last has no move-right", () => {
280 render(<Kanban initial={board()} />);
281 expect(screen.queryByLabelText("把 写文档 左移")).toBeNull(); // todo is the first column
282 expect(screen.getByLabelText("把 评审 右移")).toBeInTheDocument();
283});
284
285test("[378] a newly added card goes into the todo column", async () => {
286 render(<Kanban initial={board()} />);
287 await userEvent.type(screen.getByTestId("card-input"), "新任务");
288 await userEvent.click(screen.getByTestId("card-submit"));
289
290 expect(screen.getByTestId("col-todo")).toHaveTextContent("新任务");
291 expect(screen.getByTestId("count-todo")).toHaveTextContent("3");
292});