Merge pull request #1541 from selfxyz/release/staging-2026-01-02

Release to Staging - 2026-01-02
This commit is contained in:
Justin Hernandez
2026-01-03 15:05:41 -08:00
committed by GitHub
20 changed files with 426 additions and 320 deletions

View File

@@ -19,6 +19,9 @@ jest.mock('@/services/analytics', () => ({
trackScreenView: jest.fn(),
flush: jest.fn(),
})),
trackEvent: jest.fn(),
trackScreenView: jest.fn(),
flush: jest.fn(),
}));
describe('navigation', () => {

View File

@@ -11,16 +11,20 @@ import {
checkAndUpdateRegistrationStates,
getAlternativeCSCA,
} from '@/proving/validateDocument';
import analytics from '@/services/analytics';
import { trackEvent } from '@/services/analytics';
// Mock the analytics module to avoid side effects in tests
jest.mock('@/services/analytics', () => {
// Create mock inside factory to avoid temporal dead zone
const mockTrackEvent = jest.fn();
return jest.fn(() => ({
trackEvent: mockTrackEvent,
}));
});
jest.mock('@/services/analytics', () => ({
__esModule: true,
default: jest.fn(() => ({
trackEvent: jest.fn(),
trackScreenView: jest.fn(),
flush: jest.fn(),
})),
trackEvent: jest.fn(),
trackScreenView: jest.fn(),
flush: jest.fn(),
}));
// Mock the passport data provider to avoid database operations
const mockGetAllDocumentsDirectlyFromKeychain = jest.fn();
@@ -153,8 +157,7 @@ describe('getAlternativeCSCA', () => {
beforeEach(() => {
jest.clearAllMocks();
// Get the mocked trackEvent from the analytics module
const mockAnalytics = jest.mocked(analytics);
mockTrackEvent = mockAnalytics().trackEvent as jest.Mock;
mockTrackEvent = jest.mocked(trackEvent) as jest.Mock;
});
it('should return public keys in Record format for Aadhaar with valid public keys', () => {
@@ -245,8 +248,7 @@ describe('checkAndUpdateRegistrationStates', () => {
beforeEach(() => {
jest.clearAllMocks();
// Get the mocked trackEvent from the analytics module
const mockAnalytics = jest.mocked(analytics);
mockTrackEvent = mockAnalytics().trackEvent as jest.Mock;
mockTrackEvent = jest.mocked(trackEvent) as jest.Mock;
mockGetState.mockReturnValue(
buildState({

View File

@@ -2,19 +2,17 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import analytics from '@/services/analytics';
import { trackEvent, trackScreenView } from '@/services/analytics';
// Mock the Segment client
jest.mock('@/config/segment', () => ({
createSegmentClient: jest.fn(() => ({
track: jest.fn(),
screen: jest.fn(),
track: jest.fn().mockResolvedValue(undefined),
flush: jest.fn().mockResolvedValue(undefined),
})),
}));
describe('analytics', () => {
const { trackEvent, trackScreenView } = analytics();
beforeEach(() => {
jest.clearAllMocks();
});
@@ -38,7 +36,7 @@ describe('analytics', () => {
});
it('should handle event tracking with null properties', () => {
expect(() => trackEvent('test_event', null)).not.toThrow();
expect(() => trackEvent('test_event', null as any)).not.toThrow();
});
it('should handle event tracking with undefined properties', () => {
@@ -87,7 +85,7 @@ describe('analytics', () => {
it('should handle invalid duration values gracefully', () => {
const properties = {
duration_seconds: 'not_a_number',
duration_seconds: 'not_a_number' as any,
};
expect(() => trackEvent('test_event', properties)).not.toThrow();
@@ -144,6 +142,22 @@ describe('analytics', () => {
expect(() => trackEvent('test_event', properties)).not.toThrow();
});
it('should NOT transform regular event names (only screen views get "Viewed" prefix)', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
trackEvent('user_login', { method: 'google' });
expect(consoleSpy).toHaveBeenCalledWith(
'[DEV: Analytics EVENT]',
expect.objectContaining({
name: 'user_login', // No "Viewed" prefix for regular events
properties: expect.objectContaining({ method: 'google' }),
}),
);
consoleSpy.mockRestore();
});
});
describe('trackScreenView', () => {
@@ -176,6 +190,98 @@ describe('analytics', () => {
expect(() => trackScreenView('test_screen', properties)).not.toThrow();
});
it('should transform screen views to "Viewed ScreenName" format', () => {
// Mock console.log to capture dev mode output
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
trackScreenView('SplashScreen', { user_id: 123 });
expect(consoleSpy).toHaveBeenCalledWith(
'[DEV: Analytics SCREEN]',
expect.objectContaining({
name: 'Viewed SplashScreen',
properties: expect.objectContaining({ user_id: 123 }),
}),
);
consoleSpy.mockRestore();
});
it('should transform screen names correctly without properties', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
trackScreenView('DocumentNFCScanScreen');
expect(consoleSpy).toHaveBeenCalledWith(
'[DEV: Analytics SCREEN]',
expect.objectContaining({
name: 'Viewed DocumentNFCScanScreen',
properties: undefined,
}),
);
consoleSpy.mockRestore();
});
it('should pass through properties unchanged', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const properties = {
referrer: 'home',
user_id: 456,
navigation_method: 'swipe',
};
trackScreenView('SettingsScreen', properties);
expect(consoleSpy).toHaveBeenCalledWith(
'[DEV: Analytics SCREEN]',
expect.objectContaining({
name: 'Viewed SettingsScreen',
properties: expect.objectContaining(properties),
}),
);
consoleSpy.mockRestore();
});
it('should call segment client with transformed event name in production', () => {
// Temporarily mock __DEV__ to false for production testing
const originalDev = (global as any).__DEV__;
(global as any).__DEV__ = false;
try {
// Reset modules first to clear the cache
jest.resetModules();
// Get the mocked segment client factory after reset
const segmentModule = require('@/config/segment');
const mockTrack = jest.fn().mockResolvedValue(undefined);
// Set up the mock implementation before re-requiring analytics
// This ensures the mock is properly configured when analytics module loads
segmentModule.createSegmentClient.mockImplementation(() => ({
track: mockTrack,
flush: jest.fn().mockResolvedValue(undefined),
}));
// Now re-require analytics to get a fresh segmentClient instance
// that uses our mocked createSegmentClient
const analyticsModule = require('@/services/analytics');
analyticsModule.trackScreenView('HomeScreen', { user_type: 'premium' });
expect(mockTrack).toHaveBeenCalledWith('Viewed HomeScreen', {
user_type: 'premium',
});
} finally {
// Restore original __DEV__ value
(global as any).__DEV__ = originalDev;
// Reset modules again to restore original state for other tests
jest.resetModules();
}
});
});
describe('edge cases', () => {

View File

@@ -94,10 +94,9 @@ describe('Logging Service - Severity Updates', () => {
});
});
it('should update severity on root logger and all extended loggers when settings change', async () => {
it('should update severity on root logger when settings change', async () => {
// Clear any calls from initialization
mockRootSetSeverity.mockClear();
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
// Change the logging severity in the store
useSettingStore.getState().setLoggingSeverity('debug');
@@ -106,30 +105,21 @@ describe('Logging Service - Severity Updates', () => {
await new Promise(resolve => setTimeout(resolve, 10));
// Verify root logger was updated
// Extended loggers inherit severity from root logger automatically
expect(mockRootSetSeverity).toHaveBeenCalledTimes(1);
expect(mockRootSetSeverity).toHaveBeenCalledWith('debug');
// Verify each extended logger was updated
mockLoggerInstances.forEach(logger => {
expect(logger.setSeverity).toHaveBeenCalledTimes(1);
expect(logger.setSeverity).toHaveBeenCalledWith('debug');
});
});
it('should update each specific extended logger individually', async () => {
it('should update root logger severity which extends to all loggers', async () => {
mockRootSetSeverity.mockClear();
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
useSettingStore.getState().setLoggingSeverity('info');
await new Promise(resolve => setTimeout(resolve, 10));
// Verify specific loggers by name
const specificLoggers = ['APP', 'NFC', 'PASSPORT', 'PROOF'];
specificLoggers.forEach(loggerName => {
const logger = mockLoggerInstances.get(loggerName);
expect(logger).toBeDefined();
expect(logger?.setSeverity).toHaveBeenCalledWith('info');
});
// Verify root logger was updated
// Extended loggers (APP, NFC, PASSPORT, PROOF, etc.) inherit from root
expect(mockRootSetSeverity).toHaveBeenCalledTimes(1);
expect(mockRootSetSeverity).toHaveBeenCalledWith('info');
});
it('should update severity for all severity levels', async () => {
@@ -142,24 +132,18 @@ describe('Logging Service - Severity Updates', () => {
for (const level of severityLevels) {
mockRootSetSeverity.mockClear();
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
useSettingStore.getState().setLoggingSeverity(level);
await new Promise(resolve => setTimeout(resolve, 10));
// Verify root logger
// Verify root logger was updated
// Extended loggers inherit severity from root automatically
expect(mockRootSetSeverity).toHaveBeenCalledWith(level);
// Verify all extended loggers
mockLoggerInstances.forEach(logger => {
expect(logger.setSeverity).toHaveBeenCalledWith(level);
});
}
});
it('should not call setSeverity if severity has not changed', async () => {
mockRootSetSeverity.mockClear();
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
// Get current severity
const currentSeverity = useSettingStore.getState().loggingSeverity;
@@ -171,16 +155,10 @@ describe('Logging Service - Severity Updates', () => {
// Should not call setSeverity on root logger
expect(mockRootSetSeverity).not.toHaveBeenCalled();
// Should not call setSeverity on any extended logger
mockLoggerInstances.forEach(logger => {
expect(logger.setSeverity).not.toHaveBeenCalled();
});
});
it('should handle rapid severity changes correctly', async () => {
mockRootSetSeverity.mockClear();
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
// Rapidly change severity multiple times
useSettingStore.getState().setLoggingSeverity('debug');
@@ -192,15 +170,10 @@ describe('Logging Service - Severity Updates', () => {
await new Promise(resolve => setTimeout(resolve, 50));
// Should have been called 4 times (once per change)
// Extended loggers inherit severity from root automatically
expect(mockRootSetSeverity).toHaveBeenCalledTimes(4);
// The last call should be 'error'
expect(mockRootSetSeverity).toHaveBeenLastCalledWith('error');
// Each extended logger should also have been called 4 times
mockLoggerInstances.forEach(logger => {
expect(logger.setSeverity).toHaveBeenCalledTimes(4);
expect(logger.setSeverity).toHaveBeenLastCalledWith('error');
});
});
});