fix(qa): 第五轮系统性质量保障缺陷修复 + 单元/集成测试
安全/鉴权:登出清服务端 httpOnly 令牌、登录 IP 限流前置 bcrypt、停用账号 令牌在 6 个内联 role handler 失效、admin/items 存在性 oracle 认证前置、 同意偏好 sanitize fail-closed、api-crypto 按 HTTP 方法判体。 CMS:workflow 状态更新+审计收进交互式 $transaction(失败即回滚)、编辑器 richtext 走 textarea 防换行损毁、array/object 子字段按作用域读写防污染、 about/contact/erp-upgrade 补 ISR revalidate 与路由映射。 UI/a11y:AnimatedCounter 入视口前显起点、CLS 累计上报、MetricCard 动画入 effect 并卸载取消 rAF、后台状态四态映射/搜索防抖+序号守卫/媒体单 input/ 吞错改横幅/表单 label-aria 关联。 新增对应判别性单测与 pagination/rate-limit/client-ip/sanitize/validate-content-data 等模块及 tests-integration 真库集成层。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────
|
||||
@@ -445,6 +445,61 @@ describe('ProductValueSection', () => {
|
||||
expect(screen.queryByText('以可量化的目标约束交付')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('指标计数动画由 effect 驱动并在卸载时取消排队帧(Q-6 回归)', async () => {
|
||||
// Q-6:动画曾在 render 体内 setState + requestAnimationFrame——每帧 setState 又触发
|
||||
// 重渲染并重启一条新动画链,且从不 cancelAnimationFrame;卸载后帧回调继续排队。
|
||||
// 判别面:已排队的帧必须被逐一取消(sched === cancel),旧实现 sched>0 且 cancel=0 必红。
|
||||
jest.useFakeTimers();
|
||||
// faithful rAF 桩:cancel 必须真的撤销已排队的回调,否则「卸载后不再排队」无从判别
|
||||
const pending = new Map<number, ReturnType<typeof setTimeout>>();
|
||||
let handle = 0;
|
||||
const sched = jest.fn((cb: FrameRequestCallback) => {
|
||||
const id = ++handle;
|
||||
pending.set(
|
||||
id,
|
||||
setTimeout(() => {
|
||||
pending.delete(id);
|
||||
cb(Date.now());
|
||||
}, 16),
|
||||
);
|
||||
return id;
|
||||
});
|
||||
const cancel = jest.fn((id: number) => {
|
||||
const t = pending.get(id);
|
||||
if (t) clearTimeout(t);
|
||||
pending.delete(id);
|
||||
});
|
||||
const prevRaf = global.requestAnimationFrame;
|
||||
const prevCancel = global.cancelAnimationFrame;
|
||||
global.requestAnimationFrame = sched as unknown as typeof requestAnimationFrame;
|
||||
global.cancelAnimationFrame = cancel as unknown as typeof cancelAnimationFrame;
|
||||
try {
|
||||
const { ProductValueSection } = await import('./detail-product-value');
|
||||
const { container, unmount } = render(<ProductValueSection product={mockProduct} />);
|
||||
const card = container.querySelector('.tabular-nums') as HTMLElement;
|
||||
expect(card.textContent).toBe('0');
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
// 动画真的在推进(中途值),而不是停在 0 或瞬间跳终值
|
||||
expect(card.textContent).not.toBe('0');
|
||||
expect(card.textContent).not.toBe('300%');
|
||||
const scheduledBeforeUnmount = sched.mock.calls.length;
|
||||
expect(scheduledBeforeUnmount).toBeGreaterThan(0);
|
||||
unmount();
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(3000);
|
||||
});
|
||||
// 泄漏判别:卸载后动画链必须死透——旧实现(render 期 rAF、从不 cancel)
|
||||
// 在这里会继续排队每一帧;effect 版在 cleanup 里 cancel 掉最后一帧。
|
||||
expect(sched.mock.calls.length).toBe(scheduledBeforeUnmount);
|
||||
} finally {
|
||||
global.requestAnimationFrame = prevRaf;
|
||||
global.cancelAnimationFrame = prevCancel;
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('DetailTrustSection', () => {
|
||||
|
||||
Reference in New Issue
Block a user