UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

220 lines (219 loc) 9.04 kB
import { act, renderHook } from "@testing-library/react"; import isEqual from "lodash/isEqual"; import { Graph } from "../../graph"; import { Layer } from "../../services/Layer"; import { useLayer } from "./useLayer"; // Mock dependencies jest.mock("lodash/isEqual"); const mockedIsEqual = isEqual; // Helper function to create valid layer props for testing const createValidLayerProps = () => ({ canvas: { zIndex: 1 }, html: { zIndex: 2 }, }); // Mock Layer constructor for testing class TestLayer extends Layer { constructor() { super(...arguments); this.setProps = jest.fn(); } } describe("useLayer hook", () => { // Real instances let graph; let addLayerSpy; let detachLayerSpy; // Mock console.error to suppress act warnings let consoleErrorSpy; beforeEach(() => { // Clear all mocks jest.clearAllMocks(); // Create a real Graph instance graph = new Graph({}); // Spy on its methods addLayerSpy = jest.spyOn(graph, "addLayer"); detachLayerSpy = jest.spyOn(graph, "detachLayer"); // Mock the Layer's setProps method when it's created addLayerSpy.mockImplementation(() => { const layer = new TestLayer({ camera: graph.cameraService, graph }); jest.spyOn(layer, "setProps"); return layer; }); // Setup isEqual mock with default implementation mockedIsEqual.mockImplementation((a, b) => JSON.stringify(a) === JSON.stringify(b)); // Mock console.error to avoid act warnings consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { }); }); afterEach(() => { // Restore console.error consoleErrorSpy.mockRestore(); }); describe("Initial rendering", () => { it("should return null when graph is null", () => { // Execute const { result } = renderHook(() => useLayer(null, TestLayer, createValidLayerProps())); // Verify expect(result.current).toBeNull(); expect(addLayerSpy).not.toHaveBeenCalled(); }); it("should add layer to graph when graph is provided", () => { // Setup const props = createValidLayerProps(); // Execute const { result } = renderHook(() => useLayer(graph, TestLayer, props)); // Verify expect(result.current).not.toBeNull(); expect(addLayerSpy).toHaveBeenCalledWith(TestLayer, props); expect(addLayerSpy).toHaveBeenCalledTimes(1); }); }); describe("Layer lifecycle", () => { it("should detach layer when component unmounts", () => { // Execute const { unmount, result } = renderHook(() => useLayer(graph, TestLayer, createValidLayerProps())); // Get the created layer const layer = result.current; // Trigger unmount inside act act(() => { unmount(); }); // Verify expect(detachLayerSpy).toHaveBeenCalledWith(layer); expect(detachLayerSpy).toHaveBeenCalledTimes(1); }); it("should handle null graph safely on unmount", () => { // First render with a graph const { rerender, unmount } = renderHook(({ g }) => useLayer(g, TestLayer, createValidLayerProps()), { initialProps: { g: graph }, }); // Then change to null graph act(() => { rerender({ g: null }); }); // Unmount should not cause errors act(() => { unmount(); }); // Test passes if no error is thrown expect(true).toBe(true); }); }); describe("Props management", () => { it("should pass initial props to layer via addLayer", () => { // Setup const initialProps = createValidLayerProps(); // Execute renderHook(() => useLayer(graph, TestLayer, initialProps)); // Verify expect(addLayerSpy).toHaveBeenCalledWith(TestLayer, initialProps); }); it("should call setProps when props change", () => { // Setup const initialProps = createValidLayerProps(); const newProps = { ...createValidLayerProps(), canvas: { zIndex: 3 }, // Changed prop }; // Mock isEqual to return false, indicating props have changed mockedIsEqual.mockReturnValue(false); // Execute initial render const { rerender, result } = renderHook(({ props }) => useLayer(graph, TestLayer, props), { initialProps: { props: initialProps }, }); // Get the layer instance const layer = result.current; // Reset the calls to setProps after initial render layer.setProps.mockClear(); // Re-render with new props - use act for React 18 act(() => { rerender({ props: newProps }); }); // Verify expect(layer.setProps).toHaveBeenCalledWith(newProps); expect(layer.setProps).toHaveBeenCalledTimes(1); }); it("should not call setProps when props have not changed", () => { // Setup const props = createValidLayerProps(); // Mock isEqual to return true, indicating props have not changed mockedIsEqual.mockReturnValue(true); // Execute initial render const { rerender, result } = renderHook(({ props }) => useLayer(graph, TestLayer, props), { initialProps: { props }, }); // Get the layer instance const layer = result.current; // Reset the calls to setProps after initial render layer.setProps.mockClear(); // Re-render with identical props (new object, same values) act(() => { rerender({ props: { ...props } }); }); // Verify expect(layer.setProps).not.toHaveBeenCalled(); }); }); describe("Edge cases", () => { it("should recreate layer when graph reference changes", () => { // Setup initial graph const initialGraph = graph; // Setup new graph const newGraph = new Graph({}); const newGraphAddLayerSpy = jest.spyOn(newGraph, "addLayer"); newGraphAddLayerSpy.mockImplementation(() => { const layer = new TestLayer({ camera: newGraph.cameraService, graph: newGraph }); jest.spyOn(layer, "setProps"); return layer; }); // Execute initial render const { result, rerender } = renderHook(({ g }) => useLayer(g, TestLayer, createValidLayerProps()), { initialProps: { g: initialGraph }, }); // Get initial layer const initialLayer = result.current; // Verify initial state expect(initialLayer).not.toBeNull(); expect(addLayerSpy).toHaveBeenCalledTimes(1); // Re-render with new graph - wrap in act act(() => { rerender({ g: newGraph }); }); // Get new layer const newLayer = result.current; // Verify layer was recreated expect(newLayer).not.toBe(initialLayer); expect(newGraphAddLayerSpy).toHaveBeenCalledTimes(1); }); it("should use usePrevious hook for props comparison", () => { // Setup const initialProps = createValidLayerProps(); const newProps = { ...initialProps, canvas: { zIndex: 5 }, }; // Mock a specific implementation for isEqual that we can track let comparedNewProps = null; mockedIsEqual.mockImplementation((a, b) => { comparedNewProps = b; return false; // Always trigger setProps }); // Execute initial render const { rerender, result } = renderHook(({ props }) => useLayer(graph, TestLayer, props), { initialProps: { props: initialProps }, }); // Get the layer instance const layer = result.current; // Clear calls from initial render mockedIsEqual.mockClear(); layer.setProps.mockClear(); // Re-render with new props act(() => { rerender({ props: newProps }); }); // Verify isEqual was called with previous and current props expect(mockedIsEqual).toHaveBeenCalledTimes(1); expect(comparedNewProps).toEqual(newProps); expect(layer.setProps).toHaveBeenCalledWith(newProps); }); }); });