Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions src/hooks/__tests__/useRiveFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHook, waitFor } from '@testing-library/react-native';
import { renderHook, waitFor, act } from '@testing-library/react-native';
import { useRiveFile } from '../useRiveFile';
import type { RiveFile } from '../../specs/RiveFile.nitro';

Expand All @@ -8,15 +8,95 @@ jest.mock('react-native/Libraries/Image/Image', () => ({
})),
}));

describe('useRiveFile - updateReferencedAssets', () => {
const mockRiveFile: RiveFile = {
function createMockRiveFile(): RiveFile {
return {
dispose: jest.fn(),
updateReferencedAssets: jest.fn(),
viewModelCount: 0,
viewModelByIndex: jest.fn(),
viewModelByName: jest.fn(),
defaultArtboardViewModel: jest.fn(),
} as any;
}

describe('useRiveFile - input stability', () => {
const mockRiveFile = createMockRiveFile();

beforeEach(() => {
jest.clearAllMocks();
// fromSource internally calls fromURL for http(s) URIs
(global as any).mockRiveFileFactory.fromURL.mockResolvedValue(mockRiveFile);
});

it('should not reload file when input object reference changes but uri is the same', async () => {
const { result, rerender } = renderHook(
(props: { input: { uri: string } }) => useRiveFile(props.input),
{ initialProps: { input: { uri: 'https://example.com/animation.riv' } } }
);

await waitFor(() => {
expect((result.current as any).isLoading).toBe(false);
});

const callCountBefore = (global as any).mockRiveFileFactory.fromURL.mock
.calls.length;

await act(async () => {
// Pass NEW object reference with SAME uri value
rerender({ input: { uri: 'https://example.com/animation.riv' } });
await new Promise((resolve) => setTimeout(resolve, 50));
});

const callCountAfter = (global as any).mockRiveFileFactory.fromURL.mock
.calls.length;
expect(callCountAfter).toBe(callCountBefore);
});

it('should stabilize after initial load when called with inline object', async () => {
let renderCount = 0;
const MAX_RENDERS = 10;
const url = 'https://example.com/animation.riv';

const { result, rerender } = renderHook(() => {
renderCount++;
if (renderCount > MAX_RENDERS) {
throw new Error(
`Infinite re-render detected: ${renderCount} renders exceeded max of ${MAX_RENDERS}`
);
}
// Simulate inline object creation (new reference each render)
return useRiveFile({ uri: url });
}, {});

// First render: isLoading=true
expect(renderCount).toBe(1);
expect(result.current.isLoading).toBe(true);

// Wait for file to load - this triggers setState and a re-render
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

const renderCountAfterLoad = renderCount;

// Simulate parent re-render (which creates new inline object)
await act(async () => {
rerender({});
await new Promise((resolve) => setTimeout(resolve, 50));
});

// Should only have 1 additional render from rerender(), no cascading re-renders
expect(renderCount).toBe(renderCountAfterLoad + 1);

// File should not have been reloaded
expect((global as any).mockRiveFileFactory.fromURL.mock.calls.length).toBe(
1
);
});
});

describe('useRiveFile - updateReferencedAssets', () => {
const mockRiveFile = createMockRiveFile();

beforeEach(() => {
jest.clearAllMocks();
Expand Down
16 changes: 14 additions & 2 deletions src/hooks/useRiveFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import type {
export type { ReferencedAssets, ResolvedReferencedAssets };
export type RiveFileInput = number | { uri: string } | string | ArrayBuffer;

function isUriInput(
input: RiveFileInput | undefined
): input is { uri: string } {
return input != null && typeof input === 'object' && 'uri' in input;
}

export type UseRiveFileOptions = {
referencedAssets?: ReferencedAssets;
};
Expand Down Expand Up @@ -101,12 +107,18 @@ export function useRiveFile(
);
const initialReferencedAssets = useRef(referencedAssets);

const inputKind = isUriInput(input) ? 'uri' : 'primitive';
const inputValue = isUriInput(input) ? input.uri : input;

useEffect(() => {
let currentFile: RiveFile | null = null;

const loadRiveFile = async () => {
try {
const currentInput = input;
const currentInput =
inputKind === 'uri'
? { uri: inputValue as string }
: (inputValue as Exclude<RiveFileInput, { uri: string }>);

if (currentInput == null) {
setResult({
Expand Down Expand Up @@ -164,7 +176,7 @@ export function useRiveFile(
callDispose(currentFile);
}
};
}, [input]);
}, [inputKind, inputValue]);

const { riveFile } = result;
useEffect(() => {
Expand Down
Loading