import React from "react"; import renderer, {act} from "react-test-renderer"; // Mock RNViewShot before importing ViewShot const mockCaptureRef = jest.fn().mockResolvedValue("/tmp/captured.png"); const mockReleaseCapture = jest.fn(); jest.mock("../RNViewShot", () => ({ __esModule: true, default: { captureRef: mockCaptureRef, captureScreen: jest.fn(), releaseCapture: mockReleaseCapture, }, })); import ViewShot from "../index"; describe("ViewShot component", () => { beforeEach(() => { jest.clearAllMocks(); }); it("renders with collapsable={false} and onLayout", async () => { let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( Hello , ); }); const json = tree!.toJSON() as any; expect(json).toBeTruthy(); expect(json.props.collapsable).toBe(false); expect(json.props.onLayout).toBeDefined(); await act(async () => tree!.unmount()); }); it("passes style prop to the outer View", async () => { const style = {flex: 1, backgroundColor: "red"}; let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); const json = tree!.toJSON() as any; expect(json.props.style).toEqual(style); await act(async () => tree!.unmount()); }); it("warns when captureMode is set without onCapture", async () => { const warnSpy = jest.spyOn(console, "warn").mockImplementation(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("onCapture prop callback is missing"), ); warnSpy.mockRestore(); await act(async () => tree!.unmount()); }); it("warns when continuous mode uses non-tmpfile result", async () => { const warnSpy = jest.spyOn(console, "warn").mockImplementation(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("result=tmpfile is recommended"), ); warnSpy.mockRestore(); await act(async () => tree!.unmount()); }); it("warns when update mode uses non-tmpfile result", async () => { const warnSpy = jest.spyOn(console, "warn").mockImplementation(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("result=tmpfile is recommended"), ); warnSpy.mockRestore(); await act(async () => tree!.unmount()); }); it("does not warn when captureMode is undefined with onCapture", async () => { const warnSpy = jest.spyOn(console, "warn").mockImplementation(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); const viewShotWarnings = warnSpy.mock.calls.filter( call => typeof call[0] === "string" && call[0].includes("react-native-view-shot"), ); expect(viewShotWarnings).toHaveLength(0); warnSpy.mockRestore(); await act(async () => tree!.unmount()); }); it("does not warn for valid continuous + onCapture + tmpfile", async () => { const warnSpy = jest.spyOn(console, "warn").mockImplementation(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); const viewShotWarnings = warnSpy.mock.calls.filter( call => typeof call[0] === "string" && call[0].includes("react-native-view-shot"), ); expect(viewShotWarnings).toHaveLength(0); warnSpy.mockRestore(); await act(async () => tree!.unmount()); }); it("static captureRef and releaseCapture are exposed", () => { expect(typeof ViewShot.captureRef).toBe("function"); expect(typeof ViewShot.releaseCapture).toBe("function"); }); it("starts requestAnimationFrame loop for continuous mode", async () => { const rafSpy = jest.spyOn(global, "requestAnimationFrame"); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); expect(rafSpy).toHaveBeenCalled(); await act(async () => tree!.unmount()); rafSpy.mockRestore(); }); it("cancels animation frame on unmount", async () => { const cancelSpy = jest.spyOn(global, "cancelAnimationFrame"); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); await act(async () => tree!.unmount()); expect(cancelSpy).toHaveBeenCalled(); cancelSpy.mockRestore(); }); it("capture() waits for first layout before calling captureRef", async () => { let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); // captureRef not called yet — firstLayoutPromise not resolved expect(mockCaptureRef).not.toHaveBeenCalled(); await act(async () => tree!.unmount()); }); it("re-syncs capture loop when captureMode changes", async () => { const rafSpy = jest.spyOn(global, "requestAnimationFrame"); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); rafSpy.mockClear(); await act(async () => { tree!.update(); }); expect(rafSpy).toHaveBeenCalled(); await act(async () => tree!.unmount()); rafSpy.mockRestore(); }); it("captures on update when captureMode is 'update'", async () => { let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); // Trigger layout to resolve firstLayoutPromise const json = tree!.toJSON() as any; await act(async () => { json.props.onLayout({ nativeEvent: {layout: {x: 0, y: 0, width: 100, height: 100}}, }); }); mockCaptureRef.mockClear(); // Trigger componentDidUpdate await act(async () => { tree!.update( , ); }); // Flush microtask + macrotask queue for capture() promise chain await new Promise(r => setTimeout(r, 50)); expect(mockCaptureRef).toHaveBeenCalled(); await act(async () => tree!.unmount()); }); it("calls onCapture callback on successful capture", async () => { const onCapture = jest.fn(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create( , ); }); // Trigger layout const json = tree!.toJSON() as any; await act(async () => { json.props.onLayout({ nativeEvent: {layout: {x: 0, y: 0, width: 100, height: 100}}, }); }); // Trigger update await act(async () => { tree!.update( , ); }); await new Promise(r => setTimeout(r, 50)); expect(onCapture).toHaveBeenCalledWith("/tmp/captured.png"); await act(async () => tree!.unmount()); }); it("forwards ref to the inner View node with capture() attached", async () => { const ref = React.createRef(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); expect(ref.current).toBeTruthy(); // Inner View mock exposes _nativeTag via useImperativeHandle, so // findNodeHandle(ref.current) keeps working as in v4. expect(ref.current._nativeTag).toBe(1); expect(typeof ref.current.capture).toBe("function"); await act(async () => tree!.unmount()); }); it("supports a function ref forwarded to the inner View", async () => { const refFn = jest.fn(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); const calls = refFn.mock.calls.filter(([n]) => n); expect(calls.length).toBeGreaterThan(0); const node = calls[calls.length - 1][0]; expect(node._nativeTag).toBe(1); expect(typeof node.capture).toBe("function"); await act(async () => tree!.unmount()); }); it("ref.current.capture() resolves via native captureRef after first layout", async () => { const ref = React.createRef(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); const json = tree!.toJSON() as any; await act(async () => { json.props.onLayout({ nativeEvent: {layout: {x: 0, y: 0, width: 100, height: 100}}, }); }); await expect(ref.current.capture()).resolves.toBe("/tmp/captured.png"); expect(mockCaptureRef).toHaveBeenCalled(); await act(async () => tree!.unmount()); }); it("forwards onLayout to props.onLayout", async () => { const onLayout = jest.fn(); let tree: renderer.ReactTestRenderer; await act(async () => { tree = renderer.create(); }); const json = tree!.toJSON() as any; const event = { nativeEvent: {layout: {x: 0, y: 0, width: 200, height: 300}}, }; await act(async () => { json.props.onLayout(event); }); expect(onLayout).toHaveBeenCalledWith(event); await act(async () => tree!.unmount()); }); });