import { SoluCXWidget, _registerHost, _unregisterHost } from '../SoluCXWidget'; import { WidgetStateManager } from '../services/WidgetStateManager'; import { AsyncStorageService } from '../services/storage/AsyncStorageService'; import type { WidgetSamplerLog } from '../domain'; // Mock do AsyncStorage jest.mock('@react-native-async-storage/async-storage', () => ({ __esModule: true, default: { setItem: jest.fn(() => Promise.resolve()), getItem: jest.fn(() => Promise.resolve(null)), removeItem: jest.fn(() => Promise.resolve()), clear: jest.fn(() => Promise.resolve()), }, })); // Mock dos serviços jest.mock('../services/storage/AsyncStorageService'); jest.mock('../services/UserIdentificationService', () => ({ getUserId: jest.fn((data) => data?.customer_id || 'default-user-id'), })); describe('SoluCXWidget - Log Management', () => { let mockStorageService: jest.Mocked; let widget: SoluCXWidget; beforeEach(() => { jest.clearAllMocks(); // Setup do mock do AsyncStorageService mockStorageService = { read: jest.fn(), write: jest.fn(), delete: jest.fn(), exists: jest.fn(), } as any; (AsyncStorageService as jest.Mock).mockImplementation(() => mockStorageService); // Criar widget widget = SoluCXWidget.create('test-instance-key') .setData({ journey: 'test-journey', customer_id: 'test-user-123', }); }); describe('overrideTimestamp', () => { it('should override timestamp when Date object is provided', async () => { const mockLogs: WidgetSamplerLog = { attempts: 1, answeredTransactionIds: [], lastDisplay: 1000, }; mockStorageService.read.mockResolvedValue(mockLogs); mockStorageService.write.mockResolvedValue(); const testDate = new Date('2026-03-10T10:00:00.000Z'); const result = await widget.overrideTimestamp('lastDisplay', testDate); expect(result).toBe(widget); // Deve retornar this para chaining expect(mockStorageService.write).toHaveBeenCalledWith( 'test-instance-key:test-journey:test-user-123', expect.objectContaining({ lastDisplay: testDate.getTime(), }) ); }); it('should override timestamp when number in milliseconds is provided', async () => { const mockLogs: WidgetSamplerLog = { attempts: 2, answeredTransactionIds: [], }; mockStorageService.read.mockResolvedValue(mockLogs); mockStorageService.write.mockResolvedValue(); const timestamp = 1678454400000; await widget.overrideTimestamp('lastFirstAccess', timestamp); expect(mockStorageService.write).toHaveBeenCalledWith( 'test-instance-key:test-journey:test-user-123', expect.objectContaining({ lastFirstAccess: timestamp, }) ); }); it('should clear timestamp when 0 is provided', async () => { const mockLogs: WidgetSamplerLog = { attempts: 1, answeredTransactionIds: [], lastDismiss: 1000, }; mockStorageService.read.mockResolvedValue(mockLogs); mockStorageService.write.mockResolvedValue(); await widget.overrideTimestamp('lastDismiss', 0); expect(mockStorageService.write).toHaveBeenCalledWith( 'test-instance-key:test-journey:test-user-123', expect.objectContaining({ lastDismiss: 0, }) ); }); it('should return this for method chaining when overrideTimestamp is called', async () => { mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); mockStorageService.write.mockResolvedValue(); const result = await widget .overrideTimestamp('lastDisplay', Date.now()) .then((w) => w.overrideTimestamp('lastDismiss', Date.now())); expect(result).toBe(widget); expect(mockStorageService.write).toHaveBeenCalledTimes(2); }); it('should override timestamp when any timestamp field is specified', async () => { mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); mockStorageService.write.mockResolvedValue(); const timestamp = Date.now(); const fields = [ 'lastDisplayAttempt', 'lastFirstAccess', 'lastDisplay', 'lastDismiss', 'lastSubmit', 'lastPartialSubmit', ] as const; for (const field of fields) { await widget.overrideTimestamp(field, timestamp); } expect(mockStorageService.write).toHaveBeenCalledTimes(fields.length); }); }); describe('getWidgetLogs', () => { it('should return widget logs when getWidgetLogs is called', async () => { const mockLogs: WidgetSamplerLog = { attempts: 5, answeredTransactionIds: ['tx1', 'tx2'], lastDisplay: 1000, lastDismiss: 2000, lastFirstAccess: 500, }; mockStorageService.read.mockResolvedValue(mockLogs); const logs = await widget.getWidgetLogs(); expect(logs).toEqual(mockLogs); expect(mockStorageService.read).toHaveBeenCalledWith( 'test-instance-key:test-journey:test-user-123' ); }); it('should return default logs when no logs exist in storage', async () => { // Suprimir console.error esperado quando logs não são encontrados const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); mockStorageService.read.mockRejectedValue(new Error('Not found')); const logs = await widget.getWidgetLogs(); expect(logs).toEqual({ attempts: 0, answeredTransactionIds: [], }); consoleErrorSpy.mockRestore(); }); it('should use correct storage ID when widget data contains instanceKey, journey and userId', async () => { const widget2 = SoluCXWidget.create('instance2') .setData({ form_id: 'form123', customer_id: 'user456', }); mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); await widget2.getWidgetLogs(); expect(mockStorageService.read).toHaveBeenCalledWith( 'instance2:user456' ); }); }); describe('clearWidgetLogs', () => { it('should clear all widget logs when clearWidgetLogs is called', async () => { mockStorageService.delete.mockResolvedValue(); const result = await widget.clearWidgetLogs(); expect(result).toBe(widget); // Deve retornar this para chaining expect(mockStorageService.delete).toHaveBeenCalledWith( 'test-instance-key:test-journey:test-user-123' ); }); it('should return this for method chaining when clearWidgetLogs is called', async () => { mockStorageService.delete.mockResolvedValue(); mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); const result = await widget .clearWidgetLogs() .then((w) => w.getWidgetLogs()); expect(result).toBeDefined(); expect(mockStorageService.delete).toHaveBeenCalled(); }); it('should throw error when deletion fails', async () => { mockStorageService.delete.mockRejectedValue(new Error('Delete failed')); await expect(widget.clearWidgetLogs()).rejects.toThrow('Delete failed'); }); }); describe('Storage ID generation', () => { it('should build storage ID when instanceKey, journey and userId are provided', async () => { const widget = SoluCXWidget.create('my-app') .setData({ journey: 'onboarding', customer_id: 'user123', }); mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); await widget.getWidgetLogs(); expect(mockStorageService.read).toHaveBeenCalledWith( 'my-app:onboarding:user123' ); }); it('should use form_id when journey is not provided', async () => { const widget = SoluCXWidget.create('my-app') .setData({ form_id: 'form456', customer_id: 'user123', }); mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); await widget.getWidgetLogs(); expect(mockStorageService.read).toHaveBeenCalledWith( 'my-app:user123' ); }); it('should use default userId when userId is missing', async () => { const widget = SoluCXWidget.create('my-app') .setData({ journey: 'test', }); mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); await widget.getWidgetLogs(); // getUserId mock retorna 'default-user-id' quando não há customer_id expect(mockStorageService.read).toHaveBeenCalledWith( 'my-app:test:default-user-id' ); }); }); describe('Builder pattern integration', () => { it('should work with log management when full builder chain is used', async () => { mockStorageService.read.mockResolvedValue({ attempts: 0, answeredTransactionIds: [], }); mockStorageService.write.mockResolvedValue(); const widget = SoluCXWidget.create('app1') .setType('modal') .setData({ journey: 'survey', customer_id: 'user999', }) .setOptions({ maxAttemptsAfterDismiss: 1, }) .setCallbacks({ onOpened: jest.fn(), }); const timestamp = Date.now(); await widget.overrideTimestamp('lastDisplay', timestamp); const logs = await widget.getWidgetLogs(); expect(logs).toBeDefined(); expect(mockStorageService.read).toHaveBeenCalledWith( 'app1:survey:user999' ); }); }); }); describe('SoluCXWidget - Host Registration', () => { let showHandler: jest.Mock; let dismissHandler: jest.Mock; beforeEach(() => { showHandler = jest.fn(); dismissHandler = jest.fn(); _unregisterHost(); // Limpar antes de cada teste }); afterEach(() => { _unregisterHost(); }); describe('show', () => { it('should throw error when show is called without registered host', () => { const widget = SoluCXWidget.create('test-key').setData({ journey: 'test', }); expect(() => widget.show()).toThrow( '[SoluCXWidget] Cannot show widget — no is mounted. Add at the root of your app.' ); }); it('should call show handler with correct config when widget is shown', () => { _registerHost(showHandler, dismissHandler); const callbacks = { onOpened: jest.fn(), onClosed: jest.fn(), }; const widget = SoluCXWidget.create('test-key') .setType('modal') .setData({ journey: 'test-journey', customer_id: 'user123', }) .setOptions({ maxAttemptsAfterDismiss: 2, }) .setCallbacks(callbacks); widget.show(); expect(showHandler).toHaveBeenCalledWith({ instanceKey: 'test-key', type: 'modal', data: { journey: 'test-journey', customer_id: 'user123', }, options: { maxAttemptsAfterDismiss: 2, }, callbacks, }); }); it('should cleanup widget state when show is called', () => { _registerHost(showHandler, dismissHandler); const widget = SoluCXWidget.create('test-key') .setType('modal') .setData({ journey: 'test' }) .setOptions({ maxAttemptsAfterDismiss: 1 }) .setCallbacks({ onOpened: jest.fn() }); widget.show(); // Chamar show novamente deve usar valores padrão widget.show(); expect(showHandler).toHaveBeenCalledTimes(2); expect(showHandler).toHaveBeenLastCalledWith({ instanceKey: 'test-key', type: 'bottom', // Valor padrão após cleanup data: {}, options: {}, callbacks: {}, }); }); }); describe('dismiss', () => { it('should call dismiss handler when dismiss is called', () => { _registerHost(showHandler, dismissHandler); SoluCXWidget.dismiss(); expect(dismissHandler).toHaveBeenCalled(); }); it('should not throw when dismiss is called without registered handler', () => { expect(() => SoluCXWidget.dismiss()).not.toThrow(); }); }); describe('_registerHost and _unregisterHost', () => { it('should allow showing widget when new host is registered', () => { _registerHost(showHandler, dismissHandler); const widget = SoluCXWidget.create('test').setData({}); expect(() => widget.show()).not.toThrow(); }); it('should throw error when unregistered host is used', () => { _registerHost(showHandler, dismissHandler); _unregisterHost(); const widget = SoluCXWidget.create('test').setData({}); expect(() => widget.show()).toThrow(); }); }); });