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
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// @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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user