Merge pull request #5289 from storybooks/5271-persist-state

Add a persistence API to context/state -- use for keyboard shortcuts
This commit is contained in:
Norbert de Langen 2019-01-23 11:25:09 +01:00 committed by GitHub
commit 4508aa4ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 516 additions and 3273 deletions

View File

@ -97,12 +97,7 @@ module.exports = {
},
overrides: [
{
files: [
'**/__tests__/**',
'**/*.test.js/**',
'**/*.stories.js',
'**/storyshots/**/stories/**',
],
files: ['**/__tests__/**', '**/*.test.js', '**/*.stories.js', '**/storyshots/**/stories/**'],
rules: {
'import/no-extraneous-dependencies': ignore,
},

View File

@ -92,6 +92,7 @@
"webpack-hot-middleware": "^2.24.3"
},
"devDependencies": {
"flush-promises": "^1.0.2",
"mock-fs": "^4.7.0"
},
"peerDependencies": {

View File

@ -3,8 +3,7 @@ import memoize from 'memoizerific';
import { Badge } from '@storybook/components';
import { get } from '../settings/persist';
import { keyToSymbol, serializableKeyboardShortcuts } from '../settings/utils';
import { keyToSymbol } from '../settings/utils';
import Nav from '../components/nav/nav';
import { Consumer } from '../core/context';
@ -103,7 +102,7 @@ export const mapper = (state, api) => {
storiesHash,
} = state;
const shortcutKeys = get('shortcutKeys') || serializableKeyboardShortcuts;
const shortcutKeys = api.getShortcutKeys();
return {
title: name,
url,

View File

@ -6,6 +6,7 @@ import Events from '@storybook/core-events';
import initProviderApi from './init-provider-api';
import initKeyHandler from './init-key-handler';
import Store from './store';
import getInitialState from './initial-state';
import initAddons from './addons';
@ -13,6 +14,7 @@ import initChannel from './channel';
import initNotifications from './notifications';
import initStories from './stories';
import initLayout from './layout';
import initShortcuts from './shortcuts';
import initURL from './url';
import initVersions from './versions';
@ -41,10 +43,15 @@ export class Provider extends Component {
super(props);
const { provider, location, path, viewMode, storyId, navigate } = props;
const store = {
const store = new Store({
getState: () => this.state,
setState: (a, b) => this.setState(a, b),
};
});
// Initialize the state to be the initial (persisted) state of the store.
// This gives the modules the chance to read the persisted state, apply their defaults
// and override if necessary
this.state = store.getInitialState();
const apiData = {
navigate,
@ -61,12 +68,13 @@ export class Provider extends Component {
initAddons,
initLayout,
initNotifications,
initShortcuts,
initStories,
initURL,
initVersions,
].map(initModule => initModule(apiData));
// Create our initial state by combining the initial state of all modules
// Create our initial state by combining the initial state of all modules, then overlaying any saved state
const state = getInitialState(...modules.map(m => m.state));
// Get our API by combining the APIs exported by each module

View File

@ -1,13 +1,13 @@
import { document } from 'global';
import keyEvents, { features } from '../libs/key_events';
import keyEventToFeature, { features } from '../libs/key_events';
export default ({ store, api }) => {
const handle = eventType => {
const handleFeature = feature => {
const {
layout: { isFullscreen, showNav, showPanel },
} = store.getState();
switch (eventType) {
switch (feature) {
case features.ESCAPE: {
if (isFullscreen) {
api.toggleFullscreen();
@ -153,9 +153,12 @@ export default ({ store, api }) => {
};
return {
handle,
handle: handleFeature,
bind: () => {
document.addEventListener('keydown', e => handle(keyEvents(e, store), store));
document.addEventListener('keydown', e => {
const { shortcuts } = store.getState();
handleFeature(keyEventToFeature(e, shortcuts));
});
},
};
};

View File

@ -1,15 +1,8 @@
import mergeWith from 'lodash.mergewith';
import isEqual from 'lodash.isequal';
import { logger } from '@storybook/client-logger';
import { themes } from '@storybook/theming';
import { initShortcutKeys, serializedLocalStorage } from '../settings/utils';
const shortcuts = initShortcutKeys();
import merge from '../libs/merge';
const initial = {
shortcutKeys: serializedLocalStorage(shortcuts),
ui: {
name: 'STORYBOOK',
url: 'https://github.com/storybooks/storybook',
@ -28,24 +21,5 @@ const initial = {
customQueryParams: {},
};
const merge = (a, b) =>
mergeWith({}, a, b, (objValue, srcValue) => {
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
srcValue.forEach(s => {
const existing = objValue.find(o => o === s || isEqual(o, s));
if (!existing) {
objValue.push(s);
}
});
return objValue;
}
if (Array.isArray(objValue)) {
logger.log('the types mismatch, picking', objValue);
return objValue;
}
return undefined;
});
// Returns the initialState of the app
export default (...additions) => additions.reduce((acc, item) => merge(acc, item), initial);

View File

@ -0,0 +1,57 @@
import { navigator } from 'global';
export const isMacLike = () =>
navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false;
export const controlOrMetaKey = () => (isMacLike() ? 'meta' : 'control');
export const defaultShortcuts = Object.freeze({
fullScreen: ['F'],
togglePanel: ['S'], // Panel visibiliy
panelPosition: ['D'],
navigation: ['A'],
toolbar: ['T'],
search: ['/'],
focusNav: ['1'],
focusIframe: ['2'],
focusPanel: ['3'],
prevComponent: ['alt', 'ArrowUp'],
nextComponent: ['alt', 'ArrowDown'],
prevStory: ['alt', 'ArrowLeft'],
nextStory: ['alt', 'ArrowRight'],
shortcutsPage: ['shift', ',', controlOrMetaKey()],
aboutPage: [','],
});
export default function initShortcuts({ store }) {
const api = {
getShortcutKeys() {
return store.getState().shortcuts;
},
async setShortcuts(shortcuts) {
await store.setState({ shortcuts }, { persistence: 'permanent' });
return shortcuts;
},
async restoreAllDefaultShortcuts() {
api.setShortcuts(defaultShortcuts);
},
async setShortcut(action, value) {
const shortcuts = api.getShortcutKeys();
return api.setShortcuts({ ...shortcuts, [action]: value });
},
async restoreDefaultShortcut(action) {
const defaultShortcut = defaultShortcuts[action];
return api.setShortcut(action, defaultShortcut);
},
};
const { shortcuts: persistedShortcuts = {} } = store.getState();
const state = {
// Any saved shortcuts that are still in our set of defaults
shortcuts: Object.keys(defaultShortcuts).reduce(
(acc, key) => ({ ...acc, [key]: persistedShortcuts[key] || defaultShortcuts[key] }),
{}
),
};
return { api, state };
}

View File

@ -0,0 +1,79 @@
import initShortcuts from './shortcuts';
function createMockStore() {
let state = {};
return {
getState: jest.fn().mockImplementation(() => state),
setState: jest.fn().mockImplementation(s => {
state = { ...state, ...s };
}),
};
}
describe('shortcuts api', () => {
it('sets defaults', () => {
const store = createMockStore();
const { api, state } = initShortcuts({ store });
store.setState(state);
expect(api.getShortcutKeys().fullScreen).toEqual(['F']);
});
it('sets defaults, augmenting anything that was persisted', () => {
const store = createMockStore();
store.setState({ shortcuts: { fullScreen: ['Z'] } });
const { api, state } = initShortcuts({ store });
store.setState(state);
expect(api.getShortcutKeys().fullScreen).toEqual(['Z']);
expect(api.getShortcutKeys().togglePanel).toEqual(['S']);
});
it('sets defaults, ignoring anything persisted that is out of date', () => {
const store = createMockStore();
store.setState({ shortcuts: { randomKey: ['Z'] } });
const { api, state } = initShortcuts({ store });
store.setState(state);
expect(api.getShortcutKeys().randomKey).not.toBeDefined();
});
it('sets new values', async () => {
const store = createMockStore();
const { api, state } = initShortcuts({ store });
store.setState(state);
await api.setShortcut('fullScreen', ['X']);
expect(api.getShortcutKeys().fullScreen).toEqual(['X']);
});
it('restores all defaults', async () => {
const store = createMockStore();
const { api, state } = initShortcuts({ store });
store.setState(state);
await api.setShortcut('fullScreen', ['X']);
await api.setShortcut('togglePanel', ['B']);
await api.restoreAllDefaultShortcuts();
expect(api.getShortcutKeys().fullScreen).toEqual(['F']);
expect(api.getShortcutKeys().togglePanel).toEqual(['S']);
});
it('restores single default', async () => {
const store = createMockStore();
const { api, state } = initShortcuts({ store });
store.setState(state);
await api.setShortcut('fullScreen', ['X']);
await api.setShortcut('togglePanel', ['B']);
await api.restoreDefaultShortcut('fullScreen');
expect(api.getShortcutKeys().fullScreen).toEqual(['F']);
expect(api.getShortcutKeys().togglePanel).toEqual(['B']);
});
});

View File

@ -1,33 +1,82 @@
// simple store implementation
const createStore = initialState => {
let state = initialState;
const listeners = [];
// TODO -- make this TS?
function notify() {
listeners.forEach(l => l());
import { localStorage, sessionStorage } from 'global';
export const STORAGE_KEY = '@storybook/ui/store';
function get(storage) {
const serialized = storage.getItem(STORAGE_KEY);
return serialized ? JSON.parse(serialized) : {};
}
function set(storage, value) {
storage.setItem(STORAGE_KEY, JSON.stringify(value));
}
function update(storage, patch) {
const previous = get(storage);
// Apply the same behaviour as react here
set(storage, { ...previous, ...patch });
}
// Our store piggybacks off the internal React state of the Context Provider
// It has been augmented to persist state to local/sessionStorage
export default class Store {
constructor({ setState, getState }) {
this.upstreamSetState = setState;
this.upstreamGetState = getState;
}
function setState(patch) {
if (typeof patch === 'function') {
state = { ...state, ...patch(state) };
// The assumption is that this will be called once, to initialize the React state
// when the module is instanciated
getInitialState() {
// We don't only merge at the very top level (the same way as React setState)
// when you set keys, so it makes sense to do the same in combining the two storage modes
// Really, you shouldn't store the same key in both places
return { ...get(localStorage), ...get(sessionStorage) };
}
getState() {
return this.upstreamGetState();
}
async setState(inputPatch, cbOrOptions, inputOptions) {
let callback;
let options;
if (typeof cbOrOptions === 'function') {
callback = cbOrOptions;
options = inputOptions;
} else {
state = { ...state, ...patch };
options = cbOrOptions;
}
notify();
}
const { persistence = 'none' } = options || {};
return {
getState() {
return state;
},
setState,
subscribe(listener) {
listeners.push(listener);
return function unsubscribe(l) {
listeners.splice(listeners.indexOf(l), 1);
let patch;
// What did the patch actually return
let delta;
if (typeof inputPatch === 'function') {
// Pass the same function, but just set delta on the way
patch = state => {
delta = inputPatch(state);
return delta;
};
},
};
};
} else {
patch = inputPatch;
delta = patch;
}
export default createStore;
const newState = await new Promise(resolve => {
this.upstreamSetState(patch, resolve);
});
if (persistence !== 'none') {
const storage = persistence === 'session' ? sessionStorage : localStorage;
await update(storage, delta);
}
if (callback) {
callback(newState);
}
return newState;
}
}

View File

@ -0,0 +1,162 @@
import { localStorage, sessionStorage } from 'global';
import flushPromises from 'flush-promises';
import Store, { STORAGE_KEY } from './store';
jest.mock('global', () => ({
sessionStorage: {
setItem: jest.fn(),
getItem: jest.fn(),
},
localStorage: {
setItem: jest.fn(),
getItem: jest.fn(),
},
}));
describe('store', () => {
it('sensibly combines local+session storage for inital state', () => {
sessionStorage.getItem.mockReturnValueOnce(
JSON.stringify({ foo: 'bar', combined: { a: 'b' } })
);
localStorage.getItem.mockReturnValueOnce(
JSON.stringify({ foo: 'baz', another: 'value', combined: { c: 'd' } })
);
const store = new Store({});
expect(store.getInitialState()).toEqual({
foo: 'bar',
another: 'value',
// We don't combine subfields from the two sources.
combined: { a: 'b' },
});
});
it('passes getState right through', () => {
const getState = jest.fn();
const store = new Store({ getState });
store.getState();
expect(getState).toHaveBeenCalled();
});
describe('setState', () => {
it('sets values in React only by default', async () => {
const setState = jest.fn().mockImplementation((x, cb) => cb());
const store = new Store({ setState });
await store.setState({ foo: 'bar' });
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
expect(sessionStorage.setItem).not.toHaveBeenCalled();
expect(localStorage.setItem).not.toHaveBeenCalled();
});
it('sets values in React and sessionStorage if persistence === session', async () => {
const setState = jest.fn().mockImplementation((x, cb) => cb());
const store = new Store({ setState });
await store.setState({ foo: 'bar' }, { persistence: 'session' });
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
expect(sessionStorage.setItem).toHaveBeenCalledWith(
STORAGE_KEY,
JSON.stringify({ foo: 'bar' })
);
expect(localStorage.setItem).not.toHaveBeenCalled();
});
it('sets values in React and sessionStorage if persistence === permanent', async () => {
const setState = jest.fn().mockImplementation((x, cb) => cb());
const store = new Store({ setState });
await store.setState({ foo: 'bar' }, { persistence: 'permanent' });
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
expect(sessionStorage.setItem).not.toHaveBeenCalled();
expect(localStorage.setItem).toHaveBeenCalledWith(
STORAGE_KEY,
JSON.stringify({ foo: 'bar' })
);
});
it('properly patches existing values', async () => {
const setState = jest.fn().mockImplementation((x, cb) => cb());
sessionStorage.getItem.mockReturnValueOnce(
JSON.stringify({ foo: 'baz', another: 'value', combined: { a: 'b' } })
);
const store = new Store({ setState });
await store.setState({ foo: 'bar', combined: { c: 'd' } }, { persistence: 'session' });
expect(setState).toHaveBeenCalledWith(
{ foo: 'bar', combined: { c: 'd' } },
expect.any(Function)
);
expect(sessionStorage.setItem).toHaveBeenCalledWith(
STORAGE_KEY,
JSON.stringify({ foo: 'bar', another: 'value', combined: { c: 'd' } })
);
});
it('waits for react to setState', async () => {
let cb;
const setState = jest.fn().mockImplementation((x, inputCb) => {
cb = inputCb;
});
const store = new Store({ setState });
// NOTE: not awaiting here
let done = false;
store.setState({ foo: 'bar' }).then(() => {
done = true;
});
await flushPromises();
expect(setState).toHaveBeenCalledWith({ foo: 'bar' }, expect.any(Function));
expect(done).toBe(false);
cb();
await flushPromises();
expect(done).toBe(true);
});
it('returns react.setState result', async () => {
const setState = jest.fn().mockImplementation((x, cb) => cb('RESULT'));
const store = new Store({ setState });
const result = await store.setState({ foo: 'bar' });
expect(result).toBe('RESULT');
});
it('allows a callback', async () =>
new Promise(resolve => {
const setState = jest.fn().mockImplementation((x, cb) => cb('RESULT'));
const store = new Store({ setState });
store.setState({ foo: 'bar' }, result => {
expect(result).toBe('RESULT');
resolve();
});
}));
it('allows a patch function and persists its results', async () => {
const setState = jest.fn().mockImplementation((x, cb) => {
x('OLD_STATE');
cb();
});
const store = new Store({ setState });
const patch = jest.fn().mockReturnValue({ foo: 'bar' });
await store.setState(patch, { persistence: 'session' });
expect(patch).toHaveBeenCalledWith('OLD_STATE');
expect(sessionStorage.setItem).toHaveBeenCalledWith(
STORAGE_KEY,
JSON.stringify({ foo: 'bar' })
);
});
});
});

View File

@ -1,5 +1,4 @@
import Events from '@storybook/core-events';
import { localStorage } from 'global';
import { parseKey } from '../settings/utils';
export const features = {
@ -62,8 +61,7 @@ const addKeyToTempShortcutArr = (shortcutName, shortcutKeys = {}, parsedEvent) =
return undefined;
};
export default function handle(e) {
const shortcutKeys = JSON.parse(localStorage.getItem('shortcutKeys'));
export default function keyEventToFeature(e, shortcutKeys) {
if (e.key === 'Escape') {
return features.ESCAPE;
}
@ -133,7 +131,7 @@ export default function handle(e) {
// window.keydown handler to dispatch a key event to the preview channel
export function handleKeyboardShortcuts(channel) {
return event => {
const parsedEvent = handle(event);
const parsedEvent = keyEventToFeature(event);
if (parsedEvent) {
channel.emit(Events.APPLY_SHORTCUT, { event: parsedEvent });
}

23
lib/ui/src/libs/merge.js Normal file
View File

@ -0,0 +1,23 @@
import mergeWith from 'lodash.mergewith';
import isEqual from 'lodash.isequal';
import { logger } from '@storybook/client-logger';
export default (a, b) =>
mergeWith({}, a, b, (objValue, srcValue) => {
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
srcValue.forEach(s => {
const existing = objValue.find(o => o === s || isEqual(o, s));
if (!existing) {
objValue.push(s);
}
});
return objValue;
}
if (Array.isArray(objValue)) {
logger.log('the types mismatch, picking', objValue);
return objValue;
}
return undefined;
});

View File

@ -1,6 +1,6 @@
import React from 'react';
import AboutPage from './about_page';
import ShortcutsPage from './shortcuts';
import ShortcutsPage from './shortcuts_page';
const SettingsPages = () => [<AboutPage key="about" />, <ShortcutsPage key="shortcuts" />];

View File

@ -1,18 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Icons } from '@storybook/components';
import { Route } from '@storybook/router';
import { setAll, setItem } from './persist';
import {
defaultShortcutSets,
serializableKeyboardShortcuts,
initShortcutKeys,
isShortcutTaken,
keyToSymbol,
labelsArr,
mapToKeyEl,
parseKey,
serializedLocalStorage,
} from './utils';
import { isShortcutTaken, keyToSymbol, labelsArr, mapToKeyEl, parseKey } from './utils';
import {
A,
Button,
@ -36,21 +26,29 @@ import {
Wrapper,
} from './components';
class ShortcutsPage extends Component {
function toShortcutState(shortcutKeys) {
return Object.entries(shortcutKeys).reduce(
(acc, [action, value]) => ({ ...acc, [action]: { value, error: false } }),
{}
);
}
class ShortcutsScreen extends Component {
constructor(props) {
super(props);
const shortcuts = initShortcutKeys();
this.state = {
activeInputField: '',
inputArr: [],
shortcutKeys: serializedLocalStorage(shortcuts),
successField: '',
// The initial shortcutKeys that come from props are the defaults/what was saved
// As the user interacts with the page, the state stores the temporary, unsaved shortcuts
// This object also includes the error attached to each shortcut
shortcutKeys: toShortcutState(props.shortcutKeys),
};
}
duplicateFound = () => {
const { activeInputField, inputArr, shortcutKeys } = this.state;
const { shortcutKeys, activeInputField, inputArr } = this.state;
const match = Object.entries(shortcutKeys).filter(
i => i[0] !== activeInputField && isShortcutTaken(inputArr, i[1].value)
);
@ -134,33 +132,34 @@ class ShortcutsPage extends Component {
saveShortcut = () => {
const { activeInputField, inputArr, shortcutKeys } = this.state;
const { setShortcut } = this.props;
setShortcut(activeInputField, inputArr);
this.setState({
successField: activeInputField,
shortcutKeys: { ...shortcutKeys, [activeInputField]: { value: inputArr, error: false } },
});
return setItem('shortcutKeys', activeInputField, inputArr);
};
restoreDefaults = () => {
this.setState({ shortcutKeys: defaultShortcutSets });
return setAll('shortcutKeys', serializableKeyboardShortcuts);
const { restoreAllDefaultShortcuts } = this.props;
const defaultShortcuts = restoreAllDefaultShortcuts();
this.setState({ shortcutKeys: toShortcutState(defaultShortcuts) });
};
restoreDefault = () => {
const { activeInputField, shortcutKeys } = this.state;
const { restoreDefaultShortcut } = this.props;
const defaultShortcut = restoreDefaultShortcut(activeInputField);
this.setState({
inputArr: defaultShortcutSets[activeInputField].value,
inputArr: defaultShortcut,
shortcutKeys: {
...shortcutKeys,
[activeInputField]: defaultShortcutSets[activeInputField],
...toShortcutState({ [activeInputField]: defaultShortcut }),
},
});
return setItem(
'shortcutKeys',
activeInputField,
serializableKeyboardShortcuts[activeInputField]
);
};
displaySuccessMessage = activeElement => {
@ -256,4 +255,11 @@ class ShortcutsPage extends Component {
}
}
export default ShortcutsPage;
ShortcutsScreen.propTypes = {
shortcutKeys: PropTypes.shape({}).isRequired, // Need TS for this
setShortcut: PropTypes.func.isRequired,
restoreDefaultShortcut: PropTypes.func.isRequired,
restoreAllDefaultShortcuts: PropTypes.func.isRequired,
};
export default ShortcutsScreen;

View File

@ -1,22 +1,39 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import ShortcutsPage from './shortcuts';
import ShortcutsScreen from './shortcuts';
describe('ShortcutsPage', () => {
// A limited set of keys we use in this test file
const shortcutKeys = {
fullScreen: ['F'],
togglePanel: ['S'],
navigation: ['A'],
toolbar: ['T'],
search: ['/'],
focusNav: ['1'],
focusIframe: ['2'],
};
const actions = {
setShortcut: jest.fn(),
restoreDefaultShortcut: jest.fn().mockImplementation(action => shortcutKeys[action]),
restoreAllDefaultShortcuts: jest.fn().mockReturnValue(shortcutKeys),
};
describe('ShortcutsScreen', () => {
it('renders correctly', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
expect(comp).toExist();
});
it('handles a full mount', () => {
const comp = mount(<ShortcutsPage />);
const comp = mount(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
expect(comp).toExist();
comp.unmount();
});
describe('duplicateFound', () => {
it('returns true if there are duplicates', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'fullScreen', inputArr: ['S'] });
const instance = comp.instance();
@ -24,7 +41,7 @@ describe('ShortcutsPage', () => {
});
it('returns false if there are no duplicates', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'fullScreen', inputArr: ['R'] });
const instance = comp.instance();
@ -34,7 +51,7 @@ describe('ShortcutsPage', () => {
describe('submitKeyHandler', () => {
it('restores the default if the input array is empty', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({
activeInputField: 'togglePanel',
inputArr: [],
@ -47,7 +64,7 @@ describe('ShortcutsPage', () => {
});
it('returns undefined if inputArray has keys in it', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({
activeInputField: 'togglePanel',
inputArr: ['R', '1'],
@ -63,7 +80,7 @@ describe('ShortcutsPage', () => {
describe('handleBackspace', () => {
it('cuts the last element from the inputArr', () => {
const expected = ['A', 'B'];
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'navigation', inputArr: ['A', 'B', 'C'] });
const instance = comp.instance();
instance.handleBackspace();
@ -75,7 +92,7 @@ describe('ShortcutsPage', () => {
describe('onKeyDown', () => {
it('calls handleBackspace if Backspace key is pressed', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
comp.setState({ activeInputField: 'focusIframe', inputArr: ['A', 'B', 'C'] });
@ -85,7 +102,7 @@ describe('ShortcutsPage', () => {
});
it('calls submitKeyHandler if Tab key is pressed', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'focusIframe', inputArr: ['A', 'B', 'C'] });
const instance = comp.instance();
const spy = jest.spyOn(instance, 'submitKeyHandler');
@ -94,7 +111,7 @@ describe('ShortcutsPage', () => {
});
it('calls submitKeyHandler if Enter key is pressed', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'focusIframe', inputArr: ['A', 'B', 'C'] });
const instance = comp.instance();
const spy = jest.spyOn(instance, 'submitKeyHandler');
@ -103,7 +120,7 @@ describe('ShortcutsPage', () => {
});
it('sets state for regular key down events', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({ activeInputField: 'search', inputArr: [] });
const instance = comp.instance();
instance.onKeyDown({ key: 'M' });
@ -117,7 +134,7 @@ describe('ShortcutsPage', () => {
describe('onFocus', () => {
it('calls setstate and clears the input on focus', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
instance.onFocus('toolbar')();
@ -129,7 +146,7 @@ describe('ShortcutsPage', () => {
describe('onBlur', () => {
it('if the input is empty, restores the respective default', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
comp.setState({
activeInputField: 'focusNav',
@ -144,7 +161,7 @@ describe('ShortcutsPage', () => {
});
it('if a duplicate is found, it sets error to true', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
comp.setState({
activeInputField: 'fullScreen',
@ -156,7 +173,7 @@ describe('ShortcutsPage', () => {
});
it('it saves the shortcut if it is valid', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
comp.setState({ activeInputField: 'focusNav', inputArr: ['V', 'P'] });
@ -169,7 +186,7 @@ describe('ShortcutsPage', () => {
describe('saveShortcut', () => {
it('if the input is empty, restores the respective default', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
const instance = comp.instance();
comp.setState({
activeInputField: 'focusNav',
@ -184,7 +201,7 @@ describe('ShortcutsPage', () => {
describe('restoreDefaults', () => {
it('if the input is empty, restores the respective default', () => {
const comp = shallow(<ShortcutsPage />);
const comp = shallow(<ShortcutsScreen shortcutKeys={shortcutKeys} {...actions} />);
comp.setState({
activeInputField: 'focusNav',

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Route } from '@storybook/router';
import { Consumer } from '../core/context';
import ShortcutsScreen from './shortcuts';
export default () => (
<Route path="shortcuts">
<Consumer>
{({
api: { getShortcutKeys, setShortcut, restoreDefaultShortcut, restoreAllDefaultShortcuts },
}) => (
<ShortcutsScreen
shortcutKeys={getShortcutKeys()}
{...{ setShortcut, restoreDefaultShortcut, restoreAllDefaultShortcuts }}
/>
)}
</Consumer>
</Route>
);

3156
yarn.lock

File diff suppressed because it is too large Load Diff