From 541449b1fced9eea15f415b023206b10724f5315 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 14:18:12 +0900 Subject: separate console --- test/actions/console.test.js | 51 --------------- test/console/actions/console.test.js | 51 +++++++++++++++ test/console/reducers/console.test.js | 118 ++++++++++++++++++++++++++++++++++ test/reducers/console.test.js | 118 ---------------------------------- 4 files changed, 169 insertions(+), 169 deletions(-) delete mode 100644 test/actions/console.test.js create mode 100644 test/console/actions/console.test.js create mode 100644 test/console/reducers/console.test.js delete mode 100644 test/reducers/console.test.js (limited to 'test') diff --git a/test/actions/console.test.js b/test/actions/console.test.js deleted file mode 100644 index ff905bc..0000000 --- a/test/actions/console.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import * as consoleActions from 'actions/console'; - -describe("console actions", () => { - describe("showCommand", () => { - it('create CONSOLE_SHOW_COMMAND action', () => { - let action = consoleActions.showCommand('hello'); - expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND); - expect(action.text).to.equal('hello'); - }); - }); - - describe("showError", () => { - it('create CONSOLE_SHOW_ERROR action', () => { - let action = consoleActions.showError('an error'); - expect(action.type).to.equal(actions.CONSOLE_SHOW_ERROR); - expect(action.text).to.equal('an error'); - }); - }); - - describe("hide", () => { - it('create CONSOLE_HIDE action', () => { - let action = consoleActions.hide(); - expect(action.type).to.equal(actions.CONSOLE_HIDE); - }); - }); - - describe("setCompletions", () => { - it('create CONSOLE_SET_COMPLETIONS action', () => { - let action = consoleActions.setCompletions([1,2,3]); - expect(action.type).to.equal(actions.CONSOLE_SET_COMPLETIONS); - expect(action.completions).to.deep.equal([1, 2, 3]); - }); - }); - - describe("completionPrev", () => { - it('create CONSOLE_COMPLETION_PREV action', () => { - let action = consoleActions.completionPrev(); - expect(action.type).to.equal(actions.CONSOLE_COMPLETION_PREV); - }); - }); - - describe("completionNext", () => { - it('create CONSOLE_COMPLETION_NEXT action', () => { - let action = consoleActions.completionNext(); - expect(action.type).to.equal(actions.CONSOLE_COMPLETION_NEXT); - }); - }); -}); - diff --git a/test/console/actions/console.test.js b/test/console/actions/console.test.js new file mode 100644 index 0000000..dd04c85 --- /dev/null +++ b/test/console/actions/console.test.js @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import actions from 'console/actions'; +import * as consoleActions from 'console/actions/console'; + +describe("console actions", () => { + describe("showCommand", () => { + it('create CONSOLE_SHOW_COMMAND action', () => { + let action = consoleActions.showCommand('hello'); + expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND); + expect(action.text).to.equal('hello'); + }); + }); + + describe("showError", () => { + it('create CONSOLE_SHOW_ERROR action', () => { + let action = consoleActions.showError('an error'); + expect(action.type).to.equal(actions.CONSOLE_SHOW_ERROR); + expect(action.text).to.equal('an error'); + }); + }); + + describe("hide", () => { + it('create CONSOLE_HIDE action', () => { + let action = consoleActions.hide(); + expect(action.type).to.equal(actions.CONSOLE_HIDE); + }); + }); + + describe("setCompletions", () => { + it('create CONSOLE_SET_COMPLETIONS action', () => { + let action = consoleActions.setCompletions([1,2,3]); + expect(action.type).to.equal(actions.CONSOLE_SET_COMPLETIONS); + expect(action.completions).to.deep.equal([1, 2, 3]); + }); + }); + + describe("completionPrev", () => { + it('create CONSOLE_COMPLETION_PREV action', () => { + let action = consoleActions.completionPrev(); + expect(action.type).to.equal(actions.CONSOLE_COMPLETION_PREV); + }); + }); + + describe("completionNext", () => { + it('create CONSOLE_COMPLETION_NEXT action', () => { + let action = consoleActions.completionNext(); + expect(action.type).to.equal(actions.CONSOLE_COMPLETION_NEXT); + }); + }); +}); + diff --git a/test/console/reducers/console.test.js b/test/console/reducers/console.test.js new file mode 100644 index 0000000..95ac993 --- /dev/null +++ b/test/console/reducers/console.test.js @@ -0,0 +1,118 @@ +import { expect } from "chai"; +import actions from 'console/actions'; +import reducer from 'console/reducers'; + +describe("console reducer", () => { + it('return the initial state', () => { + let state = reducer(undefined, {}); + expect(state).to.have.property('errorShown', false); + expect(state).to.have.property('errorText', ''); + expect(state).to.have.property('commandShown', false); + expect(state).to.have.property('commandText', ''); + expect(state).to.have.deep.property('completions', []); + expect(state).to.have.property('groupSelection', -1); + expect(state).to.have.property('itemSelection', -1); + }); + + it('return next state for CONSOLE_SHOW_COMMAND', () => { + let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' }; + let state = reducer({}, action); + expect(state).to.have.property('commandShown', true); + expect(state).to.have.property('commandText', 'open '); + expect(state).to.have.property('errorShown', false); + }); + + it('return next state for CONSOLE_SHOW_ERROR', () => { + let action = { type: actions.CONSOLE_SHOW_ERROR, text: 'an error' }; + let state = reducer({}, action); + expect(state).to.have.property('errorShown', true); + expect(state).to.have.property('errorText', 'an error'); + expect(state).to.have.property('commandShown', false); + }); + + it('return next state for CONSOLE_HIDE', () => { + let action = { type: actions.CONSOLE_HIDE }; + let state = reducer({}, action); + expect(state).to.have.property('errorShown', false); + expect(state).to.have.property('commandShown', false); + }); + + it ('return next state for CONSOLE_SET_COMPLETIONS', () => { + let state = { + groupSelection: 0, + itemSelection: 0, + completions: [], + } + let action = { + type: actions.CONSOLE_SET_COMPLETIONS, + completions: [{ + name: 'Apple', + items: [1, 2, 3] + }, { + name: 'Banana', + items: [4, 5, 6] + }] + } + state = reducer(state, action); + expect(state).to.have.property('completions', action.completions); + expect(state).to.have.property('groupSelection', -1); + expect(state).to.have.property('itemSelection', -1); + }); + + it ('return next state for CONSOLE_COMPLETION_NEXT', () => { + let action = { type: actions.CONSOLE_COMPLETION_NEXT }; + let state = { + groupSelection: -1, + itemSelection: -1, + completions: [{ + name: 'Apple', + items: [1, 2] + }, { + name: 'Banana', + items: [3] + }] + }; + + state = reducer(state, action); + expect(state).to.have.property('groupSelection', 0); + expect(state).to.have.property('itemSelection', 0); + + state = reducer(state, action); + expect(state).to.have.property('groupSelection', 0); + expect(state).to.have.property('itemSelection', 1); + + state = reducer(state, action); + state = reducer(state, action); + expect(state).to.have.property('groupSelection', -1); + expect(state).to.have.property('itemSelection', -1); + }); + + it ('return next state for CONSOLE_COMPLETION_PREV', () => { + let action = { type: actions.CONSOLE_COMPLETION_PREV }; + let state = { + groupSelection: -1, + itemSelection: -1, + completions: [{ + name: 'Apple', + items: [1, 2] + }, { + name: 'Banana', + items: [3] + }] + }; + + state = reducer(state, action); + expect(state).to.have.property('groupSelection', 1); + expect(state).to.have.property('itemSelection', 0); + + state = reducer(state, action); + expect(state).to.have.property('groupSelection', 0); + expect(state).to.have.property('itemSelection', 1); + + state = reducer(state, action); + state = reducer(state, action); + expect(state).to.have.property('groupSelection', -1); + expect(state).to.have.property('itemSelection', -1); + }); + +}); diff --git a/test/reducers/console.test.js b/test/reducers/console.test.js deleted file mode 100644 index 5ebf4bc..0000000 --- a/test/reducers/console.test.js +++ /dev/null @@ -1,118 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import consoleReducer from 'reducers/console'; - -describe("console reducer", () => { - it('return the initial state', () => { - let state = consoleReducer(undefined, {}); - expect(state).to.have.property('errorShown', false); - expect(state).to.have.property('errorText', ''); - expect(state).to.have.property('commandShown', false); - expect(state).to.have.property('commandText', ''); - expect(state).to.have.deep.property('completions', []); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); - }); - - it('return next state for CONSOLE_SHOW_COMMAND', () => { - let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' }; - let state = consoleReducer({}, action); - expect(state).to.have.property('commandShown', true); - expect(state).to.have.property('commandText', 'open '); - expect(state).to.have.property('errorShown', false); - }); - - it('return next state for CONSOLE_SHOW_ERROR', () => { - let action = { type: actions.CONSOLE_SHOW_ERROR, text: 'an error' }; - let state = consoleReducer({}, action); - expect(state).to.have.property('errorShown', true); - expect(state).to.have.property('errorText', 'an error'); - expect(state).to.have.property('commandShown', false); - }); - - it('return next state for CONSOLE_HIDE', () => { - let action = { type: actions.CONSOLE_HIDE }; - let state = consoleReducer({}, action); - expect(state).to.have.property('errorShown', false); - expect(state).to.have.property('commandShown', false); - }); - - it ('return next state for CONSOLE_SET_COMPLETIONS', () => { - let state = { - groupSelection: 0, - itemSelection: 0, - completions: [], - } - let action = { - type: actions.CONSOLE_SET_COMPLETIONS, - completions: [{ - name: 'Apple', - items: [1, 2, 3] - }, { - name: 'Banana', - items: [4, 5, 6] - }] - } - state = consoleReducer(state, action); - expect(state).to.have.property('completions', action.completions); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); - }); - - it ('return next state for CONSOLE_COMPLETION_NEXT', () => { - let action = { type: actions.CONSOLE_COMPLETION_NEXT }; - let state = { - groupSelection: -1, - itemSelection: -1, - completions: [{ - name: 'Apple', - items: [1, 2] - }, { - name: 'Banana', - items: [3] - }] - }; - - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 0); - - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 1); - - state = consoleReducer(state, action); - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); - }); - - it ('return next state for CONSOLE_COMPLETION_PREV', () => { - let action = { type: actions.CONSOLE_COMPLETION_PREV }; - let state = { - groupSelection: -1, - itemSelection: -1, - completions: [{ - name: 'Apple', - items: [1, 2] - }, { - name: 'Banana', - items: [3] - }] - }; - - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', 1); - expect(state).to.have.property('itemSelection', 0); - - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', 0); - expect(state).to.have.property('itemSelection', 1); - - state = consoleReducer(state, action); - state = consoleReducer(state, action); - expect(state).to.have.property('groupSelection', -1); - expect(state).to.have.property('itemSelection', -1); - }); - -}); -- cgit v1.2.3 From 58123210ab4cdd4a1f2b4720a0abbd88908baa06 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 14:44:21 +0900 Subject: separate settings --- src/actions/setting.js | 31 -------------------------- src/background/index.js | 2 +- src/components/background.js | 2 +- src/components/setting.js | 45 -------------------------------------- src/content/index.js | 2 +- src/pages/settings.html | 18 --------------- src/pages/settings.js | 15 ------------- src/pages/settings.scss | 8 ------- src/reducers/index.js | 3 ++- src/reducers/setting.js | 17 -------------- src/settings/actions/index.js | 4 ++++ src/settings/actions/setting.js | 31 ++++++++++++++++++++++++++ src/settings/components/setting.js | 45 ++++++++++++++++++++++++++++++++++++++ src/settings/index.html | 18 +++++++++++++++ src/settings/index.js | 15 +++++++++++++ src/settings/reducers/setting.js | 17 ++++++++++++++ src/settings/site.scss | 8 +++++++ test/reducers/setting.test.js | 22 ------------------- webpack.config.js | 4 ++-- 19 files changed, 145 insertions(+), 162 deletions(-) delete mode 100644 src/actions/setting.js delete mode 100644 src/components/setting.js delete mode 100644 src/pages/settings.html delete mode 100644 src/pages/settings.js delete mode 100644 src/pages/settings.scss delete mode 100644 src/reducers/setting.js create mode 100644 src/settings/actions/index.js create mode 100644 src/settings/actions/setting.js create mode 100644 src/settings/components/setting.js create mode 100644 src/settings/index.html create mode 100644 src/settings/index.js create mode 100644 src/settings/reducers/setting.js create mode 100644 src/settings/site.scss delete mode 100644 test/reducers/setting.test.js (limited to 'test') diff --git a/src/actions/setting.js b/src/actions/setting.js deleted file mode 100644 index c241428..0000000 --- a/src/actions/setting.js +++ /dev/null @@ -1,31 +0,0 @@ -import actions from 'actions'; -import messages from 'shared/messages'; -import DefaultSettings from 'shared/default-settings'; - -const load = () => { - return browser.storage.local.get('settings').then((value) => { - if (value.settings) { - return set(value.settings); - } - return set(DefaultSettings); - }, 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 }; diff --git a/src/background/index.js b/src/background/index.js index 63d13cb..c51754b 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,4 +1,4 @@ -import * as settingsActions from 'actions/setting'; +import * as settingsActions from 'settings/actions/setting'; import messages from 'shared/messages'; import BackgroundComponent from 'components/background'; import reducers from 'reducers'; diff --git a/src/components/background.js b/src/components/background.js index afb90c2..200fedf 100644 --- a/src/components/background.js +++ b/src/components/background.js @@ -1,6 +1,6 @@ import messages from 'shared/messages'; import * as operationActions from 'actions/operation'; -import * as settingsActions from 'actions/setting'; +import * as settingsActions from 'settings/actions/setting'; import * as tabActions from 'actions/tab'; import * as commands from 'shared/commands'; diff --git a/src/components/setting.js b/src/components/setting.js deleted file mode 100644 index c2f99b6..0000000 --- a/src/components/setting.js +++ /dev/null @@ -1,45 +0,0 @@ -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; - } -} diff --git a/src/content/index.js b/src/content/index.js index 25a2e74..edca510 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,6 +1,6 @@ import './console-frame.scss'; import * as consoleFrames from './console-frames'; -import * as settingActions from 'actions/setting'; +import * as settingActions from 'settings/actions/setting'; import { createStore } from 'store'; import ContentInputComponent from 'components/content-input'; import KeymapperComponent from 'components/keymapper'; diff --git a/src/pages/settings.html b/src/pages/settings.html deleted file mode 100644 index 99d6c6b..0000000 --- a/src/pages/settings.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - -

Configure

- -

Home page

-
- - - - -
- - - diff --git a/src/pages/settings.js b/src/pages/settings.js deleted file mode 100644 index 6e25e6f..0000000 --- a/src/pages/settings.js +++ /dev/null @@ -1,15 +0,0 @@ -import './settings.scss'; -import SettingComponent from 'components/setting'; -import settingReducer from 'reducers/setting'; -import { createStore } from 'store'; - -const store = createStore(settingReducer); -let settingComponent = null; - -store.subscribe(() => { - settingComponent.update(); -}); - -document.addEventListener('DOMContentLoaded', () => { - settingComponent = new SettingComponent(document.body, store); -}); diff --git a/src/pages/settings.scss b/src/pages/settings.scss deleted file mode 100644 index 5707c8a..0000000 --- a/src/pages/settings.scss +++ /dev/null @@ -1,8 +0,0 @@ -.vimvixen-settings-form { - textarea[name=plain-json] { - font-family: monospace; - width: 100%; - min-height: 64ex; - resize: vertical; - } -} diff --git a/src/reducers/index.js b/src/reducers/index.js index 3ebe491..9c86ebf 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,7 +1,8 @@ import inputReducer from 'reducers/input'; -import settingReducer from 'reducers/setting'; +import settingReducer from 'settings/reducers/setting'; import followReducer from 'reducers/follow'; +// Make setting reducer instead of re-use const defaultState = { input: inputReducer(undefined, {}), setting: settingReducer(undefined, {}), diff --git a/src/reducers/setting.js b/src/reducers/setting.js deleted file mode 100644 index 7326ed7..0000000 --- a/src/reducers/setting.js +++ /dev/null @@ -1,17 +0,0 @@ -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; - } -} - diff --git a/src/settings/actions/index.js b/src/settings/actions/index.js new file mode 100644 index 0000000..8c212c2 --- /dev/null +++ b/src/settings/actions/index.js @@ -0,0 +1,4 @@ +export default { + // Settings + SETTING_SET_SETTINGS: 'setting.set.settings', +}; diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js new file mode 100644 index 0000000..697bcf0 --- /dev/null +++ b/src/settings/actions/setting.js @@ -0,0 +1,31 @@ +import actions from 'settings/actions'; +import messages from 'shared/messages'; +import DefaultSettings from 'shared/default-settings'; + +const load = () => { + return browser.storage.local.get('settings').then((value) => { + if (value.settings) { + return set(value.settings); + } + return set(DefaultSettings); + }, 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 }; diff --git a/src/settings/components/setting.js b/src/settings/components/setting.js new file mode 100644 index 0000000..14482a3 --- /dev/null +++ b/src/settings/components/setting.js @@ -0,0 +1,45 @@ +import * as settingActions from 'settings/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; + } +} diff --git a/src/settings/index.html b/src/settings/index.html new file mode 100644 index 0000000..99d6c6b --- /dev/null +++ b/src/settings/index.html @@ -0,0 +1,18 @@ + + + + + + +

Configure

+ +

Home page

+
+ + + + +
+ + + diff --git a/src/settings/index.js b/src/settings/index.js new file mode 100644 index 0000000..8c60f80 --- /dev/null +++ b/src/settings/index.js @@ -0,0 +1,15 @@ +import './site.scss'; +import SettingComponent from 'settings/components/setting'; +import settingReducer from 'settings/reducers/setting'; +import { createStore } from 'store'; + +const store = createStore(settingReducer); +let settingComponent = null; + +store.subscribe(() => { + settingComponent.update(); +}); + +document.addEventListener('DOMContentLoaded', () => { + settingComponent = new SettingComponent(document.body, store); +}); diff --git a/src/settings/reducers/setting.js b/src/settings/reducers/setting.js new file mode 100644 index 0000000..f7d9242 --- /dev/null +++ b/src/settings/reducers/setting.js @@ -0,0 +1,17 @@ +import actions from 'settings/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; + } +} + diff --git a/src/settings/site.scss b/src/settings/site.scss new file mode 100644 index 0000000..5707c8a --- /dev/null +++ b/src/settings/site.scss @@ -0,0 +1,8 @@ +.vimvixen-settings-form { + textarea[name=plain-json] { + font-family: monospace; + width: 100%; + min-height: 64ex; + resize: vertical; + } +} diff --git a/test/reducers/setting.test.js b/test/reducers/setting.test.js deleted file mode 100644 index 1af031a..0000000 --- a/test/reducers/setting.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import settingReducer from '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', - }); - }); -}); diff --git a/webpack.config.js b/webpack.config.js index 486814a..bc3bb1c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,7 +7,7 @@ const dist = path.resolve(__dirname, 'build'); module.exports = { entry: { index: path.join(src, 'content'), - settings: path.join(src, 'pages/settings'), + settings: path.join(src, 'settings'), background: path.join(src, 'background'), console: path.join(src, 'console') }, @@ -50,7 +50,7 @@ module.exports = { inject: false }), new HtmlWebpackPlugin({ - template: path.join(src, 'pages', 'settings.html'), + template: path.join(src, 'settings', 'index.html'), filename: path.join(dist, 'settings.html'), inject: false }) -- cgit v1.2.3 From d886d7de290b6fee00c55c5487416048f3de4bf2 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 14:57:27 +0900 Subject: move background --- src/actions/operation.js | 50 +-------------------- src/actions/tab.js | 9 ---- src/background/actions/index.js | 0 src/background/actions/operation.js | 52 ++++++++++++++++++++++ src/background/actions/tab.js | 9 ++++ src/background/components/background.js | 79 +++++++++++++++++++++++++++++++++ src/background/index.js | 4 +- src/components/background.js | 79 --------------------------------- test/settings/reducers/setting.test.js | 22 +++++++++ 9 files changed, 165 insertions(+), 139 deletions(-) delete mode 100644 src/actions/tab.js create mode 100644 src/background/actions/index.js create mode 100644 src/background/actions/operation.js create mode 100644 src/background/actions/tab.js create mode 100644 src/background/components/background.js delete mode 100644 src/components/background.js create mode 100644 test/settings/reducers/setting.test.js (limited to 'test') diff --git a/src/actions/operation.js b/src/actions/operation.js index 8b1590b..a27cd02 100644 --- a/src/actions/operation.js +++ b/src/actions/operation.js @@ -1,18 +1,9 @@ import operations from 'shared/operations'; import messages from 'shared/messages'; -import * as tabs from 'background/tabs'; -import * as zooms from 'background/zooms'; import * as scrolls from 'content/scrolls'; import * as navigates from 'content/navigates'; import * as followActions from 'actions/follow'; -const sendConsoleShowCommand = (tab, command) => { - return browser.tabs.sendMessage(tab.id, { - type: messages.CONSOLE_SHOW_COMMAND, - command, - }); -}; - const exec = (operation) => { switch (operation.type) { case operations.SCROLL_LINES: @@ -49,43 +40,4 @@ const exec = (operation) => { } }; -const execBackground = (operation, tab) => { - switch (operation.type) { - case operations.TAB_CLOSE: - return tabs.closeTab(tab.id); - case operations.TAB_REOPEN: - return tabs.reopenTab(); - case operations.TAB_PREV: - return tabs.selectPrevTab(tab.index, operation.count); - case operations.TAB_NEXT: - return tabs.selectNextTab(tab.index, operation.count); - case operations.TAB_RELOAD: - return tabs.reload(tab, operation.cache); - case operations.ZOOM_IN: - return zooms.zoomIn(); - case operations.ZOOM_OUT: - return zooms.zoomOut(); - case operations.ZOOM_NEUTRAL: - return zooms.neutral(); - case operations.COMMAND_SHOW: - return sendConsoleShowCommand(tab, ''); - case operations.COMMAND_SHOW_OPEN: - if (operation.alter) { - // alter url - return sendConsoleShowCommand(tab, 'open ' + tab.url); - } - return sendConsoleShowCommand(tab, 'open '); - case operations.COMMAND_SHOW_TABOPEN: - if (operation.alter) { - // alter url - return sendConsoleShowCommand(tab, 'tabopen ' + tab.url); - } - return sendConsoleShowCommand(tab, 'tabopen '); - case operations.COMMAND_SHOW_BUFFER: - return sendConsoleShowCommand(tab, 'buffer '); - default: - return Promise.resolve(); - } -}; - -export { exec, execBackground }; +export { exec }; diff --git a/src/actions/tab.js b/src/actions/tab.js deleted file mode 100644 index e512b6f..0000000 --- a/src/actions/tab.js +++ /dev/null @@ -1,9 +0,0 @@ -const openNewTab = (url) => { - return browser.tabs.create({ url: url }); -}; - -const openToTab = (url, tab) => { - return browser.tabs.update(tab.id, { url: url }); -}; - -export { openToTab, openNewTab }; diff --git a/src/background/actions/index.js b/src/background/actions/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/background/actions/operation.js b/src/background/actions/operation.js new file mode 100644 index 0000000..d736c09 --- /dev/null +++ b/src/background/actions/operation.js @@ -0,0 +1,52 @@ +import operations from 'shared/operations'; +import messages from 'shared/messages'; +import * as tabs from 'background/tabs'; +import * as zooms from 'background/zooms'; + +const sendConsoleShowCommand = (tab, command) => { + return browser.tabs.sendMessage(tab.id, { + type: messages.CONSOLE_SHOW_COMMAND, + command, + }); +}; + +const exec = (operation, tab) => { + switch (operation.type) { + case operations.TAB_CLOSE: + return tabs.closeTab(tab.id); + case operations.TAB_REOPEN: + return tabs.reopenTab(); + case operations.TAB_PREV: + return tabs.selectPrevTab(tab.index, operation.count); + case operations.TAB_NEXT: + return tabs.selectNextTab(tab.index, operation.count); + case operations.TAB_RELOAD: + return tabs.reload(tab, operation.cache); + case operations.ZOOM_IN: + return zooms.zoomIn(); + case operations.ZOOM_OUT: + return zooms.zoomOut(); + case operations.ZOOM_NEUTRAL: + return zooms.neutral(); + case operations.COMMAND_SHOW: + return sendConsoleShowCommand(tab, ''); + case operations.COMMAND_SHOW_OPEN: + if (operation.alter) { + // alter url + return sendConsoleShowCommand(tab, 'open ' + tab.url); + } + return sendConsoleShowCommand(tab, 'open '); + case operations.COMMAND_SHOW_TABOPEN: + if (operation.alter) { + // alter url + return sendConsoleShowCommand(tab, 'tabopen ' + tab.url); + } + return sendConsoleShowCommand(tab, 'tabopen '); + case operations.COMMAND_SHOW_BUFFER: + return sendConsoleShowCommand(tab, 'buffer '); + default: + return Promise.resolve(); + } +}; + +export { exec }; diff --git a/src/background/actions/tab.js b/src/background/actions/tab.js new file mode 100644 index 0000000..e512b6f --- /dev/null +++ b/src/background/actions/tab.js @@ -0,0 +1,9 @@ +const openNewTab = (url) => { + return browser.tabs.create({ url: url }); +}; + +const openToTab = (url, tab) => { + return browser.tabs.update(tab.id, { url: url }); +}; + +export { openToTab, openNewTab }; diff --git a/src/background/components/background.js b/src/background/components/background.js new file mode 100644 index 0000000..bfe1b3f --- /dev/null +++ b/src/background/components/background.js @@ -0,0 +1,79 @@ +import messages from 'shared/messages'; +import * as operationActions from 'background/actions/operation'; +import * as settingsActions from 'settings/actions/setting'; +import * as tabActions from 'background/actions/tab'; +import * as commands from 'shared/commands'; + +export default class BackgroundComponent { + constructor(store) { + this.store = store; + this.setting = {}; + + browser.runtime.onMessage.addListener((message, sender) => { + try { + return this.onMessage(message, sender); + } catch (e) { + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.CONSOLE_SHOW_ERROR, + text: e.message, + }); + } + }); + } + + update() { + let state = this.store.getState(); + this.updateSettings(state); + } + + updateSettings(setting) { + if (!setting.settings.json) { + return; + } + this.settings = JSON.parse(setting.settings.json); + } + + onMessage(message, sender) { + switch (message.type) { + case messages.BACKGROUND_OPERATION: + return this.store.dispatch( + operationActions.exec(message.operation, sender.tab), + sender); + case messages.OPEN_URL: + if (message.newTab) { + return this.store.dispatch( + tabActions.openNewTab(message.url), sender); + } + return this.store.dispatch( + tabActions.openToTab(message.url, sender.tab), sender); + case messages.CONSOLE_BLURRED: + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.CONSOLE_HIDE, + }); + case messages.CONSOLE_ENTERED: + return commands.exec(message.text, this.settings).catch((e) => { + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.CONSOLE_SHOW_ERROR, + text: e.message, + }); + }); + case messages.SETTINGS_QUERY: + return Promise.resolve(this.store.getState().settings); + case messages.CONSOLE_QUERY_COMPLETIONS: + return commands.complete(message.text, this.settings); + case messages.SETTINGS_RELOAD: + this.store.dispatch(settingsActions.load()); + return this.broadcastSettingsChanged(); + } + } + + broadcastSettingsChanged() { + return browser.tabs.query({}).then((tabs) => { + for (let tab of tabs) { + browser.tabs.sendMessage(tab.id, { + type: messages.SETTINGS_CHANGED, + }); + } + }); + } +} diff --git a/src/background/index.js b/src/background/index.js index c51754b..587cc0b 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,7 +1,7 @@ import * as settingsActions from 'settings/actions/setting'; import messages from 'shared/messages'; -import BackgroundComponent from 'components/background'; -import reducers from 'reducers'; +import BackgroundComponent from 'background/components/background'; +import reducers from 'settings/reducers/setting'; import { createStore } from 'store'; const store = createStore(reducers, (e, sender) => { diff --git a/src/components/background.js b/src/components/background.js deleted file mode 100644 index 200fedf..0000000 --- a/src/components/background.js +++ /dev/null @@ -1,79 +0,0 @@ -import messages from 'shared/messages'; -import * as operationActions from 'actions/operation'; -import * as settingsActions from 'settings/actions/setting'; -import * as tabActions from 'actions/tab'; -import * as commands from 'shared/commands'; - -export default class BackgroundComponent { - constructor(store) { - this.store = store; - this.setting = {}; - - browser.runtime.onMessage.addListener((message, sender) => { - try { - return this.onMessage(message, sender); - } catch (e) { - return browser.tabs.sendMessage(sender.tab.id, { - type: messages.CONSOLE_SHOW_ERROR, - text: e.message, - }); - } - }); - } - - update() { - let state = this.store.getState(); - this.updateSettings(state.setting); - } - - updateSettings(setting) { - if (!setting.settings.json) { - return; - } - this.settings = JSON.parse(setting.settings.json); - } - - onMessage(message, sender) { - switch (message.type) { - case messages.BACKGROUND_OPERATION: - return this.store.dispatch( - operationActions.execBackground(message.operation, sender.tab), - sender); - case messages.OPEN_URL: - if (message.newTab) { - return this.store.dispatch( - tabActions.openNewTab(message.url), sender); - } - return this.store.dispatch( - tabActions.openToTab(message.url, sender.tab), sender); - case messages.CONSOLE_BLURRED: - return browser.tabs.sendMessage(sender.tab.id, { - type: messages.CONSOLE_HIDE, - }); - case messages.CONSOLE_ENTERED: - return commands.exec(message.text, this.settings).catch((e) => { - return browser.tabs.sendMessage(sender.tab.id, { - type: messages.CONSOLE_SHOW_ERROR, - text: e.message, - }); - }); - case messages.SETTINGS_QUERY: - return Promise.resolve(this.store.getState().setting.settings); - case messages.CONSOLE_QUERY_COMPLETIONS: - return commands.complete(message.text, this.settings); - case messages.SETTINGS_RELOAD: - this.store.dispatch(settingsActions.load()); - return this.broadcastSettingsChanged(); - } - } - - broadcastSettingsChanged() { - return browser.tabs.query({}).then((tabs) => { - for (let tab of tabs) { - browser.tabs.sendMessage(tab.id, { - type: messages.SETTINGS_CHANGED, - }); - } - }); - } -} diff --git a/test/settings/reducers/setting.test.js b/test/settings/reducers/setting.test.js new file mode 100644 index 0000000..0e84247 --- /dev/null +++ b/test/settings/reducers/setting.test.js @@ -0,0 +1,22 @@ +import { expect } from "chai"; +import actions from 'settings/actions'; +import settingReducer from 'settings/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', + }); + }); +}); -- cgit v1.2.3 From 39fb5400370b818760dc7bcfe42e74b2512a840d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 15:04:55 +0900 Subject: separate content --- src/actions/follow.js | 29 ------ src/actions/index.js | 20 ---- src/actions/input.js | 23 ----- src/actions/operation.js | 43 -------- src/components/content-input.js | 67 ------------- src/components/follow.js | 168 -------------------------------- src/components/keymapper.js | 43 -------- src/content/actions/follow.js | 29 ++++++ src/content/actions/index.js | 20 ++++ src/content/actions/input.js | 23 +++++ src/content/actions/operation.js | 43 ++++++++ src/content/components/content-input.js | 67 +++++++++++++ src/content/components/follow.js | 168 ++++++++++++++++++++++++++++++++ src/content/components/keymapper.js | 43 ++++++++ src/content/index.js | 8 +- src/content/reducers/follow.js | 32 ++++++ src/content/reducers/index.js | 18 ++++ src/content/reducers/input.js | 20 ++++ src/reducers/follow.js | 32 ------ src/reducers/index.js | 18 ---- src/reducers/input.js | 20 ---- test/actions/follow.test.js | 35 ------- test/actions/input.test.js | 26 ----- test/components/follow.html | 9 -- test/components/follow.test.js | 15 --- test/content/actions/follow.test.js | 35 +++++++ test/content/actions/input.test.js | 26 +++++ test/content/components/follow.html | 9 ++ test/content/components/follow.test.js | 15 +++ test/content/reducers/follow.test.js | 48 +++++++++ test/content/reducers/input.test.js | 26 +++++ test/reducers/follow.test.js | 48 --------- test/reducers/input.test.js | 26 ----- 33 files changed, 626 insertions(+), 626 deletions(-) delete mode 100644 src/actions/follow.js delete mode 100644 src/actions/index.js delete mode 100644 src/actions/input.js delete mode 100644 src/actions/operation.js delete mode 100644 src/components/content-input.js delete mode 100644 src/components/follow.js delete mode 100644 src/components/keymapper.js create mode 100644 src/content/actions/follow.js create mode 100644 src/content/actions/index.js create mode 100644 src/content/actions/input.js create mode 100644 src/content/actions/operation.js create mode 100644 src/content/components/content-input.js create mode 100644 src/content/components/follow.js create mode 100644 src/content/components/keymapper.js create mode 100644 src/content/reducers/follow.js create mode 100644 src/content/reducers/index.js create mode 100644 src/content/reducers/input.js delete mode 100644 src/reducers/follow.js delete mode 100644 src/reducers/index.js delete mode 100644 src/reducers/input.js delete mode 100644 test/actions/follow.test.js delete mode 100644 test/actions/input.test.js delete mode 100644 test/components/follow.html delete mode 100644 test/components/follow.test.js create mode 100644 test/content/actions/follow.test.js create mode 100644 test/content/actions/input.test.js create mode 100644 test/content/components/follow.html create mode 100644 test/content/components/follow.test.js create mode 100644 test/content/reducers/follow.test.js create mode 100644 test/content/reducers/input.test.js delete mode 100644 test/reducers/follow.test.js delete mode 100644 test/reducers/input.test.js (limited to 'test') diff --git a/src/actions/follow.js b/src/actions/follow.js deleted file mode 100644 index 708cd95..0000000 --- a/src/actions/follow.js +++ /dev/null @@ -1,29 +0,0 @@ -import actions from 'actions'; - -const enable = (newTab) => { - return { - type: actions.FOLLOW_ENABLE, - newTab, - }; -}; - -const disable = () => { - return { - type: actions.FOLLOW_DISABLE, - }; -}; - -const keyPress = (key) => { - return { - type: actions.FOLLOW_KEY_PRESS, - key: key - }; -}; - -const backspace = () => { - return { - type: actions.FOLLOW_BACKSPACE, - }; -}; - -export { enable, disable, keyPress, backspace }; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 0b3749d..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,20 +0,0 @@ -export default { - // User input - INPUT_KEY_PRESS: 'input.key,press', - INPUT_CLEAR_KEYS: 'input.clear.keys', - INPUT_SET_KEYMAPS: 'input.set,keymaps', - - // Completion - COMPLETION_SET_ITEMS: 'completion.set.items', - COMPLETION_SELECT_NEXT: 'completions.select.next', - COMPLETION_SELECT_PREV: 'completions.select.prev', - - // Settings - SETTING_SET_SETTINGS: 'setting.set.settings', - - // Follow - FOLLOW_ENABLE: 'follow.enable', - FOLLOW_DISABLE: 'follow.disable', - FOLLOW_KEY_PRESS: 'follow.key.press', - FOLLOW_BACKSPACE: 'follow.backspace', -}; diff --git a/src/actions/input.js b/src/actions/input.js deleted file mode 100644 index 61acb76..0000000 --- a/src/actions/input.js +++ /dev/null @@ -1,23 +0,0 @@ -import actions from 'actions'; - -const asKeymapChars = (key, ctrl) => { - if (ctrl) { - return ''; - } - return key; -}; - -const keyPress = (key, ctrl) => { - return { - type: actions.INPUT_KEY_PRESS, - key: asKeymapChars(key, ctrl), - }; -}; - -const clearKeys = () => { - return { - type: actions.INPUT_CLEAR_KEYS - }; -}; - -export { keyPress, clearKeys }; diff --git a/src/actions/operation.js b/src/actions/operation.js deleted file mode 100644 index a27cd02..0000000 --- a/src/actions/operation.js +++ /dev/null @@ -1,43 +0,0 @@ -import operations from 'shared/operations'; -import messages from 'shared/messages'; -import * as scrolls from 'content/scrolls'; -import * as navigates from 'content/navigates'; -import * as followActions from 'actions/follow'; - -const exec = (operation) => { - switch (operation.type) { - case operations.SCROLL_LINES: - return scrolls.scrollLines(window, operation.count); - case operations.SCROLL_PAGES: - return scrolls.scrollPages(window, operation.count); - case operations.SCROLL_TOP: - return scrolls.scrollTop(window); - case operations.SCROLL_BOTTOM: - return scrolls.scrollBottom(window); - case operations.SCROLL_HOME: - return scrolls.scrollLeft(window); - case operations.SCROLL_END: - return scrolls.scrollRight(window); - case operations.FOLLOW_START: - return followActions.enable(false); - case operations.NAVIGATE_HISTORY_PREV: - return navigates.historyPrev(window); - case operations.NAVIGATE_HISTORY_NEXT: - return navigates.historyNext(window); - case operations.NAVIGATE_LINK_PREV: - return navigates.linkPrev(window); - case operations.NAVIGATE_LINK_NEXT: - return navigates.linkNext(window); - case operations.NAVIGATE_PARENT: - return navigates.parent(window); - case operations.NAVIGATE_ROOT: - return navigates.root(window); - default: - browser.runtime.sendMessage({ - type: messages.BACKGROUND_OPERATION, - operation, - }); - } -}; - -export { exec }; diff --git a/src/components/content-input.js b/src/components/content-input.js deleted file mode 100644 index 9568caf..0000000 --- a/src/components/content-input.js +++ /dev/null @@ -1,67 +0,0 @@ -export default class ContentInputComponent { - constructor(target) { - this.pressed = {}; - this.onKeyListeners = []; - - target.addEventListener('keypress', this.onKeyPress.bind(this)); - target.addEventListener('keydown', this.onKeyDown.bind(this)); - target.addEventListener('keyup', this.onKeyUp.bind(this)); - } - - update() { - } - - onKey(cb) { - this.onKeyListeners.push(cb); - } - - onKeyPress(e) { - if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { - return; - } - this.pressed[e.key] = 'keypress'; - this.capture(e); - } - - onKeyDown(e) { - if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { - return; - } - this.pressed[e.key] = 'keydown'; - this.capture(e); - } - - onKeyUp(e) { - delete this.pressed[e.key]; - } - - capture(e) { - if (this.fromInput(e)) { - if (e.key === 'Escape' && e.target.blur) { - e.target.blur(); - } - return; - } - if (e.key === 'OS') { - return; - } - - let stop = false; - for (let listener of this.onKeyListeners) { - stop = stop || listener(e.key, e.ctrlKey); - if (stop) { - break; - } - } - if (stop) { - e.preventDefault(); - e.stopPropagation(); - } - } - - fromInput(e) { - return e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement; - } -} diff --git a/src/components/follow.js b/src/components/follow.js deleted file mode 100644 index 0ec1e87..0000000 --- a/src/components/follow.js +++ /dev/null @@ -1,168 +0,0 @@ -import * as followActions from 'actions/follow'; -import messages from 'shared/messages'; -import Hint from 'content/hint'; -import HintKeyProducer from 'content/hint-key-producer'; - -const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; - -const inWindow = (window, element) => { - let { - top, left, bottom, right - } = element.getBoundingClientRect(); - return ( - top >= 0 && left >= 0 && - bottom <= (window.innerHeight || document.documentElement.clientHeight) && - right <= (window.innerWidth || document.documentElement.clientWidth) - ); -}; - -export default class FollowComponent { - constructor(wrapper, store) { - this.wrapper = wrapper; - this.store = store; - this.hintElements = {}; - this.state = {}; - } - - update() { - let prevState = this.state; - this.state = this.store.getState().follow; - if (!prevState.enabled && this.state.enabled) { - this.create(); - } else if (prevState.enabled && !this.state.enabled) { - this.remove(); - } else if (prevState.keys !== this.state.keys) { - this.updateHints(); - } - } - - key(key) { - if (!this.state.enabled) { - return false; - } - - switch (key) { - case 'Enter': - this.activate(this.hintElements[this.state.keys].target); - return; - case 'Escape': - this.store.dispatch(followActions.disable()); - return; - case 'Backspace': - case 'Delete': - this.store.dispatch(followActions.backspace()); - break; - default: - if (DEFAULT_HINT_CHARSET.includes(key)) { - this.store.dispatch(followActions.keyPress(key)); - } - break; - } - return true; - } - - updateHints() { - let keys = this.state.keys; - let shown = Object.keys(this.hintElements).filter((key) => { - return key.startsWith(keys); - }); - let hidden = Object.keys(this.hintElements).filter((key) => { - return !key.startsWith(keys); - }); - if (shown.length === 0) { - this.remove(); - return; - } else if (shown.length === 1) { - this.activate(this.hintElements[keys].target); - this.store.dispatch(followActions.disable()); - } - - shown.forEach((key) => { - this.hintElements[key].show(); - }); - hidden.forEach((key) => { - this.hintElements[key].hide(); - }); - } - - activate(element) { - switch (element.tagName.toLowerCase()) { - case 'a': - if (this.state.newTab) { - // getAttribute() to avoid to resolve absolute path - let href = element.getAttribute('href'); - - // eslint-disable-next-line no-script-url - if (!href || href === '#' || href.startsWith('javascript:')) { - return; - } - return browser.runtime.sendMessage({ - type: messages.OPEN_URL, - url: element.href, - newTab: this.state.newTab, - }); - } - if (element.href.startsWith('http://') || - element.href.startsWith('https://') || - element.href.startsWith('ftp://')) { - return browser.runtime.sendMessage({ - type: messages.OPEN_URL, - url: element.href, - newTab: this.state.newTab, - }); - } - return element.click(); - case 'input': - switch (element.type) { - case 'file': - case 'checkbox': - case 'radio': - case 'submit': - case 'reset': - case 'button': - case 'image': - case 'color': - return element.click(); - default: - return element.focus(); - } - case 'textarea': - return element.focus(); - case 'button': - return element.click(); - } - } - - create() { - let doc = this.wrapper.ownerDocument; - let elements = FollowComponent.getTargetElements(doc); - let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); - let hintElements = {}; - Array.prototype.forEach.call(elements, (ele) => { - let keys = producer.produce(); - let hint = new Hint(ele, keys); - hintElements[keys] = hint; - }); - this.hintElements = hintElements; - } - - remove() { - let hintElements = this.hintElements; - Object.keys(this.hintElements).forEach((key) => { - hintElements[key].remove(); - }); - } - - static getTargetElements(doc) { - let all = doc.querySelectorAll('a,button,input,textarea'); - let filtered = Array.prototype.filter.call(all, (element) => { - let style = window.getComputedStyle(element); - return style.display !== 'none' && - style.visibility !== 'hidden' && - element.type !== 'hidden' && - element.offsetHeight > 0 && - inWindow(window, element); - }); - return filtered; - } -} diff --git a/src/components/keymapper.js b/src/components/keymapper.js deleted file mode 100644 index 3685a4f..0000000 --- a/src/components/keymapper.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as inputActions from 'actions/input'; -import * as operationActions from 'actions/operation'; - -export default class KeymapperComponent { - constructor(store) { - this.store = store; - } - - update() { - } - - key(key, ctrl) { - let keymaps = this.keymaps(); - if (!keymaps) { - return; - } - this.store.dispatch(inputActions.keyPress(key, ctrl)); - - let input = this.store.getState().input; - let matched = Object.keys(keymaps).filter((keyStr) => { - return keyStr.startsWith(input.keys); - }); - if (matched.length === 0) { - this.store.dispatch(inputActions.clearKeys()); - return false; - } else if (matched.length > 1 || - matched.length === 1 && input.keys !== matched[0]) { - return true; - } - let operation = keymaps[matched]; - this.store.dispatch(operationActions.exec(operation)); - this.store.dispatch(inputActions.clearKeys()); - return true; - } - - keymaps() { - let settings = this.store.getState().setting.settings; - if (!settings || !settings.json) { - return null; - } - return JSON.parse(settings.json).keymaps; - } -} diff --git a/src/content/actions/follow.js b/src/content/actions/follow.js new file mode 100644 index 0000000..5a18dd5 --- /dev/null +++ b/src/content/actions/follow.js @@ -0,0 +1,29 @@ +import actions from 'content/actions'; + +const enable = (newTab) => { + return { + type: actions.FOLLOW_ENABLE, + newTab, + }; +}; + +const disable = () => { + return { + type: actions.FOLLOW_DISABLE, + }; +}; + +const keyPress = (key) => { + return { + type: actions.FOLLOW_KEY_PRESS, + key: key + }; +}; + +const backspace = () => { + return { + type: actions.FOLLOW_BACKSPACE, + }; +}; + +export { enable, disable, keyPress, backspace }; diff --git a/src/content/actions/index.js b/src/content/actions/index.js new file mode 100644 index 0000000..0b3749d --- /dev/null +++ b/src/content/actions/index.js @@ -0,0 +1,20 @@ +export default { + // User input + INPUT_KEY_PRESS: 'input.key,press', + INPUT_CLEAR_KEYS: 'input.clear.keys', + INPUT_SET_KEYMAPS: 'input.set,keymaps', + + // Completion + COMPLETION_SET_ITEMS: 'completion.set.items', + COMPLETION_SELECT_NEXT: 'completions.select.next', + COMPLETION_SELECT_PREV: 'completions.select.prev', + + // Settings + SETTING_SET_SETTINGS: 'setting.set.settings', + + // Follow + FOLLOW_ENABLE: 'follow.enable', + FOLLOW_DISABLE: 'follow.disable', + FOLLOW_KEY_PRESS: 'follow.key.press', + FOLLOW_BACKSPACE: 'follow.backspace', +}; diff --git a/src/content/actions/input.js b/src/content/actions/input.js new file mode 100644 index 0000000..cc4efac --- /dev/null +++ b/src/content/actions/input.js @@ -0,0 +1,23 @@ +import actions from 'content/actions'; + +const asKeymapChars = (key, ctrl) => { + if (ctrl) { + return ''; + } + return key; +}; + +const keyPress = (key, ctrl) => { + return { + type: actions.INPUT_KEY_PRESS, + key: asKeymapChars(key, ctrl), + }; +}; + +const clearKeys = () => { + return { + type: actions.INPUT_CLEAR_KEYS + }; +}; + +export { keyPress, clearKeys }; diff --git a/src/content/actions/operation.js b/src/content/actions/operation.js new file mode 100644 index 0000000..d188a60 --- /dev/null +++ b/src/content/actions/operation.js @@ -0,0 +1,43 @@ +import operations from 'shared/operations'; +import messages from 'shared/messages'; +import * as scrolls from 'content/scrolls'; +import * as navigates from 'content/navigates'; +import * as followActions from 'content/actions/follow'; + +const exec = (operation) => { + switch (operation.type) { + case operations.SCROLL_LINES: + return scrolls.scrollLines(window, operation.count); + case operations.SCROLL_PAGES: + return scrolls.scrollPages(window, operation.count); + case operations.SCROLL_TOP: + return scrolls.scrollTop(window); + case operations.SCROLL_BOTTOM: + return scrolls.scrollBottom(window); + case operations.SCROLL_HOME: + return scrolls.scrollLeft(window); + case operations.SCROLL_END: + return scrolls.scrollRight(window); + case operations.FOLLOW_START: + return followActions.enable(false); + case operations.NAVIGATE_HISTORY_PREV: + return navigates.historyPrev(window); + case operations.NAVIGATE_HISTORY_NEXT: + return navigates.historyNext(window); + case operations.NAVIGATE_LINK_PREV: + return navigates.linkPrev(window); + case operations.NAVIGATE_LINK_NEXT: + return navigates.linkNext(window); + case operations.NAVIGATE_PARENT: + return navigates.parent(window); + case operations.NAVIGATE_ROOT: + return navigates.root(window); + default: + browser.runtime.sendMessage({ + type: messages.BACKGROUND_OPERATION, + operation, + }); + } +}; + +export { exec }; diff --git a/src/content/components/content-input.js b/src/content/components/content-input.js new file mode 100644 index 0000000..9568caf --- /dev/null +++ b/src/content/components/content-input.js @@ -0,0 +1,67 @@ +export default class ContentInputComponent { + constructor(target) { + this.pressed = {}; + this.onKeyListeners = []; + + target.addEventListener('keypress', this.onKeyPress.bind(this)); + target.addEventListener('keydown', this.onKeyDown.bind(this)); + target.addEventListener('keyup', this.onKeyUp.bind(this)); + } + + update() { + } + + onKey(cb) { + this.onKeyListeners.push(cb); + } + + onKeyPress(e) { + if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { + return; + } + this.pressed[e.key] = 'keypress'; + this.capture(e); + } + + onKeyDown(e) { + if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { + return; + } + this.pressed[e.key] = 'keydown'; + this.capture(e); + } + + onKeyUp(e) { + delete this.pressed[e.key]; + } + + capture(e) { + if (this.fromInput(e)) { + if (e.key === 'Escape' && e.target.blur) { + e.target.blur(); + } + return; + } + if (e.key === 'OS') { + return; + } + + let stop = false; + for (let listener of this.onKeyListeners) { + stop = stop || listener(e.key, e.ctrlKey); + if (stop) { + break; + } + } + if (stop) { + e.preventDefault(); + e.stopPropagation(); + } + } + + fromInput(e) { + return e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement; + } +} diff --git a/src/content/components/follow.js b/src/content/components/follow.js new file mode 100644 index 0000000..c87424d --- /dev/null +++ b/src/content/components/follow.js @@ -0,0 +1,168 @@ +import * as followActions from 'content/actions/follow'; +import messages from 'shared/messages'; +import Hint from 'content/hint'; +import HintKeyProducer from 'content/hint-key-producer'; + +const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; + +const inWindow = (window, element) => { + let { + top, left, bottom, right + } = element.getBoundingClientRect(); + return ( + top >= 0 && left >= 0 && + bottom <= (window.innerHeight || document.documentElement.clientHeight) && + right <= (window.innerWidth || document.documentElement.clientWidth) + ); +}; + +export default class FollowComponent { + constructor(wrapper, store) { + this.wrapper = wrapper; + this.store = store; + this.hintElements = {}; + this.state = {}; + } + + update() { + let prevState = this.state; + this.state = this.store.getState().follow; + if (!prevState.enabled && this.state.enabled) { + this.create(); + } else if (prevState.enabled && !this.state.enabled) { + this.remove(); + } else if (prevState.keys !== this.state.keys) { + this.updateHints(); + } + } + + key(key) { + if (!this.state.enabled) { + return false; + } + + switch (key) { + case 'Enter': + this.activate(this.hintElements[this.state.keys].target); + return; + case 'Escape': + this.store.dispatch(followActions.disable()); + return; + case 'Backspace': + case 'Delete': + this.store.dispatch(followActions.backspace()); + break; + default: + if (DEFAULT_HINT_CHARSET.includes(key)) { + this.store.dispatch(followActions.keyPress(key)); + } + break; + } + return true; + } + + updateHints() { + let keys = this.state.keys; + let shown = Object.keys(this.hintElements).filter((key) => { + return key.startsWith(keys); + }); + let hidden = Object.keys(this.hintElements).filter((key) => { + return !key.startsWith(keys); + }); + if (shown.length === 0) { + this.remove(); + return; + } else if (shown.length === 1) { + this.activate(this.hintElements[keys].target); + this.store.dispatch(followActions.disable()); + } + + shown.forEach((key) => { + this.hintElements[key].show(); + }); + hidden.forEach((key) => { + this.hintElements[key].hide(); + }); + } + + activate(element) { + switch (element.tagName.toLowerCase()) { + case 'a': + if (this.state.newTab) { + // getAttribute() to avoid to resolve absolute path + let href = element.getAttribute('href'); + + // eslint-disable-next-line no-script-url + if (!href || href === '#' || href.startsWith('javascript:')) { + return; + } + return browser.runtime.sendMessage({ + type: messages.OPEN_URL, + url: element.href, + newTab: this.state.newTab, + }); + } + if (element.href.startsWith('http://') || + element.href.startsWith('https://') || + element.href.startsWith('ftp://')) { + return browser.runtime.sendMessage({ + type: messages.OPEN_URL, + url: element.href, + newTab: this.state.newTab, + }); + } + return element.click(); + case 'input': + switch (element.type) { + case 'file': + case 'checkbox': + case 'radio': + case 'submit': + case 'reset': + case 'button': + case 'image': + case 'color': + return element.click(); + default: + return element.focus(); + } + case 'textarea': + return element.focus(); + case 'button': + return element.click(); + } + } + + create() { + let doc = this.wrapper.ownerDocument; + let elements = FollowComponent.getTargetElements(doc); + let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); + let hintElements = {}; + Array.prototype.forEach.call(elements, (ele) => { + let keys = producer.produce(); + let hint = new Hint(ele, keys); + hintElements[keys] = hint; + }); + this.hintElements = hintElements; + } + + remove() { + let hintElements = this.hintElements; + Object.keys(this.hintElements).forEach((key) => { + hintElements[key].remove(); + }); + } + + static getTargetElements(doc) { + let all = doc.querySelectorAll('a,button,input,textarea'); + let filtered = Array.prototype.filter.call(all, (element) => { + let style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + element.type !== 'hidden' && + element.offsetHeight > 0 && + inWindow(window, element); + }); + return filtered; + } +} diff --git a/src/content/components/keymapper.js b/src/content/components/keymapper.js new file mode 100644 index 0000000..8f2cead --- /dev/null +++ b/src/content/components/keymapper.js @@ -0,0 +1,43 @@ +import * as inputActions from 'content/actions/input'; +import * as operationActions from 'content/actions/operation'; + +export default class KeymapperComponent { + constructor(store) { + this.store = store; + } + + update() { + } + + key(key, ctrl) { + let keymaps = this.keymaps(); + if (!keymaps) { + return; + } + this.store.dispatch(inputActions.keyPress(key, ctrl)); + + let input = this.store.getState().input; + let matched = Object.keys(keymaps).filter((keyStr) => { + return keyStr.startsWith(input.keys); + }); + if (matched.length === 0) { + this.store.dispatch(inputActions.clearKeys()); + return false; + } else if (matched.length > 1 || + matched.length === 1 && input.keys !== matched[0]) { + return true; + } + let operation = keymaps[matched]; + this.store.dispatch(operationActions.exec(operation)); + this.store.dispatch(inputActions.clearKeys()); + return true; + } + + keymaps() { + let settings = this.store.getState().setting.settings; + if (!settings || !settings.json) { + return null; + } + return JSON.parse(settings.json).keymaps; + } +} diff --git a/src/content/index.js b/src/content/index.js index edca510..00873cc 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -2,10 +2,10 @@ import './console-frame.scss'; import * as consoleFrames from './console-frames'; import * as settingActions from 'settings/actions/setting'; import { createStore } from 'store'; -import ContentInputComponent from 'components/content-input'; -import KeymapperComponent from 'components/keymapper'; -import FollowComponent from 'components/follow'; -import reducers from 'reducers'; +import ContentInputComponent from 'content/components/content-input'; +import KeymapperComponent from 'content/components/keymapper'; +import FollowComponent from 'content/components/follow'; +import reducers from 'content/reducers'; import messages from 'shared/messages'; const store = createStore(reducers); diff --git a/src/content/reducers/follow.js b/src/content/reducers/follow.js new file mode 100644 index 0000000..b7c0cf3 --- /dev/null +++ b/src/content/reducers/follow.js @@ -0,0 +1,32 @@ +import actions from 'content/actions'; + +const defaultState = { + enabled: false, + newTab: false, + keys: '', +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.FOLLOW_ENABLE: + return Object.assign({}, state, { + enabled: true, + newTab: action.newTab, + keys: '', + }); + case actions.FOLLOW_DISABLE: + return Object.assign({}, state, { + enabled: false, + }); + case actions.FOLLOW_KEY_PRESS: + return Object.assign({}, state, { + keys: state.keys + action.key, + }); + case actions.FOLLOW_BACKSPACE: + return Object.assign({}, state, { + keys: state.keys.slice(0, -1), + }); + default: + return state; + } +} diff --git a/src/content/reducers/index.js b/src/content/reducers/index.js new file mode 100644 index 0000000..a62217f --- /dev/null +++ b/src/content/reducers/index.js @@ -0,0 +1,18 @@ +import settingReducer from 'settings/reducers/setting'; +import inputReducer from './input'; +import followReducer from './follow'; + +// Make setting reducer instead of re-use +const defaultState = { + input: inputReducer(undefined, {}), + setting: settingReducer(undefined, {}), + follow: followReducer(undefined, {}), +}; + +export default function reducer(state = defaultState, action = {}) { + return Object.assign({}, state, { + input: inputReducer(state.input, action), + setting: settingReducer(state.setting, action), + follow: followReducer(state.follow, action), + }); +} diff --git a/src/content/reducers/input.js b/src/content/reducers/input.js new file mode 100644 index 0000000..802020f --- /dev/null +++ b/src/content/reducers/input.js @@ -0,0 +1,20 @@ +import actions from 'content/actions'; + +const defaultState = { + keys: '', +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.INPUT_KEY_PRESS: + return Object.assign({}, state, { + keys: state.keys + action.key + }); + case actions.INPUT_CLEAR_KEYS: + return Object.assign({}, state, { + keys: '', + }); + default: + return state; + } +} diff --git a/src/reducers/follow.js b/src/reducers/follow.js deleted file mode 100644 index ed875e8..0000000 --- a/src/reducers/follow.js +++ /dev/null @@ -1,32 +0,0 @@ -import actions from 'actions'; - -const defaultState = { - enabled: false, - newTab: false, - keys: '', -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.FOLLOW_ENABLE: - return Object.assign({}, state, { - enabled: true, - newTab: action.newTab, - keys: '', - }); - case actions.FOLLOW_DISABLE: - return Object.assign({}, state, { - enabled: false, - }); - case actions.FOLLOW_KEY_PRESS: - return Object.assign({}, state, { - keys: state.keys + action.key, - }); - case actions.FOLLOW_BACKSPACE: - return Object.assign({}, state, { - keys: state.keys.slice(0, -1), - }); - default: - return state; - } -} diff --git a/src/reducers/index.js b/src/reducers/index.js deleted file mode 100644 index 9c86ebf..0000000 --- a/src/reducers/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import inputReducer from 'reducers/input'; -import settingReducer from 'settings/reducers/setting'; -import followReducer from 'reducers/follow'; - -// Make setting reducer instead of re-use -const defaultState = { - input: inputReducer(undefined, {}), - setting: settingReducer(undefined, {}), - follow: followReducer(undefined, {}), -}; - -export default function reducer(state = defaultState, action = {}) { - return Object.assign({}, state, { - input: inputReducer(state.input, action), - setting: settingReducer(state.setting, action), - follow: followReducer(state.follow, action), - }); -} diff --git a/src/reducers/input.js b/src/reducers/input.js deleted file mode 100644 index 2e4bcd8..0000000 --- a/src/reducers/input.js +++ /dev/null @@ -1,20 +0,0 @@ -import actions from 'actions'; - -const defaultState = { - keys: '', -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.INPUT_KEY_PRESS: - return Object.assign({}, state, { - keys: state.keys + action.key - }); - case actions.INPUT_CLEAR_KEYS: - return Object.assign({}, state, { - keys: '', - }); - default: - return state; - } -} diff --git a/test/actions/follow.test.js b/test/actions/follow.test.js deleted file mode 100644 index 32ab9e2..0000000 --- a/test/actions/follow.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import * as followActions from 'actions/follow'; - -describe('follow actions', () => { - describe('enable', () => { - it('creates FOLLOW_ENABLE action', () => { - let action = followActions.enable(true); - expect(action.type).to.equal(actions.FOLLOW_ENABLE); - expect(action.newTab).to.equal(true); - }); - }); - - describe('disable', () => { - it('creates FOLLOW_DISABLE action', () => { - let action = followActions.disable(true); - expect(action.type).to.equal(actions.FOLLOW_DISABLE); - }); - }); - - describe('keyPress', () => { - it('creates FOLLOW_KEY_PRESS action', () => { - let action = followActions.keyPress(100); - expect(action.type).to.equal(actions.FOLLOW_KEY_PRESS); - expect(action.key).to.equal(100); - }); - }); - - describe('backspace', () => { - it('creates FOLLOW_BACKSPACE action', () => { - let action = followActions.backspace(100); - expect(action.type).to.equal(actions.FOLLOW_BACKSPACE); - }); - }); -}); diff --git a/test/actions/input.test.js b/test/actions/input.test.js deleted file mode 100644 index 0a2ab18..0000000 --- a/test/actions/input.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import * as inputActions from 'actions/input'; - -describe("input actions", () => { - describe("keyPress", () => { - it('create INPUT_KEY_PRESS action', () => { - let action = inputActions.keyPress('a', false); - expect(action.type).to.equal(actions.INPUT_KEY_PRESS); - expect(action.key).to.equal('a'); - }); - - it('create INPUT_KEY_PRESS action from key with ctrl', () => { - let action = inputActions.keyPress('b', true); - expect(action.type).to.equal(actions.INPUT_KEY_PRESS); - expect(action.key).to.equal(''); - }); - }); - - describe("clearKeys", () => { - it('create INPUT_CLEAR_KEYSaction', () => { - let action = inputActions.clearKeys(); - expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS); - }); - }); -}); diff --git a/test/components/follow.html b/test/components/follow.html deleted file mode 100644 index 6bd8f87..0000000 --- a/test/components/follow.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - link - invisible 1 - invisible 2 - not link - - diff --git a/test/components/follow.test.js b/test/components/follow.test.js deleted file mode 100644 index 294bfc9..0000000 --- a/test/components/follow.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from "chai"; -import FollowComponent from 'components/follow'; - -describe('FollowComponent', () => { - describe('#getTargetElements', () => { - beforeEach(() => { - document.body.innerHTML = __html__['test/components/follow.html']; - }); - - it('returns visible links', () => { - let links = FollowComponent.getTargetElements(window.document); - expect(links).to.have.lengthOf(1); - }); - }); -}); diff --git a/test/content/actions/follow.test.js b/test/content/actions/follow.test.js new file mode 100644 index 0000000..3ac844c --- /dev/null +++ b/test/content/actions/follow.test.js @@ -0,0 +1,35 @@ +import { expect } from "chai"; +import actions from 'content/actions'; +import * as followActions from 'content/actions/follow'; + +describe('follow actions', () => { + describe('enable', () => { + it('creates FOLLOW_ENABLE action', () => { + let action = followActions.enable(true); + expect(action.type).to.equal(actions.FOLLOW_ENABLE); + expect(action.newTab).to.equal(true); + }); + }); + + describe('disable', () => { + it('creates FOLLOW_DISABLE action', () => { + let action = followActions.disable(true); + expect(action.type).to.equal(actions.FOLLOW_DISABLE); + }); + }); + + describe('keyPress', () => { + it('creates FOLLOW_KEY_PRESS action', () => { + let action = followActions.keyPress(100); + expect(action.type).to.equal(actions.FOLLOW_KEY_PRESS); + expect(action.key).to.equal(100); + }); + }); + + describe('backspace', () => { + it('creates FOLLOW_BACKSPACE action', () => { + let action = followActions.backspace(100); + expect(action.type).to.equal(actions.FOLLOW_BACKSPACE); + }); + }); +}); diff --git a/test/content/actions/input.test.js b/test/content/actions/input.test.js new file mode 100644 index 0000000..6031829 --- /dev/null +++ b/test/content/actions/input.test.js @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import actions from 'content/actions'; +import * as inputActions from 'content/actions/input'; + +describe("input actions", () => { + describe("keyPress", () => { + it('create INPUT_KEY_PRESS action', () => { + let action = inputActions.keyPress('a', false); + expect(action.type).to.equal(actions.INPUT_KEY_PRESS); + expect(action.key).to.equal('a'); + }); + + it('create INPUT_KEY_PRESS action from key with ctrl', () => { + let action = inputActions.keyPress('b', true); + expect(action.type).to.equal(actions.INPUT_KEY_PRESS); + expect(action.key).to.equal(''); + }); + }); + + describe("clearKeys", () => { + it('create INPUT_CLEAR_KEYSaction', () => { + let action = inputActions.clearKeys(); + expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS); + }); + }); +}); diff --git a/test/content/components/follow.html b/test/content/components/follow.html new file mode 100644 index 0000000..6bd8f87 --- /dev/null +++ b/test/content/components/follow.html @@ -0,0 +1,9 @@ + + + + link + invisible 1 + invisible 2 + not link + + diff --git a/test/content/components/follow.test.js b/test/content/components/follow.test.js new file mode 100644 index 0000000..9c00c79 --- /dev/null +++ b/test/content/components/follow.test.js @@ -0,0 +1,15 @@ +import { expect } from "chai"; +import FollowComponent from 'content/components/follow'; + +describe('FollowComponent', () => { + describe('#getTargetElements', () => { + beforeEach(() => { + document.body.innerHTML = __html__['test/content/components/follow.html']; + }); + + it('returns visible links', () => { + let links = FollowComponent.getTargetElements(window.document); + expect(links).to.have.lengthOf(1); + }); + }); +}); diff --git a/test/content/reducers/follow.test.js b/test/content/reducers/follow.test.js new file mode 100644 index 0000000..e2b3445 --- /dev/null +++ b/test/content/reducers/follow.test.js @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import actions from 'content/actions'; +import followReducer from 'content/reducers/follow'; + +describe('follow reducer', () => { + it ('returns the initial state', () => { + let state = followReducer(undefined, {}); + expect(state).to.have.property('enabled', false); + expect(state).to.have.property('newTab'); + expect(state).to.have.deep.property('keys', ''); + }); + + it ('returns next state for FOLLOW_ENABLE', () => { + let action = { type: actions.FOLLOW_ENABLE, newTab: true }; + let state = followReducer({ enabled: false, newTab: false }, action); + expect(state).to.have.property('enabled', true); + expect(state).to.have.property('newTab', true); + expect(state).to.have.property('keys', ''); + }); + + it ('returns next state for FOLLOW_DISABLE', () => { + let action = { type: actions.FOLLOW_DISABLE }; + let state = followReducer({ enabled: true }, action); + expect(state).to.have.property('enabled', false); + }); + + it ('returns next state for FOLLOW_KEY_PRESS', () => { + let action = { type: actions.FOLLOW_KEY_PRESS, key: 'a'}; + let state = followReducer({ keys: '' }, action); + expect(state).to.have.deep.property('keys', 'a'); + + action = { type: actions.FOLLOW_KEY_PRESS, key: 'b'}; + state = followReducer(state, action); + expect(state).to.have.deep.property('keys', 'ab'); + }); + + it ('returns next state for FOLLOW_BACKSPACE', () => { + let action = { type: actions.FOLLOW_BACKSPACE }; + let state = followReducer({ keys: 'ab' }, action); + expect(state).to.have.deep.property('keys', 'a'); + + state = followReducer(state, action); + expect(state).to.have.deep.property('keys', ''); + + state = followReducer(state, action); + expect(state).to.have.deep.property('keys', ''); + }); +}); diff --git a/test/content/reducers/input.test.js b/test/content/reducers/input.test.js new file mode 100644 index 0000000..d5e5f6b --- /dev/null +++ b/test/content/reducers/input.test.js @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import actions from 'content/actions'; +import inputReducer from 'content/reducers/input'; + +describe("input reducer", () => { + it('return the initial state', () => { + let state = inputReducer(undefined, {}); + expect(state).to.have.deep.property('keys', ''); + }); + + it('return next state for INPUT_KEY_PRESS', () => { + let action = { type: actions.INPUT_KEY_PRESS, key: 'a' }; + let state = inputReducer(undefined, action); + expect(state).to.have.deep.property('keys', 'a'); + + action = { type: actions.INPUT_KEY_PRESS, key: '' }; + state = inputReducer(state, action); + expect(state).to.have.deep.property('keys', 'a'); + }); + + it('return next state for INPUT_CLEAR_KEYS', () => { + let action = { type: actions.INPUT_CLEAR_KEYS }; + let state = inputReducer({ keys: 'abc' }, action); + expect(state).to.have.deep.property('keys', ''); + }); +}); diff --git a/test/reducers/follow.test.js b/test/reducers/follow.test.js deleted file mode 100644 index e1db680..0000000 --- a/test/reducers/follow.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import followReducer from 'reducers/follow'; - -describe('follow reducer', () => { - it ('returns the initial state', () => { - let state = followReducer(undefined, {}); - expect(state).to.have.property('enabled', false); - expect(state).to.have.property('newTab'); - expect(state).to.have.deep.property('keys', ''); - }); - - it ('returns next state for FOLLOW_ENABLE', () => { - let action = { type: actions.FOLLOW_ENABLE, newTab: true }; - let state = followReducer({ enabled: false, newTab: false }, action); - expect(state).to.have.property('enabled', true); - expect(state).to.have.property('newTab', true); - expect(state).to.have.property('keys', ''); - }); - - it ('returns next state for FOLLOW_DISABLE', () => { - let action = { type: actions.FOLLOW_DISABLE }; - let state = followReducer({ enabled: true }, action); - expect(state).to.have.property('enabled', false); - }); - - it ('returns next state for FOLLOW_KEY_PRESS', () => { - let action = { type: actions.FOLLOW_KEY_PRESS, key: 'a'}; - let state = followReducer({ keys: '' }, action); - expect(state).to.have.deep.property('keys', 'a'); - - action = { type: actions.FOLLOW_KEY_PRESS, key: 'b'}; - state = followReducer(state, action); - expect(state).to.have.deep.property('keys', 'ab'); - }); - - it ('returns next state for FOLLOW_BACKSPACE', () => { - let action = { type: actions.FOLLOW_BACKSPACE }; - let state = followReducer({ keys: 'ab' }, action); - expect(state).to.have.deep.property('keys', 'a'); - - state = followReducer(state, action); - expect(state).to.have.deep.property('keys', ''); - - state = followReducer(state, action); - expect(state).to.have.deep.property('keys', ''); - }); -}); diff --git a/test/reducers/input.test.js b/test/reducers/input.test.js deleted file mode 100644 index 7b5a89c..0000000 --- a/test/reducers/input.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from "chai"; -import actions from 'actions'; -import inputReducer from 'reducers/input'; - -describe("input reducer", () => { - it('return the initial state', () => { - let state = inputReducer(undefined, {}); - expect(state).to.have.deep.property('keys', ''); - }); - - it('return next state for INPUT_KEY_PRESS', () => { - let action = { type: actions.INPUT_KEY_PRESS, key: 'a' }; - let state = inputReducer(undefined, action); - expect(state).to.have.deep.property('keys', 'a'); - - action = { type: actions.INPUT_KEY_PRESS, key: '' }; - state = inputReducer(state, action); - expect(state).to.have.deep.property('keys', 'a'); - }); - - it('return next state for INPUT_CLEAR_KEYS', () => { - let action = { type: actions.INPUT_CLEAR_KEYS }; - let state = inputReducer({ keys: 'abc' }, action); - expect(state).to.have.deep.property('keys', ''); - }); -}); -- cgit v1.2.3 From 2faf44af7443b3f858e15d63295a490afba83b4e Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 15:11:09 +0900 Subject: move store to shared --- src/background/index.js | 2 +- src/console/index.js | 2 +- src/content/index.js | 2 +- src/settings/index.js | 2 +- src/shared/store/index.js | 53 +++++++++++++++++++ src/store/index.js | 53 ------------------- test/shared/store/index.test.js | 111 ++++++++++++++++++++++++++++++++++++++++ test/store/index.test.js | 111 ---------------------------------------- 8 files changed, 168 insertions(+), 168 deletions(-) create mode 100644 src/shared/store/index.js delete mode 100644 src/store/index.js create mode 100644 test/shared/store/index.test.js delete mode 100644 test/store/index.test.js (limited to 'test') diff --git a/src/background/index.js b/src/background/index.js index 587cc0b..6ba37eb 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -2,7 +2,7 @@ import * as settingsActions from 'settings/actions/setting'; import messages from 'shared/messages'; import BackgroundComponent from 'background/components/background'; import reducers from 'settings/reducers/setting'; -import { createStore } from 'store'; +import { createStore } from 'shared/store'; const store = createStore(reducers, (e, sender) => { console.error('Vim-Vixen:', e); diff --git a/src/console/index.js b/src/console/index.js index 1bcf8bc..7396a96 100644 --- a/src/console/index.js +++ b/src/console/index.js @@ -3,7 +3,7 @@ import messages from 'shared/messages'; import CompletionComponent from 'console/components/completion'; import ConsoleComponent from 'console/components/console'; import reducers from 'console/reducers'; -import { createStore } from 'store'; +import { createStore } from 'shared/store'; import * as consoleActions from 'console/actions/console'; const store = createStore(reducers); diff --git a/src/content/index.js b/src/content/index.js index 00873cc..adea871 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,7 +1,7 @@ import './console-frame.scss'; import * as consoleFrames from './console-frames'; import * as settingActions from 'settings/actions/setting'; -import { createStore } from 'store'; +import { createStore } from 'shared/store'; import ContentInputComponent from 'content/components/content-input'; import KeymapperComponent from 'content/components/keymapper'; import FollowComponent from 'content/components/follow'; diff --git a/src/settings/index.js b/src/settings/index.js index 8c60f80..c8d6cc4 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -1,7 +1,7 @@ import './site.scss'; import SettingComponent from 'settings/components/setting'; import settingReducer from 'settings/reducers/setting'; -import { createStore } from 'store'; +import { createStore } from 'shared/store'; const store = createStore(settingReducer); let settingComponent = null; diff --git a/src/shared/store/index.js b/src/shared/store/index.js new file mode 100644 index 0000000..2fafdf1 --- /dev/null +++ b/src/shared/store/index.js @@ -0,0 +1,53 @@ +class Store { + constructor(reducer, catcher) { + this.reducer = reducer; + this.catcher = catcher; + this.subscribers = []; + try { + this.state = this.reducer(undefined, {}); + } catch (e) { + catcher(e); + } + } + + dispatch(action, sender) { + if (action instanceof Promise) { + action.then((a) => { + this.transitNext(a, sender); + }).catch((e) => { + this.catcher(e, sender); + }); + } else { + try { + this.transitNext(action, sender); + } catch (e) { + this.catcher(e, sender); + } + } + return action; + } + + getState() { + return this.state; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + transitNext(action, sender) { + let newState = this.reducer(this.state, action); + if (JSON.stringify(this.state) !== JSON.stringify(newState)) { + this.state = newState; + this.subscribers.forEach(f => f(sender)); + } + } +} + +const empty = () => {}; + +const createStore = (reducer, catcher = empty) => { + return new Store(reducer, catcher); +}; + +export { createStore }; diff --git a/src/store/index.js b/src/store/index.js deleted file mode 100644 index 2fafdf1..0000000 --- a/src/store/index.js +++ /dev/null @@ -1,53 +0,0 @@ -class Store { - constructor(reducer, catcher) { - this.reducer = reducer; - this.catcher = catcher; - this.subscribers = []; - try { - this.state = this.reducer(undefined, {}); - } catch (e) { - catcher(e); - } - } - - dispatch(action, sender) { - if (action instanceof Promise) { - action.then((a) => { - this.transitNext(a, sender); - }).catch((e) => { - this.catcher(e, sender); - }); - } else { - try { - this.transitNext(action, sender); - } catch (e) { - this.catcher(e, sender); - } - } - return action; - } - - getState() { - return this.state; - } - - subscribe(callback) { - this.subscribers.push(callback); - } - - transitNext(action, sender) { - let newState = this.reducer(this.state, action); - if (JSON.stringify(this.state) !== JSON.stringify(newState)) { - this.state = newState; - this.subscribers.forEach(f => f(sender)); - } - } -} - -const empty = () => {}; - -const createStore = (reducer, catcher = empty) => { - return new Store(reducer, catcher); -}; - -export { createStore }; diff --git a/test/shared/store/index.test.js b/test/shared/store/index.test.js new file mode 100644 index 0000000..133033b --- /dev/null +++ b/test/shared/store/index.test.js @@ -0,0 +1,111 @@ +import { expect } from "chai"; +import { createStore } from 'shared/store'; + +describe("Store class", () => { + const reducer = (state, action) => { + if (state == undefined) { + return 0; + } + return state + action; + }; + + describe("#dispatch", () => { + it('transit status by immediate action', () => { + let store = createStore(reducer); + store.dispatch(10); + expect(store.getState()).to.equal(10); + + store.dispatch(-20); + expect(store.getState()).to.equal(-10); + }); + + it('returns next state by immediate action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(11); + expect(dispatchedAction).to.equal(11); + }); + + it('transit status by Promise action', () => { + let store = createStore(reducer); + let p1 = Promise.resolve(10); + + return store.dispatch(p1).then(() => { + expect(store.getState()).to.equal(10); + }).then(() => { + store.dispatch(Promise.resolve(-20)); + }).then(() => { + expect(store.getState()).to.equal(-10); + }); + }); + + it('returns next state by promise action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(Promise.resolve(11)); + return dispatchedAction.then((value) => { + expect(value).to.equal(11); + }); + }); + }); + + describe("#subscribe", () => { + it('invoke callback', (done) => { + let store = createStore(reducer); + store.subscribe(() => { + expect(store.getState()).to.equal(15); + done(); + }); + store.dispatch(15); + }); + + it('propagate sender object', (done) => { + let store = createStore(reducer); + store.subscribe((sender) => { + expect(sender).to.equal('sender'); + done(); + }); + store.dispatch(15, 'sender'); + }); + }) + + describe("catcher", () => { + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore(() => { + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + }); + + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(20); + }); + + it('catch an error in reducer on initializing by promise action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(Promise.resolve(20)); + }); + + it('catch an error in promise action', (done) => { + let store = createStore((state, action) => 0, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(new Promise(() => { throw new Error() })); + }); + }) +}); + diff --git a/test/store/index.test.js b/test/store/index.test.js deleted file mode 100644 index 5dce715..0000000 --- a/test/store/index.test.js +++ /dev/null @@ -1,111 +0,0 @@ -import { expect } from "chai"; -import { createStore } from 'store'; - -describe("Store class", () => { - const reducer = (state, action) => { - if (state == undefined) { - return 0; - } - return state + action; - }; - - describe("#dispatch", () => { - it('transit status by immediate action', () => { - let store = createStore(reducer); - store.dispatch(10); - expect(store.getState()).to.equal(10); - - store.dispatch(-20); - expect(store.getState()).to.equal(-10); - }); - - it('returns next state by immediate action', () => { - let store = createStore(reducer); - let dispatchedAction = store.dispatch(11); - expect(dispatchedAction).to.equal(11); - }); - - it('transit status by Promise action', () => { - let store = createStore(reducer); - let p1 = Promise.resolve(10); - - return store.dispatch(p1).then(() => { - expect(store.getState()).to.equal(10); - }).then(() => { - store.dispatch(Promise.resolve(-20)); - }).then(() => { - expect(store.getState()).to.equal(-10); - }); - }); - - it('returns next state by promise action', () => { - let store = createStore(reducer); - let dispatchedAction = store.dispatch(Promise.resolve(11)); - return dispatchedAction.then((value) => { - expect(value).to.equal(11); - }); - }); - }); - - describe("#subscribe", () => { - it('invoke callback', (done) => { - let store = createStore(reducer); - store.subscribe(() => { - expect(store.getState()).to.equal(15); - done(); - }); - store.dispatch(15); - }); - - it('propagate sender object', (done) => { - let store = createStore(reducer); - store.subscribe((sender) => { - expect(sender).to.equal('sender'); - done(); - }); - store.dispatch(15, 'sender'); - }); - }) - - describe("catcher", () => { - it('catch an error in reducer on initializing by immediate action', (done) => { - let store = createStore(() => { - throw new Error(); - }, (e) => { - expect(e).to.be.an('error'); - done(); - }); - }); - - it('catch an error in reducer on initializing by immediate action', (done) => { - let store = createStore((state, action) => { - if (state === undefined) return 0; - throw new Error(); - }, (e) => { - expect(e).to.be.an('error'); - done(); - }); - store.dispatch(20); - }); - - it('catch an error in reducer on initializing by promise action', (done) => { - let store = createStore((state, action) => { - if (state === undefined) return 0; - throw new Error(); - }, (e) => { - expect(e).to.be.an('error'); - done(); - }); - store.dispatch(Promise.resolve(20)); - }); - - it('catch an error in promise action', (done) => { - let store = createStore((state, action) => 0, (e) => { - expect(e).to.be.an('error'); - done(); - }); - store.dispatch(new Promise(() => { throw new Error() })); - }); - }) -}); - -- cgit v1.2.3