Skip to content
Snippets Groups Projects
Unverified Commit df9b83fd authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Merge pull request #2332 from ProjectMirador/2331-basic-plugin-validation

Basic plugin validation. Closes #2331
parents 671d801f ba383f35
No related branches found
No related tags found
No related merge requests found
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Mirador</title>
</head>
<body>
<div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div>
<script src="../../../../node_modules/react/umd/react.development.js"></script>
<script src="../../../../node_modules/react-dom/umd/react-dom.development.js"></script>
<script>document.write("<script type='text/javascript' src='../../../../dist/mirador.min.js?v=" + Date.now() + "'><\/script>");</script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
const config = { id: 'mirador' };
const validPluginA = {
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="valid-plugin-a" />),
};
const validPluginB = {
name: 'validPluginB',
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="valid-plugin-b" />),
mapStateToProps: () => ({}),
mapDispatchToProps: {},
reducers: {
bar: (state = null, action) => state,
},
};
const invalidPluginA = {
name: 'invalidPluginA',
target: 'WorkspaceControlPanelButtons',
mode: 'LURK', // invalid
component: props => (<div id="invalid-plugin-a" />),
};
const invalidPluginB = {
name: 'invalidPluginB',
target: x => x, // invalid
mode: 'add',
component: props => (<div id="invalid-plugin-b" />),
};
const invalidPluginC = {
name: 'invalidPluginC',
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="invalid-plugin-c" />),
mapStateToProps: {}, // invalid
};
const invalidPluginD = {
name: 'invalidPluginD',
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="invalid-plugin-d" />),
mapDispatchToProps: "foo" // invalid
};
const invalidPluginE = {
name: 'invalidPluginE',
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="invalid-plugin-e" />),
reducers: 3, // invalid
};
const invalidPluginF = {
name: 'invalidPluginF',
target: 'WorkspaceControlPanelButtons',
mode: 'add',
component: props => (<div id="invalid-plugin-f" />),
reducers: {
foo: "foo", // invalid
},
};
const miradorInstance = Mirador.viewer(config, [
validPluginA,
validPluginB,
invalidPluginA,
invalidPluginB,
invalidPluginC,
invalidPluginD,
invalidPluginE,
invalidPluginF,
]);
</script>
</body>
</html>
describe('pass valid and invalid plugins to <WorkspaceControlPanelButtons>', () => {
beforeAll(async () => {
await page.goto('http://127.0.0.1:4488/__tests__/integration/mirador/plugins/validate.html');
await expect(page).toMatchElement('.mirador-viewer');
await page.waitFor(1000);
});
it('valid plugins will be applied <WorkspaceControlPanelButtons>', async () => {
await expect(page).toMatchElement('.mirador-workspace-control-panel-buttons #valid-plugin-a');
await expect(page).toMatchElement('.mirador-workspace-control-panel-buttons #valid-plugin-b');
});
it('invalid plugins will not be applied <WorkspaceControlPanelButtons>', async () => {
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-a');
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-b');
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-c');
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-d');
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-e');
await expect(page).not.toMatchElement('.mirador-workspace-control-panel-buttons #invalid-plugin-f');
});
});
......@@ -14,40 +14,63 @@ describe('getPlugins', () => {
pluginStore.storePlugins();
expect(pluginStore.getPlugins('target')).not.toBeDefined();
});
it('returns mode->plugins mapping for target', () => {
/** */
const component = x => x;
const plugins = [
{ mode: 'wrap', target: 'Window' },
{ mode: 'wrap', target: 'Window' },
{ mode: 'add', target: 'Window' },
{ mode: 'add', target: 'Window' },
{ mode: 'wrap', target: 'TopBar' },
{ mode: 'wrap', target: 'TopBar' },
{ mode: 'add', target: 'TopBar' },
{ mode: 'add', target: 'TopBar' },
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
];
pluginStore.storePlugins(plugins);
expect(pluginStore.getPlugins('Window')).toEqual({
add: [
{ mode: 'add', target: 'Window' },
{ mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
],
wrap: [
{ mode: 'wrap', target: 'Window' },
{ mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
],
});
expect(pluginStore.getPlugins('TopBar')).toEqual({
add: [
{ mode: 'add', target: 'TopBar' },
{ mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
],
wrap: [
{ mode: 'wrap', target: 'TopBar' },
{ mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
],
});
});
// see also pluginValidation.test.js
it('filter out invalid plugins', () => {
/** */
const component = x => x;
const plugins = [
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'LURK', target: 'Window' },
];
pluginStore.storePlugins(plugins);
expect(pluginStore.getPlugins('Window')).toEqual({
add: [
{ component, mode: 'add', target: 'Window' },
],
});
});
......
import { validatePlugin } from '../../../src/extend';
/** */
const createPlugin = props => ({
component: x => x,
mapDispatchToProps: x => x,
mapStateToProps: x => x,
mode: 'add',
name: 'test',
reducers: {
bar: x => x,
foo: x => x,
},
target: 'Window',
...props,
});
describe('validatePlugin', () => {
it('return true if plugin is valid', () => {
const plugin = createPlugin();
expect(validatePlugin(plugin)).toBe(true);
});
it('plugin must be a object', () => {
const plugin = [];
expect(validatePlugin(plugin)).toBe(false);
});
it('name must be undefined or string', () => {
let plugin = createPlugin({ name: undefined });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ name: 'test' });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ name: [] });
expect(validatePlugin(plugin)).toBe(false);
});
it('target must be string', () => {
let plugin = createPlugin({ target: undefined });
expect(validatePlugin(plugin)).toBe(false);
plugin = createPlugin({ target: 'test' });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ target: [] });
expect(validatePlugin(plugin)).toBe(false);
});
it('mode must be "add" or "wrap"', () => {
let plugin = createPlugin({ mode: undefined });
expect(validatePlugin(plugin)).toBe(false);
plugin = createPlugin({ mode: 'somethink' });
expect(validatePlugin(plugin)).toBe(false);
plugin = createPlugin({ mode: 'add' });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mode: 'wrap' });
expect(validatePlugin(plugin)).toBe(true);
});
it('component must be function', () => {
let plugin = createPlugin({ component: undefined });
expect(validatePlugin(plugin)).toBe(false);
plugin = createPlugin({ component: 'somethink' });
expect(validatePlugin(plugin)).toBe(false);
plugin = createPlugin({ component: x => x });
expect(validatePlugin(plugin)).toBe(true);
});
it('mapStateToProps must be undefined, null or function', () => {
let plugin = createPlugin({ mapStateToProps: undefined });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapStateToProps: x => x });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapStateToProps: null });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapStateToProps: 'something' });
expect(validatePlugin(plugin)).toBe(false);
});
it('mapDispatchToProps must be undefined, null, function or object', () => {
let plugin = createPlugin({ mapDispatchToProps: undefined });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapDispatchToProps: x => x });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapDispatchToProps: {} });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapDispatchToProps: null });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ mapDispatchToProps: 'something' });
expect(validatePlugin(plugin)).toBe(false);
});
it('reducers must be undefined or object', () => {
let plugin = createPlugin({ reducers: undefined });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ reducers: {} });
expect(validatePlugin(plugin)).toBe(true);
plugin = createPlugin({ reducers: 'something' });
expect(validatePlugin(plugin)).toBe(false);
});
it('each reducer must be a function', () => {
let reducers = { bar: x => x, foo: x => x };
let plugin = createPlugin({ reducers });
expect(validatePlugin(plugin)).toBe(true);
reducers = { bar: x => x, foo: undefined };
plugin = createPlugin({ reducers });
expect(validatePlugin(plugin)).toBe(false);
});
});
export * from './pluginStore';
export * from './withPlugins';
export * from './pluginValidation';
import update from 'lodash/update';
import { validatePlugin } from '.';
export const pluginStore = {
/**
......@@ -20,8 +21,10 @@ export const pluginStore = {
*
* @param {Array} plugins
*/
storePlugins(plugins) {
this.pluginMap = mapPlugins(plugins || []);
storePlugins(plugins = []) {
const { validPlugins, invalidPlugins } = filterPlugins(plugins);
logInvalidPlugins(invalidPlugins);
this.pluginMap = mapPlugins(validPlugins);
},
};
......@@ -48,3 +51,21 @@ function mapPlugins(plugins) {
update(map, [plugin.target, plugin.mode], x => [...x || [], plugin])
), {});
}
/** */
function filterPlugins(plugins) {
const filteredPlugins = { invalidPlugins: [], validPlugins: [] };
plugins.forEach(plugin => (
validatePlugin(plugin)
? filteredPlugins.validPlugins.push(plugin)
: filteredPlugins.invalidPlugins.push(plugin)
));
return filteredPlugins;
}
/** */
function logInvalidPlugins(plugins) {
plugins.forEach(plugin => (
console.log(`Mirador: Plugin ${plugin.name} is not valid and was rejected.`)
));
}
import isString from 'lodash/isString';
import isUndefined from 'lodash/isUndefined';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import isNull from 'lodash/isNull';
import values from 'lodash/values';
/** */
export const validatePlugin = plugin => [
checkPlugin,
checkName,
checkTarget,
checkMode,
checkComponent,
checkMapStateToProps,
checkMapDispatchToProps,
checkReducers,
].every(check => check(plugin));
/** */
const checkPlugin = plugin => isObject(plugin);
/** */
const checkName = (plugin) => {
const { name } = plugin;
return isUndefined(name) || isString(name);
};
/** */
const checkTarget = (plugin) => {
const { target } = plugin;
return isString(target);
};
/** */
const checkMode = (plugin) => {
const { mode } = plugin;
return ['add', 'wrap'].some(s => s === mode);
};
/** */
const checkComponent = (plugin) => {
const { component } = plugin;
return isFunction(component);
};
/** */
const checkMapStateToProps = (plugin) => {
const { mapStateToProps } = plugin;
return isUndefined(mapStateToProps)
|| isNull(mapStateToProps)
|| isFunction(mapStateToProps);
};
/** */
const checkMapDispatchToProps = (plugin) => {
const { mapDispatchToProps } = plugin;
return isUndefined(mapDispatchToProps)
|| isNull(mapDispatchToProps)
|| isFunction(mapDispatchToProps)
|| isObject(mapDispatchToProps);
};
/** */
const checkReducers = (plugin) => {
const { reducers } = plugin;
return isUndefined(reducers)
|| (isObject(reducers) && values(reducers).every(isFunction));
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment