/** * INTEGRATION TEST: WebView Communication * * CRITICAL: Tests the core WebView message passing bridge between * the embedded widget and React Native. * * Coverage Target: handleWebViewMessage, handleWebViewLoad, handleClose * Previous Coverage: 0% (CRITICAL GAP) * * Strategy: Minimal mocking - use real WidgetEventService and components */ 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 only external dependencies we can't control jest.mock("react-native-webview", () => { const React = require("react"); const { View } = require("react-native"); return { __esModule: true, WebView: React.forwardRef((props: any, ref: any) => { // Store ref methods for testing React.useImperativeHandle(ref, () => ({ injectJavaScript: jest.fn(script => { // Simulate successful injection if (props.onLoadEnd) props.onLoadEnd(); }), })); return ( {/* Expose onMessage handler for testing */} {props.children} ); }), }; }); jest.mock("../../services/WidgetBootstrapService", () => ({ requestWidgetUrl: jest.fn().mockResolvedValue({ available: true, url: "https://mock.widget.url/form123" }), })); 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 }), })), })); // Mock storage but keep it functional 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(); }), }, })); describe("Integration: WebView Communication", () => { const baseProps = { soluCXKey: "test-key-123", type: "modal" as const, data: { customer_id: "user-456", form_id: "form-789", // Required for form mode }, options: { height: 400, }, }; beforeEach(() => { jest.clearAllMocks(); Object.keys(mockStorage).forEach(key => delete mockStorage[key]); }); describe("Message Handling - Form Events", () => { it("should handle FORM_CLOSE message and call onClosed callback", async () => { const mockOnClosed = jest.fn(); const callbacks: WidgetCallbacks = { onClosed: mockOnClosed, }; const { getByTestId } = render(); // Wait for widget to be ready await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); const webview = getByTestId("webview"); // Simulate WebView sending FORM_CLOSE message fireEvent(webview, "message", { nativeEvent: { data: "FORM_CLOSE" }, }); // Verify callback was called await waitFor(() => { expect(mockOnClosed).toHaveBeenCalledTimes(1); }); }, 15000); // 15 second timeout for slower CI environments it("should handle FORM_ERROR message with error text", async () => { const mockOnError = jest.fn(); const callbacks: WidgetCallbacks = { onError: mockOnError, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Simulate error message with details fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_ERROR-Network timeout occurred" }, }); await waitFor(() => { expect(mockOnError).toHaveBeenCalledWith("Network timeout occurred"); }); }); it("should preserve FORM_ERROR payloads containing additional hyphens", async () => { const mockOnError = jest.fn(); const callbacks: WidgetCallbacks = { onError: mockOnError, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_ERROR-timeout-api-gateway" } }); await waitFor(() => { expect(mockOnError).toHaveBeenCalledWith("timeout-api-gateway"); }); }); it("should handle FORM_RESIZE message and update height", async () => { const mockOnResize = jest.fn(); const callbacks: WidgetCallbacks = { onResize: mockOnResize, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Simulate resize message fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_RESIZE-650" }, }); await waitFor(() => { expect(mockOnResize).toHaveBeenCalledWith("650"); }); }); it("should handle FORM_COMPLETED message with userId", async () => { const mockOnCompleted = jest.fn(); const callbacks: WidgetCallbacks = { onCompleted: mockOnCompleted, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Simulate form completion fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_COMPLETED" }, }); await waitFor(() => { expect(mockOnCompleted).toHaveBeenCalledWith(expect.any(String)); }); }); it("should handle FORM_PAGECHANGED message", async () => { const mockOnPageChanged = jest.fn(); const callbacks: WidgetCallbacks = { onPageChanged: mockOnPageChanged, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Simulate page change fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_PAGECHANGED-2" }, }); await waitFor(() => { expect(mockOnPageChanged).toHaveBeenCalledWith("2"); }); }); it("should handle QUESTION_ANSWERED message", async () => { const mockOnQuestionAnswered = jest.fn(); const callbacks: WidgetCallbacks = { onQuestionAnswered: mockOnQuestionAnswered, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Simulate question answered fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "QUESTION_ANSWERED" }, }); await waitFor(() => { expect(mockOnQuestionAnswered).toHaveBeenCalledTimes(1); }); }); }); describe("Message Handling - Survey Events", () => { const surveyProps = { ...baseProps, data: { customer_id: "user-456", // No form_id = survey mode }, }; it("should handle closeSoluCXWidget survey event", async () => { const mockOnClosed = jest.fn(); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); // Survey uses different event names fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "closeSoluCXWidget" }, }); await waitFor(() => { expect(mockOnClosed).toHaveBeenCalledTimes(1); }); }); it("should handle resizeSoluCXWidget survey event", async () => { const mockOnResize = jest.fn(); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "resizeSoluCXWidget-800" }, }); await waitFor(() => { expect(mockOnResize).toHaveBeenCalledWith("800"); }); }); it("should handle completeSoluCXWidget survey event", async () => { const mockOnCompleted = jest.fn(); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "completeSoluCXWidget" }, }); await waitFor(() => { expect(mockOnCompleted).toHaveBeenCalled(); }); }); }); describe("Edge Cases - Message Handling", () => { it("should handle rapid successive messages", async () => { const mockOnResize = jest.fn(); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); const webview = getByTestId("webview"); // Send 10 resize messages rapidly for (let i = 0; i < 10; i++) { fireEvent(webview, "message", { nativeEvent: { data: `FORM_RESIZE-${300 + i * 10}` }, }); } // All should be processed await waitFor(() => { expect(mockOnResize).toHaveBeenCalledTimes(10); }); // Verify last call had correct value expect(mockOnResize).toHaveBeenLastCalledWith("390"); }); }); describe("Close Button Integration", () => { it("should render close button in modal", async () => { const { getByText, getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); expect(getByText("✕")).toBeTruthy(); }); }); }); describe("Multiple Callbacks in Single Flow", () => { it("should call multiple callbacks during form interaction flow", async () => { const mockOnPageChanged = jest.fn(); const mockOnQuestionAnswered = jest.fn(); const mockOnResize = jest.fn(); const mockOnCompleted = jest.fn(); const mockOnClosed = jest.fn(); const callbacks: WidgetCallbacks = { onPageChanged: mockOnPageChanged, onQuestionAnswered: mockOnQuestionAnswered, onResize: mockOnResize, onCompleted: mockOnCompleted, onClosed: mockOnClosed, }; const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("webview")).toBeTruthy(); }); const webview = getByTestId("webview"); // Simulate complete user flow // 1. Form resizes fireEvent(webview, "message", { nativeEvent: { data: "FORM_RESIZE-400" } }); // 2. User goes to page 2 fireEvent(webview, "message", { nativeEvent: { data: "FORM_PAGECHANGED-2" } }); // 3. User answers a question fireEvent(webview, "message", { nativeEvent: { data: "QUESTION_ANSWERED" } }); // 4. Form resizes again fireEvent(webview, "message", { nativeEvent: { data: "FORM_RESIZE-500" } }); // 5. User completes form fireEvent(webview, "message", { nativeEvent: { data: "FORM_COMPLETED" } }); // 6. User closes fireEvent(webview, "message", { nativeEvent: { data: "FORM_CLOSE" } }); // Verify all callbacks were called in order await waitFor(() => { expect(mockOnResize).toHaveBeenCalledTimes(2); expect(mockOnPageChanged).toHaveBeenCalledTimes(1); expect(mockOnQuestionAnswered).toHaveBeenCalledTimes(1); expect(mockOnCompleted).toHaveBeenCalledTimes(1); expect(mockOnClosed).toHaveBeenCalledTimes(1); }); // Verify correct arguments expect(mockOnResize).toHaveBeenNthCalledWith(1, "400"); expect(mockOnResize).toHaveBeenNthCalledWith(2, "500"); expect(mockOnPageChanged).toHaveBeenCalledWith("2"); }); }); });