feat(home): Hero 动态粒子层(入场浮现后静止)
新增 hero-particle-field 组件作为 Hero L3 层:品牌红点缀、Canvas + rAF 仅在 ~600ms 入场窗运行后冻结为静态帧,遵守「无持续循环动画」动效铁律; prefers-reduced-motion 退化为一次性静态帧。固定种子 PRNG 保证视觉快照可复现。 同步更新 home-content-v15 接入与单测、home 视觉基线。 Co-Authored-By: 阿睿 <workbuddy@tencent.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import HeroParticleField from './hero-particle-field';
|
||||
|
||||
jest.mock('@/hooks/use-reduced-motion', () => ({
|
||||
useReducedMotion: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('HeroParticleField - 动态粒子层(入场浮现后静止)', () => {
|
||||
it('renders an aria-hidden, non-interactive canvas layer', () => {
|
||||
render(<HeroParticleField />);
|
||||
const canvas = screen.getByTestId('hero-particle-field');
|
||||
expect(canvas.tagName).toBe('CANVAS');
|
||||
expect(canvas).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(canvas.className).toContain('pointer-events-none');
|
||||
});
|
||||
|
||||
it('does not throw under reduced motion (degrades to a single static frame)', () => {
|
||||
const { useReducedMotion } = require('@/hooks/use-reduced-motion');
|
||||
(useReducedMotion as jest.Mock).mockReturnValue(true);
|
||||
expect(() => render(<HeroParticleField />)).not.toThrow();
|
||||
// 静态降级下仍渲染 canvas(无 rAF 循环,仅一次绘制)
|
||||
expect(screen.getByTestId('hero-particle-field').tagName).toBe('CANVAS');
|
||||
});
|
||||
|
||||
it('bails safely when no 2d context is available (jsdom / SSR guard)', () => {
|
||||
const ctxSpy = jest
|
||||
.spyOn(HTMLCanvasElement.prototype, 'getContext')
|
||||
.mockReturnValue(null as any);
|
||||
expect(() => render(<HeroParticleField />)).not.toThrow();
|
||||
ctxSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user