/**
* 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");
});
});
});