UNPKG

@applicaster/zapp-react-native-ui-components

Version:

Applicaster Zapp React Native ui components for the Quick Brick App

380 lines (287 loc) • 12.2 kB
import { renderHook, act } from "@testing-library/react-hooks"; import { BehaviorSubject } from "rxjs"; import { useLoadingState } from "../useLoadingState"; // Mock the useRefWithInitialValue hook jest.mock( "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue", () => ({ useRefWithInitialValue: jest.fn((initializer) => ({ current: initializer(), })), }) ); describe("useLoadingState", () => { let onLoadDone: jest.Mock; beforeEach(() => { onLoadDone = jest.fn(); jest.clearAllMocks(); }); describe("initialization", () => { it("should initialize with correct default state for zero components", () => { const { result } = renderHook(() => useLoadingState(0, onLoadDone)); const initialState = result.current.loadingState.getValue(); expect(initialState).toEqual({ index: -1, done: true, waitForAllComponents: false, }); expect(result.current.shouldShowLoadingError).toBe(false); }); it("should initialize with correct default state for multiple components", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); const initialState = result.current.loadingState.getValue(); expect(initialState).toEqual({ index: -1, done: false, waitForAllComponents: false, }); expect(result.current.shouldShowLoadingError).toBe(false); }); it("should return a BehaviorSubject for loadingState", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); expect(result.current.loadingState).toBeInstanceOf(BehaviorSubject); }); }); describe("arePreviousComponentsLoaded", () => { it("should return true for index 0 (first component)", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); expect(result.current.arePreviousComponentsLoaded(0)).toBe(true); }); it("should return false when previous components are not loaded", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); expect(result.current.arePreviousComponentsLoaded(1)).toBe(false); expect(result.current.arePreviousComponentsLoaded(2)).toBe(false); }); it("should return true when all previous components are loaded", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); act(() => { result.current.onLoadFinished(0); }); expect(result.current.arePreviousComponentsLoaded(1)).toBe(true); expect(result.current.arePreviousComponentsLoaded(2)).toBe(false); act(() => { result.current.onLoadFinished(1); }); expect(result.current.arePreviousComponentsLoaded(2)).toBe(true); }); }); describe("onLoadFinished", () => { it("should update component state and loading state when component finishes loading", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); act(() => { result.current.onLoadFinished(0); }); const state = result.current.loadingState.getValue(); expect(state.index).toBe(0); expect(state.done).toBe(false); }); it("should update index to highest loaded component", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); act(() => { result.current.onLoadFinished(2); }); let state = result.current.loadingState.getValue(); expect(state.index).toBe(2); act(() => { result.current.onLoadFinished(1); }); state = result.current.loadingState.getValue(); expect(state.index).toBe(2); // Should remain 2, not decrease to 1 }); it("should mark as done when all components are loaded", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(0); }); let state = result.current.loadingState.getValue(); expect(state.done).toBe(true); // True because arePreviousComponentsLoaded(1) returns true when component 0 is loaded expect(onLoadDone).toHaveBeenCalledTimes(1); act(() => { result.current.onLoadFinished(1); }); state = result.current.loadingState.getValue(); expect(state.done).toBe(true); expect(onLoadDone).toHaveBeenCalledTimes(2); // Called again }); it("should call onLoadDone when count is 0", () => { const { result } = renderHook(() => useLoadingState(0, onLoadDone)); const state = result.current.loadingState.getValue(); expect(state.done).toBe(true); // onLoadDone is not called on initialization for count 0, only when all components are loaded via dispatch }); it("should handle loading components out of order", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); // Load component 2 first act(() => { result.current.onLoadFinished(2); }); let state = result.current.loadingState.getValue(); expect(state.done).toBe(false); // Load component 0 act(() => { result.current.onLoadFinished(0); }); state = result.current.loadingState.getValue(); expect(state.done).toBe(false); // Load component 1 - should complete loading act(() => { result.current.onLoadFinished(1); }); state = result.current.loadingState.getValue(); expect(state.done).toBe(true); expect(onLoadDone).toHaveBeenCalledTimes(1); }); it("should call onLoadDone again on subsequent dispatches", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(0); result.current.onLoadFinished(1); }); expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each dispatch when done is true // Try loading again - onLoadDone will be called again because dispatch runs again act(() => { result.current.onLoadFinished(0); }); expect(onLoadDone).toHaveBeenCalledTimes(3); // Will be called again }); }); describe("onLoadFailed", () => { it("should treat failed components as loaded when SHOULD_FAIL_ON_COMPONENT_LOADING is false", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); const error = new Error("Load failed"); act(() => { result.current.onLoadFailed({ error, index: 0 }); }); const state = result.current.loadingState.getValue(); expect(state.index).toBe(0); expect(result.current.shouldShowLoadingError).toBe(false); }); it("should complete loading when all components fail", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); const error = new Error("Load failed"); act(() => { result.current.onLoadFailed({ error, index: 0 }); result.current.onLoadFailed({ error, index: 1 }); }); const state = result.current.loadingState.getValue(); expect(state.done).toBe(true); expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each failed component }); it("should handle mixed success and failure", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); const error = new Error("Load failed"); act(() => { result.current.onLoadFinished(0); result.current.onLoadFailed({ error, index: 1 }); result.current.onLoadFinished(2); }); const state = result.current.loadingState.getValue(); expect(state.done).toBe(true); expect(onLoadDone).toHaveBeenCalledTimes(2); // Called when all components 0,1,2 are handled }); }); describe("loading state observable", () => { it("should emit state changes through BehaviorSubject", () => { const { result } = renderHook(() => useLoadingState(3, onLoadDone)); // Use 3 components so loading component 0 doesn't complete everything const mockSubscriber = jest.fn(); result.current.loadingState.subscribe(mockSubscriber); act(() => { result.current.onLoadFinished(0); }); // Should have been called twice: initial state + update expect(mockSubscriber).toHaveBeenCalledTimes(2); expect(mockSubscriber).toHaveBeenLastCalledWith({ index: 0, done: false, // Will be false because we need components 1 and 2 as well waitForAllComponents: false, }); }); it("should preserve waitForAllComponents flag in state updates", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(0); }); const state = result.current.loadingState.getValue(); expect(state.waitForAllComponents).toBe(false); }); }); describe("memoization", () => { it("should return stable references for functions", () => { const { result, rerender } = renderHook(() => useLoadingState(2, onLoadDone) ); const firstRender = { onLoadFinished: result.current.onLoadFinished, onLoadFailed: result.current.onLoadFailed, arePreviousComponentsLoaded: result.current.arePreviousComponentsLoaded, }; rerender(); expect(result.current.onLoadFinished).toBe(firstRender.onLoadFinished); expect(result.current.onLoadFailed).toBe(firstRender.onLoadFailed); expect(result.current.arePreviousComponentsLoaded).toBe( firstRender.arePreviousComponentsLoaded ); }); it("should return stable function references (current behavior)", () => { const { result, rerender } = renderHook( ({ onLoadDone }) => useLoadingState(2, onLoadDone), { initialProps: { onLoadDone } } ); const firstResult = result.current; const newOnLoadDone = jest.fn(); rerender({ onLoadDone: newOnLoadDone }); // Functions should remain the same due to empty dependency arrays (this is the current behavior) expect(result.current.onLoadFinished).toBe(firstResult.onLoadFinished); expect(result.current.onLoadFailed).toBe(firstResult.onLoadFailed); }); }); describe("edge cases", () => { it("should handle duplicate load finished calls gracefully", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(0); result.current.onLoadFinished(0); // Duplicate call }); const state = result.current.loadingState.getValue(); expect(state.index).toBe(0); expect(state.done).toBe(true); // True because loading component 0 makes it done in a 2-component setup }); it("should handle loading index greater than component count", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(5); // Index out of bounds }); const state = result.current.loadingState.getValue(); expect(state.index).toBe(5); expect(state.done).toBe(false); // Should not be done as not all components loaded }); it("should handle negative indices", () => { const { result } = renderHook(() => useLoadingState(2, onLoadDone)); act(() => { result.current.onLoadFinished(-1); }); const state = result.current.loadingState.getValue(); expect(state.index).toBe(-1); // Should remain -1 }); }); describe("component count changes", () => { it("should handle changing component count", () => { const { result, rerender } = renderHook( ({ count }) => useLoadingState(count, onLoadDone), { initialProps: { count: 2 } } ); act(() => { result.current.onLoadFinished(0); }); // Change count rerender({ count: 3 }); // The hook should work with the new count act(() => { result.current.onLoadFinished(1); result.current.onLoadFinished(2); }); const state = result.current.loadingState.getValue(); expect(state.done).toBe(true); }); }); });