Skip to content

Commit 271a281

Browse files
committed
refactor(ui): migrate loading placeholders
1 parent 37c068a commit 271a281

9 files changed

Lines changed: 554 additions & 11 deletions

‎app/src/components/NotePreviewView.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { invoke, convertFileSrc } from "@tauri-apps/api/core";
1515
import type { Note, NoteFilterResult, TextFilterDecision, TextFilterReview, TextFilterStatus } from "../types";
1616
import { escapeHtml, renderTimestampAnchors } from "../utils/html";
1717
import AiRefineCard from "./AiRefineCard";
18-
import { ConfirmDialog } from "../ui/primitives";
18+
import { ConfirmDialog, Loading } from "../ui/primitives";
1919

2020
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
2121

@@ -197,7 +197,13 @@ export default function NotePreviewView({
197197
};
198198

199199
if (!preview) {
200-
return <p style={{ fontSize: 12, color: "#9ca3af", padding: 16 }}>{status || "加载预览中…"}</p>;
200+
// 批 4 T14:加载态改由 L1 原语承载(`role="status"` + 探针 + 逐字文案)。`status` 是
201+
// 可读信息(可能是"载入失败"之类的说明),故仍作 label;排版权威留在原语的 `Text` 里。
202+
return (
203+
<div style={{ padding: 16 }}>
204+
<Loading label={status || "加载预览中…"} />
205+
</div>
206+
);
201207
}
202208

203209
const stats = preview.stats;

‎app/src/components/RefineLaunchDialog.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* 的 `kind` 分支)⇒ `open` 恒为 `true`,本组件每次由父层条件挂载。
1515
*/
1616
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
17-
import { Button, Modal } from "../ui/primitives";
17+
import { Button, Loading, Modal, Skeleton } from "../ui/primitives";
1818
import { invoke } from "@tauri-apps/api/core";
1919
import type { AiSettingsView, BalanceView, RefineEstimateView, RefineStrategyMeta } from "../types";
2020
import {
@@ -286,7 +286,13 @@ export default function RefineLaunchDialog({
286286
</label>
287287
</div>
288288
)}
289-
{!est && <div style={{ marginTop: 8, fontSize: 11, color: "#9ca3af" }}>成本预估加载中…(无预估则无法启动,请重试)</div>}
289+
{!est && (
290+
<div style={{ marginTop: 8 }}>
291+
{/* 批 4 T14:文案含可操作提示(无预估则无法启动)⇒ 逐字进 label;形状(金额 + 上限)已知 ⇒ 骨架 */}
292+
<Loading label="成本预估加载中…(无预估则无法启动,请重试)" />
293+
<Skeleton lines={2} />
294+
</div>
295+
)}
290296
{msg && <div style={{ fontSize: 11, color: "#dc2626", marginTop: 6 }}>{msg}</div>}
291297
</Modal>
292298
);

‎app/src/components/RefineStrategyPicker.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
matchIntent,
2424
type StrategyDraft,
2525
} from "../utils/refineStrategy";
26-
import { Button } from "../ui/primitives";
26+
import { Button, Loading } from "../ui/primitives";
2727

2828
const optBtn = (active: boolean): React.CSSProperties => ({
2929
padding: "2px 8px", cursor: "pointer", fontSize: 11, borderRadius: 6,
@@ -173,7 +173,7 @@ export default function RefineStrategyPicker({
173173
</div>
174174
</div>
175175
))}
176-
{!meta && <div style={{ fontSize: 11, color: "#9ca3af" }}>策略声明加载中…</div>}
176+
{!meta && <Loading label="策略声明加载中…" />}
177177
</div>
178178
</div>
179179
);

‎app/src/components/WebArticleView.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { useEffect, useState } from "react";
1010
import { invoke } from "@tauri-apps/api/core";
1111
import { save } from "@tauri-apps/plugin-dialog";
12-
import { Button } from "../ui/primitives";
12+
import { Button, Loading, Skeleton } from "../ui/primitives";
1313

1414
/** WebPage 响应结构(db_web::WebPage 为 serde camelCase——字段须 camel 读取) */
1515
interface WebPageView {
@@ -75,7 +75,18 @@ export default function WebArticleView({ sessionId, onToNote, onRemove }: Props)
7575
}, [sessionId]);
7676

7777
if (err) return <p style={{ fontSize: 12, color: "#dc2626" }}>{err}</p>;
78-
if (!page) return <p style={{ fontSize: 12, color: "#9ca3af" }}>文章加载中…</p>;
78+
if (!page) {
79+
// 批 4 T14:文章结构已知(来源行 + 段落块)⇒ 骨架占位;可读语义由 `Loading` 给
80+
// (骨架整组 `aria-hidden`:只留骨架等于把"在等什么"从无障碍树里删掉)。
81+
return (
82+
<div>
83+
<Loading label="文章加载中…" />
84+
<div style={{ marginTop: 8 }}>
85+
<Skeleton lines={4} />
86+
</div>
87+
</div>
88+
);
89+
}
7990

8091
return (
8192
<div>

‎app/src/components/WindowSelectCard.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
import { useEffect, useRef, useState } from "react";
1010
import { zIndex } from "../ui/zIndex";
11+
import { Button } from "../ui/primitives";
1112
import type { WindowInfo } from "../types";
1213

1314
interface Props {
@@ -177,9 +178,11 @@ export function WindowSelectCard({ windows, selected, onSelect, onRefresh, loadi
177178
>
178179
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0 4px 6px" }}>
179180
<span style={{ fontSize: 11, color: "#6b7280" }}>推荐窗口</span>
180-
<button onClick={() => onRefresh()} disabled={loading} style={{ fontSize: 11, color: "#0d9488", cursor: "pointer" }}>
181+
{/* 批 4 T14:原生按钮内的忙碌文案 ⇒ 按加载态三角的第 3 档落 `Button busy`
182+
(不是 `Loading`):`busy` 保留焦点与 Tab 序、由原语给探针与 aria-busy。 */}
183+
<Button variant="ghost" size="sm" busy={loading} onClick={() => onRefresh()}>
181184
{loading ? "加载中…" : "⟳ 刷新"}
182-
</button>
185+
</Button>
183186
</div>
184187
<div style={{ display: "flex", flexDirection: "column", gap: 2, maxHeight: 260, overflowY: "auto" }}>
185188
{windows.length === 0 && !loading && (

‎app/src/components/review/ReviewSessionPanel.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useCallback, useEffect, useState } from "react";
1919
import { invoke } from "@tauri-apps/api/core";
2020
import type { Flashcard } from "../../types/notes";
2121
import PromoteCardButton from "../PromoteCardButton";
22+
import { Loading, Skeleton } from "../../ui/primitives";
2223

2324
/** 四档评分按钮(文案=回忆质量自评) */
2425
const RATINGS: { value: string; label: string; color: string }[] = [
@@ -118,7 +119,15 @@ export default function ReviewSessionPanel({ groupId, groupName, active = true,
118119

119120
<div style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: 24 }}>
120121
<div style={{ maxWidth: 640, margin: "0 auto" }}>
121-
{!loaded && <p style={{ fontSize: 13, color: "#9ca3af" }}>加载中…</p>}
122+
{!loaded && (
123+
<div>
124+
{/* 批 4 T14:可读语义(role="status" + 逐字文案)由 `Loading` 给,骨架只承载形状 */}
125+
<Loading />
126+
<div style={{ marginTop: 10 }}>
127+
<Skeleton lines={3} />
128+
</div>
129+
</div>
130+
)}
122131

123132
{finished && (
124133
<div data-testid="session-finished" style={{ textAlign: "center", padding: "48px 0" }}>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* @ai-context 加载态(可见加载文案)的**冻结基线**(批 4 B11 裁决 + Task 14 的落点)。
3+
*
4+
* Why:规格 §5.1 的病灶逐字是「全站 **0 骨架屏**,长任务只有一行灰字」——本仓至今**没有加载原语**
5+
* (`Loading` / `Skeleton` / `Probe` 批 0-D 就交付了,批 4 才接进来)。全量按规格账本数是
6+
* 「85 处 / 30 文件」,T1 的机器口径(`tmp/t1/slice-manifest.mjs` 的 loading 类)实测
7+
* **18 行 / 18 文件**(计划写的 19/19 是台账 #26 的 Windows 域过滤 bug)。18 处里有 14 处在
8+
* B11 切片内(判据 ①本批已触碰 ∪ ②`pages/**` ∪ ③有同名测试),4 处是余量。
9+
*
10+
* Why 冻结在**文件 → 行数**粒度(与 `nativeButtonBaseline.ts` 同范式):逐行 key 会在文件被拆件 /
11+
* 行号漂移时产生假红;棘轮要防的是「永远迁不完」⇒ 总量 + 逐文件只减不增是完整的。
12+
* ⚠️ 本棘轮**只管可见加载文案**(`/加载[^"'`\n]{0,8}(中|…)/`),不含布尔门控与 `busy` 状态本身。
13+
*
14+
* ★ 扫描口径(与 `tmp/scan-callsites.mjs` 同源;口径本身是判据的一部分)
15+
* ① 域 = `app/src/**` 的 `.ts`/`.tsx` 减 `*.test.ts(x)` 减 `ui/primitives/**` 减 `ui/icons/**`
16+
* (原语层就是"加载态该去哪儿"的答案,不是待收敛的重复调用点);
17+
* ② **先剥注释**(`//` 与块注释抹为等长空白);字符串/模板字面量**只跳过、不抹内容**
18+
* (判据读的就是字符串里的用户可见文案);
19+
* ③ 行口径:同一行多次命中只算 1 行(与 T1 的 18 行同源,保证前后读数可比)。
20+
*
21+
* ★ 迁移读数(T14 实施者实测,仪器 = 与守卫同源的正则扫描)
22+
* 冻结时(`dev@f6dd012f`)**18 行 / 18 文件** ⇒ T14 第一提交后 **13 行 / 13 文件**。
23+
* 逐文件 Δ 之和 = −5,与迁过的 5 个文件逐一对拍(每个文件 1 行 → 0 行)⇒ 机理成立。
24+
*
25+
* ★ 余量去向(B11 附带硬要求:切片 + 棘轮 = **中间态**,不是"五类已各自收敛成一个原语")
26+
* ① 4 处余量(`LearningLibraryPanel` / `RetroTimeline` / `SessionDetailPanel` / `SessionListBody`)
27+
* 不在 B11 切片内 ⇒ 登记给**批 5/7**,由本棘轮冻结;
28+
* ② 5 处**在切片内但本提交未迁**(`AsrConfusionPanel` / `GoalDetail` / `RefineWorkbench` /
29+
* `SecondPassPanel` / `pages/ReviewPage`)——它们同时是 T13 空态切片的成员(同文件甚至
30+
* **同一行/同一三元链**),并行窗口内改同一文件即撞车 ⇒ 按纪律让路,交 T14 的第二提交
31+
* (T13 落库后)逐处迁;本文件那时把它们的上限从 1 降到 0(棘轮只降不升)。
32+
*/
33+
export const FROZEN_LOADING_TEXT_TOTAL = 13;
34+
35+
/**
36+
* 相对 `app/src` 的路径 → 允许残留的可见加载文案**行数上限**(只许降、不许升;键不许删)。
37+
*
38+
* 语义:`0` = 已由原语承载(`Loading` / `Skeleton`);`1` = 仍有一处手写文案,见 `RESIDUAL` 的理由。
39+
*/
40+
export const FROZEN_LOADING_TEXT_BY_FILE: Readonly<Record<string, number>> = {
41+
// 已迁移(0 = 该文件的加载态已交给 L1 原语)
42+
"components/NotePreviewView.tsx": 0,
43+
"components/RefineLaunchDialog.tsx": 0,
44+
"components/RefineStrategyPicker.tsx": 0,
45+
"components/WebArticleView.tsx": 0,
46+
"components/review/ReviewSessionPanel.tsx": 0,
47+
// 残留(1 = 见 RESIDUAL 的分类与理由)
48+
"components/AsrConfusionPanel.tsx": 1,
49+
"components/CaptureOverlayPanel.tsx": 1,
50+
"components/GoalDetail.tsx": 1,
51+
"components/ImageGallery.tsx": 1,
52+
"components/LearningLibraryPanel.tsx": 1,
53+
"components/RefineWorkbench.tsx": 1,
54+
"components/RetroTimeline.tsx": 1,
55+
"components/SecondPassPanel.tsx": 1,
56+
"components/SessionDetailPanel.tsx": 1,
57+
"components/SessionListBody.tsx": 1,
58+
"components/StructureImageSection.tsx": 1,
59+
"components/WindowSelectCard.tsx": 1,
60+
"pages/ReviewPage.tsx": 1,
61+
};
62+
63+
/**
64+
* 迁移面:这些文件的加载态**必须**由原语承载 ⇒ 判据要求「0 命中 + 含 `<Loading`/`<Skeleton`」。
65+
* 少一个文件就是回潮,多一个文件要先把它在 `FROZEN_LOADING_TEXT_BY_FILE` 里降到 0。
66+
*/
67+
export const MIGRATED_FILES: readonly string[] = [
68+
"components/NotePreviewView.tsx",
69+
"components/RefineLaunchDialog.tsx",
70+
"components/RefineStrategyPicker.tsx",
71+
"components/WebArticleView.tsx",
72+
"components/review/ReviewSessionPanel.tsx",
73+
];
74+
75+
/** 残留命中的分类:`exception` = 原语结构上表达不了 · `button-busy` = 按钮内忙碌文案 · `backlog` = 余量/让路 */
76+
export type ResidualKind = "exception" | "button-busy" | "backlog";
77+
78+
/**
79+
* 允许残留的**逐文件理由**(每条都必须:理由非空 ∧ 该文件此刻**仍然命中** —— 防"僵尸豁免":
80+
* 一处被迁走却仍挂在豁免表里,下一个人就会以为它还在)。
81+
*/
82+
export const RESIDUAL: readonly { file: string; kind: ResidualKind; reason: string }[] = [
83+
{
84+
file: "components/CaptureOverlayPanel.tsx",
85+
kind: "exception",
86+
reason:
87+
"B6 特殊条款未覆盖的例外:根节点 background:#000、原文案 #e5e7eb(浅字压深底);`Loading` 的墨度权威是 `Text tone=\"ink-3\"`(深灰)⇒ 迁过去等于把对比度反转(可读性回归)。消费者只有 1 个 < B6 阈值 3 ⇒ 不改原语;行内 style 覆盖类语义被 ADR-033 §4 禁止 ⇒ 保留手写文案 + 冻结",
88+
},
89+
{
90+
file: "components/ImageGallery.tsx",
91+
kind: "button-busy",
92+
reason: "该行是 `<Button variant=\"secondary\" busy={loading}>` 的**按钮文案**(`加载中…` / `⟳ 刷新`),不是独立加载态 ⇒ 已由 `Button busy` 承载(Task 14 Step 2 ③)",
93+
},
94+
{
95+
file: "components/StructureImageSection.tsx",
96+
kind: "button-busy",
97+
reason: "同 ImageGallery:`<Button busy={loading}>` 的按钮文案,已由 `Button busy` 承载",
98+
},
99+
{
100+
file: "components/WindowSelectCard.tsx",
101+
kind: "button-busy",
102+
reason: "原生 `<button disabled={loading}>` 的忙碌文案 ⇒ T14 改为 `<Button variant=\"ghost\" size=\"sm\" busy={loading}>`;文案仍随 loading 切换(保留可读标签),探针/aria-busy 由原语给",
103+
},
104+
{
105+
file: "components/AsrConfusionPanel.tsx",
106+
kind: "backlog",
107+
reason: "在切片内但让路:该行与 T13 的空态迁移点是**同一行**(`view === null ? \"加载中…\" : \"暂无新候选…\"`);T13 已在它的空态提交里把这条三元一并换成 `<Loading label=\"加载中…\"/>` ⇒ 待 T13 落库后由 T14 第二提交把它移到 `MIGRATED_FILES` 并把上限收紧为 0",
108+
},
109+
{
110+
file: "components/GoalDetail.tsx",
111+
kind: "backlog",
112+
reason: "在切片内但让路:该文件同时是 T13 空态切片成员 ⇒ 交 T14 第二提交(目标:`Skeleton lines={3}` + `Loading`,见 `Loading.tsx:10` 点名的靶子)",
113+
},
114+
{
115+
file: "components/RefineWorkbench.tsx",
116+
kind: "backlog",
117+
reason: "在切片内但让路:该文件同时是 T13 空态切片成员 ⇒ 交 T14 第二提交",
118+
},
119+
{
120+
file: "components/SecondPassPanel.tsx",
121+
kind: "backlog",
122+
reason: "在切片内但让路:加载分支与 T13 的空态分支在**同一三元链**上(`:216` / `:219`)⇒ 交 T14 第二提交",
123+
},
124+
{ file: "pages/ReviewPage.tsx", kind: "backlog", reason: "在切片内但让路:该文件同时是 T13 空态切片成员 ⇒ 交 T14 第二提交" },
125+
{ file: "components/LearningLibraryPanel.tsx", kind: "backlog", reason: "不在 B11 切片内 ⇒ 余量登记给批 5/7" },
126+
{ file: "components/RetroTimeline.tsx", kind: "backlog", reason: "不在 B11 切片内 ⇒ 余量登记给批 5/7" },
127+
{ file: "components/SessionDetailPanel.tsx", kind: "backlog", reason: "不在 B11 切片内 ⇒ 余量登记给批 5/7" },
128+
{ file: "components/SessionListBody.tsx", kind: "backlog", reason: "不在 B11 切片内 ⇒ 余量登记给批 5/7" },
129+
];
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* @ai-context `loadingMigration.test.tsx` — 批 4 T14 的**渲染级**判据(源码级棘轮见 `loadingRatchet.test.ts`)。
4+
*
5+
* Why 需要渲染级:棘轮只证明「文件里不再有手写加载文案」,证明不了**用户看到的是原语**。本文件对
6+
* 三处已迁的形态各钉一条判据(每处各带自己的变异体,见报告):
7+
* ① `RefineStrategyPicker` 的 label 与默认值**不同**(「策略声明加载中…」)⇒ 必须显式传,删掉就红;
8+
* ② `WebArticleView` 是「骨架 + 可读文案」成对的样板(骨架整组 `aria-hidden`,别只留骨架);
9+
* ③ `WindowSelectCard` 的按钮内忙碌态走 `busy`(不是原生 `disabled`,也不是 `Loading`)。
10+
* 另外每条都断言**根节点没有行内 style** —— ADR-033 §4 禁止用行内 style 覆盖类语义,而这三处的
11+
* 旧写法正是「手写灰字 + fontSize/color 内联」。
12+
*
13+
* 副作用:`invoke` 与 dialog 插件全 mock(AAA;不触真实后端、不写磁盘)。
14+
* 边界:jsdom **不排版、不加载样式表**(`vitest.config.ts` 的 `css` 默认 false)⇒ 判据只读 DOM
15+
* 事实(类名 / 文案 / role / aria / 行内 style 的有无),不断言像素与计算样式。
16+
*/
17+
import { afterEach, describe, expect, it, vi } from "vitest";
18+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
19+
import RefineStrategyPicker from "../../components/RefineStrategyPicker";
20+
import WebArticleView from "../../components/WebArticleView";
21+
import { WindowSelectCard } from "../../components/WindowSelectCard";
22+
23+
const invokeMock = vi.fn();
24+
25+
vi.mock("@tauri-apps/api/core", () => ({
26+
invoke: (...args: unknown[]) => invokeMock(...args),
27+
convertFileSrc: (p: string) => p,
28+
}));
29+
30+
vi.mock("@tauri-apps/plugin-dialog", () => ({ save: () => Promise.resolve(null) }));
31+
32+
afterEach(() => {
33+
cleanup();
34+
invokeMock.mockReset();
35+
});
36+
37+
/** 永不 resolve ⇒ 组件停在加载态(这就是要断言的相位) */
38+
const pending = (): Promise<never> => new Promise<never>(() => undefined);
39+
40+
describe("① RefineStrategyPicker:文案与默认值不同 ⇒ 必须显式传 label(删了就红)", () => {
41+
it("meta=null 时渲染原语 `Loading`:role=status + 探针 + 文案逐字 + 根节点无行内 style", () => {
42+
const { container } = render(
43+
<RefineStrategyPicker meta={null} value={{ presetId: "", dims: {} }} onChange={vi.fn()} />,
44+
);
45+
const root = container.querySelector(".ed-loading");
46+
expect(root, "加载态不是 `Loading` 原语渲染的(缺 .ed-loading)").toBeTruthy();
47+
expect(root?.querySelector(".ed-probe"), "原语的探针没渲染出来").toBeTruthy();
48+
expect(root?.getAttribute("role"), "加载态应让辅助技术读到").toBe("status");
49+
// 文案逐字:删掉 `label` 会退回原语默认「加载中…」⇒ 这条立刻红
50+
expect(root?.textContent, "加载文案被改动或被默认值吃掉").toBe("策略声明加载中…");
51+
// ADR-033 §4:不许用行内 style 覆盖类语义(旧写法是 fontSize:11 + color:#9ca3af)
52+
expect(root?.getAttribute("style"), "加载态上出现了行内 style").toBeNull();
53+
expect(screen.queryByText("加载中…"), "加载文案退化成了默认值").toBeNull();
54+
});
55+
});
56+
57+
describe("② WebArticleView:骨架与可读文案成对(骨架 aria-hidden,别只留骨架)", () => {
58+
it("文章未到时渲染 `Loading`(逐字)+ `Skeleton`(4 条 · aria-hidden)", () => {
59+
invokeMock.mockImplementation(() => pending());
60+
const { container } = render(<WebArticleView sessionId={7} onToNote={vi.fn()} onRemove={vi.fn()} />);
61+
const status = container.querySelector(".ed-loading");
62+
expect(status?.textContent, "加载文案被改动").toBe("文章加载中…");
63+
const skel = container.querySelector(".ed-skeleton");
64+
expect(skel, "结构已知(来源行 + 段落块)却没渲染骨架").toBeTruthy();
65+
// 骨架是纯视觉信息:整组 aria-hidden,可读语义由上面的 role=status 承载
66+
expect(skel?.parentElement?.getAttribute("aria-hidden")).toBe("true");
67+
});
68+
});
69+
70+
describe("③ WindowSelectCard:按钮内忙碌态走 `busy`(不是原生 disabled,也不是 `Loading`)", () => {
71+
it("loading=true 时刷新控件是 `.ed-btn` + aria-busy,文案仍是「加载中…」", () => {
72+
const { container } = render(
73+
<WindowSelectCard windows={[]} selected={null} onSelect={vi.fn()} onRefresh={vi.fn()} loading />,
74+
);
75+
// 打开浮层(第一颗按钮是「已选窗口」卡片)
76+
const toggle = container.querySelector("button");
77+
expect(toggle).toBeTruthy();
78+
fireEvent.click(toggle as HTMLButtonElement);
79+
return waitFor(() => {
80+
const label = screen.getByText("加载中…");
81+
const btn = label.closest("button");
82+
expect(btn?.className, "按钮内忙碌态没走 `Button`(缺 .ed-btn)").toContain("ed-btn");
83+
expect(btn?.getAttribute("aria-busy"), "`busy` 应声明 aria-busy").toBe("true");
84+
expect(btn?.hasAttribute("disabled"), "原生 disabled 会把焦点丢掉 —— 忙碌态应用 `busy`").toBe(false);
85+
});
86+
});
87+
});

0 commit comments

Comments
 (0)