import React from "react"; import { fireEvent, render } from "@testing-library/react-native"; import { Switch } from "react-native"; import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions"; import { Toggle } from "../Toggle"; const mockOnCellClick = jest.fn(); const mockInvokeAction = jest.fn(); const mockInitialEntryState = jest.fn().mockReturnValue({ state: 0 }); jest.mock("@applicaster/zapp-react-native-utils/reactHooks/cell-click", () => ({ useCellClick: jest.fn(() => mockOnCellClick), })); jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({ useActions: jest.fn(() => ({ invokeAction: mockInvokeAction, initialEntryState: mockInitialEntryState, })), })); const style = { trackColor: "rgba(0,0,0,1)", trackColorActive: "rgba(4,207,153,1)", thumbColor: "rgba(255,255,255,1)", thumbColorActive: "rgba(255,255,255,1)", } as any; const item = { id: "true", title: "I agree" } as any; const renderToggle = (identifier: string, cellState) => render( ); describe("Toggle with an empty action identifier", () => { beforeEach(() => { jest.clearAllMocks(); }); it("is on when the cell is selected", () => { const { getByRole } = renderToggle("", "selected"); expect(getByRole("switch").props.value).toBe(true); }); it("is off when the cell is not selected", () => { const { getByRole } = renderToggle("", "default"); expect(getByRole("switch").props.value).toBe(false); }); // A finger on a selected row resolves the cell to focused_selected. Reading // that as unselected flipped the thumb for the duration of the press and let // it jump back on release, which reads as a second tap. it("stays on while a selected cell is pressed", () => { const { getByRole } = renderToggle("", "focused_selected"); expect(getByRole("switch").props.value).toBe(true); }); it("is off while an unselected cell is pressed", () => { const { getByRole } = renderToggle("", "focused"); expect(getByRole("switch").props.value).toBe(false); }); it("routes a change through the cell click path", () => { const { getByRole } = renderToggle("", "default"); fireEvent(getByRole("switch"), "valueChange", true); expect(mockOnCellClick).toHaveBeenCalledTimes(1); expect(mockInvokeAction).not.toHaveBeenCalled(); }); it("does not read the action plugin state", () => { renderToggle("", "default"); expect(mockInitialEntryState).not.toHaveBeenCalled(); }); // Asserted on the Switch element rather than the rendered host component: // what Toggle owns is the props it hands to Switch, and React Native rewrites // them into platform-specific ones (tintColor, a style entry) underneath. it("forwards every configured color to the switch", () => { const { UNSAFE_getByType } = renderToggle("", "default"); expect(UNSAFE_getByType(Switch).props).toMatchObject({ trackColor: { false: style.trackColor, true: style.trackColorActive }, thumbColor: style.thumbColor, }); }); // ios_backgroundColor paints an opaque layer behind the track that the track // color cannot cover, which on iOS reads as a halo around the switch. The // switch is left on its platform default instead, so the prop must stay // unset even if a style happens to carry a color for it. it("does not set an iOS background color on the switch", () => { const { UNSAFE_getByType } = renderToggle("", "default"); expect(UNSAFE_getByType(Switch).props.ios_backgroundColor).toBeUndefined(); }); }); describe("Toggle with an action plugin identifier", () => { beforeEach(() => { jest.clearAllMocks(); }); it("routes a change through the action plugin", () => { const { getByRole } = renderToggle("firebase-push-topic-action", "default"); fireEvent(getByRole("switch"), "valueChange", true); expect(mockInvokeAction).toHaveBeenCalledWith( item, expect.objectContaining({ updateState: expect.any(Function) }) ); expect(mockOnCellClick).not.toHaveBeenCalled(); }); }); describe("Toggle with an identifier whose action plugin is not installed", () => { // The cell manifest no longer defaults toggle_action_identifier, but app // configurations saved while it did still carry "firebase-push-topic-action", // and a mistyped id is always possible. A toggle can therefore still arrive // with a non-empty identifier it never asked for - the presence of a real // action context, not the identifier string, is what decides the route. beforeEach(() => { jest.clearAllMocks(); (useActions as jest.Mock).mockReturnValue(undefined); }); afterEach(() => { (useActions as jest.Mock).mockReturnValue({ invokeAction: mockInvokeAction, initialEntryState: mockInitialEntryState, }); }); it("routes a change through the cell click path", () => { const { getByRole } = renderToggle("firebase-push-topic-action", "default"); fireEvent(getByRole("switch"), "valueChange", true); expect(mockOnCellClick).toHaveBeenCalledTimes(1); expect(mockInvokeAction).not.toHaveBeenCalled(); }); it("still reflects the cell selection state", () => { const { getByRole } = renderToggle( "firebase-push-topic-action", "selected" ); expect(getByRole("switch").props.value).toBe(true); }); });