diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js deleted file mode 100644 index 5eb139764d36d3e5ca43cb6f3ba0e7063806461b..0000000000000000000000000000000000000000 --- a/__tests__/src/lib/MiradorViewer.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import ReactDOM from 'react-dom'; -import { pluginStore } from '../../../src/extend'; -import MiradorViewer from '../../../src/lib/MiradorViewer'; - -jest.unmock('react-i18next'); -jest.mock('../../../src/extend'); -jest.mock('react-dom'); - -describe('MiradorViewer', () => { - let instance; - beforeAll(() => { - ReactDOM.render = jest.fn(); - instance = new MiradorViewer({}); - }); - describe('constructor', () => { - it('returns viewer actions', () => { - expect(instance.actions.addWindow).toBeDefined(); - }); - it('returns viewer store', () => { - expect(instance.store.dispatch).toBeDefined(); - }); - it('renders via ReactDOM', () => { - expect(ReactDOM.render).toHaveBeenCalled(); - }); - }); - describe('process plugins', () => { - it('should store plugins and set reducers to state', () => { - /** */ const fooReducer = (state = 0) => state; - /** */ const barReducer = (state = 0) => state; - /** */ const bazReducer = (state = 0) => state; - /** */ const plugins = [ - { - reducers: { - foo: fooReducer, - bar: barReducer, - }, - }, - { - reducers: { - baz: bazReducer, - }, - }, - ]; - instance = new MiradorViewer({}, plugins); - expect(pluginStore.storePlugins).toBeCalledWith(plugins); - expect(instance.store.getState().foo).toBeDefined(); - expect(instance.store.getState().bar).toBeDefined(); - expect(instance.store.getState().baz).toBeDefined(); - }); - }); - describe('processConfig', () => { - it('transforms config values to actions to dispatch to store', () => { - instance = new MiradorViewer({ - id: 'mirador', - windows: [ - { - loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843', - canvasIndex: 2, - }, - { - loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843', - thumbnailNavigationPosition: 'off', - view: 'book', - }, - ], - manifests: { - 'http://media.nga.gov/public/manifests/nga_highlights.json': { provider: 'National Gallery of Art' }, - }, - }); - - const { windows, manifests } = instance.store.getState(); - const windowIds = Object.keys(windows); - expect(Object.keys(windowIds).length).toBe(2); - expect(windows[windowIds[0]].canvasIndex).toBe(2); - expect(windows[windowIds[1]].canvasIndex).toBe(0); - expect(windows[windowIds[0]].thumbnailNavigationPosition).toBe('far-bottom'); - expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe('off'); - expect(windows[windowIds[0]].view).toBe('single'); - expect(windows[windowIds[1]].view).toBe('book'); - - const manifestIds = Object.keys(manifests); - expect(Object.keys(manifestIds).length).toBe(2); - expect(manifests['http://media.nga.gov/public/manifests/nga_highlights.json'].provider).toBe('National Gallery of Art'); - }); - }); -}); diff --git a/__tests__/src/lib/miradorViewer.test.js b/__tests__/src/lib/miradorViewer.test.js new file mode 100644 index 0000000000000000000000000000000000000000..4f5dc63d4f347eb4b05f9cb106ac824342f9beb1 --- /dev/null +++ b/__tests__/src/lib/miradorViewer.test.js @@ -0,0 +1,181 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import deepmerge from 'deepmerge'; +import miradorViewer from '../../../src/lib/miradorViewer'; + +jest.mock('react-dom', () => ({ + render: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + Provider: jest.fn(), +})); + +const config = { + id: 'mirador', + windows: [ + { + loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843', + canvasIndex: 2, + }, + { + loadedManifest: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/e32a277e-91e2-4a6d-8ba6-cc4bad230410.json', + thumbnailNavigationPosition: 'off', + view: 'book', + }, + ], + manifests: { + 'https://media.nga.gov/public/manifests/nga_highlights.json': { provider: 'National Gallery of Art' }, + 'https://data.ucd.ie/api/img/manifests/ucdlib:33064': { provider: 'Irish Architectural Archive' }, + }, +}; + +const settings = { + window: { + defaultView: 'single', + }, + thumbnailNavigation: { + defaultPosition: 'bottom', + height: 150, + width: 100, + }, +}; + +/** */ const fooReducer = x => x; +/** */ const barReducer = x => x; +/** */ const bazReducer = x => x; + +const plugins = [ + { + reducers: { + foo: fooReducer, + bar: barReducer, + }, + }, + { + reducers: { + baz: bazReducer, + }, + }, +]; + +const pluginStore = { + storePlugins: jest.fn(), +}; + +const store = { + dispatch: jest.fn(), +}; + +const createStore = jest.fn().mockReturnValue(store); + +const actions = { + setConfig: jest.fn().mockReturnValue('RETVAL_SET_CONFIG'), + addWindow: jest.fn().mockReturnValue('RETVAL_ADD_WINDOW'), + fetchManifest: jest.fn().mockReturnValue('RETVAL_FETCH_MANIFEST'), + requestManifest: jest.fn().mockReturnValue('RETVAL_REQUEST_MANIFEST'), +}; + +/** */const App = props => null; + +/** +* Finally invoke function under test +*/ +const retval = miradorViewer({ + config, + settings, + plugins, + pluginStore, + createStore, + actions, + App, +}); + +it('should store plugins', () => { + expect(pluginStore.storePlugins).toBeCalledWith(plugins); +}); + +it('should create store and pass plugin reducers', () => { + const pluginReducers = { + foo: fooReducer, + bar: barReducer, + baz: bazReducer, + }; + expect(createStore).toBeCalledWith(pluginReducers); +}); + +it('should merge settings and config and write it to state', () => { + const merged = deepmerge(settings, config); + expect(actions.setConfig).toBeCalledWith(merged); + expect(store.dispatch).nthCalledWith(1, 'RETVAL_SET_CONFIG'); +}); + +it('should fetch manifest for each window in config', () => { + expect(actions.fetchManifest) + .toBeCalledWith(config.windows[0].loadedManifest); + expect(actions.fetchManifest) + .toBeCalledWith(config.windows[1].loadedManifest); + expect(store.dispatch) + .nthCalledWith(2, 'RETVAL_FETCH_MANIFEST'); + expect(store.dispatch) + .nthCalledWith(4, 'RETVAL_FETCH_MANIFEST'); +}); + +it('should create a window in state for each window in config', () => { + expect(actions.addWindow).toBeCalledTimes(2); + expect(store.dispatch).nthCalledWith(3, 'RETVAL_ADD_WINDOW'); + expect(store.dispatch).nthCalledWith(5, 'RETVAL_ADD_WINDOW'); +}); + +it('should set correct canvas index to windows in state', () => { + expect(actions.addWindow.mock.calls[0][0].canvasIndex).toBe(2); + expect(actions.addWindow.mock.calls[1][0].canvasIndex).toBe(0); +}); + +it('should set correct manifest id to windows in state', () => { + expect(actions.addWindow.mock.calls[0][0].manifestId) + .toBe(config.windows[0].loadedManifest); + expect(actions.addWindow.mock.calls[1][0].manifestId) + .toBe(config.windows[1].loadedManifest); +}); + +it('should set correct thumbnail posistion to windows in state', () => { + expect(actions.addWindow.mock.calls[0][0].thumbnailNavigationPosition) + .toBe('bottom'); + expect(actions.addWindow.mock.calls[1][0].thumbnailNavigationPosition) + .toBe('off'); +}); + +it('should set correct view type to windows in state', () => { + expect(actions.addWindow.mock.calls[0][0].view).toBe('single'); + expect(actions.addWindow.mock.calls[1][0].view).toBe('book'); +}); + +it('should "request manifest" for each manifest in config', () => { + expect(actions.requestManifest).nthCalledWith( + 1, + 'https://media.nga.gov/public/manifests/nga_highlights.json', + { provider: 'National Gallery of Art' }, + ); + expect(actions.requestManifest).nthCalledWith( + 2, + 'https://data.ucd.ie/api/img/manifests/ucdlib:33064', + { provider: 'Irish Architectural Archive' }, + ); + expect(store.dispatch).nthCalledWith(6, 'RETVAL_REQUEST_MANIFEST'); + expect(store.dispatch).nthCalledWith(7, 'RETVAL_REQUEST_MANIFEST'); +}); + +it('should render app and provide it with the store', () => { + const AppWithStore = ( + <Provider store={store}> + <App /> + </Provider> + ); + expect(ReactDOM.render.mock.calls[0][0]).toEqual(AppWithStore); +}); + +it('should return store and actions', () => { + expect(retval).toEqual({ store, actions }); +}); diff --git a/src/index.js b/src/index.js index 660bc6849bf716a21fe2c8ee274ee37ad25cb6d3..610580f6ed626235071e98859ea9aa9eb1b79ad5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import init from './init'; +import { initViewer } from './init'; import * as actions from './state/actions'; import * as selectors from './state/selectors'; @@ -6,7 +6,7 @@ export * from './components'; export * from './state/reducers'; const exports = { - viewer: init, + viewer: initViewer, actions, selectors, }; diff --git a/src/init.js b/src/init.js index 0f3f58b73db07565bd79be0d6dd4e83e8490fb91..d9c9bde7eb845994d7eb84d9121a586ef56c9655 100644 --- a/src/init.js +++ b/src/init.js @@ -1,8 +1,21 @@ -import MiradorViewer from './lib/MiradorViewer'; - +import miradorViewer from './lib/miradorViewer'; +import settings from './config/settings'; +import { pluginStore } from './extend'; +import createStore from './state/createStore'; +import * as actions from './state/actions'; +import App from './containers/App'; +import './styles/index.scss'; /** - * Default Mirador instantiation + * Init mirador viewer */ -export default function (config, plugins) { - return new MiradorViewer(config, plugins); +export function initViewer(config, plugins) { + return miradorViewer({ + config, + settings, + plugins, + pluginStore, + createStore, + actions, + App, + }); } diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js deleted file mode 100644 index e9cf6296f624e6dc55e5adbdb2c640a36ed9774b..0000000000000000000000000000000000000000 --- a/src/lib/MiradorViewer.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Provider } from 'react-redux'; -import deepmerge from 'deepmerge'; -import App from '../containers/App'; -import { pluginStore } from '../extend'; -import createStore from '../state/createStore'; -import * as actions from '../state/actions'; -import settings from '../config/settings'; - -/** - * Default Mirador instantiation - */ -class MiradorViewer { - /** - */ - constructor(config, plugins) { - pluginStore.storePlugins(plugins); - const pluginReducers = getReducersFromPlugins(plugins); - this.store = createStore(pluginReducers); - this.config = config; - this.processConfig(); - const viewer = { - actions, - store: this.store, - }; - - ReactDOM.render( - <Provider store={this.store}> - <App /> - </Provider>, - document.getElementById(config.id), - ); - - return viewer; - } - - /** - * Process config into actions - */ - processConfig() { - const mergedConfig = deepmerge(settings, this.config); - const action = actions.setConfig(mergedConfig); - this.store.dispatch(action); - - mergedConfig.windows.forEach((miradorWindow) => { - let thumbnailNavigationPosition; - let view; - if (miradorWindow.thumbnailNavigationPosition !== undefined) { - ({ thumbnailNavigationPosition } = miradorWindow); - } else { - thumbnailNavigationPosition = mergedConfig.thumbnailNavigation.defaultPosition; - } - if (miradorWindow.view !== undefined) { - ({ view } = miradorWindow); - } else { - view = mergedConfig.window.defaultView; - } - this.store.dispatch(actions.fetchManifest(miradorWindow.loadedManifest)); - this.store.dispatch(actions.addWindow({ - canvasIndex: (miradorWindow.canvasIndex || 0), - manifestId: miradorWindow.loadedManifest, - view, - thumbnailNavigationPosition, - })); - }); - - Object.keys(mergedConfig.manifests || {}).forEach((manifestId) => { - this.store.dispatch( - actions.requestManifest(manifestId, mergedConfig.manifests[manifestId]), - ); - }); - } -} - -/** Return reducers from plugins */ -function getReducersFromPlugins(plugins) { - return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {}); -} - -export default MiradorViewer; diff --git a/src/lib/miradorViewer.js b/src/lib/miradorViewer.js new file mode 100644 index 0000000000000000000000000000000000000000..62747d2a89f4f764d5c998cb6fbc7bc78a996964 --- /dev/null +++ b/src/lib/miradorViewer.js @@ -0,0 +1,68 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import deepmerge from 'deepmerge'; + +/** Default Mirador instantiation */ +export default function ({ + config, + settings, + plugins, + pluginStore, + createStore, + actions, + App, +}) { + pluginStore.storePlugins(plugins); + const store = createStore(getReducersFromPlugins(plugins)); + processConfig(store, actions, config, settings); + + ReactDOM.render( + <Provider store={store}> + <App /> + </Provider>, + document.getElementById(config.id), + ); + + return { store, actions }; +} + +/** Return reducers from plugins */ +function getReducersFromPlugins(plugins) { + return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {}); +} + +/** Process config */ +function processConfig(store, actions, config, settings) { // eslint-disable-line no-shadow + const mergedConfig = writeConfigToState(store, actions, config, settings); + processWindowsFromConfig(store, actions, mergedConfig); + processManifestsFromConfig(store, actions, mergedConfig); +} + +/** Write config to state */ +function writeConfigToState(store, actions, config, settings) { // eslint-disable-line no-shadow + const mergedConfig = deepmerge(settings, config); + store.dispatch(actions.setConfig(mergedConfig)); + return mergedConfig; +} + +/** Process windows from config */ +function processWindowsFromConfig(store, actions, config) { // eslint-disable-line no-shadow + config.windows.forEach((win) => { + store.dispatch(actions.fetchManifest(win.loadedManifest)); + store.dispatch(actions.addWindow({ + canvasIndex: win.canvasIndex || 0, + manifestId: win.loadedManifest, + thumbnailNavigationPosition: + win.thumbnailNavigationPosition || config.thumbnailNavigation.defaultPosition, + view: win.view || config.window.defaultView, + })); + }); +} + +/** Process manifests from config */ +function processManifestsFromConfig(store, actions, config) { // eslint-disable-line no-shadow + Object.keys(config.manifests || {}).forEach((manifestId) => { + store.dispatch(actions.requestManifest(manifestId, config.manifests[manifestId])); + }); +}