@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
text/typescript
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);
});
});
});