# Workflow: Testing Changes

## Overview

This workflow demonstrates how to comprehensively test changes in the mixpanel-react-native library, covering both native and JavaScript implementations, edge cases, and integration scenarios.

## 🧪 Testing Strategy Overview

The library uses a multi-layered testing approach:
- **Unit Tests**: Individual module testing with mocks
- **Integration Tests**: Cross-module functionality  
- **Manual Testing**: Sample app verification
- **Platform Testing**: Native iOS/Android validation

## 🚀 Quick Test Commands

```bash
# Run all tests
npm test

# Run specific test file
npm test -- __tests__/core.test.js

# Run tests in watch mode (during development)
npm test -- --watch

# Run tests with coverage
npm test -- --coverage
```

## 📝 Unit Testing Workflow

### Step 1: Set Up Test Environment

```javascript
// __tests__/new-feature.test.js
import { jest } from "@jest/globals";

// Import required mocks (automatically loaded from jest_setup.js)
const { MixpanelQueueManager } = require("mixpanel-react-native/javascript/mixpanel-queue");
const { MixpanelNetwork } = require("mixpanel-react-native/javascript/mixpanel-network");

describe('New Feature Tests', () => {
  beforeEach(() => {
    // Reset all mocks before each test
    jest.clearAllMocks();
    
    // Reset any internal state
    MixpanelQueueManager._queues = {};
  });

  afterEach(() => {
    // Clean up any timers or async operations
    jest.clearAllTimers();
  });
});
```

### Step 2: Test Input Validation

```javascript
describe('Input Validation', () => {
  let mixpanel;

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

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

    // Test null parameter
    expect(() => {
      mixpanel.track(null, {});
    }).toThrow('eventName is not a valid string');

    // Test undefined parameter
    expect(() => {
      mixpanel.track(undefined, {});
    }).toThrow('eventName is not a valid string');

    // Test whitespace-only string
    expect(() => {
      mixpanel.track('   ', {});
    }).toThrow('eventName is not a valid string');
  });

  it('should validate object parameters', () => {
    // Test non-object properties
    expect(() => {
      mixpanel.track('event', 'invalid');
    }).toThrow('properties is not a valid json object');

    // Test function properties (should fail)
    expect(() => {
      mixpanel.track('event', { callback: () => {} });
    }).toThrow();

    // Test valid object (should not throw)
    expect(() => {
      mixpanel.track('event', { prop: 'value' });
    }).not.toThrow();

    // Test null properties (should be allowed)
    expect(() => {
      mixpanel.track('event', null);
    }).not.toThrow();
  });
});
```

### Step 3: Test Native Mode Implementation

```javascript
describe('Native Mode', () => {
  let mixpanel;

  beforeEach(() => {
    // Ensure native mode is active
    mixpanel = new Mixpanel('test-token', true, true);
  });

  it('should call native implementation for tracking', () => {
    const eventName = 'Test Event';
    const properties = { testProp: 'testValue' };

    mixpanel.track(eventName, properties);

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

  it('should handle native implementation promises', async () => {
    // Mock native method to return promise
    MixpanelReactNative.identify.mockResolvedValue();

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

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

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

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

### Step 4: Test JavaScript Mode Implementation

```javascript
describe('JavaScript Mode', () => {
  let mixpanel;

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

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

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

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

    // Verify event was added to queue
    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();
  });
});
```

### Step 5: Test Error Handling

```javascript
describe('Error Handling', () => {
  it('should handle storage failures gracefully', async () => {
    // Mock storage failure
    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);
  });
});
```

## 🔗 Integration Testing Workflow

### Step 1: End-to-End Flow Testing

```javascript
// __tests__/integration/complete-flow.test.js
describe('Complete Tracking Flow', () => {
  let mixpanel;

  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),
          // Session metadata
          $mp_metadata: expect.objectContaining({
            $mp_session_id: expect.any(String),
            $mp_event_id: 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
    );
  });
});
```

### Step 2: Multi-Token Testing

```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' })
      })
    );
  });
});
```

## 📱 Manual Testing with Sample Apps

### Step 1: Test in SimpleMixpanel Sample

```bash
# Navigate to sample app
cd Samples/SimpleMixpanel

# Install dependencies
npm install

# For iOS testing
cd ios && pod install && cd ..
npx react-native run-ios

# For Android testing  
npx react-native run-android
```

### Step 2: Verify Native Integration

```javascript
// Test in sample app
const testNativeIntegration = () => {
  // Test basic tracking
  mixpanel.track('Sample Test Event', {
    test_property: 'sample_value',
    timestamp: Date.now()
  });
  
  // Test identity
  mixpanel.identify('sample_user_' + Date.now());
  
  // Test super properties
  mixpanel.registerSuperProperties({
    app_version: '1.0.0',
    test_mode: true
  });
  
  // Test People Analytics
  mixpanel.getPeople().set('name', 'Sample User');
  
  console.log('Manual test completed');
};
```

### Step 3: Test JavaScript Fallback

```javascript
// Force JavaScript mode for testing
const testJavaScriptMode = async () => {
  const mixpanel = new Mixpanel('test-token', true, false);
  await mixpanel.init();
  
  // Same tests as native mode
  mixpanel.track('JS Mode Test', { mode: 'javascript' });
  
  // Verify console logs show JavaScript mode
  console.log('Check logs for JavaScript mode indicators');
};
```

## 🔍 Debugging Test Issues

### Common Test Debugging Steps

```javascript
describe('Debug Tests', () => {
  it('should provide debugging information', () => {
    // Enable verbose logging in tests
    console.log('Current mock state:', {
      nativeModuleCalls: MixpanelReactNative.track.mock.calls,
      queueManagerCalls: MixpanelQueueManager.enqueue.mock.calls,
      asyncStorageCalls: AsyncStorage.setItem.mock.calls
    });
    
    // Test with debug data
    mixpanel.track('Debug Event', { debug: true });
    
    // Verify what actually happened
    expect(MixpanelReactNative.track).toHaveBeenCalledTimes(1);
    expect(MixpanelReactNative.track.mock.calls[0]).toEqual([
      'test-token',
      'Debug Event',
      expect.objectContaining({ debug: true })
    ]);
  });
});
```

### Mock Verification

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

## ✅ Testing Checklist

### Unit Testing
- [ ] Input validation for all parameters
- [ ] Error handling for invalid inputs
- [ ] Native mode implementation calls
- [ ] JavaScript mode implementation logic
- [ ] Opt-out behavior respect
- [ ] Promise handling (resolve/reject)
- [ ] Mock verification and reset

### Integration Testing  
- [ ] End-to-end tracking flow
- [ ] Queue management and processing
- [ ] Network request handling
- [ ] Multi-token isolation
- [ ] Storage persistence
- [ ] Error recovery scenarios

### Manual Testing
- [ ] Sample app functionality (iOS)
- [ ] Sample app functionality (Android)
- [ ] Native module integration
- [ ] JavaScript fallback mode
- [ ] Console logging verification
- [ ] Performance under load

### Platform Testing
- [ ] iOS pod install success
- [ ] Android gradle build success
- [ ] React Native autolinking
- [ ] TypeScript compilation
- [ ] Expo compatibility

### Edge Case Testing
- [ ] Storage failures
- [ ] Network failures
- [ ] Invalid JSON in storage
- [ ] Memory pressure scenarios
- [ ] App backgrounding/foregrounding
- [ ] Multiple simultaneous instances

## 🚀 Performance Testing

### Load Testing Pattern

```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);
  });
});
```

This comprehensive testing workflow ensures all changes maintain the library's reliability and compatibility across both native and JavaScript implementations.