From Vitest Migration to Passing Tests: A Complete Debugging Journey

Debugging React Notification Tests: A Journey from Timeouts to Success

The Challenge

I encountered a perplexing issue where all tests for a React NotificationManager component were timing out after 10 seconds. The component used Material-UI's Snackbar to display notifications, integrated with Redux for state management. What seemed like a simple component test turned into a deep dive through multiple layers of modern web development.

Initial Symptoms

The test failures presented themselves with timeout errors:

Error: Test timed out in 10000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".

Eight out of nine tests were failing with identical timeout errors, suggesting a systemic issue rather than individual test problems.

Investigation Process

Step 1: Understanding the Component Structure

I first examined the NotificationManager component to understand its architecture:

cat src/components/common/NotificationManager.tsx

This revealed that the component:

  • Used Material-UI's Snackbar component
  • Connected to Redux state via useAppSelector
  • Displayed notifications from a queue (notifications[0])
  • Implemented auto-hide functionality with timers

Step 2: Examining the Test Environment

I investigated the test setup to understand the testing infrastructure:

cat src/test-utils/test-utils.tsx

The custom render utility wrapped components with necessary providers (Redux, Router, Theme), which appeared correct.

Step 3: Debugging with Console Logs

To understand what was happening during test execution, I created a debug version of the tests with extensive logging:

grep -E "(console.log|Mock|Container|Redux|Found)" test-output.log | head -50

This systematic logging revealed crucial information about the component's behavior during testing.

Root Causes Discovered

Issue 1: React Portal Rendering

Material-UI's Snackbar component uses React Portals to render content at the document body level, outside the normal component tree. In the jsdom test environment, this Portal-based rendering caused elements to be unreachable by standard DOM queries.

The Portal system works like a magician's trick - content appears to be in one place but actually renders somewhere else entirely. While this works beautifully in browsers, jsdom's simplified DOM implementation struggled to handle this pattern correctly.

Issue 2: Module Mocking Challenges

My initial attempts to mock the Snackbar component failed because I was mocking the wrong import path. The component imported from @mui/material (a barrel export) rather than @mui/material/Snackbar directly:

grep -n "import.*Snackbar" src/components/common/NotificationManager.tsx
# Output: import { Snackbar, Alert, AlertColor, Slide, SlideProps } from '@mui/material';

Issue 3: Duplicate ID Bug

Through debug logging, I discovered a critical bug in the production code. When adding multiple notifications rapidly, they all received the same timestamp-based ID:

grep -A 10 -B 10 "addNotification" src/store/slices/uiSlice.ts | grep -A 15 "id"

This revealed:

id: Date.now().toString()

When notifications were created within the same millisecond, they shared identical IDs, causing all notifications to be deleted when attempting to remove just one.

Solutions Implemented

Solution 1: Creating a Mock Snackbar

I created a mock that renders inline instead of using Portals:

vi.mock('@mui/material', async () => {
  const actual = await vi.importActual('@mui/material');
  const React = (await import('react')).default;
  
  const MockSnackbar = ({ open, children, onClose, autoHideDuration }) => {
    React.useEffect(() => {
      if (open && autoHideDuration && onClose) {
        const timer = setTimeout(() => {
          onClose(undefined, 'timeout');
        }, autoHideDuration);
        return () => clearTimeout(timer);
      }
    }, [open, autoHideDuration, onClose]);
    
    if (!open) return null;
    
    return React.createElement(
      'div',
      { 'data-testid': 'mock-snackbar', role: 'presentation' },
      children
    );
  };
  
  return {
    ...actual,
    Snackbar: MockSnackbar,
  };
});

Solution 2: Proper Test Synchronization

I wrapped all state updates in act() to ensure React completed all updates before assertions:

await act(async () => {
  store.dispatch(addNotification({
    type: 'success',
    message: 'Test notification',
  }));
  await Promise.resolve();
});

Solution 3: Ensuring Unique IDs

To work around the timestamp-based ID bug, I added small delays between creating notifications:

await new Promise(resolve => setTimeout(resolve, 1));

This ensured each notification received a unique timestamp, preventing the bulk deletion issue.

Key Debugging Commands

Throughout this journey, several CLI commands proved invaluable for understanding the system:

# Examine component imports and structure
grep -n "import.*Snackbar" src/components/common/NotificationManager.tsx

# Investigate Redux action implementations
grep -A 10 -B 10 "addNotification" src/store/slices/uiSlice.ts

# Check how the component selects notifications
grep -B 10 "currentNotification" src/components/common/NotificationManager.tsx

# Run tests with filtered output for debugging
npm test -- --run path/to/test.tsx 2>&1 | grep -E "(console.log|Redux|DOM)"

Lessons Learned

This debugging experience reinforced several important principles:

  1. Test Environment Limitations: The jsdom environment, while powerful, has limitations when dealing with advanced browser features like Portals. Understanding these limitations helps in designing appropriate workarounds.

  2. Systematic Debugging: Rather than making assumptions, I gathered concrete data through logging and examination of the actual behavior. This methodical approach revealed issues I never would have guessed from the error messages alone.

  3. Hidden Production Bugs: What appeared to be a test-specific problem actually exposed a real bug in the production code. The rapid test execution revealed the timestamp-based ID generation flaw that would be nearly impossible to reproduce manually.

  4. Module System Complexity: Modern JavaScript module systems add layers of complexity to testing. Understanding how imports are resolved and how mocking works at the module level is crucial for effective testing.

Results

After implementing these solutions, all tests now pass successfully:

Test Files  1 passed (1)
Tests  9 passed (9)
Duration  288ms (previously timing out at 10,000ms each)

The journey from complete test failure to success not only fixed the immediate problem but also improved my understanding of React testing, discovered a production bug, and created a more robust test suite for the future.

Conclusion

What began as a frustrating series of timeout errors evolved into a masterclass in debugging complex JavaScript applications. By maintaining patience, following systematic debugging practices, and understanding the tools at a deep level, I transformed a failing test suite into a robust validation system that actively prevents bugs from reaching production.

This experience demonstrates that the most challenging debugging sessions often yield the most valuable insights. Every layer of complexity peeled back revealed new understanding about how modern web applications work, making me a more effective developer in the process.


If you enjoyed this article, you can also find it published on LinkedIn and Medium.