- 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
209 lines
6.6 KiB
TypeScript
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();
|
|
});
|
|
}); |