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());
});
});