// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import ReactProfiler from '@utils/performance/react_profiler'; describe('ReactProfilerManager', () => { beforeEach(() => { jest.clearAllMocks(); ReactProfiler.clearMetrics(); ReactProfiler.setEnabled(true); }); describe('when profiling is disabled', () => { beforeEach(() => { (global as any).__DEV__ = false; ReactProfiler.setEnabled(false); }); it('should return false for isEnabled', () => { expect(ReactProfiler.isEnabled()).toBe(false); }); it('should not track metrics when disabled', () => { ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100); const metrics = ReactProfiler.getMetrics('TestComponent'); expect(metrics).toHaveLength(0); }); it('should not print summary when disabled', () => { ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100); // Should not throw ReactProfiler.printSummary(); }); }); describe('when profiling is enabled', () => { beforeEach(() => { (global as any).__DEV__ = true; ReactProfiler.setEnabled(true); }); it('should return true for isEnabled', () => { expect(ReactProfiler.isEnabled()).toBe(true); }); describe('onRender', () => { it('should track mount phase metrics', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); const metrics = ReactProfiler.getMetrics('Post'); expect(metrics).toHaveLength(1); expect(metrics[0]).toEqual({ componentName: 'Post', phase: 'mount', actualDuration: 50, baseDuration: 45, startTime: 1000, commitTime: 1050, }); }); it('should track update phase metrics', () => { ReactProfiler.onRender('Channel', 'update', 25, 20, 2000, 2025); const metrics = ReactProfiler.getMetrics('Channel'); expect(metrics).toHaveLength(1); expect(metrics[0]).toEqual({ componentName: 'Channel', phase: 'update', actualDuration: 25, baseDuration: 20, startTime: 2000, commitTime: 2025, }); }); it('should track nested-update phase metrics', () => { ReactProfiler.onRender('User', 'nested-update', 10, 8, 3000, 3010); const metrics = ReactProfiler.getMetrics('User'); expect(metrics).toHaveLength(1); expect(metrics[0].phase).toBe('nested-update'); }); it('should track multiple renders for same component', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Post', 'update', 25, 20, 2000, 2025); ReactProfiler.onRender('Post', 'update', 15, 12, 3000, 3015); const metrics = ReactProfiler.getMetrics('Post'); expect(metrics).toHaveLength(3); expect(metrics[0].phase).toBe('mount'); expect(metrics[1].phase).toBe('update'); expect(metrics[2].phase).toBe('update'); }); it('should track renders for multiple components', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130); ReactProfiler.onRender('User', 'mount', 10, 8, 1200, 1210); expect(ReactProfiler.getMetrics('Post')).toHaveLength(1); expect(ReactProfiler.getMetrics('Channel')).toHaveLength(1); expect(ReactProfiler.getMetrics('User')).toHaveLength(1); }); it('should not log renders under 50ms', () => { ReactProfiler.onRender('FastComponent', 'mount', 30, 25, 1000, 1030); const metrics = ReactProfiler.getMetrics('FastComponent'); expect(metrics).toHaveLength(1); }); it('should log renders over 50ms', () => { ReactProfiler.onRender('SlowComponent', 'mount', 75, 70, 1000, 1075); const metrics = ReactProfiler.getMetrics('SlowComponent'); expect(metrics).toHaveLength(1); expect(metrics[0].actualDuration).toBe(75); }); }); describe('getMetrics', () => { it('should return empty array for component with no metrics', () => { const metrics = ReactProfiler.getMetrics('NonexistentComponent'); expect(metrics).toEqual([]); }); it('should return all metrics for a component', () => { ReactProfiler.onRender('TestComponent', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('TestComponent', 'update', 25, 20, 2000, 2025); const metrics = ReactProfiler.getMetrics('TestComponent'); expect(metrics).toHaveLength(2); }); }); describe('getAllMetrics', () => { it('should return empty map when no metrics tracked', () => { const allMetrics = ReactProfiler.getAllMetrics(); expect(allMetrics.size).toBe(0); }); it('should return all tracked metrics', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130); const allMetrics = ReactProfiler.getAllMetrics(); expect(allMetrics.size).toBe(2); expect(allMetrics.has('Post')).toBe(true); expect(allMetrics.has('Channel')).toBe(true); }); }); describe('printSummary', () => { it('should handle no metrics collected', () => { // Should not throw ReactProfiler.printSummary(); }); it('should print summary for single component', () => { ReactProfiler.onRender('Post', 'mount', 100, 90, 1000, 1100); ReactProfiler.onRender('Post', 'update', 50, 45, 2000, 2050); ReactProfiler.printSummary(); }); it('should print summary for multiple components', () => { ReactProfiler.onRender('Post', 'mount', 100, 90, 1000, 1100); ReactProfiler.onRender('Channel', 'mount', 80, 75, 1100, 1180); ReactProfiler.onRender('User', 'mount', 60, 55, 1200, 1260); ReactProfiler.printSummary(); }); it('should sort components by total actual time', () => { ReactProfiler.onRender('Fast', 'mount', 10, 8, 1000, 1010); ReactProfiler.onRender('Slow', 'mount', 100, 90, 1100, 1200); ReactProfiler.onRender('Medium', 'mount', 50, 45, 1200, 1250); ReactProfiler.printSummary(); }); it('should limit output to top 10 components', () => { // Add 15 components for (let i = 0; i < 15; i++) { ReactProfiler.onRender(`Component${i}`, 'mount', 10 + i, 8 + i, 1000 + (i * 100), 1010 + (i * 100)); } ReactProfiler.printSummary(); }); it('should calculate correct statistics', () => { // Add multiple renders to test statistics ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100); ReactProfiler.onRender('TestComponent', 'update', 50, 45, 2000, 2050); ReactProfiler.onRender('TestComponent', 'update', 25, 20, 3000, 3025); ReactProfiler.printSummary(); }); it('should identify memoization opportunities', () => { // Component with actual duration much higher than base duration ReactProfiler.onRender('UnoptimizedComponent', 'mount', 200, 100, 1000, 1200); ReactProfiler.printSummary(); }); it('should not show memoization warning for well-optimized components', () => { // Component with actual duration close to base duration ReactProfiler.onRender('OptimizedComponent', 'mount', 105, 100, 1000, 1105); ReactProfiler.printSummary(); }); }); describe('clearMetrics', () => { it('should clear all metrics when no component specified', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130); ReactProfiler.clearMetrics(); expect(ReactProfiler.getMetrics('Post')).toEqual([]); expect(ReactProfiler.getMetrics('Channel')).toEqual([]); expect(ReactProfiler.getAllMetrics().size).toBe(0); }); it('should clear metrics for specific component', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130); ReactProfiler.clearMetrics('Post'); expect(ReactProfiler.getMetrics('Post')).toEqual([]); expect(ReactProfiler.getMetrics('Channel')).toHaveLength(1); }); it('should handle clearing nonexistent component', () => { ReactProfiler.clearMetrics('NonexistentComponent'); // Should not throw expect(ReactProfiler.getMetrics('NonexistentComponent')).toEqual([]); }); }); describe('setEnabled', () => { it('should disable profiling when set to false', () => { ReactProfiler.setEnabled(false); ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); expect(ReactProfiler.getMetrics('Post')).toEqual([]); }); it('should enable profiling when set to true', () => { ReactProfiler.setEnabled(false); ReactProfiler.setEnabled(true); ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050); expect(ReactProfiler.getMetrics('Post')).toHaveLength(1); }); }); describe('phase tracking', () => { it('should correctly count mount renders', () => { ReactProfiler.onRender('Component', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Component', 'mount', 55, 50, 2000, 2055); const metrics = ReactProfiler.getMetrics('Component'); const mountCount = metrics.filter((m) => m.phase === 'mount').length; expect(mountCount).toBe(2); }); it('should correctly count update renders', () => { ReactProfiler.onRender('Component', 'mount', 50, 45, 1000, 1050); ReactProfiler.onRender('Component', 'update', 25, 20, 2000, 2025); ReactProfiler.onRender('Component', 'update', 30, 25, 3000, 3030); const metrics = ReactProfiler.getMetrics('Component'); const updateCount = metrics.filter((m) => m.phase === 'update').length; expect(updateCount).toBe(2); }); }); describe('timing data', () => { it('should track actual duration correctly', () => { ReactProfiler.onRender('Post', 'mount', 75.5, 70.2, 1000, 1075.5); const metrics = ReactProfiler.getMetrics('Post'); expect(metrics[0].actualDuration).toBe(75.5); }); it('should track base duration correctly', () => { ReactProfiler.onRender('Post', 'mount', 75.5, 70.2, 1000, 1075.5); const metrics = ReactProfiler.getMetrics('Post'); expect(metrics[0].baseDuration).toBe(70.2); }); it('should track start and commit times correctly', () => { ReactProfiler.onRender('Post', 'mount', 50, 45, 12345, 12395); const metrics = ReactProfiler.getMetrics('Post'); expect(metrics[0].startTime).toBe(12345); expect(metrics[0].commitTime).toBe(12395); }); }); }); });