From ede8e429867042a6e0107204c0b2a4abf1979354 Mon Sep 17 00:00:00 2001 From: Jared Stoffan Date: Wed, 4 Nov 2020 17:30:21 -0800 Subject: [PATCH] feat(controls): Create react core controls for image viewers (#1285) --- src/lib/viewers/image/ImageBaseViewer.js | 12 ++++++- src/lib/viewers/image/ImageControls.tsx | 17 ++++++++++ src/lib/viewers/image/ImageViewer.js | 31 ++++++++++++++--- src/lib/viewers/image/MultiImageControls.tsx | 16 +++++++++ src/lib/viewers/image/MultiImageViewer.js | 31 ++++++++++++++--- .../image/__tests__/ImageBaseViewer-test.js | 24 ++++++++++++-- .../image/__tests__/ImageControls-test.jsx | 24 ++++++++++++++ .../image/__tests__/ImageViewer-test.js | 25 ++++++++++++-- .../__tests__/MultiImageControls-test.jsx | 24 ++++++++++++++ .../image/__tests__/MultiImageViewer-test.js | 33 +++++++++++++++---- 10 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 src/lib/viewers/image/ImageControls.tsx create mode 100644 src/lib/viewers/image/MultiImageControls.tsx create mode 100644 src/lib/viewers/image/__tests__/ImageControls-test.jsx create mode 100644 src/lib/viewers/image/__tests__/MultiImageControls-test.jsx diff --git a/src/lib/viewers/image/ImageBaseViewer.js b/src/lib/viewers/image/ImageBaseViewer.js index 380fc99f3..a4b7fe95e 100644 --- a/src/lib/viewers/image/ImageBaseViewer.js +++ b/src/lib/viewers/image/ImageBaseViewer.js @@ -1,6 +1,7 @@ import BaseViewer from '../BaseViewer'; import Browser from '../../Browser'; import Controls from '../../Controls'; +import ControlsRoot from '../controls/controls-root'; import PreviewError from '../../PreviewError'; import ZoomControls from '../../ZoomControls'; @@ -78,7 +79,12 @@ class ImageBaseViewer extends BaseViewer { const loadOriginalDimensions = this.setOriginalImageSize(this.imageEl); loadOriginalDimensions.then(() => { this.zoom(); - this.loadUI(); + + if (this.options.useReactControls) { + this.loadUIReact(); + } else { + this.loadUI(); + } this.imageEl.classList.remove(CLASS_INVISIBLE); this.loaded = true; @@ -203,6 +209,10 @@ class ImageBaseViewer extends BaseViewer { this.zoomControls.init(this.scale, { onZoomIn: this.zoomIn, onZoomOut: this.zoomOut }); } + loadUIReact() { + this.controls = new ControlsRoot({ containerEl: this.containerEl }); + } + /** * Sets the original image width and height on the img element. Can be removed when * naturalHeight and naturalWidth attributes work correctly in IE 11. diff --git a/src/lib/viewers/image/ImageControls.tsx b/src/lib/viewers/image/ImageControls.tsx new file mode 100644 index 000000000..b4bba1b9f --- /dev/null +++ b/src/lib/viewers/image/ImageControls.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ControlsBar from '../controls/controls-bar'; +import FullscreenToggle, { Props as FullscreenToggleProps } from '../controls/fullscreen'; +import ZoomControls, { Props as ZoomControlsProps } from '../controls/zoom'; + +export type Props = FullscreenToggleProps & ZoomControlsProps; + +export default function ImageControls({ onFullscreenToggle, onZoomIn, onZoomOut, scale }: Props): JSX.Element { + return ( + + + {/* TODO: RotateControl */} + + {/* TODO: AnnotationControls (separate group) */} + + ); +} diff --git a/src/lib/viewers/image/ImageViewer.js b/src/lib/viewers/image/ImageViewer.js index c1e248907..6344a2c5c 100644 --- a/src/lib/viewers/image/ImageViewer.js +++ b/src/lib/viewers/image/ImageViewer.js @@ -1,6 +1,8 @@ -import ImageBaseViewer from './ImageBaseViewer'; +import React from 'react'; import AnnotationControls, { AnnotationMode } from '../../AnnotationControls'; import AnnotationControlsFSM, { AnnotationInput, AnnotationState, stateModeMap } from '../../AnnotationControlsFSM'; +import ImageBaseViewer from './ImageBaseViewer'; +import ImageControls from './ImageControls'; import { CLASS_INVISIBLE, DISCOVERABILITY_ATTRIBUTE } from '../../constants'; import { ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT, ICON_ROTATE_LEFT } from '../../icons/icons'; import './Image.scss'; @@ -338,9 +340,8 @@ class ImageViewer extends ImageBaseViewer { ? width / this.imageEl.getAttribute('originalWidth') : height / this.imageEl.getAttribute('originalHeight'); this.rotationAngle = (this.currentRotationAngle % 3600) % 360; - if (this.zoomControls) { - this.zoomControls.setCurrentScale(this.scale); - } + this.renderUI(); + this.emit('scale', { scale: this.scale, rotationAngle: this.rotationAngle, @@ -378,6 +379,28 @@ class ImageViewer extends ImageBaseViewer { } } + loadUIReact() { + super.loadUIReact(); + this.renderUI(); + } + + renderUI() { + if (this.zoomControls) { + this.zoomControls.setCurrentScale(this.scale); + } + + if (this.controls && this.options.useReactControls) { + this.controls.render( + , + ); + } + } + /** * Determines if Image file has been rotated 90 or 270 degrees to the left * diff --git a/src/lib/viewers/image/MultiImageControls.tsx b/src/lib/viewers/image/MultiImageControls.tsx new file mode 100644 index 000000000..9506ac91d --- /dev/null +++ b/src/lib/viewers/image/MultiImageControls.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ControlsBar from '../controls/controls-bar'; +import FullscreenToggle, { Props as FullscreenToggleProps } from '../controls/fullscreen'; +import ZoomControls, { Props as ZoomControlsProps } from '../controls/zoom'; + +export type Props = FullscreenToggleProps & ZoomControlsProps; + +export default function MultiImageControls({ onFullscreenToggle, onZoomIn, onZoomOut, scale }: Props): JSX.Element { + return ( + + + {/* TODO: PageControls */} + + + ); +} diff --git a/src/lib/viewers/image/MultiImageViewer.js b/src/lib/viewers/image/MultiImageViewer.js index 078ec48f1..89a0d0d6c 100644 --- a/src/lib/viewers/image/MultiImageViewer.js +++ b/src/lib/viewers/image/MultiImageViewer.js @@ -1,9 +1,10 @@ +import React from 'react'; import ImageBaseViewer from './ImageBaseViewer'; +import MultiImageControls from './MultiImageControls'; import PageControls from '../../PageControls'; -import { ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../icons/icons'; import { CLASS_INVISIBLE, CLASS_MULTI_IMAGE_PAGE, CLASS_IS_SCROLLABLE } from '../../constants'; +import { ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../icons/icons'; import { pageNumberFromScroll } from '../../util'; - import './MultiImage.scss'; const PADDING_BUFFER = 100; @@ -246,9 +247,7 @@ class MultiImageViewer extends ImageBaseViewer { // Grab the first page image dimensions const imageEl = this.singleImageEls[0]; this.scale = width ? width / imageEl.naturalWidth : height / imageEl.naturalHeight; - if (this.zoomControls) { - this.zoomControls.setCurrentScale(this.scale); - } + this.renderUI(); this.emit('scale', { scale: this.scale }); } @@ -265,6 +264,28 @@ class MultiImageViewer extends ImageBaseViewer { this.bindPageControlListeners(); } + loadUIReact() { + super.loadUIReact(); + this.renderUI(); + } + + renderUI() { + if (this.zoomControls) { + this.zoomControls.setCurrentScale(this.scale); + } + + if (this.controls && this.options.useReactControls) { + this.controls.render( + , + ); + } + } + /** * Binds listeners for the page and the rest of the document controls. * diff --git a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js index 887167786..a142b3b35 100644 --- a/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageBaseViewer-test.js @@ -522,6 +522,7 @@ describe('lib/viewers/image/ImageBaseViewer', () => { imageBase.loaded = false; stubs.zoom = jest.spyOn(imageBase, 'zoom'); stubs.loadUI = jest.spyOn(imageBase, 'loadUI'); + stubs.loadUIReact = jest.spyOn(imageBase, 'loadUIReact'); stubs.setOriginalImageSize = jest.spyOn(imageBase, 'setOriginalImageSize'); imageBase.options = { file: { @@ -543,21 +544,40 @@ describe('lib/viewers/image/ImageBaseViewer', () => { expect(stubs.zoom).not.toBeCalled(); expect(stubs.setOriginalImageSize).not.toBeCalled(); expect(stubs.loadUI).not.toBeCalled(); + expect(stubs.loadUIReact).not.toBeCalled(); }); test('should load UI if not destroyed', done => { + stubs.setOriginalImageSize.mockResolvedValue(undefined); + imageBase.on(VIEWER_EVENT.load, () => { expect(imageBase.loaded).toBe(true); expect(stubs.zoom).toBeCalled(); expect(stubs.loadUI).toBeCalled(); + expect(stubs.loadUIReact).not.toBeCalled(); done(); }); - stubs.setOriginalImageSize.mockResolvedValue(undefined); - imageBase.destroyed = false; + imageBase.destroyed = false; imageBase.finishLoading(); + expect(stubs.setOriginalImageSize).toBeCalled(); }); + + test('should load react UI if option is provided', done => { + stubs.setOriginalImageSize.mockResolvedValue(undefined); + + imageBase.on(VIEWER_EVENT.load, () => { + expect(imageBase.loaded).toBe(true); + expect(stubs.zoom).toBeCalled(); + expect(stubs.loadUI).not.toBeCalled(); + expect(stubs.loadUIReact).toBeCalled(); + done(); + }); + imageBase.destroyed = false; + imageBase.options.useReactControls = true; + imageBase.finishLoading('', true); + }); }); describe('disableViewerControls()', () => { diff --git a/src/lib/viewers/image/__tests__/ImageControls-test.jsx b/src/lib/viewers/image/__tests__/ImageControls-test.jsx new file mode 100644 index 000000000..6cb3acee7 --- /dev/null +++ b/src/lib/viewers/image/__tests__/ImageControls-test.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ControlsBar from '../../controls/controls-bar'; +import FullscreenToggle from '../../controls/fullscreen'; +import ImageControls from '../ImageControls'; +import ZoomControls from '../../controls/zoom'; + +describe('ImageControls', () => { + describe('render', () => { + test('should return a valid wrapper', () => { + const onToggle = jest.fn(); + const onZoomIn = jest.fn(); + const onZoomOut = jest.fn(); + const wrapper = shallow( + , + ); + + expect(wrapper.exists(ControlsBar)); + expect(wrapper.find(FullscreenToggle).prop('onFullscreenToggle')).toEqual(onToggle); + expect(wrapper.find(ZoomControls).prop('onZoomIn')).toEqual(onZoomIn); + expect(wrapper.find(ZoomControls).prop('onZoomOut')).toEqual(onZoomOut); + }); + }); +}); diff --git a/src/lib/viewers/image/__tests__/ImageViewer-test.js b/src/lib/viewers/image/__tests__/ImageViewer-test.js index c2f453e7a..0a5c86437 100644 --- a/src/lib/viewers/image/__tests__/ImageViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageViewer-test.js @@ -1,9 +1,13 @@ -/* eslint-disable no-unused-expressions */ +import React from 'react'; import AnnotationControls, { AnnotationMode } from '../../../AnnotationControls'; import AnnotationControlsFSM, { AnnotationState, stateModeMap } from '../../../AnnotationControlsFSM'; -import ImageViewer from '../ImageViewer'; import BaseViewer from '../../BaseViewer'; import Browser from '../../../Browser'; +import ControlsRoot from '../../controls/controls-root'; +import ImageControls from '../ImageControls'; +import ImageViewer from '../ImageViewer'; + +jest.mock('../../controls/controls-root'); const imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAA4klEQVQoFWNkQABG7QlvU/7/Z0xmYGTQBgv/Z7jKyPh/7tUC4TlA/n+QGCOI0Ox/LcnIwLKEkZHBCcRHB///M+z7z/An5nqh6HMmoCRQHapiN1VWhlZXLrg+kEEgNSC1LCBnABkoJvOyMTKIcoMtR9EEUsuo1f/uBCMjozlcBg/j////J5ngHkRS6KrCylDvxIkkAmUCAwPkBwygIszE4KDEiiEOEmACBtZVrDLYBIFqmUDhjE0OmxhILSgogB5/vwdXHMA0guLiWqGgC8gPQPafGJAATBKdhkUcSC1yYBOVNAAVx0qxuz8xqgAAAABJRU5ErkJggg=='; @@ -411,6 +415,23 @@ describe('lib/viewers/image/ImageViewer', () => { ); }); + describe('loadUIReact()', () => { + test('should create controls root and render the react controls', () => { + image.options.useReactControls = true; + image.loadUIReact(); + + expect(image.controls).toBeInstanceOf(ControlsRoot); + expect(image.controls.render).toBeCalledWith( + , + ); + }); + }); + describe('isRotated()', () => { test('should return false if image is not rotated', () => { const result = image.isRotated(); diff --git a/src/lib/viewers/image/__tests__/MultiImageControls-test.jsx b/src/lib/viewers/image/__tests__/MultiImageControls-test.jsx new file mode 100644 index 000000000..af03ab878 --- /dev/null +++ b/src/lib/viewers/image/__tests__/MultiImageControls-test.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ControlsBar from '../../controls/controls-bar'; +import FullscreenToggle from '../../controls/fullscreen'; +import MultiImageControls from '../MultiImageControls'; +import ZoomControls from '../../controls/zoom'; + +describe('MultiImageControls', () => { + describe('render', () => { + test('should return a valid wrapper', () => { + const onToggle = jest.fn(); + const onZoomIn = jest.fn(); + const onZoomOut = jest.fn(); + const wrapper = shallow( + , + ); + + expect(wrapper.exists(ControlsBar)); + expect(wrapper.find(FullscreenToggle).prop('onFullscreenToggle')).toEqual(onToggle); + expect(wrapper.find(ZoomControls).prop('onZoomIn')).toEqual(onZoomIn); + expect(wrapper.find(ZoomControls).prop('onZoomOut')).toEqual(onZoomOut); + }); + }); +}); diff --git a/src/lib/viewers/image/__tests__/MultiImageViewer-test.js b/src/lib/viewers/image/__tests__/MultiImageViewer-test.js index 75971979e..81b5ecc50 100644 --- a/src/lib/viewers/image/__tests__/MultiImageViewer-test.js +++ b/src/lib/viewers/image/__tests__/MultiImageViewer-test.js @@ -1,14 +1,18 @@ -/* eslint-disable no-unused-expressions */ +import React from 'react'; +import * as util from '../../../util'; +import BaseViewer from '../../BaseViewer'; +import Browser from '../../../Browser'; +import ControlsRoot from '../../controls/controls-root'; +import ImageBaseViewer from '../ImageBaseViewer'; +import MultiImageControls from '../MultiImageControls'; import MultiImageViewer from '../MultiImageViewer'; import PageControls from '../../../PageControls'; +import ZoomControls from '../../../ZoomControls'; import fullscreen from '../../../Fullscreen'; import { CLASS_MULTI_IMAGE_PAGE } from '../../../constants'; -import BaseViewer from '../../BaseViewer'; -import ImageBaseViewer from '../ImageBaseViewer'; -import Browser from '../../../Browser'; -import * as util from '../../../util'; import { ICON_FULLSCREEN_IN, ICON_FULLSCREEN_OUT } from '../../../icons/icons'; -import ZoomControls from '../../../ZoomControls'; + +jest.mock('../../controls/controls-root'); const CLASS_INVISIBLE = 'bp-is-invisible'; @@ -377,6 +381,23 @@ describe('lib/viewers/image/MultiImageViewer', () => { }); }); + describe('loadUIReact()', () => { + test('should create controls root and render the react controls', () => { + multiImage.options.useReactControls = true; + multiImage.loadUIReact(); + + expect(multiImage.controls).toBeInstanceOf(ControlsRoot); + expect(multiImage.controls.render).toBeCalledWith( + , + ); + }); + }); + describe('bindPageControlListeners()', () => { beforeEach(() => { multiImage.currentPageNumber = 1;