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:
2026-09-28 10:48:07 +08:00
parent 040951c0a3
commit a366bd1400
224 changed files with 8938 additions and 5579 deletions
+56 -1
View File
@@ -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', () => {