import React from 'react'; import { mocked } from 'jest-mock'; import { renderHook, act } from '@testing-library/react-hooks'; import useRive from '../src/hooks/useRive'; import * as rive from '@rive-app/canvas'; import { render, waitFor } from '@testing-library/react'; jest.mock('@rive-app/canvas', () => ({ Rive: jest.fn().mockImplementation(() => ({ on: jest.fn(), stop: jest.fn(), })), Layout: jest.fn(), Fit: { Cover: 'cover', }, Alignment: { Center: 'center', }, EventType: { Load: 'load', }, StateMachineInputType: { Number: 1, Boolean: 2, Trigger: 3, }, })); describe('useRive', () => { let controlledRiveloadCb: () => void; let baseRiveMock: Partial; beforeEach(() => { baseRiveMock = { on: (_: rive.EventType, cb: rive.EventCallback) => ((controlledRiveloadCb as rive.EventCallback) = cb), stop: jest.fn(), stopRendering: jest.fn(), startRendering: jest.fn(), }; }); afterEach(() => { controlledRiveloadCb = () => {}; }); it('returns rive as null if no params are passed', () => { const { result } = renderHook(() => useRive()); expect(result.current.rive).toBe(null); expect(result.current.canvas).toBe(null); }); it('returns a rive object if the src object is set on the rive params and setCanvas is called', async () => { const params = { src: 'file-src', }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); expect(result.current.rive).toBe(baseRiveMock); expect(result.current.canvas).toBe(canvasSpy); }); it('updates the bounds if the container ref is set', async () => { const params = { src: 'file-src', }; const resizeToCanvasMock = jest.fn(); const riveMock = { ...baseRiveMock, resizeToCanvas: resizeToCanvasMock, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => riveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); await act(async () => { jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500); containerSpy.dispatchEvent(new Event('resize')); }); expect(result.current.rive).toBe(riveMock); expect(result.current.canvas).toBe(canvasSpy); expect(resizeToCanvasMock).toBeCalled(); }); it('calls cleanup on the rive object on unmount', async () => { const params = { src: 'file-src', }; const cleanupMock = jest.fn(); const riveMock = { ...baseRiveMock, cleanup: cleanupMock, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => riveMock); const canvasSpy = document.createElement('canvas'); const { result, unmount } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); unmount(); expect(cleanupMock).toBeCalled(); }); it('sets the bounds with the devicePixelRatio by default', async () => { const params = { src: 'file-src', }; global.devicePixelRatio = 2; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); // Height and width should be 2* the width and height returned from containers // bounding rect expect(canvasSpy).toHaveAttribute('height', '200'); expect(canvasSpy).toHaveAttribute('width', '200'); // Style height and width should be the same as returned from containers // bounding rect expect(canvasSpy).toHaveAttribute('style', 'width: 100px; height: 100px;'); }); it('sets the bounds with a specified customDevicePixelRatio if one is set', async () => { const params = { src: 'file-src', }; global.devicePixelRatio = 2; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params, { customDevicePixelRatio: 1 }) ); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); // Height and width should be 2* the width and height returned from containers // bounding rect expect(canvasSpy).toHaveAttribute('height', '100'); expect(canvasSpy).toHaveAttribute('width', '100'); // Style height and width should be the same as returned from containers // bounding rect expect(canvasSpy).toHaveAttribute('style', 'width: 100px; height: 100px;'); }); it('sets the a bounds without the devicePixelRatio if useDevicePixelRatio is false', async () => { const params = { src: 'file-src', }; const opts = { useDevicePixelRatio: false, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params, opts)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); // Height and width should be same as containers bounding rect expect(canvasSpy).toHaveAttribute('height', '100'); expect(canvasSpy).toHaveAttribute('width', '100'); }); it('uses artboard height to set bounds if fitCanvasToArtboardHeight is true', async () => { const params = { src: 'file-src', }; const opts = { useDevicePixelRatio: false, fitCanvasToArtboardHeight: true, }; const riveMock = { ...baseRiveMock, bounds: { maxX: 100, maxY: 50, }, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => riveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params, opts)); await act(async () => { result.current.setContainerRef(containerSpy); result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); // Height and width should be same as containers bounding rect expect(canvasSpy).toHaveAttribute('height', '50'); expect(canvasSpy).toHaveAttribute('width', '100'); // Container should have style set to height expect(containerSpy).toHaveAttribute('style', 'height: 50px;'); }); it('updates the playing animations when the animations param changes', async () => { const params = { src: 'file-src', animations: 'light', }; const playMock = jest.fn(); const stopMock = jest.fn(); const riveMock = { ...baseRiveMock, stop: stopMock, play: playMock, animationNames: ['light'], isPlaying: true, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => riveMock); const canvasSpy = document.createElement('canvas'); const { result, rerender } = renderHook((params) => useRive(params), { initialProps: params, }); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); rerender({ src: 'file-src', animations: 'dark', }); expect(stopMock).toBeCalledWith(['light']); expect(playMock).toBeCalledWith('dark'); }); it('updates the paused animation when the animations param changes if the animation is paused', async () => { const params = { src: 'file-src', animations: 'light', }; const playMock = jest.fn(); const pauseMock = jest.fn(); const stopMock = jest.fn(); const riveMock = { ...baseRiveMock, stop: stopMock, play: playMock, pause: pauseMock, animationNames: ['light'], isPlaying: false, isPaused: true, }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => riveMock); const canvasSpy = document.createElement('canvas'); const { result, rerender } = renderHook((params) => useRive(params), { initialProps: params, }); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); rerender({ src: 'file-src', animations: 'dark', }); expect(stopMock).toBeCalledWith(['light']); expect(pauseMock).toBeCalledWith('dark'); expect(playMock).not.toBeCalled(); }); it('does not set styles if className is passed in for the canvas container', async () => { const params = { src: 'file-src', }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); const { RiveComponent: RiveTestComponent } = result.current; const { container } = render( ); expect(container.firstChild).not.toHaveStyle('width: 50%'); }); it('has a canvas size of 0 by default', async () => { const params = { src: 'file-src', }; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); const { RiveComponent: RiveTestComponent } = result.current; const { container } = render(); expect(container.querySelector('canvas')).toHaveStyle('width: 0'); }); it('sets the canvas width and height after calculating the container size', async () => { const params = { src: 'file-src', }; global.devicePixelRatio = 2; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); expect(canvasSpy).toHaveStyle('height: 100px'); expect(canvasSpy).toHaveStyle('width: 100px'); }); it('updates the canvas dimensions and size if there is a new canvas size calculation', async () => { const params = { src: 'file-src', }; window.devicePixelRatio = 2; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); const containerSpy = document.createElement('div'); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); const { result } = renderHook(() => useRive(params)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(200); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(200); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); await act(async () => { containerSpy.dispatchEvent(new Event('resize')); }); expect(canvasSpy).toHaveAttribute('width', '400'); expect(canvasSpy).toHaveAttribute('height', '400'); }); it('prevents resizing if shouldResizeCanvasToContainer option is false', async () => { const params = { src: 'file-src', }; const options = { shouldResizeCanvasToContainer: false, }; window.devicePixelRatio = 2; // @ts-ignore mocked(rive.Rive).mockImplementation(() => baseRiveMock); const canvasSpy = document.createElement('canvas'); canvasSpy.width = 200; canvasSpy.height = 200; const containerSpy = document.createElement('div'); const { result } = renderHook(() => useRive(params, options)); await act(async () => { result.current.setCanvasRef(canvasSpy); result.current.setContainerRef(containerSpy); }); await waitFor(() => { expect(result.current.canvas).toBe(canvasSpy); }); await act(async () => { controlledRiveloadCb(); }); await act(async () => { jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500); jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500); containerSpy.dispatchEvent(new Event('resize')); }); expect(canvasSpy.width).toBe(200); expect(canvasSpy.height).toBe(200); }); });