Merge branch 'settings-validation'

jh-changes
Shin'ya Ueoka 7 years ago
commit 1b2554adee
  1. 5
      src/actions/index.js
  2. 27
      src/actions/setting.js
  3. 45
      src/components/setting.js
  4. 29
      src/pages/settings.js
  5. 17
      src/reducers/setting.js
  6. 33
      src/shared/validators/setting.js
  7. 22
      test/reducers/setting.test.js
  8. 25
      test/shared/validators/setting.test.js

@ -13,5 +13,8 @@ export default {
// Completion
COMPLETION_SET_ITEMS: 'completion.set.items',
COMPLETION_SELECT_NEXT: 'completions.select.next',
COMPLETION_SELECT_PREV: 'completions.select.prev'
COMPLETION_SELECT_PREV: 'completions.select.prev',
// Settings
SETTING_SET_SETTINGS: 'setting.set.settings',
};

@ -0,0 +1,27 @@
import actions from '../actions';
import messages from '../content/messages';
const load = () => {
return browser.storage.local.get('settings').then((value) => {
return set(value.settings);
}, console.error);
};
const save = (settings) => {
return browser.storage.local.set({
settings
}).then(() => {
return browser.runtime.sendMessage({
type: messages.SETTINGS_RELOAD
});
});
};
const set = (settings) => {
return {
type: actions.SETTING_SET_SETTINGS,
settings,
};
};
export { load, save, set };

@ -0,0 +1,45 @@
import * as settingActions from '../actions/setting';
import { validate } from '../shared/validators/setting';
export default class SettingComponent {
constructor(wrapper, store) {
this.wrapper = wrapper;
this.store = store;
let doc = wrapper.ownerDocument;
let form = doc.getElementById('vimvixen-settings-form');
form.addEventListener('submit', this.onSubmit.bind(this));
let plainJson = form.elements['plain-json'];
plainJson.addEventListener('input', this.onPlainJsonChanged.bind(this));
store.dispatch(settingActions.load());
}
onSubmit(e) {
let settings = {
json: e.target.elements['plain-json'].value,
};
this.store.dispatch(settingActions.save(settings));
e.preventDefault();
}
onPlainJsonChanged(e) {
try {
let settings = JSON.parse(e.target.value);
validate(settings);
e.target.setCustomValidity('');
} catch (err) {
e.target.setCustomValidity(err.message);
}
}
update() {
let { settings } = this.store.getState();
let doc = this.wrapper.ownerDocument;
let form = doc.getElementById('vimvixen-settings-form');
let plainJsonInput = form.elements['plain-json'];
plainJsonInput.value = settings.json;
}
}

@ -1,22 +1,15 @@
import './settings.scss';
import messages from '../content/messages';
import SettingComponent from '../components/setting';
import settingReducer from '../reducers/setting';
import * as store from '../store';
document.addEventListener('DOMContentLoaded', () => {
let form = document.getElementById('vimvixen-settings-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
browser.storage.local.set({
settings: {
json: e.target.elements['plain-json'].value
}
}).then(() => {
return browser.runtime.sendMessage({
type: messages.SETTINGS_RELOAD
});
});
});
const settingStore = store.createStore(settingReducer);
let settingComponent = null;
settingStore.subscribe(() => {
settingComponent.update();
});
browser.storage.local.get('settings').then((value) => {
form.elements['plain-json'].value = value.settings.json;
}, console.error);
document.addEventListener('DOMContentLoaded', () => {
settingComponent = new SettingComponent(document.body, settingStore);
});

@ -0,0 +1,17 @@
import actions from '../actions';
const defaultState = {
settings: {}
};
export default function reducer(state = defaultState, action = {}) {
switch (action.type) {
case actions.SETTING_SET_SETTINGS:
return Object.assign({}, state, {
settings: action.settings,
});
default:
return state;
}
}

@ -0,0 +1,33 @@
import operations from '../../operations';
const VALID_TOP_KEYS = ['keymaps'];
const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
return operations[key];
});
const validateInvalidTopKeys = (settings) => {
let invalidKey = Object.keys(settings).find((key) => {
return !VALID_TOP_KEYS.includes(key);
});
if (invalidKey) {
throw Error(`Unknown key: "${invalidKey}"`);
}
};
const validateKeymaps = (keymaps) => {
for (let key of Object.keys(keymaps)) {
let value = keymaps[key];
if (!VALID_OPERATION_VALUES.includes(value.type)) {
throw Error(`Unknown operation: "${value.type}"`);
}
}
};
const validate = (settings) => {
validateInvalidTopKeys(settings);
if (settings.keymaps) {
validateKeymaps(settings.keymaps);
}
};
export { validate };

@ -0,0 +1,22 @@
import { expect } from "chai";
import actions from '../../src/actions';
import settingReducer from '../../src/reducers/setting';
describe("setting reducer", () => {
it('return the initial state', () => {
let state = settingReducer(undefined, {});
expect(state).to.have.deep.property('settings', {});
});
it('return next state for SETTING_SET_SETTINGS', () => {
let action = {
type: actions.SETTING_SET_SETTINGS,
settings: { value1: 'hello', value2: 'world' },
};
let state = settingReducer(undefined, action);
expect(state).to.have.deep.property('settings', {
value1: 'hello',
value2: 'world',
});
});
});

@ -0,0 +1,25 @@
import { expect } from "chai";
import { validate } from '../../../src/shared/validators/setting';
describe("setting validator", () => {
describe("unknown top keys", () => {
it('throws an error for unknown settings', () => {
let settings = { keymaps: {}, poison: 123 };
let fn = validate.bind(undefined, settings)
expect(fn).to.throw(Error, 'poison');
})
});
describe("keymaps settings", () => {
it('throws an error for unknown operation', () => {
let settings = {
keymaps: {
a: { 'type': 'scroll.home' },
b: { 'type': 'poison.dressing' },
}
};
let fn = validate.bind(undefined, settings)
expect(fn).to.throw(Error, 'poison.dressing');
});
});
});