feat: Header 倒计时数字滚动动画 — @number-flow/react 替换静态文本
This commit is contained in:
@@ -543,6 +543,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
|
||||
| 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 |
|
||||
| 图表 | Recharts | 拨测趋势折线图 |
|
||||
| 动画 | @number-flow/react | 倒计时数字滚动过渡 |
|
||||
| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 |
|
||||
|
||||
**不引入的依赖**:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 `useState` 足够)、Vite(已由 Bun 原生 fullstack 替代)
|
||||
@@ -702,6 +703,7 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
|
||||
| `TrendChart` | `components/TrendChart.tsx` | Recharts 趋势折线图(耗时+延迟范围) |
|
||||
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
|
||||
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块 + Tooltip 提示时间和状态) |
|
||||
| `RefreshCountdown` | `components/RefreshCountdown.tsx` | Header 刷新倒计时(NumberFlow 数字滚动),手动刷新按钮,刷新中/等待首次刷新文本 |
|
||||
|
||||
### 2.5 新增功能开发步骤
|
||||
|
||||
|
||||
7
bun.lock
7
bun.lock
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "gateway-checker",
|
||||
"dependencies": {
|
||||
"@number-flow/react": "^0.6.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@tanstack/react-query": "^5.100.10",
|
||||
"@xmldom/xmldom": "^0.9.10",
|
||||
@@ -186,6 +187,8 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@number-flow/react": ["@number-flow/react@0.6.0", "https://registry.npmmirror.com/@number-flow/react/-/react-0.6.0.tgz", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-77Yfc9+zkV2UDSP8phhZzxJGuwxi/Tt1TikmipL+1r3e9GFKEYDZ1XwInj67NoSt3OnOB0KLvvcl3lfPZgBHVQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.130.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.130.0.tgz", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.2.9", "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
||||
@@ -584,6 +587,8 @@
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "https://registry.npmmirror.com/esm-env/-/esm-env-1.2.2.tgz", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"espree": ["espree@11.2.0", "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
@@ -852,6 +857,8 @@
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"number-flow": ["number-flow@0.6.0", "https://registry.npmmirror.com/number-flow/-/number-flow-0.6.0.tgz", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
@@ -105,6 +105,25 @@ styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关
|
||||
- **WHEN** Drawer 概览统计区渲染
|
||||
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card` 类(background: var(--td-bg-color-container-hover)),并使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `.summary-stat-col` 类(text-align: center)实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item` 和 `.overview-stat-value` 类。
|
||||
|
||||
### Requirement: NumberFlow 倒计时样式类
|
||||
styles.css SHALL 定义 NumberFlow 倒计时相关样式类,供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens,不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。
|
||||
|
||||
#### Scenario: 倒计时滚动容器类
|
||||
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
|
||||
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类,保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums
|
||||
|
||||
#### Scenario: 倒计时数字类
|
||||
- **WHEN** NumberFlow 数字渲染
|
||||
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量,减少滚动边缘突兀感
|
||||
|
||||
#### Scenario: 倒计时单位类
|
||||
- **WHEN** 分钟或秒单位文本渲染
|
||||
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐,并继承当前 TDesign 文本色
|
||||
|
||||
#### Scenario: 不使用内联样式
|
||||
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
|
||||
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类,不得通过 React `style` prop 设置 NumberFlow 展示样式
|
||||
|
||||
### Requirement: 异常行背景类
|
||||
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`。
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新
|
||||
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
|
||||
|
||||
### Requirement: 倒计时显示
|
||||
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时文本。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中,App 组件 SHALL NOT 持有每秒更新的 `now` state。
|
||||
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中,App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡,非倒计时状态 SHALL 保持普通文本或按钮语义。
|
||||
|
||||
#### Scenario: RefreshCountdown 组件封装
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
@@ -30,22 +30,38 @@ RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时文本。倒计
|
||||
- **WHEN** RefreshCountdown 组件渲染
|
||||
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number`、`refreshInterval: number`、`isFetching: boolean`、`isManualRefresh: boolean`、`onRefresh: () => void` 作为 props
|
||||
|
||||
#### Scenario: 短时间格式
|
||||
- **WHEN** 距下次刷新剩余时间小于 60 秒
|
||||
#### Scenario: NumberFlow 数字滚动
|
||||
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
|
||||
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react` 的 `NumberFlow` 渲染,并使用向下滚动趋势表达倒计时递减
|
||||
|
||||
#### Scenario: 秒级间隔格式
|
||||
- **WHEN** 自动刷新间隔小于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"xx秒"格式(如"26秒")
|
||||
|
||||
#### Scenario: 长时间格式
|
||||
- **WHEN** 距下次刷新剩余时间大于等于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"x分x秒"格式(如"4分30秒")
|
||||
#### Scenario: 分钟级稳定格式
|
||||
- **WHEN** 自动刷新间隔大于等于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式,秒数 SHALL 固定为两位(如"4分30秒"、"0分09秒")
|
||||
|
||||
#### Scenario: 时间数字边界
|
||||
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
|
||||
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动
|
||||
|
||||
#### Scenario: 无前缀
|
||||
- **WHEN** 倒计时显示
|
||||
- **THEN** 倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
|
||||
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
|
||||
|
||||
#### Scenario: 可访问文本
|
||||
- **WHEN** NumberFlow 倒计时渲染
|
||||
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本,供测试和辅助技术读取
|
||||
|
||||
#### Scenario: 刷新中状态
|
||||
- **WHEN** 数据正在刷新(isFetching=true 且 isLoading=false)
|
||||
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
|
||||
|
||||
#### Scenario: 等待首次刷新状态
|
||||
- **WHEN** 自动刷新模式下尚未完成首次刷新
|
||||
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"
|
||||
|
||||
### Requirement: App 组件渲染隔离
|
||||
App 组件 SHALL NOT 持有任何高频更新的 state(如每秒更新的时钟),确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
|
||||
|
||||
@@ -73,12 +89,16 @@ App 组件 SHALL NOT 持有任何高频更新的 state(如每秒更新的时
|
||||
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled,防止连续点击
|
||||
|
||||
### Requirement: 布局稳定性
|
||||
倒计时/按钮容器 SHALL 保持布局稳定,避免内容变化导致的抖动。
|
||||
倒计时/按钮容器 SHALL 保持布局稳定,避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。
|
||||
|
||||
#### Scenario: 数字等宽
|
||||
- **WHEN** 倒计时数字变化
|
||||
- **THEN** 容器 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
|
||||
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
|
||||
|
||||
#### Scenario: NumberFlow 分组同步
|
||||
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
|
||||
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化
|
||||
|
||||
#### Scenario: 格式切换不抖动
|
||||
- **WHEN** 倒计时在"秒"和"分秒"格式间切换
|
||||
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
|
||||
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度,避免 RadioGroup 位移
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"vite": "^8.0.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@number-flow/react": "^0.6.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@tanstack/react-query": "^5.100.10",
|
||||
"@xmldom/xmldom": "^0.9.10",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshIcon } from "tdesign-icons-react";
|
||||
import { Button, Typography } from "tdesign-react";
|
||||
|
||||
import { formatCountdown } from "../utils/time";
|
||||
|
||||
interface RefreshCountdownProps {
|
||||
dashboardUpdatedAt: number;
|
||||
isFetching: boolean;
|
||||
@@ -45,8 +44,55 @@ export function RefreshCountdown({
|
||||
);
|
||||
}
|
||||
|
||||
const refreshText =
|
||||
dashboardUpdatedAt > 0 ? (isFetching ? "刷新中..." : formatCountdown(nextRefreshSeconds ?? 0)) : "等待首次刷新";
|
||||
if (dashboardUpdatedAt <= 0) {
|
||||
return <Typography.Text theme="secondary">等待首次刷新</Typography.Text>;
|
||||
}
|
||||
|
||||
return <Typography.Text theme="secondary">{refreshText}</Typography.Text>;
|
||||
if (isFetching) {
|
||||
return <Typography.Text theme="secondary">刷新中...</Typography.Text>;
|
||||
}
|
||||
|
||||
const seconds = nextRefreshSeconds ?? 0;
|
||||
const isMinuteMode = refreshInterval >= 60000;
|
||||
|
||||
if (isMinuteMode) {
|
||||
const mm = Math.floor(seconds / 60);
|
||||
const ss = seconds % 60;
|
||||
|
||||
return (
|
||||
<span aria-label={formatAccessibleLabel(seconds, true)} className="refresh-countdown-flow">
|
||||
<NumberFlow
|
||||
className="refresh-countdown-flow__number"
|
||||
format={{ minimumIntegerDigits: 1 }}
|
||||
trend={-1}
|
||||
value={mm}
|
||||
/>
|
||||
<span className="refresh-countdown-flow__unit">分</span>
|
||||
<NumberFlow
|
||||
className="refresh-countdown-flow__number"
|
||||
digits={{ 1: { max: 5 } }}
|
||||
format={{ minimumIntegerDigits: 2 }}
|
||||
trend={-1}
|
||||
value={ss}
|
||||
/>
|
||||
<span className="refresh-countdown-flow__unit">秒</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span aria-label={formatAccessibleLabel(seconds, false)} className="refresh-countdown-flow">
|
||||
<NumberFlow className="refresh-countdown-flow__number" trend={-1} value={seconds} />
|
||||
<span className="refresh-countdown-flow__unit">秒</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function formatAccessibleLabel(seconds: number, isMinuteMode: boolean): string {
|
||||
if (isMinuteMode) {
|
||||
const mm = Math.floor(seconds / 60);
|
||||
const ss = seconds % 60;
|
||||
return `${mm}分${String(ss).padStart(2, "0")}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,24 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.refresh-countdown-flow {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.refresh-countdown-flow__number {
|
||||
line-height: 0.85;
|
||||
--number-flow-mask-height: 0.15em;
|
||||
--number-flow-mask-width: 0.25em;
|
||||
}
|
||||
|
||||
.refresh-countdown-flow__unit {
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
|
||||
@@ -88,3 +88,23 @@ Object.defineProperty(dom.window, "matchMedia", {
|
||||
|
||||
dom.window.Element.prototype.scrollTo = () => {};
|
||||
dom.window.Element.prototype.scrollIntoView = () => {};
|
||||
|
||||
Object.defineProperty(dom.window, "customElements", {
|
||||
value: {
|
||||
define: () => {},
|
||||
get: () => undefined,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
globalThis.customElements = dom.window.customElements;
|
||||
|
||||
// Mock @number-flow/react globally (custom elements not supported in jsdom)
|
||||
import { mock } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
void mock.module("@number-flow/react", () => {
|
||||
const NumberFlow = () => createElement("span", { "data-testid": "number-flow" });
|
||||
const NumberFlowGroup = ({ children }: { children: unknown }) => children;
|
||||
return { default: NumberFlow, NumberFlowGroup };
|
||||
});
|
||||
|
||||
@@ -1,27 +1,57 @@
|
||||
import "../../../tests/web/test-utils";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, test, vi } from "bun:test";
|
||||
|
||||
import { RefreshCountdown } from "../../../src/web/components/RefreshCountdown";
|
||||
|
||||
describe("RefreshCountdown", () => {
|
||||
test("手动模式不崩溃", () => {
|
||||
const { container } = render(
|
||||
test("手动模式渲染刷新按钮", () => {
|
||||
const onRefresh = vi.fn();
|
||||
render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={0}
|
||||
isFetching={false}
|
||||
isManualRefresh={true}
|
||||
onRefresh={onRefresh}
|
||||
refreshInterval={30000}
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button", { name: "刷新 Dashboard" });
|
||||
expect(button).toBeTruthy();
|
||||
});
|
||||
|
||||
test("等待首次刷新显示文本", () => {
|
||||
render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={0}
|
||||
isFetching={false}
|
||||
isManualRefresh={false}
|
||||
onRefresh={vi.fn()}
|
||||
refreshInterval={30000}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
expect(screen.getByText("等待首次刷新")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("自动模式不崩溃", () => {
|
||||
test("刷新中显示文本", () => {
|
||||
render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={Date.now()}
|
||||
isFetching={true}
|
||||
isManualRefresh={false}
|
||||
onRefresh={vi.fn()}
|
||||
refreshInterval={30000}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("刷新中...")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("秒级间隔渲染 NumberFlow 倒计时", () => {
|
||||
const now = Date.now();
|
||||
const { container } = render(
|
||||
render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={now - 10000}
|
||||
isFetching={false}
|
||||
@@ -31,34 +61,42 @@ describe("RefreshCountdown", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
const countdown = screen.getByLabelText(/秒$/);
|
||||
expect(countdown).toBeTruthy();
|
||||
expect(countdown.className).toContain("refresh-countdown-flow");
|
||||
});
|
||||
|
||||
test("fetching 状态不崩溃", () => {
|
||||
const { container } = render(
|
||||
test("分钟级间隔渲染带分秒的 NumberFlow 倒计时", () => {
|
||||
const now = Date.now();
|
||||
render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={1000}
|
||||
isFetching={true}
|
||||
isManualRefresh={false}
|
||||
onRefresh={vi.fn()}
|
||||
refreshInterval={30000}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
|
||||
test("未刷新状态不崩溃", () => {
|
||||
const { container } = render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={0}
|
||||
dashboardUpdatedAt={now - 10000}
|
||||
isFetching={false}
|
||||
isManualRefresh={false}
|
||||
onRefresh={vi.fn()}
|
||||
refreshInterval={30000}
|
||||
refreshInterval={60000}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
const countdown = screen.getByLabelText(/分\d{2}秒$/);
|
||||
expect(countdown).toBeTruthy();
|
||||
expect(countdown.className).toContain("refresh-countdown-flow");
|
||||
});
|
||||
|
||||
test("5 分钟间隔使用稳定分钟格式", () => {
|
||||
const now = Date.now();
|
||||
const { container } = render(
|
||||
<RefreshCountdown
|
||||
dashboardUpdatedAt={now - 290000}
|
||||
isFetching={false}
|
||||
isManualRefresh={false}
|
||||
onRefresh={vi.fn()}
|
||||
refreshInterval={300000}
|
||||
/>,
|
||||
);
|
||||
|
||||
const countdown = container.querySelector(".refresh-countdown-flow");
|
||||
expect(countdown).toBeTruthy();
|
||||
expect(countdown?.getAttribute("aria-label")).toMatch(/分\d{2}秒$/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user