/** * END-TO-END TEST: Widget Lifecycle * * Tests complete user journeys from widget mount to close, * simulating real-world scenarios that companies would encounter. * * Strategy: Minimal mocking, test full integration flow */ import React from 'react'; import { render, waitFor, fireEvent } from '@testing-library/react-native'; import { SoluCXWidgetView as SoluCXWidget } from '../../SoluCXWidgetView'; import type { WidgetCallbacks } from '../../domain'; // Mock storage const mockStorage: Record = {}; jest.mock('@react-native-async-storage/async-storage', () => ({ __esModule: true, default: { getItem: jest.fn((key: string) => Promise.resolve(mockStorage[key] || null)), setItem: jest.fn((key: string, value: string) => { mockStorage[key] = value; return Promise.resolve(); }), removeItem: jest.fn((key: string) => { delete mockStorage[key]; return Promise.resolve(); }), clear: jest.fn(() => { Object.keys(mockStorage).forEach(key => delete mockStorage[key]); return Promise.resolve(); }), }, })); // Mock only what we absolutely must jest.mock('react-native-webview', () => { const React = require('react'); const { View, Text } = require('react-native'); return { __esModule: true, WebView: React.forwardRef((props: any, ref: any) => { React.useImperativeHandle(ref, () => ({ injectJavaScript: jest.fn(), })); return ( WebView Content ); }), }; }); jest.mock('../../services/WidgetBootstrapService', () => ({ requestWidgetUrl: jest.fn().mockResolvedValue({ available: true, url: 'https://widget.solucx.com/survey/456' }), })); jest.mock('../../services/WidgetValidationService', () => ({ WidgetValidationService: jest.fn().mockImplementation(() => ({ shouldDisplayForTransaction: jest.fn().mockResolvedValue({ canDisplay: true }), shouldDisplayForTransactionAlreadyAnswered: jest.fn().mockResolvedValue({ canDisplay: true }), shouldDisplayWidget: jest.fn().mockResolvedValue({ canDisplay: true }), })), })); describe('E2E: Widget Lifecycle', () => { beforeEach(async () => { jest.clearAllMocks(); Object.keys(mockStorage).forEach(key => delete mockStorage[key]); }); describe('E-commerce Post-Purchase Survey Flow', () => { it('should complete full flow: mount → display → interact → complete → close', async () => { const analytics = { track: jest.fn(), }; const callbacks: WidgetCallbacks = { onOpened: (userId) => analytics.track('widget_shown', { userId }), onQuestionAnswered: () => analytics.track('question_answered'), onCompleted: (userId) => { analytics.track('survey_completed', { userId }); }, onClosed: () => analytics.track('widget_closed'), }; // 1. Mount widget after purchase const { getByTestId } = render( ); // 2. Widget should render await waitFor(() => { expect(getByTestId('webview')).toBeTruthy(); }, { timeout: 10000 }); const webview = getByTestId('webview'); // 3. First page loads, customer sees question // (WebView automatically expands as content loads) fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-400' }, }); // 4. Customer answers first question fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' }, }); await waitFor(() => { expect(analytics.track).toHaveBeenCalledWith('question_answered'); }); // Reset counter after first question to track only upcoming interactions analytics.track.mockClear(); // 5. Customer goes to next page fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-2' }, }); // 6. Customer answers second question fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' }, }); await waitFor(() => { expect(analytics.track).toHaveBeenCalledWith('question_answered'); expect(analytics.track).toHaveBeenCalledTimes(1); // Called once after reset }); // 7. Customer submits survey fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_COMPLETED' }, }); await waitFor(() => { expect(analytics.track).toHaveBeenCalledWith('survey_completed', { userId: expect.any(String), }); }); // 8. Customer closes widget fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_CLOSE' }, }); await waitFor(() => { expect(analytics.track).toHaveBeenCalledWith('widget_closed'); }); // Verify complete tracking flow after reset (question_answered + survey_completed + widget_closed) expect(analytics.track).toHaveBeenCalledTimes(3); }, 15000); }); describe('SaaS NPS Survey Flow with Error Recovery', () => { it('should handle network error gracefully when error occurs', async () => { const errorHandler = jest.fn(); const callbacks: WidgetCallbacks = { onError: errorHandler, }; const { getByTestId } = render( ); await waitFor(() => { expect(getByTestId('webview')).toBeTruthy(); }, { timeout: 10000 }); const webview = getByTestId('webview'); // User rates NPS fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-1' }, }); // Network error occurs during submission fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_ERROR-Network timeout occurred' }, }); await waitFor(() => { expect(errorHandler).toHaveBeenCalledWith('Network timeout occurred'); }, { timeout: 10000 }); }, 15000); }); describe('Banking Compliance Survey with Dynamic Resizing', () => { it('should handle multiple resize events as form expands', async () => { const resizeHandler = jest.fn(); const { getByTestId } = render( ); await waitFor(() => { expect(getByTestId('webview')).toBeTruthy(); }); const webview = getByTestId('webview'); // Form starts small fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-300' }, }); // User expands section, form grows fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-500' }, }); // User expands another section fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-700' }, }); // User collapses section fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-600' }, }); await waitFor(() => { expect(resizeHandler).toHaveBeenCalledTimes(4); }); // Verify resize values expect(resizeHandler).toHaveBeenNthCalledWith(1, '300'); expect(resizeHandler).toHaveBeenNthCalledWith(2, '500'); expect(resizeHandler).toHaveBeenNthCalledWith(3, '700'); expect(resizeHandler).toHaveBeenNthCalledWith(4, '600'); }); }); describe('Multi-Step Survey with Page Tracking', () => { it('should track progress through multi-step survey', async () => { const pageChangeHandler = jest.fn(); const questionHandler = jest.fn(); const completionHandler = jest.fn(); const { getByTestId } = render( ); await waitFor(() => { expect(getByTestId('webview')).toBeTruthy(); }); const webview = getByTestId('webview'); // Page 1: Answer question fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' }, }); // Go to page 2 fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-2' }, }); // Page 2: Answer question fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' }, }); // Go to page 3 fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-3' }, }); // Page 3: Answer final question fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' }, }); // Complete survey fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_COMPLETED' }, }); await waitFor(() => { expect(pageChangeHandler).toHaveBeenCalledTimes(2); expect(questionHandler).toHaveBeenCalledTimes(3); expect(completionHandler).toHaveBeenCalledTimes(1); }); // Verify page progression expect(pageChangeHandler).toHaveBeenNthCalledWith(1, '2'); expect(pageChangeHandler).toHaveBeenNthCalledWith(2, '3'); }); }); });