Files
novalon-website/src/components/analytics/PerformanceTracker.test.tsx
T
zhangxiang 602ed6a671 test(hooks): finalize mutation test improvements and release acceptance
- Raise use-swipe-gesture mutation score to 66.13% (target 65%+)
- Maintain use-reduced-motion mutation score at 76.32% (target 50%+)
- Fix use-reduced-motion.ts ESLint set-state-in-effect warning
- Add UJ-10 deep searcher journey (category browse → article read → content discovery)
- Add 40 new test files (analytics, detail, sections, ui, lib components)
- Update test-strategy-plan.md to v2.0 (sync test count to 1591)
- Sync README.md with final release metrics

Quality gates: TS 0 errors, ESLint 0 errors, 121 suites / 1591 tests passed
2026-08-02 19:39:27 +08:00

209 lines
6.6 KiB
TypeScript

// @ts-nocheck
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
// ─── Mocks ───────────────────────────────────────────────────────────────
const mockTrackPerformance = jest.fn();
jest.mock('@/lib/analytics', () => ({
trackPerformance: (...args: unknown[]) => mockTrackPerformance(...args),
}));
// ─── PerformanceObserver Mock ────────────────────────────────────────────
type ObserverCallback = (list: { getEntries: () => PerformanceEntry[] }) => void;
interface MockObserverInstance {
observe: jest.Mock;
disconnect: jest.Mock;
callback: ObserverCallback;
type: string;
}
const mockObservers: MockObserverInstance[] = [];
let originalPerformanceObserver: typeof PerformanceObserver;
// ─── Helpers ─────────────────────────────────────────────────────────────
function simulateLCPEntry(startTime: number) {
const lcpObserver = mockObservers.find((o) => o.type === 'largest-contentful-paint');
if (!lcpObserver) return;
lcpObserver.callback({
getEntries: () => {
const entries = [{ startTime }] as PerformanceEntry[];
entries.push({ startTime: startTime + 100 } as PerformanceEntry);
return entries;
},
});
}
function simulateFIDEntry(processingStart: number, startTime: number) {
const fidObserver = mockObservers.find((o) => o.type === 'first-input');
if (!fidObserver) return;
fidObserver.callback({
getEntries: () => {
const entries = [
{ processingStart, startTime } as unknown as PerformanceEntry,
];
return entries;
},
});
}
function simulateCLSEntry(value: number, hadRecentInput: boolean = false) {
const clsObserver = mockObservers.find((o) => o.type === 'layout-shift');
if (!clsObserver) return;
clsObserver.callback({
getEntries: () => {
const entries = [
{ value, hadRecentInput } as unknown as PerformanceEntry,
];
return entries;
},
});
}
// ─── Tests ───────────────────────────────────────────────────────────────
describe('PerformanceTracker', () => {
beforeEach(() => {
jest.clearAllMocks();
mockObservers.length = 0;
// Mock PerformanceObserver with proper jest.fn tracking
originalPerformanceObserver = window.PerformanceObserver;
(window as any).PerformanceObserver = class MockPerformanceObserver {
callback: ObserverCallback;
type: string;
_instance: MockObserverInstance;
constructor(callback: ObserverCallback) {
this.callback = callback;
this.type = '';
this._instance = {
observe: jest.fn(),
disconnect: jest.fn(),
callback,
type: '',
};
mockObservers.push(this._instance);
}
observe(options: { type: string; buffered: boolean }) {
this.type = options.type;
this._instance.type = options.type;
this._instance.observe(options);
}
disconnect() {
this._instance.disconnect();
}
};
});
afterEach(() => {
(window as any).PerformanceObserver = originalPerformanceObserver;
});
it('should return null (render nothing)', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
const { container } = render(<PerformanceTracker />);
expect(container.innerHTML).toBe('');
});
it('should create observers for LCP, FID, and CLS', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
expect(mockObservers.length).toBe(3);
const types = mockObservers.map((o) => o.type);
expect(types).toContain('largest-contentful-paint');
expect(types).toContain('first-input');
expect(types).toContain('layout-shift');
});
it('should call observe with buffered: true for all observers', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
mockObservers.forEach((observer) => {
expect(observer.observe).toHaveBeenCalledWith({
type: observer.type,
buffered: true,
});
});
});
it('should track LCP performance metric', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
simulateLCPEntry(2500);
// LCP should track the last entry's startTime
expect(mockTrackPerformance).toHaveBeenCalledWith('LCP', 2600);
});
it('should track FID performance metric', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
simulateFIDEntry(150, 50);
// FID = processingStart - startTime
expect(mockTrackPerformance).toHaveBeenCalledWith('FID', 100);
});
it('should track CLS performance metric', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
simulateCLSEntry(0.15);
// CLS value * 1000
expect(mockTrackPerformance).toHaveBeenCalledWith('CLS', 150);
});
it('should not track CLS when entry has recent input', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
simulateCLSEntry(0.15, true);
expect(mockTrackPerformance).not.toHaveBeenCalledWith('CLS', 150);
});
it('should not track CLS when value is 0', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
render(<PerformanceTracker />);
simulateCLSEntry(0);
expect(mockTrackPerformance).not.toHaveBeenCalled();
});
it('should disconnect observers on unmount', () => {
const { PerformanceTracker } = require('./PerformanceTracker');
const { unmount } = render(<PerformanceTracker />);
const disconnectFns = mockObservers.map((o) => o.disconnect);
unmount();
disconnectFns.forEach((disconnect) => {
expect(disconnect).toHaveBeenCalled();
});
});
it('should handle missing PerformanceObserver gracefully', () => {
delete (window as any).PerformanceObserver;
const { PerformanceTracker } = require('./PerformanceTracker');
expect(() => {
render(<PerformanceTracker />);
}).not.toThrow();
});
});