The Snapshot API provides methods for creating render baselines and measuring render deltas. This is the primary tool for testing React optimizations like `React.memo`, `useCallback`, and ensuring single renders per user action. ## Table of Contents - [Overview](#overview) - [Core Methods](#core-methods) - [snapshot()](#snapshot) - [getRendersSinceSnapshot()](#getrenderssincenapshot) - [Sync Matchers](#sync-matchers) - [toHaveRerenderedOnce()](#tohavererenderedonce) - [toNotHaveRerendered()](#tonothavererendered) - [toHaveRerendered()](#tohavererendered) (v1.11.0) - [toHaveRerendered(n)](#tohaverenderedn) (v1.11.0) - [Async Matchers](#async-matchers) (v1.11.0) - [toEventuallyRerender()](#toeventuallyrerender) - [toEventuallyRerenderTimes(n)](#toeventuallyrerendertimesn) - [Use Cases](#use-cases) - [Examples](#examples) --- ## Overview **Since:** v1.10.0 | **Extended:** v1.11.0 The Snapshot API enables delta-based render testing. Instead of counting total renders, you create a baseline (snapshot) and then measure how many renders occurred after that point. **Key concepts:** 1. **Snapshot** - Creates a baseline at the current render count 2. **Delta** - The number of renders since the last snapshot 3. **Matchers** - Assertions that use the delta for testing **When to use:** - Testing `React.memo` effectiveness - Verifying `useCallback` stability - Ensuring single render per user action - Performance budget testing --- ## Core Methods ### `snapshot()` Creates a baseline for render counting. All subsequent `getRendersSinceSnapshot()` calls return the number of renders since this baseline. **Signature:** ```typescript function snapshot(): void ``` **Example:** ```typescript const ProfiledComponent = withProfiler(MyComponent); render(); // Before snapshot: includes initial mount expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(1); ProfiledComponent.snapshot(); // Create baseline // Immediately after snapshot: delta is 0 expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(0); ``` --- ### `getRendersSinceSnapshot()` Returns the number of renders since the last `snapshot()` call. **Signature:** ```typescript function getRendersSinceSnapshot(): number ``` **Example:** ```typescript const ProfiledComponent = withProfiler(Counter); const { rerender } = render(); ProfiledComponent.snapshot(); // Baseline rerender(); expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(1); rerender(); expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(2); // Take new snapshot to reset baseline ProfiledComponent.snapshot(); expect(ProfiledComponent.getRendersSinceSnapshot()).toBe(0); ``` --- ## Sync Matchers ### `toHaveRerenderedOnce()` **Since:** v1.10.0 Asserts that exactly one rerender occurred since the last `snapshot()` call. Perfect for verifying single render per user action. **Example:** ```typescript const ProfiledComponent = withProfiler(Counter); render(); ProfiledComponent.snapshot(); fireEvent.click(screen.getByText('Increment')); // Single state change = single rerender expect(ProfiledComponent).toHaveRerenderedOnce(); ``` **Error Message:** ``` Expected component to rerender once after snapshot, but it rerendered 3 times #1 [mount phase] #2 [update phase] <- snapshot here #3 [update phase] #4 [update phase] #5 [update phase] ``` --- ### `toNotHaveRerendered()` **Since:** v1.10.0 Asserts that no rerenders occurred since the last `snapshot()` call. Perfect for testing `React.memo` effectiveness and optimization validations. **Example:** ```typescript const ProfiledComponent = withProfiler(MemoizedComponent); const { rerender } = render(); ProfiledComponent.snapshot(); // Change unrelated prop - memo should prevent rerender rerender(); expect(ProfiledComponent).toNotHaveRerendered(); ``` **Error Message:** ``` Expected component not to rerender after snapshot, but it rerendered 1 time #1 [mount phase] #2 [update phase] ``` --- ### `toHaveRerendered()` **Since:** v1.11.0 Asserts that component rerendered at least once after snapshot. Use this when you want to verify that an action triggered a rerender, regardless of how many times. **Signature:** ```typescript expect(component).toHaveRerendered(); expect(component).not.toHaveRerendered(); ``` **Example:** ```typescript const ProfiledComponent = withProfiler(Counter); render(); ProfiledComponent.snapshot(); fireEvent.click(screen.getByText('Increment')); // Verify rerender happened (at least 1) expect(ProfiledComponent).toHaveRerendered(); ``` **With `.not` modifier:** ```typescript ProfiledComponent.snapshot(); // No action taken - verify no rerender expect(ProfiledComponent).not.toHaveRerendered(); ``` **Error Message (pass case with `.not`):** ``` Expected component not to rerender after snapshot, but it rerendered 1 time ``` **Error Message (fail case):** ``` Expected component to rerender after snapshot, but it did not ``` --- ### `toHaveRerendered(n)` **Since:** v1.11.0 Asserts that component rerendered exactly `n` times after snapshot. Use this to verify exact render counts for detecting double-render bugs or validating performance budgets. **Signature:** ```typescript expect(component).toHaveRerendered(n: number); ``` **Parameters:** - `n: number` - Expected exact number of rerenders (non-negative integer) **Example:** ```typescript const ProfiledComponent = withProfiler(Counter); render(); ProfiledComponent.snapshot(); // 3 clicks = 3 state updates = 3 rerenders fireEvent.click(screen.getByText('Increment')); fireEvent.click(screen.getByText('Increment')); fireEvent.click(screen.getByText('Increment')); expect(ProfiledComponent).toHaveRerendered(3); ``` **Detecting double-render bugs:** ```typescript ProfiledComponent.snapshot(); fireEvent.click(screen.getByText('Submit')); // If this fails with n > 1, you have a double-render bug! expect(ProfiledComponent).toHaveRerendered(1); ``` **Zero rerenders:** ```typescript ProfiledComponent.snapshot(); // Alternative to toNotHaveRerendered() expect(ProfiledComponent).toHaveRerendered(0); ``` **With `.not` modifier:** ```typescript ProfiledComponent.snapshot(); fireEvent.click(screen.getByText('Increment')); fireEvent.click(screen.getByText('Increment')); // 2 rerenders - NOT 1, NOT 3 expect(ProfiledComponent).not.toHaveRerendered(1); expect(ProfiledComponent).not.toHaveRerendered(3); expect(ProfiledComponent).toHaveRerendered(2); ``` **Error Message:** ``` Expected component to rerender 3 times after snapshot, but it rerendered 1 time #1 [mount phase] #2 [update phase] 💡 Tip: Use Component.getRenderHistory() to inspect all render details ``` **Validation:** The matcher validates the `n` parameter and throws if invalid: ```typescript expect(ProfiledComponent).toHaveRerendered(-1); // Error: Must be non-negative expect(ProfiledComponent).toHaveRerendered(1.5); // Error: Must be integer expect(ProfiledComponent).toHaveRerendered("3"); // Error: Must be number ``` --- ## Async Matchers **Since:** v1.11.0 Async matchers wait for rerenders to occur. They use event-based approach with `onRender()` for instant notification - no polling overhead. ### `toEventuallyRerender()` Waits for at least one rerender after snapshot within the timeout period. Resolves immediately if component already rerendered. **Signature:** ```typescript await expect(component).toEventuallyRerender(options?: WaitOptions); ``` **Parameters:** - `options.timeout?: number` - Maximum wait time in ms (default: 1000) **Example:** ```typescript const ProfiledComponent = withProfiler(AsyncCounter); render(); ProfiledComponent.snapshot(); // Trigger async action (setTimeout, fetch, etc.) fireEvent.click(screen.getByText('Async Increment')); // Wait for rerender (default 1000ms timeout) await expect(ProfiledComponent).toEventuallyRerender(); // Verify state updated expect(screen.getByTestId('count')).toHaveTextContent('1'); ``` **Custom timeout:** ```typescript await expect(ProfiledComponent).toEventuallyRerender({ timeout: 5000 }); ``` **Immediate resolution:** If component already rerendered before `await`, resolves immediately: ```typescript ProfiledComponent.snapshot(); fireEvent.click(screen.getByText('Sync Increment')); // Sync update // Resolves instantly - no waiting await expect(ProfiledComponent).toEventuallyRerender(); ``` **With `.not` modifier:** ```typescript ProfiledComponent.snapshot(); // Assert component does NOT rerender within timeout await expect(ProfiledComponent).not.toEventuallyRerender({ timeout: 100 }); ``` **Error Message (timeout):** ``` Expected component to rerender after snapshot within 1000ms, but it did not #1 [mount phase] 💡 Tip: Use Component.getRenderHistory() to inspect all render details ``` --- ### `toEventuallyRerenderTimes(n)` Waits for exact number of rerenders after snapshot. Fails early if count exceeded (doesn't wait for timeout). **Signature:** ```typescript await expect(component).toEventuallyRerenderTimes(n: number, options?: WaitOptions); ``` **Parameters:** - `n: number` - Expected exact number of rerenders - `options.timeout?: number` - Maximum wait time in ms (default: 1000) **Example:** ```typescript const ProfiledComponent = withProfiler(AsyncCounter); render(); ProfiledComponent.snapshot(); // Trigger action that causes 3 sequential async updates fireEvent.click(screen.getByText('Async +3')); // Wait for exactly 3 rerenders await expect(ProfiledComponent).toEventuallyRerenderTimes(3, { timeout: 500 }); expect(screen.getByTestId('count')).toHaveTextContent('3'); ``` **Immediate resolution when count already met:** ```typescript ProfiledComponent.snapshot(); // Sync updates fireEvent.click(screen.getByText('+1')); fireEvent.click(screen.getByText('+1')); // Resolves instantly - count already met await expect(ProfiledComponent).toEventuallyRerenderTimes(2); ``` **Early failure when count exceeded:** ```typescript ProfiledComponent.snapshot(); // Create 5 rerenders for (let i = 0; i < 5; i++) { fireEvent.click(screen.getByText('+1')); } // Fails IMMEDIATELY - doesn't wait for timeout await expect(ProfiledComponent).toEventuallyRerenderTimes(2); // Error: Expected 2, but already got 5 (exceeded) ``` **Zero rerenders:** ```typescript ProfiledComponent.snapshot(); // Assert no rerenders (immediate success if count is 0) await expect(ProfiledComponent).toEventuallyRerenderTimes(0); ``` **Error Message (timeout):** ``` Expected component to rerender 3 times after snapshot within 1000ms, but got 1 time #1 [mount phase] #2 [update phase] 💡 Tip: Use Component.getRenderHistory() to inspect all render details ``` **Error Message (exceeded):** ``` Expected component to rerender 2 times after snapshot, but already got 5 times (exceeded) #1 [mount phase] #2 [update phase] #3 [update phase] #4 [update phase] #5 [update phase] #6 [update phase] ``` --- ## Use Cases ### 1. Testing Single Render Per Action ```typescript it('should render once per click', () => { const ProfiledCounter = withProfiler(Counter); render(); ProfiledCounter.snapshot(); fireEvent.click(screen.getByText('Increment')); expect(ProfiledCounter).toHaveRerenderedOnce(); }); ``` ### 2. Testing React.memo Effectiveness ```typescript it('should not rerender when unrelated props change', () => { const ProfiledList = withProfiler(MemoizedList); const items = [1, 2, 3]; const { rerender } = render(); ProfiledList.snapshot(); rerender(); // memo prevents rerender since items didn't change expect(ProfiledList).toNotHaveRerendered(); }); ``` ### 3. Testing useCallback Stability ```typescript it('should not rerender child when parent state changes', () => { // Child receives stable callback via useCallback ProfiledChild.snapshot(); fireEvent.click(screen.getByText('Update Parent')); expect(ProfiledChild).toNotHaveRerendered(); }); ``` ### 4. Performance Budget Testing ```typescript it('should stay within render budget', () => { const ProfiledDashboard = withProfiler(Dashboard); render(); ProfiledDashboard.snapshot(); performComplexOperation(); // Exact count check expect(ProfiledDashboard).toHaveRerendered(3); // Or flexible check const delta = ProfiledDashboard.getRendersSinceSnapshot(); expect(delta).toBeLessThanOrEqual(5); }); ``` ### 5. Testing Async State Updates ```typescript it('should handle async data loading', async () => { const ProfiledDataFetcher = withProfiler(DataFetcher); render(); ProfiledDataFetcher.snapshot(); // Trigger data fetch fireEvent.click(screen.getByText('Load Data')); // Wait for loading + success state updates await expect(ProfiledDataFetcher).toEventuallyRerenderTimes(2, { timeout: 3000, }); expect(screen.getByTestId('data')).toBeInTheDocument(); }); ``` ### 6. Iterative Optimization Testing ```typescript it('should test multiple optimization scenarios', () => { const { rerender } = render(); // Scenario 1: Same data reference ProfiledComponent.snapshot(); rerender(); expect(ProfiledComponent).toNotHaveRerendered(); // Scenario 2: New data reference ProfiledComponent.snapshot(); rerender(); expect(ProfiledComponent).toHaveRerendered(1); // Scenario 3: Multiple updates ProfiledComponent.snapshot(); rerender(); rerender(); rerender(); expect(ProfiledComponent).toHaveRerendered(3); }); ``` --- ## Examples See the example files for comprehensive usage patterns: - **[SnapshotBasics.test.tsx](../../examples/snapshot/SnapshotBasics.test.tsx)** - Core Snapshot API usage (v1.10.0) - **[OptimizationTesting.test.tsx](../../examples/snapshot/OptimizationTesting.test.tsx)** - Testing React optimizations - **[ExtendedMatchers.test.tsx](../../examples/snapshot/ExtendedMatchers.test.tsx)** - Extended matchers (v1.11.0) --- ## See Also - [API Reference](API-Reference) - Full API documentation - [Best Practices](Best-Practices) - Testing recommendations - [Hook Profiling](Hook-Profiling) - Testing custom hooks