---
description: Comprehensive testing patterns for dual-implementation architecture
globs: ["__tests__/**/*.js", "**/*.test.js", "**/*jest*"]
alwaysApply: false
---

# Testing Patterns

## Test Structure (MANDATORY)
ALWAYS test both native and JavaScript implementation modes:

```javascript
describe('Feature Name', () => {
  let mixpanel;
  
  beforeEach(() => {
    jest.clearAllMocks();
    // Reset internal state
    MixpanelQueueManager._queues = {};
  });

  afterEach(() => {
    jest.clearAllTimers();
  });

  describe('Input Validation', () => {
    beforeEach(() => {
      mixpanel = new Mixpanel('test-token', true);
    });

    it('should validate required parameters', () => {
      expect(() => {
        mixpanel.track('', {});
      }).toThrow('eventName is not a valid string');
    });
  });

  describe('Native Mode', () => {
    beforeEach(() => {
      // Force native mode
      mixpanel = new Mixpanel('test-token', true, true);
    });

    it('should call native implementation', () => {
      mixpanel.track('Event', {prop: 'value'});
      
      expect(MixpanelReactNative.track).toHaveBeenCalledWith(
        'test-token',
        'Event',
        expect.objectContaining({prop: 'value'})
      );
    });
  });

  describe('JavaScript Mode', () => {
    beforeEach(() => {
      // Force JavaScript mode
      mixpanel = new Mixpanel('test-token', true, false);
    });

    it('should use JavaScript implementation', () => {
      expect(mixpanel.mixpanelImpl).toBeInstanceOf(MixpanelMain);
      expect(mixpanel.mixpanelImpl).not.toBe(MixpanelReactNative);
    });
  });
});
```

## Input Validation Testing (REQUIRED)
Test ALL parameter combinations for each public method:

```javascript
describe('Input Validation', () => {
  const invalidStrings = ['', null, undefined, '   ', 123, {}, []];
  const invalidObjects = ['string', 123, true, () => {}, Symbol('test')];

  invalidStrings.forEach((invalid) => {
    it(`should reject invalid string: ${JSON.stringify(invalid)}`, () => {
      expect(() => {
        mixpanel.track(invalid, {});
      }).toThrow('eventName is not a valid string');
    });
  });

  invalidObjects.forEach((invalid) => {
    it(`should reject invalid object: ${typeof invalid}`, () => {
      expect(() => {
        mixpanel.track('event', invalid);
      }).toThrow('properties is not a valid json object');
    });
  });

  it('should allow null properties', () => {
    expect(() => {
      mixpanel.track('event', null);
    }).not.toThrow();
  });
});
```

## Native Mode Testing
Test native bridge calls and error handling:

```javascript
describe('Native Mode', () => {
  it('should handle native implementation promises', async () => {
    MixpanelReactNative.identify.mockResolvedValue();

    await expect(
      mixpanel.identify('user123')
    ).resolves.toBeUndefined();

    expect(MixpanelReactNative.identify).toHaveBeenCalledWith(
      'test-token',
      'user123'
    );
  });

  it('should handle native implementation errors', async () => {
    const error = new Error('Native error');
    MixpanelReactNative.identify.mockRejectedValue(error);

    await expect(
      mixpanel.identify('user123')
    ).rejects.toThrow('Native error');
  });

  it('should include metadata in native calls', () => {
    const eventName = 'Test Event';
    const properties = { testProp: 'testValue' };

    mixpanel.track(eventName, properties);

    expect(MixpanelReactNative.track).toHaveBeenCalledWith(
      'test-token',
      eventName,
      expect.objectContaining({
        ...properties,
        mp_lib: 'react-native',
        $lib_version: expect.any(String)
      })
    );
  });
});
```

## JavaScript Mode Testing
Test queue management and async operations:

```javascript
describe('JavaScript Mode', () => {
  it('should queue events for batch processing', async () => {
    const eventData = {
      event: 'Test Event',
      properties: { prop: 'value' }
    };

    mixpanel.track(eventData.event, eventData.properties);

    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
      'test-token',
      MixpanelType.EVENTS,
      expect.objectContaining({
        event: eventData.event,
        properties: expect.objectContaining(eventData.properties)
      })
    );
  });

  it('should respect opt-out status', async () => {
    // Mock opt-out status
    const mockPersistent = {
      getOptedOut: jest.fn().mockReturnValue(true)
    };
    MixpanelPersistent.getInstance.mockReturnValue(mockPersistent);

    mixpanel.track('Test Event', {});

    // Should not queue event when opted out
    expect(MixpanelQueueManager.enqueue).not.toHaveBeenCalled();
  });

  it('should enrich events with metadata', async () => {
    await mixpanel.identify('user123');
    await mixpanel.registerSuperProperties({ segment: 'premium' });

    mixpanel.track('Purchase', { amount: 99.99 });

    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
      'test-token',
      MixpanelType.EVENTS,
      expect.objectContaining({
        event: 'Purchase',
        properties: expect.objectContaining({
          // Event properties
          amount: 99.99,
          // Super properties
          segment: 'premium',
          // Identity properties
          distinct_id: 'user123',
          // Metadata
          mp_lib: 'react-native',
          $lib_version: expect.any(String),
          // Session metadata
          $mp_metadata: expect.objectContaining({
            $mp_session_id: expect.any(String),
            $mp_event_id: expect.any(String)
          })
        })
      })
    );
  });
});
```

## Error Handling Testing
Test graceful failure scenarios:

```javascript
describe('Error Handling', () => {
  it('should handle storage failures gracefully', async () => {
    const storageError = new Error('Storage unavailable');
    AsyncStorage.setItem.mockRejectedValue(storageError);

    // Should not throw error
    await expect(
      mixpanel.registerSuperProperties({ prop: 'value' })
    ).resolves.toBeUndefined();

    // Should log error
    expect(console.error).toHaveBeenCalledWith(
      expect.stringContaining('error setting item in storage')
    );
  });

  it('should handle network failures with retry', async () => {
    // Mock network failure then success
    const networkError = new Error('Network timeout');
    global.fetch
      .mockRejectedValueOnce(networkError)
      .mockResolvedValueOnce({
        status: 200,
        json: () => Promise.resolve(1)
      });

    await MixpanelNetwork.sendRequest({
      token: 'test-token',
      endpoint: '/track/',
      data: [{ event: 'test' }],
      serverURL: 'https://api.mixpanel.com',
      useIPAddressForGeoLocation: true
    });

    // Should retry on failure
    expect(global.fetch).toHaveBeenCalledTimes(2);
  });

  it('should handle corrupted storage data', async () => {
    // Mock corrupted JSON in storage
    AsyncStorage.getItem.mockResolvedValue('invalid-json{');

    const superProps = await mixpanel.getSuperProperties();

    // Should return empty object as fallback
    expect(superProps).toEqual({});
    
    // Should log error
    expect(console.error).toHaveBeenCalledWith(
      expect.stringContaining('Failed to parse')
    );
  });
});
```

## Integration Testing
Test complete workflows end-to-end:

```javascript
describe('Complete Tracking Flow', () => {
  beforeEach(async () => {
    mixpanel = new Mixpanel('test-token', true, false); // JavaScript mode
    await mixpanel.init();
  });

  it('should track event through complete pipeline', async () => {
    // Set up user identity
    await mixpanel.identify('user123');
    
    // Add super properties
    await mixpanel.registerSuperProperties({ 
      user_segment: 'premium' 
    });

    // Track event
    mixpanel.track('Purchase', { 
      amount: 99.99, 
      currency: 'USD' 
    });

    // Verify complete event structure
    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
      'test-token',
      MixpanelType.EVENTS,
      expect.objectContaining({
        event: 'Purchase',
        properties: expect.objectContaining({
          // Event properties
          amount: 99.99,
          currency: 'USD',
          // Super properties
          user_segment: 'premium',
          // Identity properties
          distinct_id: 'user123',
          // Metadata
          mp_lib: 'react-native',
          $lib_version: expect.any(String)
        })
      })
    );
  });

  it('should handle queue processing and network requests', async () => {
    // Add multiple events to queue
    for (let i = 0; i < 5; i++) {
      mixpanel.track(`Event ${i}`, { index: i });
    }

    // Mock successful network response
    global.fetch.mockResolvedValue({
      status: 200,
      json: () => Promise.resolve(1)
    });

    // Trigger queue processing
    await mixpanel.flush();

    // Verify network request was made
    expect(global.fetch).toHaveBeenCalledWith(
      expect.stringContaining('https://api.mixpanel.com/track/'),
      expect.objectContaining({
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: expect.stringContaining('data=')
      })
    );

    // Verify queue was cleared after successful send
    expect(MixpanelQueueManager.spliceQueue).toHaveBeenCalledWith(
      'test-token',
      MixpanelType.EVENTS,
      0,
      5 // All 5 events should be removed
    );
  });
});
```

## Multi-Token Testing
Test token isolation:

```javascript
describe('Multi-Token Support', () => {
  it('should isolate data between different tokens', async () => {
    const mixpanel1 = new Mixpanel('token1', true, false);
    const mixpanel2 = new Mixpanel('token2', true, false);

    await mixpanel1.init();
    await mixpanel2.init();

    // Set different properties for each token
    await mixpanel1.registerSuperProperties({ source: 'app1' });
    await mixpanel2.registerSuperProperties({ source: 'app2' });

    // Track events
    mixpanel1.track('Event1', {});
    mixpanel2.track('Event2', {});

    // Verify token isolation in queue calls
    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
      'token1',
      MixpanelType.EVENTS,
      expect.objectContaining({
        properties: expect.objectContaining({ source: 'app1' })
      })
    );

    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledWith(
      'token2', 
      MixpanelType.EVENTS,
      expect.objectContaining({
        properties: expect.objectContaining({ source: 'app2' })
      })
    );
  });
});
```

## Performance Testing
Test under load conditions:

```javascript
describe('Performance Tests', () => {
  it('should handle high-volume event tracking', async () => {
    const startTime = Date.now();
    
    // Track 1000 events
    for (let i = 0; i < 1000; i++) {
      mixpanel.track(`Event ${i}`, { index: i });
    }
    
    const endTime = Date.now();
    const duration = endTime - startTime;
    
    // Should complete within reasonable time
    expect(duration).toBeLessThan(5000); // 5 seconds
    
    // Verify all events were queued
    expect(MixpanelQueueManager.enqueue).toHaveBeenCalledTimes(1000);
  });
});
```

## Debug Utilities for Tests
```javascript
// Debug helper for test failures
const debugTestState = () => {
  console.log('Current mock state:', {
    nativeModuleCalls: MixpanelReactNative.track.mock.calls,
    queueManagerCalls: MixpanelQueueManager.enqueue.mock.calls,
    asyncStorageCalls: AsyncStorage.setItem.mock.calls
  });
};

// Verify mocks are working correctly
beforeEach(() => {
  expect(jest.isMockFunction(MixpanelReactNative.track)).toBe(true);
  expect(jest.isMockFunction(AsyncStorage.getItem)).toBe(true);
  expect(jest.isMockFunction(global.fetch)).toBe(true);
});
```