From c60d0e7392fc708e961614d6b756a045de74f458 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 30 Apr 2019 14:00:07 +0900 Subject: Rename .js/.jsx to .ts/.tsx --- test/content/actions/setting.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/content/actions/setting.test.ts (limited to 'test/content/actions/setting.test.ts') diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts new file mode 100644 index 0000000..10f6807 --- /dev/null +++ b/test/content/actions/setting.test.ts @@ -0,0 +1,35 @@ +import actions from 'content/actions'; +import * as settingActions from 'content/actions/setting'; + +describe("setting actions", () => { + describe("set", () => { + it('create SETTING_SET action', () => { + let action = settingActions.set({ red: 'apple', yellow: 'banana' }); + expect(action.type).to.equal(actions.SETTING_SET); + expect(action.value.red).to.equal('apple'); + expect(action.value.yellow).to.equal('banana'); + expect(action.value.keymaps).to.be.empty; + }); + + it('converts keymaps', () => { + let action = settingActions.set({ + keymaps: { + 'dd': 'remove current tab', + 'z': 'increment', + } + }); + let keymaps = action.value.keymaps; + let map = new Map(keymaps); + expect(map).to.have.deep.all.keys( + [ + [{ key: 'Esc', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], + [{ key: '[', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], + [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], + [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + { key: 'a', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], + ] + ); + }); + }); +}); -- cgit v1.2.3 From d01db82c0dca352de2d7644c383d388fc3ec0366 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 2 May 2019 14:08:51 +0900 Subject: Types src/content --- .eslintrc | 2 + package.json | 1 + src/background/controllers/OperationController.ts | 4 +- src/background/controllers/VersionController.ts | 2 +- src/background/domains/Setting.ts | 20 +- src/background/infrastructures/ConsoleClient.ts | 2 +- .../infrastructures/ContentMessageClient.ts | 2 +- .../infrastructures/ContentMessageListener.ts | 6 +- src/background/presenters/NotifyPresenter.ts | 6 +- src/background/usecases/VersionUseCase.ts | 2 +- src/console/actions/console.ts | 2 +- src/console/index.tsx | 11 +- src/content/MessageListener.ts | 32 ++ src/content/actions/addon.ts | 10 +- src/content/actions/find.ts | 40 +- src/content/actions/follow-controller.ts | 12 +- src/content/actions/index.ts | 151 ++++-- src/content/actions/input.ts | 6 +- src/content/actions/mark.ts | 20 +- src/content/actions/operation.ts | 25 +- src/content/actions/setting.ts | 14 +- src/content/components/common/follow.ts | 79 +++- src/content/components/common/hint.ts | 33 +- src/content/components/common/index.ts | 43 +- src/content/components/common/input.ts | 46 +- src/content/components/common/keymapper.ts | 8 +- src/content/components/common/mark.ts | 2 +- src/content/components/top-content/find.ts | 25 +- .../components/top-content/follow-controller.ts | 65 ++- src/content/components/top-content/index.ts | 28 +- src/content/console-frames.ts | 18 +- src/content/focuses.ts | 8 +- src/content/hint-key-producer.ts | 10 +- src/content/index.ts | 9 +- src/content/navigates.ts | 45 +- src/content/reducers/addon.ts | 13 +- src/content/reducers/find.ts | 14 +- src/content/reducers/follow-controller.ts | 16 +- src/content/reducers/index.ts | 22 +- src/content/reducers/input.ts | 13 +- src/content/reducers/mark.ts | 20 +- src/content/reducers/setting.ts | 11 +- src/content/scrolls.ts | 38 +- src/content/store/index.ts | 8 + src/content/urls.ts | 8 +- src/shared/messages.ts | 346 +++++++++++--- src/shared/operations.ts | 523 ++++++++++++++++++--- src/shared/settings/validator.ts | 2 +- src/shared/utils/keys.ts | 2 +- test/content/actions/follow-controller.test.ts | 2 +- test/content/actions/input.test.ts | 2 +- test/content/actions/mark.test.ts | 2 +- test/content/actions/setting.test.ts | 2 +- test/content/components/common/input.test.ts | 14 +- test/content/reducers/addon.test.ts | 2 +- test/content/reducers/find.test.ts | 2 +- test/content/reducers/follow-controller.test.ts | 2 +- test/content/reducers/input.test.ts | 2 +- test/content/reducers/mark.test.ts | 2 +- test/content/reducers/setting.test.ts | 2 +- test/shared/operations.test.ts | 41 ++ tsconfig.json | 9 +- 62 files changed, 1426 insertions(+), 483 deletions(-) create mode 100644 src/content/MessageListener.ts create mode 100644 src/content/store/index.ts create mode 100644 test/shared/operations.test.ts (limited to 'test/content/actions/setting.test.ts') diff --git a/.eslintrc b/.eslintrc index fb60bc2..7845ca5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -35,6 +35,7 @@ "indent": ["error", 2], "jsx-quotes": ["error", "prefer-single"], "max-classes-per-file": "off", + "max-lines": "off", "max-params": ["error", 5], "max-statements": ["error", 15], "multiline-comment-style": "off", @@ -47,6 +48,7 @@ "no-console": ["error", { "allow": ["warn", "error"] }], "no-continue": "off", "no-empty-function": "off", + "no-extra-parens": "off", "no-magic-numbers": "off", "no-mixed-operators": "off", "no-plusplus": "off", diff --git a/package.json b/package.json index 5d44a1b..a799554 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "NODE_ENV=production webpack --mode production --progress --display-error-details", "package": "npm run build && script/package", "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "type-checks": "tsc", "test": "karma start", "test:e2e": "mocha --timeout 8000 e2e" }, diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 4e9c106..fa09512 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -1,4 +1,4 @@ -import operations from '../../shared/operations'; +import * as operations from '../../shared/operations'; import FindUseCase from '../usecases/FindUseCase'; import ConsoleUseCase from '../usecases/ConsoleUseCase'; import TabUseCase from '../usecases/TabUseCase'; @@ -25,7 +25,7 @@ export default class OperationController { } // eslint-disable-next-line complexity, max-lines-per-function - exec(operation: any): Promise { + exec(operation: operations.Operation): Promise { switch (operation.type) { case operations.TAB_CLOSE: return this.tabUseCase.close(false); diff --git a/src/background/controllers/VersionController.ts b/src/background/controllers/VersionController.ts index f402ed0..2e2a197 100644 --- a/src/background/controllers/VersionController.ts +++ b/src/background/controllers/VersionController.ts @@ -7,7 +7,7 @@ export default class VersionController { this.versionUseCase = new VersionUseCase(); } - notify(): void { + notify(): Promise { return this.versionUseCase.notify(); } } diff --git a/src/background/domains/Setting.ts b/src/background/domains/Setting.ts index 106ec0f..b2b1ff2 100644 --- a/src/background/domains/Setting.ts +++ b/src/background/domains/Setting.ts @@ -1,22 +1,30 @@ import DefaultSettings from '../../shared/settings/default'; import * as settingsValues from '../../shared/settings/values'; +type SettingValue = { + source: string, + json: string, + form: any +} + export default class Setting { - constructor({ source, json, form }) { + private obj: SettingValue; + + constructor({ source, json, form }: SettingValue) { this.obj = { source, json, form }; } - get source() { + get source(): string { return this.obj.source; } - get json() { + get json(): string { return this.obj.json; } - get form() { + get form(): any { return this.obj.form; } @@ -33,11 +41,11 @@ export default class Setting { return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; } - serialize() { + serialize(): SettingValue { return this.obj; } - static deserialize(obj) { + static deserialize(obj: SettingValue): Setting { return new Setting({ source: obj.source, json: obj.json, form: obj.form }); } diff --git a/src/background/infrastructures/ConsoleClient.ts b/src/background/infrastructures/ConsoleClient.ts index 7ad5d24..c162634 100644 --- a/src/background/infrastructures/ConsoleClient.ts +++ b/src/background/infrastructures/ConsoleClient.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ConsoleClient { showCommand(tabId: number, command: string): Promise { diff --git a/src/background/infrastructures/ContentMessageClient.ts b/src/background/infrastructures/ContentMessageClient.ts index 20057c7..d4bc476 100644 --- a/src/background/infrastructures/ContentMessageClient.ts +++ b/src/background/infrastructures/ContentMessageClient.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ContentMessageClient { async broadcastSettingsChanged(): Promise { diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 81d3232..1cc2696 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; import CompletionGroup from '../domains/CompletionGroup'; import CommandController from '../controllers/CommandController'; import SettingController from '../controllers/SettingController'; @@ -68,7 +68,9 @@ export default class ContentMessageListener { browser.runtime.onConnect.addListener(this.onConnected.bind(this)); } - onMessage(message: any, senderTab: browser.tabs.Tab): Promise | any { + onMessage( + message: messages.Message, senderTab: browser.tabs.Tab, + ): Promise | any { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); diff --git a/src/background/presenters/NotifyPresenter.ts b/src/background/presenters/NotifyPresenter.ts index c83c205..23932f7 100644 --- a/src/background/presenters/NotifyPresenter.ts +++ b/src/background/presenters/NotifyPresenter.ts @@ -1,11 +1,11 @@ const NOTIFICATION_ID = 'vimvixen-update'; export default class NotifyPresenter { - notify( + async notify( title: string, message: string, onclick: () => void, - ): Promise { + ): Promise { const listener = (id: string) => { if (id !== NOTIFICATION_ID) { return; @@ -17,7 +17,7 @@ export default class NotifyPresenter { }; browser.notifications.onClicked.addListener(listener); - return browser.notifications.create(NOTIFICATION_ID, { + await browser.notifications.create(NOTIFICATION_ID, { 'type': 'basic', 'iconUrl': browser.extension.getURL('resources/icon_48x48.png'), title, diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts index 207f9e2..3a3cc2e 100644 --- a/src/background/usecases/VersionUseCase.ts +++ b/src/background/usecases/VersionUseCase.ts @@ -12,7 +12,7 @@ export default class VersionUseCase { this.notifyPresenter = new NotifyPresenter(); } - notify(): Promise { + notify(): Promise { let title = `Vim Vixen ${manifest.version} has been installed`; let message = 'Click here to see release notes'; let url = this.releaseNoteUrl(manifest.version); diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index ceb419c..b1494b0 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; import * as actions from './index'; const hide = (): actions.ConsoleAction => { diff --git a/src/console/index.tsx b/src/console/index.tsx index ee3a8ee..b655154 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,4 +1,4 @@ -import messages from '../shared/messages'; +import * as messages from '../shared/messages'; import reducers from './reducers'; import { createStore, applyMiddleware } from 'redux'; import promise from 'redux-promise'; @@ -23,15 +23,16 @@ window.addEventListener('load', () => { }); const onMessage = (message: any): any => { - switch (message.type) { + let msg = messages.valueOf(message); + switch (msg.type) { case messages.CONSOLE_SHOW_COMMAND: - return store.dispatch(consoleActions.showCommand(message.command)); + return store.dispatch(consoleActions.showCommand(msg.command)); case messages.CONSOLE_SHOW_FIND: return store.dispatch(consoleActions.showFind()); case messages.CONSOLE_SHOW_ERROR: - return store.dispatch(consoleActions.showError(message.text)); + return store.dispatch(consoleActions.showError(msg.text)); case messages.CONSOLE_SHOW_INFO: - return store.dispatch(consoleActions.showInfo(message.text)); + return store.dispatch(consoleActions.showInfo(msg.text)); case messages.CONSOLE_HIDE: return store.dispatch(consoleActions.hide()); } diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts new file mode 100644 index 0000000..105d028 --- /dev/null +++ b/src/content/MessageListener.ts @@ -0,0 +1,32 @@ +import { Message, valueOf } from '../shared/messages'; + +export type WebMessageSender = Window | MessagePort | ServiceWorker | null; +export type WebExtMessageSender = browser.runtime.MessageSender; + +export default class MessageListener { + onWebMessage( + listener: (msg: Message, sender: WebMessageSender) => void, + ) { + window.addEventListener('message', (event: MessageEvent) => { + let sender = event.source; + let message = null; + try { + message = JSON.parse(event.data); + } catch (e) { + // ignore unexpected message + return; + } + listener(message, sender); + }); + } + + onBackgroundMessage( + listener: (msg: Message, sender: WebExtMessageSender) => any, + ) { + browser.runtime.onMessage.addListener( + (msg: any, sender: WebExtMessageSender) => { + listener(valueOf(msg), sender); + }, + ); + } +} diff --git a/src/content/actions/addon.ts b/src/content/actions/addon.ts index b30cf16..8dedae0 100644 --- a/src/content/actions/addon.ts +++ b/src/content/actions/addon.ts @@ -1,11 +1,11 @@ -import messages from 'shared/messages'; -import actions from 'content/actions'; +import * as messages from '../../shared/messages'; +import * as actions from './index'; -const enable = () => setEnabled(true); +const enable = (): Promise => setEnabled(true); -const disable = () => setEnabled(false); +const disable = (): Promise => setEnabled(false); -const setEnabled = async(enabled) => { +const setEnabled = async(enabled: boolean): Promise => { await browser.runtime.sendMessage({ type: messages.ADDON_ENABLED_RESPONSE, enabled, diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts index e08d7e5..6dd2ae6 100644 --- a/src/content/actions/find.ts +++ b/src/content/actions/find.ts @@ -5,28 +5,41 @@ // NOTE: window.find is not standard API // https://developer.mozilla.org/en-US/docs/Web/API/Window/find -import messages from 'shared/messages'; -import actions from 'content/actions'; +import * as messages from '../../shared/messages'; +import * as actions from './index'; import * as consoleFrames from '../console-frames'; -const find = (string, backwards) => { +const find = (str: string, backwards: boolean): boolean => { let caseSensitive = false; let wrapScan = true; // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work // because of same origin policy - let found = window.find(string, caseSensitive, backwards, wrapScan); + + // eslint-disable-next-line no-extra-parens + let found = (window).find(str, caseSensitive, backwards, wrapScan); if (found) { return found; } - window.getSelection().removeAllRanges(); - return window.find(string, caseSensitive, backwards, wrapScan); + let sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + } + + // eslint-disable-next-line no-extra-parens + return (window).find(str, caseSensitive, backwards, wrapScan); }; -const findNext = async(currentKeyword, reset, backwards) => { +// eslint-disable-next-line max-statements +const findNext = async( + currentKeyword: string, reset: boolean, backwards: boolean, +): Promise => { if (reset) { - window.getSelection().removeAllRanges(); + let sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + } } let keyword = currentKeyword; @@ -41,7 +54,8 @@ const findNext = async(currentKeyword, reset, backwards) => { }); } if (!keyword) { - return consoleFrames.postError('No previous search keywords'); + await consoleFrames.postError('No previous search keywords'); + return { type: actions.NOOP }; } let found = find(keyword, backwards); if (found) { @@ -57,11 +71,15 @@ const findNext = async(currentKeyword, reset, backwards) => { }; }; -const next = (currentKeyword, reset) => { +const next = ( + currentKeyword: string, reset: boolean, +): Promise => { return findNext(currentKeyword, reset, false); }; -const prev = (currentKeyword, reset) => { +const prev = ( + currentKeyword: string, reset: boolean, +): Promise => { return findNext(currentKeyword, reset, true); }; diff --git a/src/content/actions/follow-controller.ts b/src/content/actions/follow-controller.ts index 006b248..115b3b6 100644 --- a/src/content/actions/follow-controller.ts +++ b/src/content/actions/follow-controller.ts @@ -1,6 +1,8 @@ -import actions from 'content/actions'; +import * as actions from './index'; -const enable = (newTab, background) => { +const enable = ( + newTab: boolean, background: boolean, +): actions.FollowAction => { return { type: actions.FOLLOW_CONTROLLER_ENABLE, newTab, @@ -8,20 +10,20 @@ const enable = (newTab, background) => { }; }; -const disable = () => { +const disable = (): actions.FollowAction => { return { type: actions.FOLLOW_CONTROLLER_DISABLE, }; }; -const keyPress = (key) => { +const keyPress = (key: string): actions.FollowAction => { return { type: actions.FOLLOW_CONTROLLER_KEY_PRESS, key: key }; }; -const backspace = () => { +const backspace = (): actions.FollowAction => { return { type: actions.FOLLOW_CONTROLLER_BACKSPACE, }; diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index 0a16fdf..18d0a69 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,31 +1,120 @@ -export default { - // Enable/disable - ADDON_SET_ENABLED: 'addon.set.enabled', - - // Settings - SETTING_SET: 'setting.set', - - // User input - INPUT_KEY_PRESS: 'input.key.press', - INPUT_CLEAR_KEYS: 'input.clear.keys', - - // Completion - COMPLETION_SET_ITEMS: 'completion.set.items', - COMPLETION_SELECT_NEXT: 'completions.select.next', - COMPLETION_SELECT_PREV: 'completions.select.prev', - - // Follow - FOLLOW_CONTROLLER_ENABLE: 'follow.controller.enable', - FOLLOW_CONTROLLER_DISABLE: 'follow.controller.disable', - FOLLOW_CONTROLLER_KEY_PRESS: 'follow.controller.key.press', - FOLLOW_CONTROLLER_BACKSPACE: 'follow.controller.backspace', - - // Find - FIND_SET_KEYWORD: 'find.set.keyword', - - // Mark - MARK_START_SET: 'mark.start.set', - MARK_START_JUMP: 'mark.start.jump', - MARK_CANCEL: 'mark.cancel', - MARK_SET_LOCAL: 'mark.set.local', -}; +import Redux from 'redux'; + +// Enable/disable +export const ADDON_SET_ENABLED = 'addon.set.enabled'; + +// Find +export const FIND_SET_KEYWORD = 'find.set.keyword'; + +// Settings +export const SETTING_SET = 'setting.set'; + +// User input +export const INPUT_KEY_PRESS = 'input.key.press'; +export const INPUT_CLEAR_KEYS = 'input.clear.keys'; + +// Completion +export const COMPLETION_SET_ITEMS = 'completion.set.items'; +export const COMPLETION_SELECT_NEXT = 'completions.select.next'; +export const COMPLETION_SELECT_PREV = 'completions.select.prev'; + +// Follow +export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable'; +export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable'; +export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press'; +export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace'; + +// Mark +export const MARK_START_SET = 'mark.start.set'; +export const MARK_START_JUMP = 'mark.start.jump'; +export const MARK_CANCEL = 'mark.cancel'; +export const MARK_SET_LOCAL = 'mark.set.local'; + +export const NOOP = 'noop'; + +export interface AddonSetEnabledAction extends Redux.Action { + type: typeof ADDON_SET_ENABLED; + enabled: boolean; +} + +export interface FindSetKeywordAction extends Redux.Action { + type: typeof FIND_SET_KEYWORD; + keyword: string; + found: boolean; +} + +export interface SettingSetAction extends Redux.Action { + type: typeof SETTING_SET; + value: any; +} + +export interface InputKeyPressAction extends Redux.Action { + type: typeof INPUT_KEY_PRESS; + key: string; +} + +export interface InputClearKeysAction extends Redux.Action { + type: typeof INPUT_CLEAR_KEYS; +} + +export interface FollowControllerEnableAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_ENABLE; + newTab: boolean; + background: boolean; +} + +export interface FollowControllerDisableAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_DISABLE; +} + +export interface FollowControllerKeyPressAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_KEY_PRESS; + key: string; +} + +export interface FollowControllerBackspaceAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_BACKSPACE; +} + +export interface MarkStartSetAction extends Redux.Action { + type: typeof MARK_START_SET; +} + +export interface MarkStartJumpAction extends Redux.Action { + type: typeof MARK_START_JUMP; +} + +export interface MarkCancelAction extends Redux.Action { + type: typeof MARK_CANCEL; +} + +export interface MarkSetLocalAction extends Redux.Action { + type: typeof MARK_SET_LOCAL; + key: string; + x: number; + y: number; +} + +export interface NoopAction extends Redux.Action { + type: typeof NOOP; +} + +export type AddonAction = AddonSetEnabledAction; +export type FindAction = FindSetKeywordAction | NoopAction; +export type SettingAction = SettingSetAction; +export type InputAction = InputKeyPressAction | InputClearKeysAction; +export type FollowAction = + FollowControllerEnableAction | FollowControllerDisableAction | + FollowControllerKeyPressAction | FollowControllerBackspaceAction; +export type MarkAction = + MarkStartSetAction | MarkStartJumpAction | + MarkCancelAction | MarkSetLocalAction | NoopAction; + +export type Action = + AddonAction | + FindAction | + SettingAction | + InputAction | + FollowAction | + MarkAction | + NoopAction; diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts index 465a486..21c912e 100644 --- a/src/content/actions/input.ts +++ b/src/content/actions/input.ts @@ -1,13 +1,13 @@ -import actions from 'content/actions'; +import * as actions from './index'; -const keyPress = (key) => { +const keyPress = (key: string): actions.InputAction => { return { type: actions.INPUT_KEY_PRESS, key, }; }; -const clearKeys = () => { +const clearKeys = (): actions.InputAction => { return { type: actions.INPUT_CLEAR_KEYS }; diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts index 712a811..5eb9554 100644 --- a/src/content/actions/mark.ts +++ b/src/content/actions/mark.ts @@ -1,19 +1,19 @@ -import actions from 'content/actions'; -import messages from 'shared/messages'; +import * as actions from './index'; +import * as messages from '../../shared/messages'; -const startSet = () => { +const startSet = (): actions.MarkAction => { return { type: actions.MARK_START_SET }; }; -const startJump = () => { +const startJump = (): actions.MarkAction => { return { type: actions.MARK_START_JUMP }; }; -const cancel = () => { +const cancel = (): actions.MarkAction => { return { type: actions.MARK_CANCEL }; }; -const setLocal = (key, x, y) => { +const setLocal = (key: string, x: number, y: number): actions.MarkAction => { return { type: actions.MARK_SET_LOCAL, key, @@ -22,22 +22,22 @@ const setLocal = (key, x, y) => { }; }; -const setGlobal = (key, x, y) => { +const setGlobal = (key: string, x: number, y: number): actions.MarkAction => { browser.runtime.sendMessage({ type: messages.MARK_SET_GLOBAL, key, x, y, }); - return { type: '' }; + return { type: actions.NOOP }; }; -const jumpGlobal = (key) => { +const jumpGlobal = (key: string): actions.MarkAction => { browser.runtime.sendMessage({ type: messages.MARK_JUMP_GLOBAL, key, }); - return { type: '' }; + return { type: actions.NOOP }; }; export { diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index ed9b2cf..6acb407 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -1,16 +1,21 @@ -import operations from 'shared/operations'; -import messages from 'shared/messages'; -import * as scrolls from 'content/scrolls'; -import * as navigates from 'content/navigates'; -import * as focuses from 'content/focuses'; -import * as urls from 'content/urls'; -import * as consoleFrames from 'content/console-frames'; +import * as operations from '../../shared/operations'; +import * as actions from './index'; +import * as messages from '../../shared/messages'; +import * as scrolls from '../scrolls'; +import * as navigates from '../navigates'; +import * as focuses from '../focuses'; +import * as urls from '../urls'; +import * as consoleFrames from '../console-frames'; import * as addonActions from './addon'; import * as markActions from './mark'; -import * as properties from 'shared/settings/properties'; +import * as properties from '../../shared/settings/properties'; // eslint-disable-next-line complexity, max-lines-per-function -const exec = (operation, settings, addonEnabled) => { +const exec = ( + operation: operations.Operation, + settings: any, + addonEnabled: boolean, +): Promise | actions.Action => { let smoothscroll = settings.properties.smoothscroll || properties.defaults.smoothscroll; switch (operation.type) { @@ -98,7 +103,7 @@ const exec = (operation, settings, addonEnabled) => { operation, }); } - return { type: '' }; + return { type: actions.NOOP }; }; export { exec }; diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts index 1c15dd7..a8f049a 100644 --- a/src/content/actions/setting.ts +++ b/src/content/actions/setting.ts @@ -1,15 +1,15 @@ -import actions from 'content/actions'; -import * as keyUtils from 'shared/utils/keys'; -import operations from 'shared/operations'; -import messages from 'shared/messages'; +import * as actions from './index'; +import * as keyUtils from '../../shared/utils/keys'; +import * as operations from '../../shared/operations'; +import * as messages from '../../shared/messages'; const reservedKeymaps = { '': { type: operations.CANCEL }, '': { type: operations.CANCEL }, }; -const set = (value) => { - let entries = []; +const set = (value: any): actions.SettingAction => { + let entries: any[] = []; if (value.keymaps) { let keymaps = { ...value.keymaps, ...reservedKeymaps }; entries = Object.entries(keymaps).map((entry) => { @@ -27,7 +27,7 @@ const set = (value) => { }; }; -const load = async() => { +const load = async(): Promise => { let settings = await browser.runtime.sendMessage({ type: messages.SETTINGS_QUERY, }); diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index 63ce603..67f2dd9 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -1,6 +1,8 @@ -import messages from 'shared/messages'; +import MessageListener from '../../MessageListener'; import Hint from './hint'; -import * as dom from 'shared/utils/dom'; +import * as dom from '../../../shared/utils/dom'; +import * as messages from '../../../shared/messages'; +import * as keyUtils from '../../../shared/utils/keys'; const TARGET_SELECTOR = [ 'a', 'button', 'input', 'textarea', 'area', @@ -8,8 +10,22 @@ const TARGET_SELECTOR = [ '[role="button"]', 'summary' ].join(','); +interface Size { + width: number; + height: number; +} + +interface Point { + x: number; + y: number; +} -const inViewport = (win, element, viewSize, framePosition) => { +const inViewport = ( + win: Window, + element: Element, + viewSize: Size, + framePosition: Point, +): boolean => { let { top, left, bottom, right } = dom.viewportRect(element); @@ -30,34 +46,44 @@ const inViewport = (win, element, viewSize, framePosition) => { return true; }; -const isAriaHiddenOrAriaDisabled = (win, element) => { +const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { if (!element || win.document.documentElement === element) { return false; } for (let attr of ['aria-hidden', 'aria-disabled']) { - if (element.hasAttribute(attr)) { - let hidden = element.getAttribute(attr).toLowerCase(); + let value = element.getAttribute(attr); + if (value !== null) { + let hidden = value.toLowerCase(); if (hidden === '' || hidden === 'true') { return true; } } } - return isAriaHiddenOrAriaDisabled(win, element.parentNode); + return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element); }; export default class Follow { - constructor(win, store) { + private win: Window; + + private newTab: boolean; + + private background: boolean; + + private hints: {[key: string]: Hint }; + + private targets: HTMLElement[] = []; + + constructor(win: Window) { this.win = win; - this.store = store; this.newTab = false; this.background = false; this.hints = {}; this.targets = []; - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onMessage.bind(this)); } - key(key) { + key(key: keyUtils.Key): boolean { if (Object.keys(this.hints).length === 0) { return false; } @@ -69,7 +95,7 @@ export default class Follow { return true; } - openLink(element) { + openLink(element: HTMLAreaElement|HTMLAnchorElement) { // Browser prevent new tab by link with target='_blank' if (!this.newTab && element.getAttribute('target') !== '_blank') { element.click(); @@ -90,7 +116,7 @@ export default class Follow { }); } - countHints(sender, viewSize, framePosition) { + countHints(sender: any, viewSize: Size, framePosition: Point) { this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); sender.postMessage(JSON.stringify({ type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, @@ -98,7 +124,7 @@ export default class Follow { }), '*'); } - createHints(keysArray, newTab, background) { + createHints(keysArray: string[], newTab: boolean, background: boolean) { if (keysArray.length !== this.targets.length) { throw new Error('illegal hint count'); } @@ -113,7 +139,7 @@ export default class Follow { } } - showHints(keys) { + showHints(keys: string) { Object.keys(this.hints).filter(key => key.startsWith(keys)) .forEach(key => this.hints[key].show()); Object.keys(this.hints).filter(key => !key.startsWith(keys)) @@ -128,18 +154,19 @@ export default class Follow { this.targets = []; } - activateHints(keys) { + activateHints(keys: string) { let hint = this.hints[keys]; if (!hint) { return; } - let element = hint.target; + let element = hint.getTarget(); switch (element.tagName.toLowerCase()) { case 'a': + return this.openLink(element as HTMLAnchorElement); case 'area': - return this.openLink(element); + return this.openLink(element as HTMLAreaElement); case 'input': - switch (element.type) { + switch ((element as HTMLInputElement).type) { case 'file': case 'checkbox': case 'radio': @@ -166,7 +193,7 @@ export default class Follow { } } - onMessage(message, sender) { + onMessage(message: messages.Message, sender: any) { switch (message.type) { case messages.FOLLOW_REQUEST_COUNT_TARGETS: return this.countHints(sender, message.viewSize, message.framePosition); @@ -178,19 +205,23 @@ export default class Follow { case messages.FOLLOW_ACTIVATE: return this.activateHints(message.keys); case messages.FOLLOW_REMOVE_HINTS: - return this.removeHints(message.keys); + return this.removeHints(); } } - static getTargetElements(win, viewSize, framePosition) { + static getTargetElements( + win: Window, + viewSize: + Size, framePosition: Point, + ): HTMLElement[] { let all = win.document.querySelectorAll(TARGET_SELECTOR); - let filtered = Array.prototype.filter.call(all, (element) => { + let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => { let style = win.getComputedStyle(element); // AREA's 'display' in Browser style is 'none' return (element.tagName === 'AREA' || style.display !== 'none') && style.visibility !== 'hidden' && - element.type !== 'hidden' && + (element as HTMLInputElement).type !== 'hidden' && element.offsetHeight > 0 && !isAriaHiddenOrAriaDisabled(win, element) && inViewport(win, element, viewSize, framePosition); diff --git a/src/content/components/common/hint.ts b/src/content/components/common/hint.ts index 1472587..2fcbb0f 100644 --- a/src/content/components/common/hint.ts +++ b/src/content/components/common/hint.ts @@ -1,6 +1,11 @@ -import * as dom from 'shared/utils/dom'; +import * as dom from '../../../shared/utils/dom'; -const hintPosition = (element) => { +interface Point { + x: number; + y: number; +} + +const hintPosition = (element: Element): Point => { let { left, top, right, bottom } = dom.viewportRect(element); if (element.tagName !== 'AREA') { @@ -14,17 +19,21 @@ const hintPosition = (element) => { }; export default class Hint { - constructor(target, tag) { - if (!(document.body instanceof HTMLElement)) { - throw new TypeError('target is not an HTMLElement'); - } + private target: HTMLElement; - this.target = target; + private element: HTMLElement; + constructor(target: HTMLElement, tag: string) { let doc = target.ownerDocument; + if (doc === null) { + throw new TypeError('ownerDocument is null'); + } + let { x, y } = hintPosition(target); let { scrollX, scrollY } = window; + this.target = target; + this.element = doc.createElement('span'); this.element.className = 'vimvixen-hint'; this.element.textContent = tag; @@ -35,15 +44,19 @@ export default class Hint { doc.body.append(this.element); } - show() { + show(): void { this.element.style.display = 'inline'; } - hide() { + hide(): void { this.element.style.display = 'none'; } - remove() { + remove(): void { this.element.remove(); } + + getTarget(): HTMLElement { + return this.target; + } } diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index bcab4fa..9b5164e 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -2,33 +2,37 @@ import InputComponent from './input'; import FollowComponent from './follow'; import MarkComponent from './mark'; import KeymapperComponent from './keymapper'; -import * as settingActions from 'content/actions/setting'; -import messages from 'shared/messages'; +import * as settingActions from '../../actions/setting'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; import * as addonActions from '../../actions/addon'; -import * as blacklists from 'shared/blacklists'; +import * as blacklists from '../../../shared/blacklists'; +import * as keys from '../../../shared/utils/keys'; export default class Common { - constructor(win, store) { - const input = new InputComponent(win.document.body, store); - const follow = new FollowComponent(win, store); + private win: Window; + + private store: any; + + constructor(win: Window, store: any) { + const input = new InputComponent(win.document.body); + const follow = new FollowComponent(win); const mark = new MarkComponent(win.document.body, store); const keymapper = new KeymapperComponent(store); - input.onKey(key => follow.key(key)); - input.onKey(key => mark.key(key)); - input.onKey(key => keymapper.key(key)); + input.onKey((key: keys.Key) => follow.key(key)); + input.onKey((key: keys.Key) => mark.key(key)); + input.onKey((key: keys.Key) => keymapper.key(key)); this.win = win; this.store = store; - this.prevEnabled = undefined; - this.prevBlacklist = undefined; this.reloadSettings(); - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); } - onMessage(message) { + onMessage(message: messages.Message) { let { enabled } = this.store.getState().addon; switch (message.type) { case messages.SETTINGS_CHANGED: @@ -40,12 +44,13 @@ export default class Common { reloadSettings() { try { - this.store.dispatch(settingActions.load()).then(({ value: settings }) => { - let enabled = !blacklists.includes( - settings.blacklist, this.win.location.href - ); - this.store.dispatch(addonActions.setEnabled(enabled)); - }); + this.store.dispatch(settingActions.load()) + .then(({ value: settings }: any) => { + let enabled = !blacklists.includes( + settings.blacklist, this.win.location.href + ); + this.store.dispatch(addonActions.setEnabled(enabled)); + }); } catch (e) { // Sometime sendMessage fails when background script is not ready. console.warn(e); diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts index eefaf10..64eb5f3 100644 --- a/src/content/components/common/input.ts +++ b/src/content/components/common/input.ts @@ -1,12 +1,16 @@ -import * as dom from 'shared/utils/dom'; -import * as keys from 'shared/utils/keys'; +import * as dom from '../../../shared/utils/dom'; +import * as keys from '../../../shared/utils/keys'; -const cancelKey = (e) => { +const cancelKey = (e: KeyboardEvent): boolean => { return e.key === 'Escape' || e.key === '[' && e.ctrlKey; }; export default class InputComponent { - constructor(target) { + private pressed: {[key: string]: string} = {}; + + private onKeyListeners: ((key: keys.Key) => boolean)[] = []; + + constructor(target: HTMLElement) { this.pressed = {}; this.onKeyListeners = []; @@ -15,11 +19,11 @@ export default class InputComponent { target.addEventListener('keyup', this.onKeyUp.bind(this)); } - onKey(cb) { + onKey(cb: (key: keys.Key) => boolean) { this.onKeyListeners.push(cb); } - onKeyPress(e) { + onKeyPress(e: KeyboardEvent) { if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { return; } @@ -27,7 +31,7 @@ export default class InputComponent { this.capture(e); } - onKeyDown(e) { + onKeyDown(e: KeyboardEvent) { if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { return; } @@ -35,14 +39,19 @@ export default class InputComponent { this.capture(e); } - onKeyUp(e) { + onKeyUp(e: KeyboardEvent) { delete this.pressed[e.key]; } - capture(e) { - if (this.fromInput(e)) { - if (cancelKey(e) && e.target.blur) { - e.target.blur(); + // eslint-disable-next-line max-statements + capture(e: KeyboardEvent) { + let target = e.target; + if (!(target instanceof HTMLElement)) { + return; + } + if (this.fromInput(target)) { + if (cancelKey(e) && target.blur) { + target.blur(); } return; } @@ -63,13 +72,10 @@ export default class InputComponent { } } - fromInput(e) { - if (!e.target) { - return false; - } - return e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement || - dom.isContentEditable(e.target); + fromInput(e: Element) { + return e instanceof HTMLInputElement || + e instanceof HTMLTextAreaElement || + e instanceof HTMLSelectElement || + dom.isContentEditable(e); } } diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts index ec0d093..d9c9834 100644 --- a/src/content/components/common/keymapper.ts +++ b/src/content/components/common/keymapper.ts @@ -1,7 +1,7 @@ -import * as inputActions from 'content/actions/input'; -import * as operationActions from 'content/actions/operation'; -import operations from 'shared/operations'; -import * as keyUtils from 'shared/utils/keys'; +import * as inputActions from '../../actions/input'; +import * as operationActions from '../../actions/operation'; +import * as operations from '../../../shared/operations'; +import * as keyUtils from '../../../shared/utils/keys'; const mapStartsWith = (mapping, keys) => { if (mapping.length < keys.length) { diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index 0f838a9..500d03b 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -3,7 +3,7 @@ import * as scrolls from 'content/scrolls'; import * as consoleFrames from 'content/console-frames'; import * as properties from 'shared/settings/properties'; -const cancelKey = (key) => { +const cancelKey = (key): boolean => { return key.key === 'Esc' || key.key === '[' && key.ctrlKey; }; diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts index 4d46d79..74b95bc 100644 --- a/src/content/components/top-content/find.ts +++ b/src/content/components/top-content/find.ts @@ -1,15 +1,17 @@ -import * as findActions from 'content/actions/find'; -import messages from 'shared/messages'; +import * as findActions from '../../actions/find'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; export default class FindComponent { - constructor(win, store) { - this.win = win; + private store: any; + + constructor(store: any) { this.store = store; - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onMessage.bind(this)); } - onMessage(message) { + onMessage(message: messages.Message) { switch (message.type) { case messages.CONSOLE_ENTER_FIND: return this.start(message.text); @@ -20,22 +22,25 @@ export default class FindComponent { } } - start(text) { + start(text: string) { let state = this.store.getState().find; if (text.length === 0) { - return this.store.dispatch(findActions.next(state.keyword, true)); + return this.store.dispatch( + findActions.next(state.keyword as string, true)); } return this.store.dispatch(findActions.next(text, true)); } next() { let state = this.store.getState().find; - return this.store.dispatch(findActions.next(state.keyword, false)); + return this.store.dispatch( + findActions.next(state.keyword as string, false)); } prev() { let state = this.store.getState().find; - return this.store.dispatch(findActions.prev(state.keyword, false)); + return this.store.dispatch( + findActions.prev(state.keyword as string, false)); } } diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index 7f36604..be71f6e 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -1,30 +1,46 @@ -import * as followControllerActions from 'content/actions/follow-controller'; -import messages from 'shared/messages'; -import HintKeyProducer from 'content/hint-key-producer'; -import * as properties from 'shared/settings/properties'; +import * as followControllerActions from '../../actions/follow-controller'; +import * as messages from '../../../shared/messages'; +import MessageListener, { WebMessageSender } from '../../MessageListener'; +import HintKeyProducer from '../../hint-key-producer'; +import * as properties from '../../../shared/settings/properties'; -const broadcastMessage = (win, message) => { +const broadcastMessage = (win: Window, message: messages.Message): void => { let json = JSON.stringify(message); - let frames = [window.self].concat(Array.from(window.frames)); + let frames = [win.self].concat(Array.from(win.frames as any)); frames.forEach(frame => frame.postMessage(json, '*')); }; export default class FollowController { - constructor(win, store) { + private win: Window; + + private store: any; + + private state: { + enabled?: boolean; + newTab?: boolean; + background?: boolean; + keys?: string, + }; + + private keys: string[]; + + private producer: HintKeyProducer | null; + + constructor(win: Window, store: any) { this.win = win; this.store = store; this.state = {}; this.keys = []; this.producer = null; - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onMessage.bind(this)); store.subscribe(() => { this.update(); }); } - onMessage(message, sender) { + onMessage(message: messages.Message, sender: WebMessageSender) { switch (message.type) { case messages.FOLLOW_START: return this.store.dispatch( @@ -36,7 +52,7 @@ export default class FollowController { } } - update() { + update(): void { let prevState = this.state; this.state = this.store.getState().followController; @@ -49,8 +65,10 @@ export default class FollowController { } } - updateHints() { - let shown = this.keys.filter(key => key.startsWith(this.state.keys)); + updateHints(): void { + let shown = this.keys.filter((key) => { + return key.startsWith(this.state.keys as string); + }); if (shown.length === 1) { this.activate(); this.store.dispatch(followControllerActions.disable()); @@ -58,18 +76,18 @@ export default class FollowController { broadcastMessage(this.win, { type: messages.FOLLOW_SHOW_HINTS, - keys: this.state.keys, + keys: this.state.keys as string, }); } - activate() { + activate(): void { broadcastMessage(this.win, { type: messages.FOLLOW_ACTIVATE, - keys: this.state.keys, + keys: this.state.keys as string, }); } - keyPress(key, ctrlKey) { + keyPress(key: string, ctrlKey: boolean): boolean { if (key === '[' && ctrlKey) { this.store.dispatch(followControllerActions.disable()); return true; @@ -107,25 +125,28 @@ export default class FollowController { viewSize: { width: viewWidth, height: viewHeight }, framePosition: { x: 0, y: 0 }, }), '*'); - frameElements.forEach((element) => { - let { left: frameX, top: frameY } = element.getBoundingClientRect(); + frameElements.forEach((ele) => { + let { left: frameX, top: frameY } = ele.getBoundingClientRect(); let message = JSON.stringify({ type: messages.FOLLOW_REQUEST_COUNT_TARGETS, viewSize: { width: viewWidth, height: viewHeight }, framePosition: { x: frameX, y: frameY }, }); - element.contentWindow.postMessage(message, '*'); + if (ele instanceof HTMLFrameElement && ele.contentWindow || + ele instanceof HTMLIFrameElement && ele.contentWindow) { + ele.contentWindow.postMessage(message, '*'); + } }); } - create(count, sender) { + create(count: number, sender: WebMessageSender) { let produced = []; for (let i = 0; i < count; ++i) { - produced.push(this.producer.produce()); + produced.push((this.producer as HintKeyProducer).produce()); } this.keys = this.keys.concat(produced); - sender.postMessage(JSON.stringify({ + (sender as Window).postMessage(JSON.stringify({ type: messages.FOLLOW_CREATE_HINTS, keysArray: produced, newTab: this.state.newTab, diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts index 1aaef1b..ac95ea9 100644 --- a/src/content/components/top-content/index.ts +++ b/src/content/components/top-content/index.ts @@ -2,33 +2,43 @@ import CommonComponent from '../common'; import FollowController from './follow-controller'; import FindComponent from './find'; import * as consoleFrames from '../../console-frames'; -import messages from 'shared/messages'; -import * as scrolls from 'content/scrolls'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; +import * as scrolls from '../../scrolls'; export default class TopContent { + private win: Window; - constructor(win, store) { + private store: any; + + constructor(win: Window, store: any) { this.win = win; this.store = store; new CommonComponent(win, store); // eslint-disable-line no-new new FollowController(win, store); // eslint-disable-line no-new - new FindComponent(win, store); // eslint-disable-line no-new + new FindComponent(store); // eslint-disable-line no-new // TODO make component consoleFrames.initialize(this.win.document); - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onWebMessage.bind(this)); + new MessageListener().onBackgroundMessage( + this.onBackgroundMessage.bind(this)); } - onMessage(message) { - let addonState = this.store.getState().addon; - + onWebMessage(message: messages.Message) { switch (message.type) { case messages.CONSOLE_UNFOCUS: this.win.focus(); consoleFrames.blur(window.document); - return Promise.resolve(); + } + } + + onBackgroundMessage(message: messages.Message) { + let addonState = this.store.getState().addon; + + switch (message.type) { case messages.ADDON_ENABLED_QUERY: return Promise.resolve({ type: messages.ADDON_ENABLED_RESPONSE, diff --git a/src/content/console-frames.ts b/src/content/console-frames.ts index ecb5a87..bd6b835 100644 --- a/src/content/console-frames.ts +++ b/src/content/console-frames.ts @@ -1,6 +1,6 @@ -import messages from 'shared/messages'; +import * as messages from '../shared/messages'; -const initialize = (doc) => { +const initialize = (doc: Document): HTMLIFrameElement => { let iframe = doc.createElement('iframe'); iframe.src = browser.runtime.getURL('build/console.html'); iframe.id = 'vimvixen-console-frame'; @@ -10,13 +10,13 @@ const initialize = (doc) => { return iframe; }; -const blur = (doc) => { - let iframe = doc.getElementById('vimvixen-console-frame'); - iframe.blur(); +const blur = (doc: Document) => { + let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement; + ele.blur(); }; -const postError = (text) => { - browser.runtime.sendMessage({ +const postError = (text: string): Promise => { + return browser.runtime.sendMessage({ type: messages.CONSOLE_FRAME_MESSAGE, message: { type: messages.CONSOLE_SHOW_ERROR, @@ -25,8 +25,8 @@ const postError = (text) => { }); }; -const postInfo = (text) => { - browser.runtime.sendMessage({ +const postInfo = (text: string): Promise => { + return browser.runtime.sendMessage({ type: messages.CONSOLE_FRAME_MESSAGE, message: { type: messages.CONSOLE_SHOW_INFO, diff --git a/src/content/focuses.ts b/src/content/focuses.ts index a6f6cc8..8f53881 100644 --- a/src/content/focuses.ts +++ b/src/content/focuses.ts @@ -1,11 +1,13 @@ -import * as doms from 'shared/utils/dom'; +import * as doms from '../shared/utils/dom'; -const focusInput = () => { +const focusInput = (): void => { let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url']; let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(','); let targets = window.document.querySelectorAll(inputSelector + ',textarea'); let target = Array.from(targets).find(doms.isVisible); - if (target) { + if (target instanceof HTMLInputElement) { + target.focus(); + } else if (target instanceof HTMLTextAreaElement) { target.focus(); } }; diff --git a/src/content/hint-key-producer.ts b/src/content/hint-key-producer.ts index 14b23b6..935394e 100644 --- a/src/content/hint-key-producer.ts +++ b/src/content/hint-key-producer.ts @@ -1,5 +1,9 @@ export default class HintKeyProducer { - constructor(charset) { + private charset: string; + + private counter: number[]; + + constructor(charset: string) { if (charset.length === 0) { throw new TypeError('charset is empty'); } @@ -8,13 +12,13 @@ export default class HintKeyProducer { this.counter = []; } - produce() { + produce(): string { this.increment(); return this.counter.map(x => this.charset[x]).join(''); } - increment() { + private increment(): void { let max = this.charset.length - 1; if (this.counter.every(x => x === max)) { this.counter = new Array(this.counter.length + 1).fill(0); diff --git a/src/content/index.ts b/src/content/index.ts index 9edb712..309f27f 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,14 +1,9 @@ -import { createStore, applyMiddleware } from 'redux'; -import promise from 'redux-promise'; -import reducers from 'content/reducers'; import TopContentComponent from './components/top-content'; import FrameContentComponent from './components/frame-content'; import consoleFrameStyle from './site-style'; +import { newStore } from './store'; -const store = createStore( - reducers, - applyMiddleware(promise), -); +const store = newStore(); if (window.self === window.top) { new TopContentComponent(window, store); // eslint-disable-line no-new diff --git a/src/content/navigates.ts b/src/content/navigates.ts index c9baa30..a2007a6 100644 --- a/src/content/navigates.ts +++ b/src/content/navigates.ts @@ -1,58 +1,63 @@ -const REL_PATTERN = { +const REL_PATTERN: {[key: string]: RegExp} = { prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<>/i, }; // Return the last element in the document matching the supplied selector // and the optional filter, or null if there are no matches. -const selectLast = (win, selector, filter) => { - let nodes = win.document.querySelectorAll(selector); +// eslint-disable-next-line func-style +function selectLast( + win: Window, + selector: string, + filter?: (e: E) => boolean, +): E | null { + let nodes = Array.from( + win.document.querySelectorAll(selector) as NodeListOf + ); if (filter) { - nodes = Array.from(nodes).filter(filter); + nodes = nodes.filter(filter); } - return nodes.length ? nodes[nodes.length - 1] : null; -}; +} -const historyPrev = (win) => { +const historyPrev = (win: Window): void => { win.history.back(); }; -const historyNext = (win) => { +const historyNext = (win: Window): void => { win.history.forward(); }; // Code common to linkPrev and linkNext which navigates to the specified page. -const linkRel = (win, rel) => { - let link = selectLast(win, `link[rel~=${rel}][href]`); - +const linkRel = (win: Window, rel: string): void => { + let link = selectLast(win, `link[rel~=${rel}][href]`); if (link) { - win.location = link.href; + win.location.href = link.href; return; } const pattern = REL_PATTERN[rel]; - link = selectLast(win, `a[rel~=${rel}][href]`) || + let a = selectLast(win, `a[rel~=${rel}][href]`) || // `innerText` is much slower than `textContent`, but produces much better // (i.e. less unexpected) results selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText)); - if (link) { - link.click(); + if (a) { + a.click(); } }; -const linkPrev = (win) => { +const linkPrev = (win: Window): void => { linkRel(win, 'prev'); }; -const linkNext = (win) => { +const linkNext = (win: Window): void => { linkRel(win, 'next'); }; -const parent = (win) => { +const parent = (win: Window): void => { const loc = win.location; if (loc.hash !== '') { loc.hash = ''; @@ -71,8 +76,8 @@ const parent = (win) => { } }; -const root = (win) => { - win.location = win.location.origin; +const root = (win: Window): void => { + win.location.href = win.location.origin; }; export { historyPrev, historyNext, linkPrev, linkNext, parent, root }; diff --git a/src/content/reducers/addon.ts b/src/content/reducers/addon.ts index 0def55a..2131228 100644 --- a/src/content/reducers/addon.ts +++ b/src/content/reducers/addon.ts @@ -1,10 +1,17 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; -const defaultState = { +export interface State { + enabled: boolean; +} + +const defaultState: State = { enabled: true, }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.AddonAction, +): State { switch (action.type) { case actions.ADDON_SET_ENABLED: return { ...state, diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts index 4560e2c..8c3e637 100644 --- a/src/content/reducers/find.ts +++ b/src/content/reducers/find.ts @@ -1,11 +1,19 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; -const defaultState = { +export interface State { + keyword: string | null; + found: boolean; +} + +const defaultState: State = { keyword: null, found: false, }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.FindAction, +): State { switch (action.type) { case actions.FIND_SET_KEYWORD: return { ...state, diff --git a/src/content/reducers/follow-controller.ts b/src/content/reducers/follow-controller.ts index 5869c47..6965704 100644 --- a/src/content/reducers/follow-controller.ts +++ b/src/content/reducers/follow-controller.ts @@ -1,13 +1,23 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; -const defaultState = { +export interface State { + enabled: boolean; + newTab: boolean; + background: boolean; + keys: string, +} + +const defaultState: State = { enabled: false, newTab: false, background: false, keys: '', }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.FollowAction, +): State { switch (action.type) { case actions.FOLLOW_CONTROLLER_ENABLE: return { ...state, diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts index bf612a3..fb5eb84 100644 --- a/src/content/reducers/index.ts +++ b/src/content/reducers/index.ts @@ -1,10 +1,20 @@ import { combineReducers } from 'redux'; -import addon from './addon'; -import find from './find'; -import setting from './setting'; -import input from './input'; -import followController from './follow-controller'; -import mark from './mark'; +import addon, { State as AddonState } from './addon'; +import find, { State as FindState } from './find'; +import setting, { State as SettingState } from './setting'; +import input, { State as InputState } from './input'; +import followController, { State as FollowControllerState } + from './follow-controller'; +import mark, { State as MarkState } from './mark'; + +export interface State { + addon: AddonState; + find: FindState; + setting: SettingState; + input: InputState; + followController: FollowControllerState; + mark: MarkState; +} export default combineReducers({ addon, find, setting, input, followController, mark, diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts index 23e7dd2..6257e49 100644 --- a/src/content/reducers/input.ts +++ b/src/content/reducers/input.ts @@ -1,10 +1,17 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; -const defaultState = { +export interface State { + keys: string[]; +} + +const defaultState: State = { keys: [] }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.InputAction, +): State { switch (action.type) { case actions.INPUT_KEY_PRESS: return { ...state, diff --git a/src/content/reducers/mark.ts b/src/content/reducers/mark.ts index 2c96cc5..e78b7b9 100644 --- a/src/content/reducers/mark.ts +++ b/src/content/reducers/mark.ts @@ -1,12 +1,26 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; -const defaultState = { +interface Mark { + x: number; + y: number; +} + +export interface State { + setMode: boolean; + jumpMode: boolean; + marks: { [key: string]: Mark }; +} + +const defaultState: State = { setMode: false, jumpMode: false, marks: {}, }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.MarkAction, +): State { switch (action.type) { case actions.MARK_START_SET: return { ...state, setMode: true }; diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts index a49db6d..fa8e8ee 100644 --- a/src/content/reducers/setting.ts +++ b/src/content/reducers/setting.ts @@ -1,11 +1,18 @@ -import actions from 'content/actions'; +import * as actions from '../actions'; + +export interface State { + keymaps: any[]; +} const defaultState = { // keymaps is and arrays of key-binding pairs, which is entries of Map keymaps: [], }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.SettingAction, +): State { switch (action.type) { case actions.SETTING_SET: return { ...action.value }; diff --git a/src/content/scrolls.ts b/src/content/scrolls.ts index bbf2491..6a35315 100644 --- a/src/content/scrolls.ts +++ b/src/content/scrolls.ts @@ -1,19 +1,19 @@ -import * as doms from 'shared/utils/dom'; +import * as doms from '../shared/utils/dom'; const SCROLL_DELTA_X = 64; const SCROLL_DELTA_Y = 64; // dirty way to store scrolling state on globally let scrolling = false; -let lastTimeoutId = null; +let lastTimeoutId: number | null = null; -const isScrollableStyle = (element) => { +const isScrollableStyle = (element: Element): boolean => { let { overflowX, overflowY } = window.getComputedStyle(element); return !(overflowX !== 'scroll' && overflowX !== 'auto' && overflowY !== 'scroll' && overflowY !== 'auto'); }; -const isOverflowed = (element) => { +const isOverflowed = (element: Element): boolean => { return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight; }; @@ -22,7 +22,7 @@ const isOverflowed = (element) => { // this method is called by each scrolling, and the returned value of this // method is not cached. That does not cause performance issue because in the // most pages, the window is root element i,e, documentElement. -const findScrollable = (element) => { +const findScrollable = (element: Element): Element | null => { if (isScrollableStyle(element) && isOverflowed(element)) { return element; } @@ -56,12 +56,16 @@ const resetScrolling = () => { }; class Scroller { - constructor(element, smooth) { + private element: Element; + + private smooth: boolean; + + constructor(element: Element, smooth: boolean) { this.element = element; this.smooth = smooth; } - scrollTo(x, y) { + scrollTo(x: number, y: number): void { if (!this.smooth) { this.element.scrollTo(x, y); return; @@ -74,13 +78,13 @@ class Scroller { this.prepareReset(); } - scrollBy(x, y) { + scrollBy(x: number, y: number): void { let left = this.element.scrollLeft + x; let top = this.element.scrollTop + y; this.scrollTo(left, top); } - prepareReset() { + prepareReset(): void { scrolling = true; if (lastTimeoutId) { clearTimeout(lastTimeoutId); @@ -95,7 +99,7 @@ const getScroll = () => { return { x: target.scrollLeft, y: target.scrollTop }; }; -const scrollVertically = (count, smooth) => { +const scrollVertically = (count: number, smooth: boolean): void => { let target = scrollTarget(); let delta = SCROLL_DELTA_Y * count; if (scrolling) { @@ -104,7 +108,7 @@ const scrollVertically = (count, smooth) => { new Scroller(target, smooth).scrollBy(0, delta); }; -const scrollHorizonally = (count, smooth) => { +const scrollHorizonally = (count: number, smooth: boolean): void => { let target = scrollTarget(); let delta = SCROLL_DELTA_X * count; if (scrolling) { @@ -113,7 +117,7 @@ const scrollHorizonally = (count, smooth) => { new Scroller(target, smooth).scrollBy(delta, 0); }; -const scrollPages = (count, smooth) => { +const scrollPages = (count: number, smooth: boolean): void => { let target = scrollTarget(); let height = target.clientHeight; let delta = height * count; @@ -123,33 +127,33 @@ const scrollPages = (count, smooth) => { new Scroller(target, smooth).scrollBy(0, delta); }; -const scrollTo = (x, y, smooth) => { +const scrollTo = (x: number, y: number, smooth: boolean): void => { let target = scrollTarget(); new Scroller(target, smooth).scrollTo(x, y); }; -const scrollToTop = (smooth) => { +const scrollToTop = (smooth: boolean): void => { let target = scrollTarget(); let x = target.scrollLeft; let y = 0; new Scroller(target, smooth).scrollTo(x, y); }; -const scrollToBottom = (smooth) => { +const scrollToBottom = (smooth: boolean): void => { let target = scrollTarget(); let x = target.scrollLeft; let y = target.scrollHeight; new Scroller(target, smooth).scrollTo(x, y); }; -const scrollToHome = (smooth) => { +const scrollToHome = (smooth: boolean): void => { let target = scrollTarget(); let x = 0; let y = target.scrollTop; new Scroller(target, smooth).scrollTo(x, y); }; -const scrollToEnd = (smooth) => { +const scrollToEnd = (smooth: boolean): void => { let target = scrollTarget(); let x = target.scrollWidth; let y = target.scrollTop; diff --git a/src/content/store/index.ts b/src/content/store/index.ts new file mode 100644 index 0000000..5c41744 --- /dev/null +++ b/src/content/store/index.ts @@ -0,0 +1,8 @@ +import promise from 'redux-promise'; +import reducers from '../reducers'; +import { createStore, applyMiddleware } from 'redux'; + +export const newStore = () => createStore( + reducers, + applyMiddleware(promise), +); diff --git a/src/content/urls.ts b/src/content/urls.ts index 6e7ea31..390efde 100644 --- a/src/content/urls.ts +++ b/src/content/urls.ts @@ -1,7 +1,7 @@ -import messages from 'shared/messages'; +import * as messages from '../shared/messages'; import * as urls from '../shared/urls'; -const yank = (win) => { +const yank = (win: Window) => { let input = win.document.createElement('input'); win.document.body.append(input); @@ -15,7 +15,7 @@ const yank = (win) => { input.remove(); }; -const paste = (win, newTab, searchSettings) => { +const paste = (win: Window, newTab: boolean, searchSettings: any) => { let textarea = win.document.createElement('textarea'); win.document.body.append(textarea); @@ -25,7 +25,7 @@ const paste = (win, newTab, searchSettings) => { textarea.focus(); if (win.document.execCommand('paste')) { - let value = textarea.textContent; + let value = textarea.textContent as string; let url = urls.searchUrl(value, searchSettings); browser.runtime.sendMessage({ type: messages.OPEN_URL, diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 2bc12d8..41b0f0b 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -1,78 +1,276 @@ -type WebMessageSender = Window | MessagePort | ServiceWorker | null; -type WebMessageListener = (msg: any, sender: WebMessageSender | null) => void; - -const onWebMessage = (listener: WebMessageListener) => { - window.addEventListener('message', (event: MessageEvent) => { - let sender = event.source; - let message = null; - try { - message = JSON.parse(event.data); - } catch (e) { - // ignore unexpected message - return; - } - listener(message, sender); - }); -}; +import * as operations from './operations'; -const onBackgroundMessage = ( - listener: (msg: any, sender: browser.runtime.MessageSender, -) => void) => { - browser.runtime.onMessage.addListener(listener); -}; +export const BACKGROUND_OPERATION = 'background.operation'; -const onMessage = ( - listener: (msg: any, sender: WebMessageSender | browser.runtime.MessageSender, -) => void) => { - onWebMessage(listener); - onBackgroundMessage(listener); -}; +export const CONSOLE_UNFOCUS = 'console.unfocus'; +export const CONSOLE_ENTER_COMMAND = 'console.enter.command'; +export const CONSOLE_ENTER_FIND = 'console.enter.find'; +export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions'; +export const CONSOLE_SHOW_COMMAND = 'console.show.command'; +export const CONSOLE_SHOW_ERROR = 'console.show.error'; +export const CONSOLE_SHOW_INFO = 'console.show.info'; +export const CONSOLE_SHOW_FIND = 'console.show.find'; +export const CONSOLE_HIDE = 'console.hide'; + +export const FOLLOW_START = 'follow.start'; +export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets'; +export const FOLLOW_RESPONSE_COUNT_TARGETS = 'follow.response.count.targets'; +export const FOLLOW_CREATE_HINTS = 'follow.create.hints'; +export const FOLLOW_SHOW_HINTS = 'follow.update.hints'; +export const FOLLOW_REMOVE_HINTS = 'follow.remove.hints'; +export const FOLLOW_ACTIVATE = 'follow.activate'; +export const FOLLOW_KEY_PRESS = 'follow.key.press'; + +export const MARK_SET_GLOBAL = 'mark.set.global'; +export const MARK_JUMP_GLOBAL = 'mark.jump.global'; + +export const TAB_SCROLL_TO = 'tab.scroll.to'; + +export const FIND_NEXT = 'find.next'; +export const FIND_PREV = 'find.prev'; +export const FIND_GET_KEYWORD = 'find.get.keyword'; +export const FIND_SET_KEYWORD = 'find.set.keyword'; + +export const ADDON_ENABLED_QUERY = 'addon.enabled.query'; +export const ADDON_ENABLED_RESPONSE = 'addon.enabled.response'; +export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled'; + +export const OPEN_URL = 'open.url'; + +export const SETTINGS_CHANGED = 'settings.changed'; +export const SETTINGS_QUERY = 'settings.query'; + +export const CONSOLE_FRAME_MESSAGE = 'console.frame.message'; + +interface BackgroundOperationMessage { + type: typeof BACKGROUND_OPERATION; + operation: operations.Operation; +} + +interface ConsoleUnfocusMessage { + type: typeof CONSOLE_UNFOCUS; +} + +interface ConsoleEnterCommandMessage { + type: typeof CONSOLE_ENTER_COMMAND; + text: string; +} + +interface ConsoleEnterFindMessage { + type: typeof CONSOLE_ENTER_FIND; + text: string; +} + +interface ConsoleQueryCompletionsMessage { + type: typeof CONSOLE_QUERY_COMPLETIONS; + text: string; +} + +interface ConsoleShowCommandMessage { + type: typeof CONSOLE_SHOW_COMMAND; + command: string; +} + +interface ConsoleShowErrorMessage { + type: typeof CONSOLE_SHOW_ERROR; + text: string; +} + +interface ConsoleShowInfoMessage { + type: typeof CONSOLE_SHOW_INFO; + text: string; +} + +interface ConsoleShowFindMessage { + type: typeof CONSOLE_SHOW_FIND; +} + +interface ConsoleHideMessage { + type: typeof CONSOLE_HIDE; +} + +interface FollowStartMessage { + type: typeof FOLLOW_START; + newTab: boolean; + background: boolean; +} + +interface FollowRequestCountTargetsMessage { + type: typeof FOLLOW_REQUEST_COUNT_TARGETS; + viewSize: { width: number, height: number }; + framePosition: { x: number, y: number }; +} + +interface FollowResponseCountTargetsMessage { + type: typeof FOLLOW_RESPONSE_COUNT_TARGETS; + count: number; +} + +interface FollowCreateHintsMessage { + type: typeof FOLLOW_CREATE_HINTS; + keysArray: string[]; + newTab: boolean; + background: boolean; +} + +interface FollowShowHintsMessage { + type: typeof FOLLOW_SHOW_HINTS; + keys: string; +} + +interface FollowRemoveHintsMessage { + type: typeof FOLLOW_REMOVE_HINTS; +} + +interface FollowActivateMessage { + type: typeof FOLLOW_ACTIVATE; + keys: string; +} + +interface FollowKeyPressMessage { + type: typeof FOLLOW_KEY_PRESS; + key: string; + ctrlKey: boolean; +} + +interface MarkSetGlobalMessage { + type: typeof MARK_SET_GLOBAL; + key: string; + x: number; + y: number; +} + +interface MarkJumpGlobalMessage { + type: typeof MARK_JUMP_GLOBAL; + key: string; +} + +interface TabScrollToMessage { + type: typeof TAB_SCROLL_TO; + x: number; + y: number; +} + +interface FindNextMessage { + type: typeof FIND_NEXT; +} + +interface FindPrevMessage { + type: typeof FIND_PREV; +} + +interface FindGetKeywordMessage { + type: typeof FIND_GET_KEYWORD; +} + +interface FindSetKeywordMessage { + type: typeof FIND_SET_KEYWORD; + keyword: string; + found: boolean; +} + +interface AddonEnabledQueryMessage { + type: typeof ADDON_ENABLED_QUERY; +} + +interface AddonEnabledResponseMessage { + type: typeof ADDON_ENABLED_RESPONSE; + enabled: boolean; +} + +interface AddonToggleEnabledMessage { + type: typeof ADDON_TOGGLE_ENABLED; +} + +interface OpenUrlMessage { + type: typeof OPEN_URL; + url: string; + newTab: boolean; + background: boolean; +} + +interface SettingsChangedMessage { + type: typeof SETTINGS_CHANGED; +} + +interface SettingsQueryMessage { + type: typeof SETTINGS_QUERY; +} + +interface ConsoleFrameMessageMessage { + type: typeof CONSOLE_FRAME_MESSAGE; + message: any; +} + +export type Message = + BackgroundOperationMessage | + ConsoleUnfocusMessage | + ConsoleEnterCommandMessage | + ConsoleEnterFindMessage | + ConsoleQueryCompletionsMessage | + ConsoleShowCommandMessage | + ConsoleShowErrorMessage | + ConsoleShowInfoMessage | + ConsoleShowFindMessage | + ConsoleHideMessage | + FollowStartMessage | + FollowRequestCountTargetsMessage | + FollowResponseCountTargetsMessage | + FollowCreateHintsMessage | + FollowShowHintsMessage | + FollowRemoveHintsMessage | + FollowActivateMessage | + FollowKeyPressMessage | + MarkSetGlobalMessage | + MarkJumpGlobalMessage | + TabScrollToMessage | + FindNextMessage | + FindPrevMessage | + FindGetKeywordMessage | + FindSetKeywordMessage | + AddonEnabledQueryMessage | + AddonEnabledResponseMessage | + AddonToggleEnabledMessage | + OpenUrlMessage | + SettingsChangedMessage | + SettingsQueryMessage | + ConsoleFrameMessageMessage; -export default { - BACKGROUND_OPERATION: 'background.operation', - - CONSOLE_UNFOCUS: 'console.unfocus', - CONSOLE_ENTER_COMMAND: 'console.enter.command', - CONSOLE_ENTER_FIND: 'console.enter.find', - CONSOLE_QUERY_COMPLETIONS: 'console.query.completions', - CONSOLE_SHOW_COMMAND: 'console.show.command', - CONSOLE_SHOW_ERROR: 'console.show.error', - CONSOLE_SHOW_INFO: 'console.show.info', - CONSOLE_SHOW_FIND: 'console.show.find', - CONSOLE_HIDE: 'console.hide', - - FOLLOW_START: 'follow.start', - FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets', - FOLLOW_RESPONSE_COUNT_TARGETS: 'follow.response.count.targets', - FOLLOW_CREATE_HINTS: 'follow.create.hints', - FOLLOW_SHOW_HINTS: 'follow.update.hints', - FOLLOW_REMOVE_HINTS: 'follow.remove.hints', - FOLLOW_ACTIVATE: 'follow.activate', - FOLLOW_KEY_PRESS: 'follow.key.press', - - MARK_SET_GLOBAL: 'mark.set.global', - MARK_JUMP_GLOBAL: 'mark.jump.global', - - TAB_SCROLL_TO: 'tab.scroll.to', - - FIND_NEXT: 'find.next', - FIND_PREV: 'find.prev', - FIND_GET_KEYWORD: 'find.get.keyword', - FIND_SET_KEYWORD: 'find.set.keyword', - - ADDON_ENABLED_QUERY: 'addon.enabled.query', - ADDON_ENABLED_RESPONSE: 'addon.enabled.response', - ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled', - - OPEN_URL: 'open.url', - - SETTINGS_CHANGED: 'settings.changed', - SETTINGS_QUERY: 'settings.query', - - WINDOW_TOP_MESSAGE: 'window.top.message', - CONSOLE_FRAME_MESSAGE: 'console.frame.message', - - onWebMessage, - onBackgroundMessage, - onMessage, +// eslint-disable-next-line complexity +export const valueOf = (o: any): Message => { + switch (o.type) { + case CONSOLE_UNFOCUS: + case CONSOLE_ENTER_COMMAND: + case CONSOLE_ENTER_FIND: + case CONSOLE_QUERY_COMPLETIONS: + case CONSOLE_SHOW_COMMAND: + case CONSOLE_SHOW_ERROR: + case CONSOLE_SHOW_INFO: + case CONSOLE_SHOW_FIND: + case CONSOLE_HIDE: + case FOLLOW_START: + case FOLLOW_REQUEST_COUNT_TARGETS: + case FOLLOW_RESPONSE_COUNT_TARGETS: + case FOLLOW_CREATE_HINTS: + case FOLLOW_SHOW_HINTS: + case FOLLOW_REMOVE_HINTS: + case FOLLOW_ACTIVATE: + case FOLLOW_KEY_PRESS: + case MARK_SET_GLOBAL: + case MARK_JUMP_GLOBAL: + case TAB_SCROLL_TO: + case FIND_NEXT: + case FIND_PREV: + case FIND_GET_KEYWORD: + case FIND_SET_KEYWORD: + case ADDON_ENABLED_QUERY: + case ADDON_ENABLED_RESPONSE: + case ADDON_TOGGLE_ENABLED: + case OPEN_URL: + case SETTINGS_CHANGED: + case SETTINGS_QUERY: + case CONSOLE_FRAME_MESSAGE: + return o; + } + throw new Error('unknown operation type: ' + o.type); }; diff --git a/src/shared/operations.ts b/src/shared/operations.ts index d59723e..cc22f75 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -1,80 +1,447 @@ -const operations: { [key: string]: string } = { - // Hide console, or cancel some user actions - CANCEL: 'cancel', - - // Addons - ADDON_ENABLE: 'addon.enable', - ADDON_DISABLE: 'addon.disable', - ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled', - - // Command - COMMAND_SHOW: 'command.show', - COMMAND_SHOW_OPEN: 'command.show.open', - COMMAND_SHOW_TABOPEN: 'command.show.tabopen', - COMMAND_SHOW_WINOPEN: 'command.show.winopen', - COMMAND_SHOW_BUFFER: 'command.show.buffer', - COMMAND_SHOW_ADDBOOKMARK: 'command.show.addbookmark', - - // Scrolls - SCROLL_VERTICALLY: 'scroll.vertically', - SCROLL_HORIZONALLY: 'scroll.horizonally', - SCROLL_PAGES: 'scroll.pages', - SCROLL_TOP: 'scroll.top', - SCROLL_BOTTOM: 'scroll.bottom', - SCROLL_HOME: 'scroll.home', - SCROLL_END: 'scroll.end', - - // Follows - FOLLOW_START: 'follow.start', - - // Navigations - NAVIGATE_HISTORY_PREV: 'navigate.history.prev', - NAVIGATE_HISTORY_NEXT: 'navigate.history.next', - NAVIGATE_LINK_PREV: 'navigate.link.prev', - NAVIGATE_LINK_NEXT: 'navigate.link.next', - NAVIGATE_PARENT: 'navigate.parent', - NAVIGATE_ROOT: 'navigate.root', - - // Focus - FOCUS_INPUT: 'focus.input', - - // Page - PAGE_SOURCE: 'page.source', - PAGE_HOME: 'page.home', - - // Tabs - TAB_CLOSE: 'tabs.close', - TAB_CLOSE_FORCE: 'tabs.close.force', - TAB_CLOSE_RIGHT: 'tabs.close.right', - TAB_REOPEN: 'tabs.reopen', - TAB_PREV: 'tabs.prev', - TAB_NEXT: 'tabs.next', - TAB_FIRST: 'tabs.first', - TAB_LAST: 'tabs.last', - TAB_PREV_SEL: 'tabs.prevsel', - TAB_RELOAD: 'tabs.reload', - TAB_PIN: 'tabs.pin', - TAB_UNPIN: 'tabs.unpin', - TAB_TOGGLE_PINNED: 'tabs.pin.toggle', - TAB_DUPLICATE: 'tabs.duplicate', - - // Zooms - ZOOM_IN: 'zoom.in', - ZOOM_OUT: 'zoom.out', - ZOOM_NEUTRAL: 'zoom.neutral', - - // Url yank/paste - URLS_YANK: 'urls.yank', - URLS_PASTE: 'urls.paste', - - // Find - FIND_START: 'find.start', - FIND_NEXT: 'find.next', - FIND_PREV: 'find.prev', - - // Mark - MARK_SET_PREFIX: 'mark.set.prefix', - MARK_JUMP_PREFIX: 'mark.jump.prefix', +// Hide console; or cancel some user actions +export const CANCEL = 'cancel'; + +// Addons +export const ADDON_ENABLE = 'addon.enable'; +export const ADDON_DISABLE = 'addon.disable'; +export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled'; + +// Command +export const COMMAND_SHOW = 'command.show'; +export const COMMAND_SHOW_OPEN = 'command.show.open'; +export const COMMAND_SHOW_TABOPEN = 'command.show.tabopen'; +export const COMMAND_SHOW_WINOPEN = 'command.show.winopen'; +export const COMMAND_SHOW_BUFFER = 'command.show.buffer'; +export const COMMAND_SHOW_ADDBOOKMARK = 'command.show.addbookmark'; + +// Scrolls +export const SCROLL_VERTICALLY = 'scroll.vertically'; +export const SCROLL_HORIZONALLY = 'scroll.horizonally'; +export const SCROLL_PAGES = 'scroll.pages'; +export const SCROLL_TOP = 'scroll.top'; +export const SCROLL_BOTTOM = 'scroll.bottom'; +export const SCROLL_HOME = 'scroll.home'; +export const SCROLL_END = 'scroll.end'; + +// Follows +export const FOLLOW_START = 'follow.start'; + +// Navigations +export const NAVIGATE_HISTORY_PREV = 'navigate.history.prev'; +export const NAVIGATE_HISTORY_NEXT = 'navigate.history.next'; +export const NAVIGATE_LINK_PREV = 'navigate.link.prev'; +export const NAVIGATE_LINK_NEXT = 'navigate.link.next'; +export const NAVIGATE_PARENT = 'navigate.parent'; +export const NAVIGATE_ROOT = 'navigate.root'; + +// Focus +export const FOCUS_INPUT = 'focus.input'; + +// Page +export const PAGE_SOURCE = 'page.source'; +export const PAGE_HOME = 'page.home'; + +// Tabs +export const TAB_CLOSE = 'tabs.close'; +export const TAB_CLOSE_FORCE = 'tabs.close.force'; +export const TAB_CLOSE_RIGHT = 'tabs.close.right'; +export const TAB_REOPEN = 'tabs.reopen'; +export const TAB_PREV = 'tabs.prev'; +export const TAB_NEXT = 'tabs.next'; +export const TAB_FIRST = 'tabs.first'; +export const TAB_LAST = 'tabs.last'; +export const TAB_PREV_SEL = 'tabs.prevsel'; +export const TAB_RELOAD = 'tabs.reload'; +export const TAB_PIN = 'tabs.pin'; +export const TAB_UNPIN = 'tabs.unpin'; +export const TAB_TOGGLE_PINNED = 'tabs.pin.toggle'; +export const TAB_DUPLICATE = 'tabs.duplicate'; + +// Zooms +export const ZOOM_IN = 'zoom.in'; +export const ZOOM_OUT = 'zoom.out'; +export const ZOOM_NEUTRAL = 'zoom.neutral'; + +// Url yank/paste +export const URLS_YANK = 'urls.yank'; +export const URLS_PASTE = 'urls.paste'; + +// Find +export const FIND_START = 'find.start'; +export const FIND_NEXT = 'find.next'; +export const FIND_PREV = 'find.prev'; + +// Mark +export const MARK_SET_PREFIX = 'mark.set.prefix'; +export const MARK_JUMP_PREFIX = 'mark.jump.prefix'; + +export interface CancelOperation { + type: typeof CANCEL; +} + +export interface AddonEnableOperation { + type: typeof ADDON_ENABLE; +} + +export interface AddonDisableOperation { + type: typeof ADDON_DISABLE; +} + +export interface AddonToggleEnabledOperation { + type: typeof ADDON_TOGGLE_ENABLED; +} + +export interface CommandShowOperation { + type: typeof COMMAND_SHOW; +} + +export interface CommandShowOpenOperation { + type: typeof COMMAND_SHOW_OPEN; + alter: boolean; +} + +export interface CommandShowTabopenOperation { + type: typeof COMMAND_SHOW_TABOPEN; + alter: boolean; +} + +export interface CommandShowWinopenOperation { + type: typeof COMMAND_SHOW_WINOPEN; + alter: boolean; +} + +export interface CommandShowBufferOperation { + type: typeof COMMAND_SHOW_BUFFER; +} + +export interface CommandShowAddbookmarkOperation { + type: typeof COMMAND_SHOW_ADDBOOKMARK; + alter: boolean; +} + +export interface ScrollVerticallyOperation { + type: typeof SCROLL_VERTICALLY; + count: number; +} + +export interface ScrollHorizonallyOperation { + type: typeof SCROLL_HORIZONALLY; + count: number; +} + +export interface ScrollPagesOperation { + type: typeof SCROLL_PAGES; + count: number; +} + +export interface ScrollTopOperation { + type: typeof SCROLL_TOP; +} + +export interface ScrollBottomOperation { + type: typeof SCROLL_BOTTOM; +} + +export interface ScrollHomeOperation { + type: typeof SCROLL_HOME; +} + +export interface ScrollEndOperation { + type: typeof SCROLL_END; +} + +export interface FollowStartOperation { + type: typeof FOLLOW_START; + newTab: boolean; + background: boolean; +} + +export interface NavigateHistoryPrevOperation { + type: typeof NAVIGATE_HISTORY_PREV; +} + +export interface NavigateHistoryNextOperation { + type: typeof NAVIGATE_HISTORY_NEXT; +} + +export interface NavigateLinkPrevOperation { + type: typeof NAVIGATE_LINK_PREV; +} + +export interface NavigateLinkNextOperation { + type: typeof NAVIGATE_LINK_NEXT; +} + +export interface NavigateParentOperation { + type: typeof NAVIGATE_PARENT; +} + +export interface NavigateRootOperation { + type: typeof NAVIGATE_ROOT; +} + +export interface FocusInputOperation { + type: typeof FOCUS_INPUT; +} + +export interface PageSourceOperation { + type: typeof PAGE_SOURCE; +} + +export interface PageHomeOperation { + type: typeof PAGE_HOME; + newTab: boolean; +} + +export interface TabCloseOperation { + type: typeof TAB_CLOSE; +} + +export interface TabCloseForceOperation { + type: typeof TAB_CLOSE_FORCE; +} + +export interface TabCloseRightOperation { + type: typeof TAB_CLOSE_RIGHT; +} + +export interface TabReopenOperation { + type: typeof TAB_REOPEN; +} + +export interface TabPrevOperation { + type: typeof TAB_PREV; +} + +export interface TabNextOperation { + type: typeof TAB_NEXT; +} + +export interface TabFirstOperation { + type: typeof TAB_FIRST; +} + +export interface TabLastOperation { + type: typeof TAB_LAST; +} + +export interface TabPrevSelOperation { + type: typeof TAB_PREV_SEL; +} + +export interface TabReloadOperation { + type: typeof TAB_RELOAD; + cache: boolean; +} + +export interface TabPinOperation { + type: typeof TAB_PIN; +} + +export interface TabUnpinOperation { + type: typeof TAB_UNPIN; +} + +export interface TabTogglePinnedOperation { + type: typeof TAB_TOGGLE_PINNED; +} + +export interface TabDuplicateOperation { + type: typeof TAB_DUPLICATE; +} + +export interface ZoomInOperation { + type: typeof ZOOM_IN; +} + +export interface ZoomOutOperation { + type: typeof ZOOM_OUT; +} + +export interface ZoomNeutralOperation { + type: typeof ZOOM_NEUTRAL; +} + +export interface UrlsYankOperation { + type: typeof URLS_YANK; +} + +export interface UrlsPasteOperation { + type: typeof URLS_PASTE; + newTab: boolean; +} + +export interface FindStartOperation { + type: typeof FIND_START; +} + +export interface FindNextOperation { + type: typeof FIND_NEXT; +} + +export interface FindPrevOperation { + type: typeof FIND_PREV; +} + +export interface MarkSetPrefixOperation { + type: typeof MARK_SET_PREFIX; +} + +export interface MarkJumpPrefixOperation { + type: typeof MARK_JUMP_PREFIX; +} + +export type Operation = + CancelOperation | + AddonEnableOperation | + AddonDisableOperation | + AddonToggleEnabledOperation | + CommandShowOperation | + CommandShowOpenOperation | + CommandShowTabopenOperation | + CommandShowWinopenOperation | + CommandShowBufferOperation | + CommandShowAddbookmarkOperation | + ScrollVerticallyOperation | + ScrollHorizonallyOperation | + ScrollPagesOperation | + ScrollTopOperation | + ScrollBottomOperation | + ScrollHomeOperation | + ScrollEndOperation | + FollowStartOperation | + NavigateHistoryPrevOperation | + NavigateHistoryNextOperation | + NavigateLinkPrevOperation | + NavigateLinkNextOperation | + NavigateParentOperation | + NavigateRootOperation | + FocusInputOperation | + PageSourceOperation | + PageHomeOperation | + TabCloseOperation | + TabCloseForceOperation | + TabCloseRightOperation | + TabReopenOperation | + TabPrevOperation | + TabNextOperation | + TabFirstOperation | + TabLastOperation | + TabPrevSelOperation | + TabReloadOperation | + TabPinOperation | + TabUnpinOperation | + TabTogglePinnedOperation | + TabDuplicateOperation | + ZoomInOperation | + ZoomOutOperation | + ZoomNeutralOperation | + UrlsYankOperation | + UrlsPasteOperation | + FindStartOperation | + FindNextOperation | + FindPrevOperation | + MarkSetPrefixOperation | + MarkJumpPrefixOperation; + +const assertOptionalBoolean = (obj: any, name: string) => { + if (Object.prototype.hasOwnProperty.call(obj, name) && + typeof obj[name] !== 'boolean') { + throw new TypeError(`Not a boolean parameter '${name}'`); + } +}; + +const assertRequiredNumber = (obj: any, name: string) => { + if (!Object.prototype.hasOwnProperty.call(obj, name) || + typeof obj[name] !== 'number') { + throw new TypeError(`Missing number parameter '${name}`); + } }; -export default operations; +// eslint-disable-next-line complexity, max-lines-per-function +export const valueOf = (o: any): Operation => { + if (!Object.prototype.hasOwnProperty.call(o, 'type')) { + throw new TypeError(`missing 'type' field`); + } + switch (o.type) { + case COMMAND_SHOW_OPEN: + case COMMAND_SHOW_TABOPEN: + case COMMAND_SHOW_WINOPEN: + case COMMAND_SHOW_ADDBOOKMARK: + assertOptionalBoolean(o, 'alter'); + return { type: o.type, alter: Boolean(o.alter) }; + case SCROLL_VERTICALLY: + case SCROLL_HORIZONALLY: + case SCROLL_PAGES: + assertRequiredNumber(o, 'count'); + return { type: o.type, count: Number(o.count) }; + case FOLLOW_START: + assertOptionalBoolean(o, 'newTab'); + assertOptionalBoolean(o, 'background'); + return { + type: FOLLOW_START, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len + }; + case PAGE_HOME: + assertOptionalBoolean(o, 'newTab'); + return { + type: PAGE_HOME, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + }; + case TAB_RELOAD: + assertOptionalBoolean(o, 'cache'); + return { + type: TAB_RELOAD, + cache: Boolean(typeof o.cache === undefined ? false : o.cache), + }; + case URLS_PASTE: + assertOptionalBoolean(o, 'newTab'); + return { + type: URLS_PASTE, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + }; + case CANCEL: + case ADDON_ENABLE: + case ADDON_DISABLE: + case ADDON_TOGGLE_ENABLED: + case COMMAND_SHOW: + case COMMAND_SHOW_BUFFER: + case SCROLL_TOP: + case SCROLL_BOTTOM: + case SCROLL_HOME: + case SCROLL_END: + case NAVIGATE_HISTORY_PREV: + case NAVIGATE_HISTORY_NEXT: + case NAVIGATE_LINK_PREV: + case NAVIGATE_LINK_NEXT: + case NAVIGATE_PARENT: + case NAVIGATE_ROOT: + case FOCUS_INPUT: + case PAGE_SOURCE: + case TAB_CLOSE: + case TAB_CLOSE_FORCE: + case TAB_CLOSE_RIGHT: + case TAB_REOPEN: + case TAB_PREV: + case TAB_NEXT: + case TAB_FIRST: + case TAB_LAST: + case TAB_PREV_SEL: + case TAB_PIN: + case TAB_UNPIN: + case TAB_TOGGLE_PINNED: + case TAB_DUPLICATE: + case ZOOM_IN: + case ZOOM_OUT: + case ZOOM_NEUTRAL: + case URLS_YANK: + case FIND_START: + case FIND_NEXT: + case FIND_PREV: + case MARK_SET_PREFIX: + case MARK_JUMP_PREFIX: + return { type: o.type }; + } + throw new Error('unknown operation type: ' + o.type); +}; diff --git a/src/shared/settings/validator.ts b/src/shared/settings/validator.ts index 0483931..71cc466 100644 --- a/src/shared/settings/validator.ts +++ b/src/shared/settings/validator.ts @@ -1,4 +1,4 @@ -import operations from '../operations'; +import * as operations from '../operations'; import * as properties from './properties'; const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; diff --git a/src/shared/utils/keys.ts b/src/shared/utils/keys.ts index d9abef7..e9b0365 100644 --- a/src/shared/utils/keys.ts +++ b/src/shared/utils/keys.ts @@ -1,4 +1,4 @@ -interface Key { +export interface Key { key: string; shiftKey: boolean | undefined; ctrlKey: boolean | undefined; diff --git a/test/content/actions/follow-controller.test.ts b/test/content/actions/follow-controller.test.ts index 718a90a..a4b1710 100644 --- a/test/content/actions/follow-controller.test.ts +++ b/test/content/actions/follow-controller.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as followControllerActions from 'content/actions/follow-controller'; describe('follow-controller actions', () => { diff --git a/test/content/actions/input.test.ts b/test/content/actions/input.test.ts index fe9db5f..33238a5 100644 --- a/test/content/actions/input.test.ts +++ b/test/content/actions/input.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as inputActions from 'content/actions/input'; describe("input actions", () => { diff --git a/test/content/actions/mark.test.ts b/test/content/actions/mark.test.ts index adbf06b..6c6d59e 100644 --- a/test/content/actions/mark.test.ts +++ b/test/content/actions/mark.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as markActions from 'content/actions/mark'; describe('mark actions', () => { diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts index 10f6807..0721d5d 100644 --- a/test/content/actions/setting.test.ts +++ b/test/content/actions/setting.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as settingActions from 'content/actions/setting'; describe("setting actions", () => { diff --git a/test/content/components/common/input.test.ts b/test/content/components/common/input.test.ts index 2ba5507..f3a943c 100644 --- a/test/content/components/common/input.test.ts +++ b/test/content/components/common/input.test.ts @@ -21,12 +21,14 @@ describe('InputComponent', () => { ++b; } }); - component.onKeyDown({ key: 'a' }); - component.onKeyDown({ key: 'b' }); - component.onKeyPress({ key: 'a' }); - component.onKeyUp({ key: 'a' }); - component.onKeyPress({ key: 'b' }); - component.onKeyUp({ key: 'b' }); + + let elem = document.body; + component.onKeyDown({ key: 'a', target: elem }); + component.onKeyDown({ key: 'b', target: elem }); + component.onKeyPress({ key: 'a', target: elem }); + component.onKeyUp({ key: 'a', target: elem }); + component.onKeyPress({ key: 'b', target: elem }); + component.onKeyUp({ key: 'b', target: elem }); expect(a).is.equals(1); expect(b).is.equals(1); diff --git a/test/content/reducers/addon.test.ts b/test/content/reducers/addon.test.ts index d4eb845..fb05244 100644 --- a/test/content/reducers/addon.test.ts +++ b/test/content/reducers/addon.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import addonReducer from 'content/reducers/addon'; describe("addon reducer", () => { diff --git a/test/content/reducers/find.test.ts b/test/content/reducers/find.test.ts index a8c30d7..66a2c67 100644 --- a/test/content/reducers/find.test.ts +++ b/test/content/reducers/find.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import findReducer from 'content/reducers/find'; describe("find reducer", () => { diff --git a/test/content/reducers/follow-controller.test.ts b/test/content/reducers/follow-controller.test.ts index 8a4c2d4..39f326c 100644 --- a/test/content/reducers/follow-controller.test.ts +++ b/test/content/reducers/follow-controller.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import followControllerReducer from 'content/reducers/follow-controller'; describe('follow-controller reducer', () => { diff --git a/test/content/reducers/input.test.ts b/test/content/reducers/input.test.ts index 0011943..f892201 100644 --- a/test/content/reducers/input.test.ts +++ b/test/content/reducers/input.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import inputReducer from 'content/reducers/input'; describe("input reducer", () => { diff --git a/test/content/reducers/mark.test.ts b/test/content/reducers/mark.test.ts index 76efbf7..1a51c3e 100644 --- a/test/content/reducers/mark.test.ts +++ b/test/content/reducers/mark.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import reducer from 'content/reducers/mark'; describe("mark reducer", () => { diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts index 4e4c095..226fc58 100644 --- a/test/content/reducers/setting.test.ts +++ b/test/content/reducers/setting.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import settingReducer from 'content/reducers/setting'; describe("content setting reducer", () => { diff --git a/test/shared/operations.test.ts b/test/shared/operations.test.ts new file mode 100644 index 0000000..42a3eed --- /dev/null +++ b/test/shared/operations.test.ts @@ -0,0 +1,41 @@ +import * as operations from 'shared/operations'; + +describe('operations', () => { + describe('#valueOf', () => { + it('returns an Operation', () => { + let op: operations.Operation = operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + count: 10, + }); + expect(op.type).to.equal(operations.SCROLL_VERTICALLY); + expect(op.count).to.equal(10); + }); + + it('throws an Error on missing required parameter', () => { + expect(() => operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + })).to.throw(TypeError); + }); + + it('fills default valus of optional parameter', () => { + let op: operations.Operation = operations.valueOf({ + type: operations.COMMAND_SHOW_OPEN, + }); + + expect(op.type).to.equal(operations.COMMAND_SHOW_OPEN) + expect(op.alter).to.be.false; + }); + + it('throws an Error on mismatch of parameter', () => { + expect(() => operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + count: '10', + })).to.throw(TypeError); + + expect(() => valueOf({ + type: operations.COMMAND_SHOW_OPEN, + alter: 'true', + })).to.throw(TypeError); + }); + }); +}) diff --git a/tsconfig.json b/tsconfig.json index 575601b..b61ee23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,11 @@ "compilerOptions": { "target": "es2017", "module": "commonjs", + "lib": ["es6", "dom", "es2017"], "allowJs": true, "checkJs": true, + "noEmit": true, "jsx": "react", - "declaration": true, - "declarationMap": true, "sourceMap": true, "outDir": "./build", "removeComments": true, @@ -29,5 +29,8 @@ "esModuleInterop": true, "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"] - } + }, + "include": [ + "src" + ] } -- cgit v1.2.3 From a0882bbceb7ed71d56bf8557620449fbc3f19749 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 5 May 2019 08:03:29 +0900 Subject: Declare setting types --- src/background/controllers/SettingController.ts | 3 +- src/background/domains/GlobalMark.ts | 3 +- src/background/domains/Setting.ts | 59 --- .../repositories/PersistentSettingRepository.ts | 6 +- src/background/repositories/SettingRepository.ts | 34 +- src/background/usecases/CommandUseCase.ts | 7 +- src/background/usecases/CompletionsUseCase.ts | 30 +- src/background/usecases/SettingUseCase.ts | 17 +- src/background/usecases/parsers.ts | 21 +- src/content/actions/index.ts | 3 +- src/content/actions/operation.ts | 4 +- src/content/actions/setting.ts | 23 +- src/content/components/common/index.ts | 5 +- src/content/components/common/input.ts | 1 - src/content/components/common/keymapper.ts | 28 +- src/content/components/common/mark.ts | 10 +- .../components/top-content/follow-controller.ts | 4 +- src/content/reducers/setting.ts | 24 +- src/settings/actions/index.ts | 16 +- src/settings/actions/setting.ts | 60 +-- src/settings/components/form/KeymapsForm.tsx | 23 +- src/settings/components/form/SearchForm.tsx | 30 +- src/settings/components/index.tsx | 104 ++++-- src/settings/keymaps.ts | 3 - src/settings/reducers/setting.ts | 22 +- src/settings/storage.ts | 15 + src/shared/SettingData.ts | 414 +++++++++++++++++++++ src/shared/Settings.ts | 200 ++++++++++ src/shared/operations.ts | 2 +- src/shared/properties.ts | 50 +++ src/shared/property-defs.ts | 50 +++ src/shared/settings/default.ts | 85 ----- src/shared/settings/properties.ts | 24 -- src/shared/settings/storage.ts | 32 -- src/shared/settings/validator.ts | 76 ---- src/shared/settings/values.ts | 108 ------ test/background/usecases/parsers.test.ts | 41 +- test/content/actions/setting.test.ts | 46 ++- test/content/reducers/setting.test.ts | 21 +- test/settings/components/form/KeymapsForm.test.tsx | 14 +- .../components/form/SearchEngineForm.test.tsx | 60 ++- test/settings/reducers/setting.test.ts | 3 +- test/shared/SettingData.test.ts | 293 +++++++++++++++ test/shared/Settings.test.ts | 190 ++++++++++ test/shared/properties.test.js | 18 + test/shared/property-defs.test.js | 18 + test/shared/settings/validator.test.ts | 81 ---- test/shared/settings/values.test.ts | 138 ------- 48 files changed, 1617 insertions(+), 902 deletions(-) delete mode 100644 src/background/domains/Setting.ts create mode 100644 src/settings/storage.ts create mode 100644 src/shared/SettingData.ts create mode 100644 src/shared/Settings.ts create mode 100644 src/shared/properties.ts create mode 100644 src/shared/property-defs.ts delete mode 100644 src/shared/settings/default.ts delete mode 100644 src/shared/settings/properties.ts delete mode 100644 src/shared/settings/storage.ts delete mode 100644 src/shared/settings/validator.ts delete mode 100644 src/shared/settings/values.ts create mode 100644 test/shared/SettingData.test.ts create mode 100644 test/shared/Settings.test.ts create mode 100644 test/shared/properties.test.js create mode 100644 test/shared/property-defs.test.js delete mode 100644 test/shared/settings/validator.test.ts delete mode 100644 test/shared/settings/values.test.ts (limited to 'test/content/actions/setting.test.ts') diff --git a/src/background/controllers/SettingController.ts b/src/background/controllers/SettingController.ts index f8b7302..dfd2817 100644 --- a/src/background/controllers/SettingController.ts +++ b/src/background/controllers/SettingController.ts @@ -1,5 +1,6 @@ import SettingUseCase from '../usecases/SettingUseCase'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; +import Settings from '../../shared/Settings'; export default class SettingController { private settingUseCase: SettingUseCase; @@ -11,7 +12,7 @@ export default class SettingController { this.contentMessageClient = new ContentMessageClient(); } - getSetting(): any { + getSetting(): Promise { return this.settingUseCase.get(); } diff --git a/src/background/domains/GlobalMark.ts b/src/background/domains/GlobalMark.ts index 0964373..1ae912e 100644 --- a/src/background/domains/GlobalMark.ts +++ b/src/background/domains/GlobalMark.ts @@ -1,6 +1,7 @@ -export interface GlobalMark { +export default interface GlobalMark { readonly tabId: number; readonly url: string; readonly x: number; readonly y: number; + // eslint-disable-next-line semi } diff --git a/src/background/domains/Setting.ts b/src/background/domains/Setting.ts deleted file mode 100644 index b2b1ff2..0000000 --- a/src/background/domains/Setting.ts +++ /dev/null @@ -1,59 +0,0 @@ -import DefaultSettings from '../../shared/settings/default'; -import * as settingsValues from '../../shared/settings/values'; - -type SettingValue = { - source: string, - json: string, - form: any -} - -export default class Setting { - private obj: SettingValue; - - constructor({ source, json, form }: SettingValue) { - this.obj = { - source, json, form - }; - } - - get source(): string { - return this.obj.source; - } - - get json(): string { - return this.obj.json; - } - - get form(): any { - return this.obj.form; - } - - value() { - let value = JSON.parse(DefaultSettings.json); - if (this.obj.source === 'json') { - value = settingsValues.valueFromJson(this.obj.json); - } else if (this.obj.source === 'form') { - value = settingsValues.valueFromForm(this.obj.form); - } - if (!value.properties) { - value.properties = {}; - } - return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; - } - - serialize(): SettingValue { - return this.obj; - } - - static deserialize(obj: SettingValue): Setting { - return new Setting({ source: obj.source, json: obj.json, form: obj.form }); - } - - static defaultSettings() { - return new Setting({ - source: DefaultSettings.source, - json: DefaultSettings.json, - form: {}, - }); - } -} diff --git a/src/background/repositories/PersistentSettingRepository.ts b/src/background/repositories/PersistentSettingRepository.ts index 3f2f4a1..18476fd 100644 --- a/src/background/repositories/PersistentSettingRepository.ts +++ b/src/background/repositories/PersistentSettingRepository.ts @@ -1,12 +1,12 @@ -import Setting from '../domains/Setting'; +import SettingData from '../../shared/SettingData'; export default class SettingRepository { - async load(): Promise { + async load(): Promise { let { settings } = await browser.storage.local.get('settings'); if (!settings) { return null; } - return Setting.deserialize(settings); + return SettingData.valueOf(settings); } } diff --git a/src/background/repositories/SettingRepository.ts b/src/background/repositories/SettingRepository.ts index 15355ba..eb83a2c 100644 --- a/src/background/repositories/SettingRepository.ts +++ b/src/background/repositories/SettingRepository.ts @@ -1,4 +1,6 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; +import Settings from '../../shared/Settings'; +import * as PropertyDefs from '../../shared/property-defs'; const CACHED_SETTING_KEY = 'setting'; @@ -9,17 +11,41 @@ export default class SettingRepository { this.cache = new MemoryStorage(); } - get(): Promise { + get(): Promise { return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); } - update(value: any): any { + update(value: Settings): void { return this.cache.set(CACHED_SETTING_KEY, value); } - async setProperty(name: string, value: string): Promise { + async setProperty( + name: string, value: string | number | boolean, + ): Promise { + let def = PropertyDefs.defs.find(d => name === d.name); + if (!def) { + throw new Error('unknown property: ' + name); + } + if (typeof value !== def.type) { + throw new TypeError(`property type of ${name} mismatch: ${typeof value}`); + } + let newValue = value; + if (typeof value === 'string' && value === '') { + newValue = def.defaultValue; + } + let current = await this.get(); - current.properties[name] = value; + switch (name) { + case 'hintchars': + current.properties.hintchars = newValue as string; + break; + case 'smoothscroll': + current.properties.smoothscroll = newValue as boolean; + break; + case 'complete': + current.properties.complete = newValue as string; + break; + } return this.update(current); } } diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts index e0e3ada..2247d7b 100644 --- a/src/background/usecases/CommandUseCase.ts +++ b/src/background/usecases/CommandUseCase.ts @@ -6,7 +6,6 @@ import SettingRepository from '../repositories/SettingRepository'; import BookmarkRepository from '../repositories/BookmarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; -import * as properties from '../../shared/settings/properties'; export default class CommandIndicator { private tabPresenter: TabPresenter; @@ -115,16 +114,16 @@ export default class CommandIndicator { async addbookmark(title: string): Promise { let tab = await this.tabPresenter.getCurrent(); - let item = await this.bookmarkRepository.create(title, tab.url); + let item = await this.bookmarkRepository.create(title, tab.url as string); let message = 'Saved current page: ' + item.url; - return this.consoleClient.showInfo(tab.id, message); + return this.consoleClient.showInfo(tab.id as number, message); } async set(keywords: string): Promise { if (keywords.length === 0) { return; } - let [name, value] = parsers.parseSetOption(keywords, properties.types); + let [name, value] = parsers.parseSetOption(keywords); await this.settingRepository.setProperty(name, value); return this.contentMessageClient.broadcastSettingsChanged(); diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index f3a808b..ae1ceed 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -4,7 +4,7 @@ import CompletionsRepository from '../repositories/CompletionsRepository'; import * as filters from './filters'; import SettingRepository from '../repositories/SettingRepository'; import TabPresenter from '../presenters/TabPresenter'; -import * as properties from '../../shared/settings/properties'; +import * as PropertyDefs from '../../shared/property-defs'; const COMPLETION_ITEM_LIMIT = 10; @@ -44,7 +44,7 @@ export default class CompletionsUseCase { let settings = await this.settingRepository.get(); let groups: CompletionGroup[] = []; - let complete = settings.properties.complete || properties.defaults.complete; + let complete = settings.properties.complete; for (let c of complete) { if (c === 's') { // eslint-disable-next-line no-await-in-loop @@ -127,25 +127,25 @@ export default class CompletionsUseCase { } querySet(name: string, keywords: string): Promise { - let items = Object.keys(properties.docs).map((key) => { - if (properties.types[key] === 'boolean') { + let items = PropertyDefs.defs.map((def) => { + if (def.type === 'boolean') { return [ { - caption: key, - content: name + ' ' + key, - url: 'Enable ' + properties.docs[key], + caption: def.name, + content: name + ' ' + def.name, + url: 'Enable ' + def.description, }, { - caption: 'no' + key, - content: name + ' no' + key, - url: 'Disable ' + properties.docs[key], + caption: 'no' + def.name, + content: name + ' no' + def.name, + url: 'Disable ' + def.description } ]; } return [ { - caption: key, - content: name + ' ' + key, - url: 'Set ' + properties.docs[key], + caption: def.name, + content: name + ' ' + def.name, + url: 'Set ' + def.description, } ]; }); @@ -195,8 +195,8 @@ export default class CompletionsUseCase { .map(filters.filterByTailingSlash) .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT)) .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0] - .sort((x: HistoryItem, y: HistoryItem) => { - return Number(x.visitCount) < Number(y.visitCount); + .sort((x: HistoryItem, y: HistoryItem): number => { + return Number(x.visitCount) - Number(y.visitCount); }) .slice(0, COMPLETION_ITEM_LIMIT); return histories.map(page => ({ diff --git a/src/background/usecases/SettingUseCase.ts b/src/background/usecases/SettingUseCase.ts index b66ce02..aa3b534 100644 --- a/src/background/usecases/SettingUseCase.ts +++ b/src/background/usecases/SettingUseCase.ts @@ -1,7 +1,8 @@ -import Setting from '../domains/Setting'; // eslint-disable-next-line max-len import PersistentSettingRepository from '../repositories/PersistentSettingRepository'; import SettingRepository from '../repositories/SettingRepository'; +import { DefaultSettingData } from '../../shared/SettingData'; +import Settings from '../../shared/Settings'; export default class SettingUseCase { private persistentSettingRepository: PersistentSettingRepository; @@ -13,20 +14,18 @@ export default class SettingUseCase { this.settingRepository = new SettingRepository(); } - get(): Promise { + get(): Promise { return this.settingRepository.get(); } - async reload(): Promise { - let settings = await this.persistentSettingRepository.load(); - if (!settings) { - settings = Setting.defaultSettings(); + async reload(): Promise { + let data = await this.persistentSettingRepository.load(); + if (!data) { + data = DefaultSettingData; } - let value = settings.value(); - + let value = data.toSettings(); this.settingRepository.update(value); - return value; } } diff --git a/src/background/usecases/parsers.ts b/src/background/usecases/parsers.ts index 3616ac3..6135fd8 100644 --- a/src/background/usecases/parsers.ts +++ b/src/background/usecases/parsers.ts @@ -1,3 +1,5 @@ +import * as PropertyDefs from '../../shared//property-defs'; + const mustNumber = (v: any): number => { let num = Number(v); if (isNaN(num)) { @@ -7,29 +9,28 @@ const mustNumber = (v: any): number => { }; const parseSetOption = ( - word: string, - types: { [key: string]: string }, + args: string, ): any[] => { - let [key, value]: any[] = word.split('='); + let [key, value]: any[] = args.split('='); if (value === undefined) { value = !key.startsWith('no'); key = value ? key : key.slice(2); } - let type = types[key]; - if (!type) { + let def = PropertyDefs.defs.find(d => d.name === key); + if (!def) { throw new Error('Unknown property: ' + key); } - if (type === 'boolean' && typeof value !== 'boolean' || - type !== 'boolean' && typeof value === 'boolean') { - throw new Error('Invalid argument: ' + word); + if (def.type === 'boolean' && typeof value !== 'boolean' || + def.type !== 'boolean' && typeof value === 'boolean') { + throw new Error('Invalid argument: ' + args); } - switch (type) { + switch (def.type) { case 'string': return [key, value]; case 'number': return [key, mustNumber(value)]; case 'boolean': return [key, value]; } - throw new Error('Unknown property type: ' + type); + throw new Error('Unknown property type: ' + def.type); }; export { parseSetOption }; diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index 18d0a69..a259211 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,4 +1,5 @@ import Redux from 'redux'; +import Settings from '../../shared/Settings'; // Enable/disable export const ADDON_SET_ENABLED = 'addon.set.enabled'; @@ -45,7 +46,7 @@ export interface FindSetKeywordAction extends Redux.Action { export interface SettingSetAction extends Redux.Action { type: typeof SETTING_SET; - value: any; + settings: Settings, } export interface InputKeyPressAction extends Redux.Action { diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index 6acb407..41e080b 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -8,7 +8,6 @@ import * as urls from '../urls'; import * as consoleFrames from '../console-frames'; import * as addonActions from './addon'; import * as markActions from './mark'; -import * as properties from '../../shared/settings/properties'; // eslint-disable-next-line complexity, max-lines-per-function const exec = ( @@ -16,8 +15,7 @@ const exec = ( settings: any, addonEnabled: boolean, ): Promise | actions.Action => { - let smoothscroll = settings.properties.smoothscroll || - properties.defaults.smoothscroll; + let smoothscroll = settings.properties.smoothscroll; switch (operation.type) { case operations.ADDON_ENABLE: return addonActions.enable(); diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts index a8f049a..92f8559 100644 --- a/src/content/actions/setting.ts +++ b/src/content/actions/setting.ts @@ -1,29 +1,20 @@ import * as actions from './index'; -import * as keyUtils from '../../shared/utils/keys'; import * as operations from '../../shared/operations'; import * as messages from '../../shared/messages'; +import Settings, { Keymaps } from '../../shared/Settings'; -const reservedKeymaps = { +const reservedKeymaps: Keymaps = { '': { type: operations.CANCEL }, '': { type: operations.CANCEL }, }; -const set = (value: any): actions.SettingAction => { - let entries: any[] = []; - if (value.keymaps) { - let keymaps = { ...value.keymaps, ...reservedKeymaps }; - entries = Object.entries(keymaps).map((entry) => { - return [ - keyUtils.fromMapKeys(entry[0]), - entry[1], - ]; - }); - } - +const set = (settings: Settings): actions.SettingAction => { return { type: actions.SETTING_SET, - value: { ...value, - keymaps: entries, } + settings: { + ...settings, + keymaps: { ...settings.keymaps, ...reservedKeymaps }, + } }; }; diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index 9b5164e..8bd697f 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -8,6 +8,7 @@ import MessageListener from '../../MessageListener'; import * as addonActions from '../../actions/addon'; import * as blacklists from '../../../shared/blacklists'; import * as keys from '../../../shared/utils/keys'; +import * as actions from '../../actions'; export default class Common { private win: Window; @@ -45,9 +46,9 @@ export default class Common { reloadSettings() { try { this.store.dispatch(settingActions.load()) - .then(({ value: settings }: any) => { + .then((action: actions.SettingAction) => { let enabled = !blacklists.includes( - settings.blacklist, this.win.location.href + action.settings.blacklist, this.win.location.href ); this.store.dispatch(addonActions.setEnabled(enabled)); }); diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts index 64eb5f3..1fe34c9 100644 --- a/src/content/components/common/input.ts +++ b/src/content/components/common/input.ts @@ -61,7 +61,6 @@ export default class InputComponent { } let key = keys.fromKeyboardEvent(e); - for (let listener of this.onKeyListeners) { let stop = listener(key); if (stop) { diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts index d9c9834..c94bae0 100644 --- a/src/content/components/common/keymapper.ts +++ b/src/content/components/common/keymapper.ts @@ -3,7 +3,10 @@ import * as operationActions from '../../actions/operation'; import * as operations from '../../../shared/operations'; import * as keyUtils from '../../../shared/utils/keys'; -const mapStartsWith = (mapping, keys) => { +const mapStartsWith = ( + mapping: keyUtils.Key[], + keys: keyUtils.Key[], +): boolean => { if (mapping.length < keys.length) { return false; } @@ -16,26 +19,33 @@ const mapStartsWith = (mapping, keys) => { }; export default class KeymapperComponent { - constructor(store) { + private store: any; + + constructor(store: any) { this.store = store; } // eslint-disable-next-line max-statements - key(key) { + key(key: keyUtils.Key): boolean { this.store.dispatch(inputActions.keyPress(key)); let state = this.store.getState(); let input = state.input; - let keymaps = new Map(state.setting.keymaps); + let keymaps = new Map( + state.setting.keymaps.map( + (e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op], + ) + ); - let matched = Array.from(keymaps.keys()).filter((mapping) => { - return mapStartsWith(mapping, input.keys); - }); + let matched = Array.from(keymaps.keys()).filter( + (mapping: keyUtils.Key[]) => { + return mapStartsWith(mapping, input.keys); + }); if (!state.addon.enabled) { // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if // the addon disabled matched = matched.filter((keys) => { - let type = keymaps.get(keys).type; + let type = (keymaps.get(keys) as operations.Operation).type; return type === operations.ADDON_ENABLE || type === operations.ADDON_TOGGLE_ENABLED; }); @@ -47,7 +57,7 @@ export default class KeymapperComponent { matched.length === 1 && input.keys.length < matched[0].length) { return true; } - let operation = keymaps.get(matched[0]); + let operation = keymaps.get(matched[0]) as operations.Operation; let act = operationActions.exec( operation, state.setting, state.addon.enabled ); diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index 500d03b..686116c 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -1,7 +1,6 @@ -import * as markActions from 'content/actions/mark'; -import * as scrolls from 'content/scrolls'; -import * as consoleFrames from 'content/console-frames'; -import * as properties from 'shared/settings/properties'; +import * as markActions from '../../actions/mark'; +import * as scrolls from '../..//scrolls'; +import * as consoleFrames from '../..//console-frames'; const cancelKey = (key): boolean => { return key.key === 'Esc' || key.key === '[' && key.ctrlKey; @@ -20,8 +19,7 @@ export default class MarkComponent { // eslint-disable-next-line max-statements key(key) { let { mark: markStage, setting } = this.store.getState(); - let smoothscroll = setting.properties.smoothscroll || - properties.defaults.smoothscroll; + let smoothscroll = setting.properties.smoothscroll; if (!markStage.setMode && !markStage.jumpMode) { return false; diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index be71f6e..d49b22a 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -2,7 +2,6 @@ import * as followControllerActions from '../../actions/follow-controller'; import * as messages from '../../../shared/messages'; import MessageListener, { WebMessageSender } from '../../MessageListener'; import HintKeyProducer from '../../hint-key-producer'; -import * as properties from '../../../shared/settings/properties'; const broadcastMessage = (win: Window, message: messages.Message): void => { let json = JSON.stringify(message); @@ -162,7 +161,6 @@ export default class FollowController { } hintchars() { - return this.store.getState().setting.properties.hintchars || - properties.defaults.hintchars; + return this.store.getState().setting.properties.hintchars; } } diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts index fa8e8ee..a3dc3aa 100644 --- a/src/content/reducers/setting.ts +++ b/src/content/reducers/setting.ts @@ -1,12 +1,20 @@ import * as actions from '../actions'; +import * as keyUtils from '../../shared/utils/keys'; +import * as operations from '../../shared/operations'; +import { Properties } from '../../shared/Settings'; export interface State { - keymaps: any[]; + keymaps: { key: keyUtils.Key[], op: operations.Operation }[]; + properties: Properties; } -const defaultState = { - // keymaps is and arrays of key-binding pairs, which is entries of Map +const defaultState: State = { keymaps: [], + properties: { + complete: '', + smoothscroll: false, + hintchars: '', + }, }; export default function reducer( @@ -15,7 +23,15 @@ export default function reducer( ): State { switch (action.type) { case actions.SETTING_SET: - return { ...action.value }; + return { + keymaps: Object.entries(action.settings.keymaps).map((entry) => { + return { + key: keyUtils.fromMapKeys(entry[0]), + op: entry[1], + }; + }), + properties: action.settings.properties, + }; default: return state; } diff --git a/src/settings/actions/index.ts b/src/settings/actions/index.ts index 75c6bb5..b1e996e 100644 --- a/src/settings/actions/index.ts +++ b/src/settings/actions/index.ts @@ -1,3 +1,7 @@ +import { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; + // Settings export const SETTING_SET_SETTINGS = 'setting.set.settings'; export const SETTING_SHOW_ERROR = 'setting.show.error'; @@ -6,25 +10,25 @@ export const SETTING_SWITCH_TO_JSON = 'setting.switch.to.json'; interface SettingSetSettingsAcion { type: typeof SETTING_SET_SETTINGS; - source: string; - json: string; - form: any; + source: SettingSource; + json?: JSONSettings; + form?: FormSettings; } interface SettingShowErrorAction { type: typeof SETTING_SHOW_ERROR; error: string; - json: string; + json: JSONSettings; } interface SettingSwitchToFormAction { type: typeof SETTING_SWITCH_TO_FORM; - form: any; + form: FormSettings, } interface SettingSwitchToJsonAction { type: typeof SETTING_SWITCH_TO_JSON; - json: string; + json: JSONSettings, } export type SettingAction = diff --git a/src/settings/actions/setting.ts b/src/settings/actions/setting.ts index b03cd80..9eb416e 100644 --- a/src/settings/actions/setting.ts +++ b/src/settings/actions/setting.ts @@ -1,35 +1,35 @@ import * as actions from './index'; -import * as validator from '../../shared/settings/validator'; -import * as settingsValues from '../../shared/settings/values'; -import * as settingsStorage from '../../shared/settings/storage'; -import keymaps from '../keymaps'; +import * as storages from '../storage'; +import SettingData, { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; const load = async(): Promise => { - let settings = await settingsStorage.loadRaw(); - return set(settings); + let data = await storages.load(); + return set(data); }; -const save = async(settings: any): Promise => { +const save = async(data: SettingData): Promise => { try { - if (settings.source === 'json') { - let value = JSON.parse(settings.json); - validator.validate(value); + if (data.getSource() === SettingSource.JSON) { + // toSettings exercise validation + data.toSettings(); } } catch (e) { return { type: actions.SETTING_SHOW_ERROR, error: e.toString(), - json: settings.json, + json: data.getJSON(), }; } - await settingsStorage.save(settings); - return set(settings); + await storages.save(data); + return set(data); }; -const switchToForm = (json: string): actions.SettingAction => { +const switchToForm = (json: JSONSettings): actions.SettingAction => { try { - validator.validate(JSON.parse(json)); - let form = settingsValues.formFromJson(json, keymaps.allowedOps); + // toSettings exercise validation + let form = FormSettings.fromSettings(json.toSettings()); return { type: actions.SETTING_SWITCH_TO_FORM, form, @@ -43,21 +43,31 @@ const switchToForm = (json: string): actions.SettingAction => { } }; -const switchToJson = (form: any): actions.SettingAction => { - let json = settingsValues.jsonFromForm(form); +const switchToJson = (form: FormSettings): actions.SettingAction => { + let json = JSONSettings.fromSettings(form.toSettings()); return { type: actions.SETTING_SWITCH_TO_JSON, json, }; }; -const set = (settings: any): actions.SettingAction => { - return { - type: actions.SETTING_SET_SETTINGS, - source: settings.source, - json: settings.json, - form: settings.form, - }; +const set = (data: SettingData): actions.SettingAction => { + let source = data.getSource(); + switch (source) { + case SettingSource.JSON: + return { + type: actions.SETTING_SET_SETTINGS, + source: source, + json: data.getJSON(), + }; + case SettingSource.Form: + return { + type: actions.SETTING_SET_SETTINGS, + source: source, + form: data.getForm(), + }; + } + throw new Error(`unknown source: ${source}`); }; export { load, save, set, switchToForm, switchToJson }; diff --git a/src/settings/components/form/KeymapsForm.tsx b/src/settings/components/form/KeymapsForm.tsx index ab44464..ad4d0e7 100644 --- a/src/settings/components/form/KeymapsForm.tsx +++ b/src/settings/components/form/KeymapsForm.tsx @@ -2,32 +2,30 @@ import './KeymapsForm.scss'; import React from 'react'; import Input from '../ui/Input'; import keymaps from '../../keymaps'; +import { FormKeymaps } from '../../../shared/SettingData'; -type Value = {[key: string]: string}; - -interface Props{ - value: Value; - onChange: (e: Value) => void; +interface Props { + value: FormKeymaps; + onChange: (e: FormKeymaps) => void; onBlur: () => void; } class KeymapsForm extends React.Component { public static defaultProps: Props = { - value: {}, + value: FormKeymaps.valueOf({}), onChange: () => {}, onBlur: () => {}, } render() { + let values = this.props.value.toJSON(); return
{ keymaps.fields.map((group, index) => { return
{ - group.map((field) => { - let name = field[0]; - let label = field[1]; - let value = this.props.value[name] || ''; + group.map(([name, label]) => { + let value = values[name] || ''; return { } bindValue(name: string, value: string) { - let next = { ...this.props.value }; - next[name] = value; - - this.props.onChange(next); + this.props.onChange(this.props.value.buildWithOverride(name, value)); } } diff --git a/src/settings/components/form/SearchForm.tsx b/src/settings/components/form/SearchForm.tsx index 737e291..67dbeba 100644 --- a/src/settings/components/form/SearchForm.tsx +++ b/src/settings/components/form/SearchForm.tsx @@ -2,31 +2,23 @@ import './SearchForm.scss'; import React from 'react'; import AddButton from '../ui/AddButton'; import DeleteButton from '../ui/DeleteButton'; - -interface Value { - default: string; - engines: string[][]; -} +import { FormSearch } from '../../../shared/SettingData'; interface Props { - value: Value; - onChange: (value: Value) => void; + value: FormSearch; + onChange: (value: FormSearch) => void; onBlur: () => void; } class SearchForm extends React.Component { public static defaultProps: Props = { - value: { default: '', engines: []}, + value: FormSearch.valueOf({ default: '', engines: []}), onChange: () => {}, onBlur: () => {}, } render() { - let value = this.props.value; - if (!value.engines) { - value.engines = []; - } - + let value = this.props.value.toJSON(); return
Name
@@ -63,28 +55,28 @@ class SearchForm extends React.Component { } bindValue(e: any) { - let value = this.props.value; + let value = this.props.value.toJSON(); let name = e.target.name; let index = Number(e.target.getAttribute('data-index')); - let next: Value = { + let next: typeof value = { default: value.default, - engines: value.engines ? value.engines.slice() : [], + engines: value.engines.slice(), }; if (name === 'name') { next.engines[index][0] = e.target.value; - next.default = this.props.value.engines[index][0]; + next.default = value.engines[index][0]; } else if (name === 'url') { next.engines[index][1] = e.target.value; } else if (name === 'default') { - next.default = this.props.value.engines[index][0]; + next.default = value.engines[index][0]; } else if (name === 'add') { next.engines.push(['', '']); } else if (name === 'delete') { next.engines.splice(index, 1); } - this.props.onChange(next); + this.props.onChange(FormSearch.valueOf(next)); if (name === 'delete' || name === 'default') { this.props.onBlur(); } diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx index f56e93f..b4a0866 100644 --- a/src/settings/components/index.tsx +++ b/src/settings/components/index.tsx @@ -6,22 +6,26 @@ import SearchForm from './form/SearchForm'; import KeymapsForm from './form/KeymapsForm'; import BlacklistForm from './form/BlacklistForm'; import PropertiesForm from './form/PropertiesForm'; -import * as properties from '../../shared/settings/properties'; import * as settingActions from '../../settings/actions/setting'; +import SettingData, { + JSONSettings, FormKeymaps, FormSearch, FormSettings, +} from '../../shared/SettingData'; +import { State as AppState } from '../reducers/setting'; +import * as settings from '../../shared/Settings'; +import * as PropertyDefs from '../../shared/property-defs'; const DO_YOU_WANT_TO_CONTINUE = 'Some settings in JSON can be lost when migrating. ' + 'Do you want to continue?'; -interface Props { - source: string; - json: string; - form: any; - error: string; - +type StateProps = ReturnType; +interface DispatchProps { + dispatch: (action: any) => void, +} +type Props = StateProps & DispatchProps & { // FIXME store: any; -} +}; class SettingsComponent extends React.Component { componentDidMount() { @@ -29,12 +33,17 @@ class SettingsComponent extends React.Component { } renderFormFields(form: any) { + let types = PropertyDefs.defs.reduce( + (o: {[key: string]: string}, def) => { + o[def.name] = def.type; + return o; + }, {}); return
Keybindings this.bindForm('keymaps', value)} + onChange={this.bindKeymapsForm.bind(this)} onBlur={this.save.bind(this)} />
@@ -42,7 +51,7 @@ class SettingsComponent extends React.Component { Search Engines this.bindForm('search', value)} + onChange={this.bindSearchForm.bind(this)} onBlur={this.save.bind(this)} /> @@ -50,23 +59,23 @@ class SettingsComponent extends React.Component { Blacklist this.bindForm('blacklist', value)} + onChange={this.bindBlacklistForm.bind(this)} onBlur={this.save.bind(this)} />
Properties this.bindForm('properties', value)} + onChange={this.bindPropertiesForm.bind(this)} onBlur={this.save.bind(this)} />
; } - renderJsonFields(json: string, error: string) { + renderJsonFields(json: JSONSettings, error: string) { return
{ error={error} onValueChange={this.bindJson.bind(this)} onBlur={this.save.bind(this)} - value={json} + value={json.toJSON()} />
; } @@ -87,7 +96,8 @@ class SettingsComponent extends React.Component { if (this.props.source === 'form') { fields = this.renderFormFields(this.props.form); } else if (this.props.source === 'json') { - fields = this.renderJsonFields(this.props.json, this.props.error); + fields = this.renderJsonFields( + this.props.json as JSONSettings, this.props.error); } return (
@@ -117,45 +127,73 @@ class SettingsComponent extends React.Component { ); } - bindForm(name: string, value: any) { - let settings = { + bindKeymapsForm(value: FormKeymaps) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithKeymaps(value), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindSearchForm(value: any) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithSearch( + FormSearch.valueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindBlacklistForm(value: any) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithBlacklist( + settings.blacklistValueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindPropertiesForm(value: any) { + let data = new SettingData({ source: this.props.source, - json: this.props.json, - form: { ...this.props.form }, - }; - settings.form[name] = value; - this.props.dispatch(settingActions.set(settings)); + form: (this.props.form as FormSettings).buildWithProperties( + settings.propertiesValueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); } bindJson(_name: string, value: string) { - let settings = { + let data = new SettingData({ source: this.props.source, - json: value, - form: this.props.form, - }; - this.props.dispatch(settingActions.set(settings)); + json: JSONSettings.valueOf(value), + }); + this.props.dispatch(settingActions.set(data)); } bindSource(_name: string, value: string) { let from = this.props.source; if (from === 'form' && value === 'json') { - this.props.dispatch(settingActions.switchToJson(this.props.form)); + this.props.dispatch(settingActions.switchToJson( + this.props.form as FormSettings)); } else if (from === 'json' && value === 'form') { let b = window.confirm(DO_YOU_WANT_TO_CONTINUE); if (!b) { this.forceUpdate(); return; } - this.props.dispatch(settingActions.switchToForm(this.props.json)); + this.props.dispatch( + settingActions.switchToForm(this.props.json as JSONSettings)); } } save() { - let settings = this.props.store.getState(); - this.props.dispatch(settingActions.save(settings)); + let { source, json, form } = this.props.store.getState(); + this.props.dispatch(settingActions.save( + new SettingData({ source, json, form }), + )); } } -const mapStateToProps = (state: any) => state; +const mapStateToProps = (state: AppState) => ({ ...state }); export default connect(mapStateToProps)(SettingsComponent); diff --git a/src/settings/keymaps.ts b/src/settings/keymaps.ts index ccfc74c..38045ad 100644 --- a/src/settings/keymaps.ts +++ b/src/settings/keymaps.ts @@ -66,9 +66,6 @@ const fields = [ ] ]; -const allowedOps = [].concat(...fields.map(group => group.map(e => e[0]))); - export default { fields, - allowedOps, }; diff --git a/src/settings/reducers/setting.ts b/src/settings/reducers/setting.ts index 47c21bf..c4a21c7 100644 --- a/src/settings/reducers/setting.ts +++ b/src/settings/reducers/setting.ts @@ -1,23 +1,25 @@ import * as actions from '../actions'; +import { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; -interface State { - source: string; - json: string; - form: any; +export interface State { + source: SettingSource; + json?: JSONSettings; + form?: FormSettings; error: string; } const defaultState: State = { - source: '', - json: '', - form: null, + source: SettingSource.JSON, + json: JSONSettings.valueOf(''), error: '', }; export default function reducer( state = defaultState, action: actions.SettingAction, -) { +): State { switch (action.type) { case actions.SETTING_SET_SETTINGS: return { ...state, @@ -32,12 +34,12 @@ export default function reducer( case actions.SETTING_SWITCH_TO_FORM: return { ...state, error: '', - source: 'form', + source: SettingSource.Form, form: action.form, }; case actions.SETTING_SWITCH_TO_JSON: return { ...state, error: '', - source: 'json', + source: SettingSource.JSON, json: action.json, }; default: return state; diff --git a/src/settings/storage.ts b/src/settings/storage.ts new file mode 100644 index 0000000..748b9ab --- /dev/null +++ b/src/settings/storage.ts @@ -0,0 +1,15 @@ +import SettingData, { DefaultSettingData } from '../shared/SettingData'; + +export const load = async(): Promise => { + let { settings } = await browser.storage.local.get('settings'); + if (!settings) { + return DefaultSettingData; + } + return SettingData.valueOf(settings); +}; + +export const save = (data: SettingData) => { + return browser.storage.local.set({ + settings: data.toJSON(), + }); +}; diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts new file mode 100644 index 0000000..05e21fa --- /dev/null +++ b/src/shared/SettingData.ts @@ -0,0 +1,414 @@ +import * as operations from './operations'; +import Settings, * as settings from './Settings'; + +export class FormKeymaps { + private data: {[op: string]: string}; + + constructor(data: {[op: string]: string}) { + this.data = data; + } + + toKeymaps(): settings.Keymaps { + let keymaps: settings.Keymaps = {}; + for (let name of Object.keys(this.data)) { + let [type, argStr] = name.split('?'); + let args = {}; + if (argStr) { + args = JSON.parse(argStr); + } + let key = this.data[name]; + keymaps[key] = operations.valueOf({ type, ...args }); + } + return keymaps; + } + + toJSON(): {[op: string]: string} { + return this.data; + } + + buildWithOverride(op: string, keys: string): FormKeymaps { + let newData = { + ...this.data, + [op]: keys, + }; + return new FormKeymaps(newData); + } + + static valueOf(o: ReturnType): FormKeymaps { + let data: {[op: string]: string} = {}; + for (let op of Object.keys(o)) { + data[op] = o[op] as string; + } + return new FormKeymaps(data); + } + + static fromKeymaps(keymaps: settings.Keymaps): FormKeymaps { + let data: {[op: string]: string} = {}; + for (let key of Object.keys(keymaps)) { + let op = keymaps[key]; + let args = { ...op }; + delete args.type; + + let name = op.type; + if (Object.keys(args).length > 0) { + name += '?' + JSON.stringify(args); + } + data[name] = key; + } + return new FormKeymaps(data); + } +} + +export class FormSearch { + private default: string; + + private engines: string[][]; + + constructor(defaultEngine: string, engines: string[][]) { + this.default = defaultEngine; + this.engines = engines; + } + + toSearchSettings(): settings.Search { + return { + default: this.default, + engines: this.engines.reduce( + (o: {[key: string]: string}, [name, url]) => { + o[name] = url; + return o; + }, {}), + }; + } + + toJSON(): { + default: string; + engines: string[][]; + } { + return { + default: this.default, + engines: this.engines, + }; + } + + static valueOf(o: ReturnType): FormSearch { + if (!Object.prototype.hasOwnProperty.call(o, 'default')) { + throw new TypeError(`"default" field not set`); + } + if (!Object.prototype.hasOwnProperty.call(o, 'engines')) { + throw new TypeError(`"engines" field not set`); + } + return new FormSearch(o.default, o.engines); + } + + static fromSearch(search: settings.Search): FormSearch { + let engines = Object.entries(search.engines).reduce( + (o: string[][], [name, url]) => { + return o.concat([[name, url]]); + }, []); + return new FormSearch(search.default, engines); + } +} + +export class JSONSettings { + private json: string; + + constructor(json: any) { + this.json = json; + } + + toSettings(): Settings { + return settings.valueOf(JSON.parse(this.json)); + } + + toJSON(): string { + return this.json; + } + + static valueOf(o: ReturnType): JSONSettings { + return new JSONSettings(o); + } + + static fromSettings(data: Settings): JSONSettings { + return new JSONSettings(JSON.stringify(data, undefined, 2)); + } +} + +export class FormSettings { + private keymaps: FormKeymaps; + + private search: FormSearch; + + private properties: settings.Properties; + + private blacklist: string[]; + + constructor( + keymaps: FormKeymaps, + search: FormSearch, + properties: settings.Properties, + blacklist: string[], + ) { + this.keymaps = keymaps; + this.search = search; + this.properties = properties; + this.blacklist = blacklist; + } + + buildWithKeymaps(keymaps: FormKeymaps): FormSettings { + return new FormSettings( + keymaps, + this.search, + this.properties, + this.blacklist, + ); + } + + buildWithSearch(search: FormSearch): FormSettings { + return new FormSettings( + this.keymaps, + search, + this.properties, + this.blacklist, + ); + } + + buildWithProperties(props: settings.Properties): FormSettings { + return new FormSettings( + this.keymaps, + this.search, + props, + this.blacklist, + ); + } + + buildWithBlacklist(blacklist: string[]): FormSettings { + return new FormSettings( + this.keymaps, + this.search, + this.properties, + blacklist, + ); + } + + toSettings(): Settings { + return settings.valueOf({ + keymaps: this.keymaps.toKeymaps(), + search: this.search.toSearchSettings(), + properties: this.properties, + blacklist: this.blacklist, + }); + } + + toJSON(): { + keymaps: ReturnType; + search: ReturnType; + properties: settings.Properties; + blacklist: string[]; + } { + return { + keymaps: this.keymaps.toJSON(), + search: this.search.toJSON(), + properties: this.properties, + blacklist: this.blacklist, + }; + } + + static valueOf(o: ReturnType): FormSettings { + for (let name of ['keymaps', 'search', 'properties', 'blacklist']) { + if (!Object.prototype.hasOwnProperty.call(o, name)) { + throw new Error(`"${name}" field not set`); + } + } + return new FormSettings( + FormKeymaps.valueOf(o.keymaps), + FormSearch.valueOf(o.search), + settings.propertiesValueOf(o.properties), + settings.blacklistValueOf(o.blacklist), + ); + } + + static fromSettings(data: Settings): FormSettings { + return new FormSettings( + FormKeymaps.fromKeymaps(data.keymaps), + FormSearch.fromSearch(data.search), + data.properties, + data.blacklist); + } +} + +export enum SettingSource { + JSON = 'json', + Form = 'form', +} + +export default class SettingData { + private source: SettingSource; + + private json?: JSONSettings; + + private form?: FormSettings; + + constructor({ + source, json, form + }: { + source: SettingSource, + json?: JSONSettings, + form?: FormSettings, + }) { + this.source = source; + this.json = json; + this.form = form; + } + + getSource(): SettingSource { + return this.source; + } + + getJSON(): JSONSettings { + if (!this.json) { + throw new TypeError('json settings not set'); + } + return this.json; + } + + getForm(): FormSettings { + if (!this.form) { + throw new TypeError('form settings not set'); + } + return this.form; + } + + toJSON(): any { + switch (this.source) { + case SettingSource.JSON: + return { + source: this.source, + json: (this.json as JSONSettings).toJSON(), + }; + case SettingSource.Form: + return { + source: this.source, + form: (this.form as FormSettings).toJSON(), + }; + } + throw new Error(`unknown settings source: ${this.source}`); + } + + toSettings(): Settings { + switch (this.source) { + case SettingSource.JSON: + return this.getJSON().toSettings(); + case SettingSource.Form: + return this.getForm().toSettings(); + } + throw new Error(`unknown settings source: ${this.source}`); + } + + static valueOf(o: { + source: string; + json?: string; + form?: ReturnType; + }): SettingData { + switch (o.source) { + case SettingSource.JSON: + return new SettingData({ + source: o.source, + json: JSONSettings.valueOf( + o.json as ReturnType), + }); + case SettingSource.Form: + return new SettingData({ + source: o.source, + form: FormSettings.valueOf( + o.form as ReturnType), + }); + } + throw new Error(`unknown settings source: ${o.source}`); + } +} + +export const DefaultSettingData: SettingData = SettingData.valueOf({ + source: 'json', + json: `{ + "keymaps": { + "0": { "type": "scroll.home" }, + ":": { "type": "command.show" }, + "o": { "type": "command.show.open", "alter": false }, + "O": { "type": "command.show.open", "alter": true }, + "t": { "type": "command.show.tabopen", "alter": false }, + "T": { "type": "command.show.tabopen", "alter": true }, + "w": { "type": "command.show.winopen", "alter": false }, + "W": { "type": "command.show.winopen", "alter": true }, + "b": { "type": "command.show.buffer" }, + "a": { "type": "command.show.addbookmark", "alter": true }, + "k": { "type": "scroll.vertically", "count": -1 }, + "j": { "type": "scroll.vertically", "count": 1 }, + "h": { "type": "scroll.horizonally", "count": -1 }, + "l": { "type": "scroll.horizonally", "count": 1 }, + "": { "type": "scroll.pages", "count": -0.5 }, + "": { "type": "scroll.pages", "count": 0.5 }, + "": { "type": "scroll.pages", "count": -1 }, + "": { "type": "scroll.pages", "count": 1 }, + "gg": { "type": "scroll.top" }, + "G": { "type": "scroll.bottom" }, + "$": { "type": "scroll.end" }, + "d": { "type": "tabs.close" }, + "D": { "type": "tabs.close.right" }, + "!d": { "type": "tabs.close.force" }, + "u": { "type": "tabs.reopen" }, + "K": { "type": "tabs.prev" }, + "J": { "type": "tabs.next" }, + "gT": { "type": "tabs.prev" }, + "gt": { "type": "tabs.next" }, + "g0": { "type": "tabs.first" }, + "g$": { "type": "tabs.last" }, + "": { "type": "tabs.prevsel" }, + "r": { "type": "tabs.reload", "cache": false }, + "R": { "type": "tabs.reload", "cache": true }, + "zp": { "type": "tabs.pin.toggle" }, + "zd": { "type": "tabs.duplicate" }, + "zi": { "type": "zoom.in" }, + "zo": { "type": "zoom.out" }, + "zz": { "type": "zoom.neutral" }, + "f": { "type": "follow.start", "newTab": false }, + "F": { "type": "follow.start", "newTab": true, "background": false }, + "m": { "type": "mark.set.prefix" }, + "'": { "type": "mark.jump.prefix" }, + "H": { "type": "navigate.history.prev" }, + "L": { "type": "navigate.history.next" }, + "[[": { "type": "navigate.link.prev" }, + "]]": { "type": "navigate.link.next" }, + "gu": { "type": "navigate.parent" }, + "gU": { "type": "navigate.root" }, + "gi": { "type": "focus.input" }, + "gf": { "type": "page.source" }, + "gh": { "type": "page.home" }, + "gH": { "type": "page.home", "newTab": true }, + "y": { "type": "urls.yank" }, + "p": { "type": "urls.paste", "newTab": false }, + "P": { "type": "urls.paste", "newTab": true }, + "/": { "type": "find.start" }, + "n": { "type": "find.next" }, + "N": { "type": "find.prev" }, + "": { "type": "addon.toggle.enabled" } + }, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + "bing": "https://www.bing.com/search?q={}", + "duckduckgo": "https://duckduckgo.com/?q={}", + "twitter": "https://twitter.com/search?q={}", + "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [ + ] +}`, +}); diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts new file mode 100644 index 0000000..ce6b9ee --- /dev/null +++ b/src/shared/Settings.ts @@ -0,0 +1,200 @@ +import * as operations from './operations'; +import * as PropertyDefs from './property-defs'; + +export type Keymaps = {[key: string]: operations.Operation}; + +export interface Search { + default: string; + engines: { [key: string]: string }; +} + +export interface Properties { + hintchars: string; + smoothscroll: boolean; + complete: string; +} + +export default interface Settings { + keymaps: Keymaps; + search: Search; + properties: Properties; + blacklist: string[]; + // eslint-disable-next-line semi +} + +const DefaultProperties: Properties = PropertyDefs.defs.reduce( + (o: {[name: string]: PropertyDefs.Type}, def) => { + o[def.name] = def.defaultValue; + return o; + }, {}) as Properties; + + +export const keymapsValueOf = (o: any): Keymaps => { + return Object.keys(o).reduce((keymaps: Keymaps, key: string): Keymaps => { + let op = operations.valueOf(o[key]); + keymaps[key] = op; + return keymaps; + }, {}); +}; + +export const searchValueOf = (o: any): Search => { + if (typeof o.default !== 'string') { + throw new TypeError('string field "default" not set"'); + } + for (let name of Object.keys(o.engines)) { + if ((/\s/).test(name)) { + throw new TypeError( + `While space in the search engine not allowed: "${name}"`); + } + let url = o.engines[name]; + if (typeof url !== 'string') { + throw new TypeError('"engines" not an object of string'); + } + let matches = url.match(/{}/g); + if (matches === null) { + throw new TypeError(`No {}-placeholders in URL of "${name}"`); + } else if (matches.length > 1) { + throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`); + } + + } + if (!Object.prototype.hasOwnProperty.call(o.engines, o.default)) { + throw new TypeError(`Default engine "${o.default}" not found`); + } + return { + default: o.default as string, + engines: { ...o.engines }, + }; +}; + +export const propertiesValueOf = (o: any): Properties => { + let defNames = new Set(PropertyDefs.defs.map(def => def.name)); + let unknownName = Object.keys(o).find(name => !defNames.has(name)); + if (unknownName) { + throw new TypeError(`Unknown property name: "${unknownName}"`); + } + + for (let def of PropertyDefs.defs) { + if (!Object.prototype.hasOwnProperty.call(o, def.name)) { + continue; + } + if (typeof o[def.name] !== def.type) { + throw new TypeError(`property "${def.name}" is not ${def.type}`); + } + } + return { + ...DefaultProperties, + ...o, + }; +}; + +export const blacklistValueOf = (o: any): string[] => { + if (!Array.isArray(o)) { + throw new TypeError(`"blacklist" is not an array of string`); + } + for (let x of o) { + if (typeof x !== 'string') { + throw new TypeError(`"blacklist" is not an array of string`); + } + } + return o as string[]; +}; + +export const valueOf = (o: any): Settings => { + let settings = { ...DefaultSetting }; + if (Object.prototype.hasOwnProperty.call(o, 'keymaps')) { + settings.keymaps = keymapsValueOf(o.keymaps); + } + if (Object.prototype.hasOwnProperty.call(o, 'search')) { + settings.search = searchValueOf(o.search); + } + if (Object.prototype.hasOwnProperty.call(o, 'properties')) { + settings.properties = propertiesValueOf(o.properties); + } + if (Object.prototype.hasOwnProperty.call(o, 'blacklist')) { + settings.blacklist = blacklistValueOf(o.blacklist); + } + return settings; +}; + +const DefaultSetting: Settings = { + keymaps: { + '0': { 'type': 'scroll.home' }, + ':': { 'type': 'command.show' }, + 'o': { 'type': 'command.show.open', 'alter': false }, + 'O': { 'type': 'command.show.open', 'alter': true }, + 't': { 'type': 'command.show.tabopen', 'alter': false }, + 'T': { 'type': 'command.show.tabopen', 'alter': true }, + 'w': { 'type': 'command.show.winopen', 'alter': false }, + 'W': { 'type': 'command.show.winopen', 'alter': true }, + 'b': { 'type': 'command.show.buffer' }, + 'a': { 'type': 'command.show.addbookmark', 'alter': true }, + 'k': { 'type': 'scroll.vertically', 'count': -1 }, + 'j': { 'type': 'scroll.vertically', 'count': 1 }, + 'h': { 'type': 'scroll.horizonally', 'count': -1 }, + 'l': { 'type': 'scroll.horizonally', 'count': 1 }, + '': { 'type': 'scroll.pages', 'count': -0.5 }, + '': { 'type': 'scroll.pages', 'count': 0.5 }, + '': { 'type': 'scroll.pages', 'count': -1 }, + '': { 'type': 'scroll.pages', 'count': 1 }, + 'gg': { 'type': 'scroll.top' }, + 'G': { 'type': 'scroll.bottom' }, + '$': { 'type': 'scroll.end' }, + 'd': { 'type': 'tabs.close' }, + 'D': { 'type': 'tabs.close.right' }, + '!d': { 'type': 'tabs.close.force' }, + 'u': { 'type': 'tabs.reopen' }, + 'K': { 'type': 'tabs.prev' }, + 'J': { 'type': 'tabs.next' }, + 'gT': { 'type': 'tabs.prev' }, + 'gt': { 'type': 'tabs.next' }, + 'g0': { 'type': 'tabs.first' }, + 'g$': { 'type': 'tabs.last' }, + '': { 'type': 'tabs.prevsel' }, + 'r': { 'type': 'tabs.reload', 'cache': false }, + 'R': { 'type': 'tabs.reload', 'cache': true }, + 'zp': { 'type': 'tabs.pin.toggle' }, + 'zd': { 'type': 'tabs.duplicate' }, + 'zi': { 'type': 'zoom.in' }, + 'zo': { 'type': 'zoom.out' }, + 'zz': { 'type': 'zoom.neutral' }, + 'f': { 'type': 'follow.start', 'newTab': false, 'background': false }, + 'F': { 'type': 'follow.start', 'newTab': true, 'background': false }, + 'm': { 'type': 'mark.set.prefix' }, + '\'': { 'type': 'mark.jump.prefix' }, + 'H': { 'type': 'navigate.history.prev' }, + 'L': { 'type': 'navigate.history.next' }, + '[[': { 'type': 'navigate.link.prev' }, + ']]': { 'type': 'navigate.link.next' }, + 'gu': { 'type': 'navigate.parent' }, + 'gU': { 'type': 'navigate.root' }, + 'gi': { 'type': 'focus.input' }, + 'gf': { 'type': 'page.source' }, + 'gh': { 'type': 'page.home', 'newTab': false }, + 'gH': { 'type': 'page.home', 'newTab': true }, + 'y': { 'type': 'urls.yank' }, + 'p': { 'type': 'urls.paste', 'newTab': false }, + 'P': { 'type': 'urls.paste', 'newTab': true }, + '/': { 'type': 'find.start' }, + 'n': { 'type': 'find.next' }, + 'N': { 'type': 'find.prev' }, + '': { 'type': 'addon.toggle.enabled' } + }, + search: { + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + 'bing': 'https://www.bing.com/search?q={}', + 'duckduckgo': 'https://duckduckgo.com/?q={}', + 'twitter': 'https://twitter.com/search?q={}', + 'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}' + } + }, + properties: { + hintchars: 'abcdefghijklmnopqrstuvwxyz', + smoothscroll: false, + complete: 'sbh' + }, + blacklist: [] +}; diff --git a/src/shared/operations.ts b/src/shared/operations.ts index cc22f75..688c240 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -443,5 +443,5 @@ export const valueOf = (o: any): Operation => { case MARK_JUMP_PREFIX: return { type: o.type }; } - throw new Error('unknown operation type: ' + o.type); + throw new TypeError('unknown operation type: ' + o.type); }; diff --git a/src/shared/properties.ts b/src/shared/properties.ts new file mode 100644 index 0000000..6315030 --- /dev/null +++ b/src/shared/properties.ts @@ -0,0 +1,50 @@ +export type Type = string | number | boolean; + +export class Def { + private name0: string; + + private description0: string; + + private defaultValue0: Type; + + constructor( + name: string, + description: string, + defaultValue: Type, + ) { + this.name0 = name; + this.description0 = description; + this.defaultValue0 = defaultValue; + } + + public get name(): string { + return this.name0; + } + + public get defaultValue(): Type { + return this.defaultValue0; + } + + public get description(): Type { + return this.description0; + } + + public get type(): string { + return typeof this.defaultValue; + } +} + +export const defs: Def[] = [ + new Def( + 'hintchars', + 'hint characters on follow mode', + 'abcdefghijklmnopqrstuvwxyz'), + new Def( + 'smoothscroll', + 'smooth scroll', + false), + new Def( + 'complete', + 'which are completed at the open page', + 'sbh'), +]; diff --git a/src/shared/property-defs.ts b/src/shared/property-defs.ts new file mode 100644 index 0000000..6315030 --- /dev/null +++ b/src/shared/property-defs.ts @@ -0,0 +1,50 @@ +export type Type = string | number | boolean; + +export class Def { + private name0: string; + + private description0: string; + + private defaultValue0: Type; + + constructor( + name: string, + description: string, + defaultValue: Type, + ) { + this.name0 = name; + this.description0 = description; + this.defaultValue0 = defaultValue; + } + + public get name(): string { + return this.name0; + } + + public get defaultValue(): Type { + return this.defaultValue0; + } + + public get description(): Type { + return this.description0; + } + + public get type(): string { + return typeof this.defaultValue; + } +} + +export const defs: Def[] = [ + new Def( + 'hintchars', + 'hint characters on follow mode', + 'abcdefghijklmnopqrstuvwxyz'), + new Def( + 'smoothscroll', + 'smooth scroll', + false), + new Def( + 'complete', + 'which are completed at the open page', + 'sbh'), +]; diff --git a/src/shared/settings/default.ts b/src/shared/settings/default.ts deleted file mode 100644 index 6523a74..0000000 --- a/src/shared/settings/default.ts +++ /dev/null @@ -1,85 +0,0 @@ -export default { - source: 'json', - json: `{ - "keymaps": { - "0": { "type": "scroll.home" }, - ":": { "type": "command.show" }, - "o": { "type": "command.show.open", "alter": false }, - "O": { "type": "command.show.open", "alter": true }, - "t": { "type": "command.show.tabopen", "alter": false }, - "T": { "type": "command.show.tabopen", "alter": true }, - "w": { "type": "command.show.winopen", "alter": false }, - "W": { "type": "command.show.winopen", "alter": true }, - "b": { "type": "command.show.buffer" }, - "a": { "type": "command.show.addbookmark", "alter": true }, - "k": { "type": "scroll.vertically", "count": -1 }, - "j": { "type": "scroll.vertically", "count": 1 }, - "h": { "type": "scroll.horizonally", "count": -1 }, - "l": { "type": "scroll.horizonally", "count": 1 }, - "": { "type": "scroll.pages", "count": -0.5 }, - "": { "type": "scroll.pages", "count": 0.5 }, - "": { "type": "scroll.pages", "count": -1 }, - "": { "type": "scroll.pages", "count": 1 }, - "gg": { "type": "scroll.top" }, - "G": { "type": "scroll.bottom" }, - "$": { "type": "scroll.end" }, - "d": { "type": "tabs.close" }, - "D": { "type": "tabs.close.right" }, - "!d": { "type": "tabs.close.force" }, - "u": { "type": "tabs.reopen" }, - "K": { "type": "tabs.prev", "count": 1 }, - "J": { "type": "tabs.next", "count": 1 }, - "gT": { "type": "tabs.prev", "count": 1 }, - "gt": { "type": "tabs.next", "count": 1 }, - "g0": { "type": "tabs.first" }, - "g$": { "type": "tabs.last" }, - "": { "type": "tabs.prevsel" }, - "r": { "type": "tabs.reload", "cache": false }, - "R": { "type": "tabs.reload", "cache": true }, - "zp": { "type": "tabs.pin.toggle" }, - "zd": { "type": "tabs.duplicate" }, - "zi": { "type": "zoom.in" }, - "zo": { "type": "zoom.out" }, - "zz": { "type": "zoom.neutral" }, - "f": { "type": "follow.start", "newTab": false }, - "F": { "type": "follow.start", "newTab": true, "background": false }, - "m": { "type": "mark.set.prefix" }, - "'": { "type": "mark.jump.prefix" }, - "H": { "type": "navigate.history.prev" }, - "L": { "type": "navigate.history.next" }, - "[[": { "type": "navigate.link.prev" }, - "]]": { "type": "navigate.link.next" }, - "gu": { "type": "navigate.parent" }, - "gU": { "type": "navigate.root" }, - "gi": { "type": "focus.input" }, - "gf": { "type": "page.source" }, - "gh": { "type": "page.home" }, - "gH": { "type": "page.home", "newTab": true }, - "y": { "type": "urls.yank" }, - "p": { "type": "urls.paste", "newTab": false }, - "P": { "type": "urls.paste", "newTab": true }, - "/": { "type": "find.start" }, - "n": { "type": "find.next" }, - "N": { "type": "find.prev" }, - "": { "type": "addon.toggle.enabled" } - }, - "search": { - "default": "google", - "engines": { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - "bing": "https://www.bing.com/search?q={}", - "duckduckgo": "https://duckduckgo.com/?q={}", - "twitter": "https://twitter.com/search?q={}", - "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" - } - }, - "properties": { - "hintchars": "abcdefghijklmnopqrstuvwxyz", - "smoothscroll": false, - "complete": "sbh" - }, - "blacklist": [ - ] -}`, -}; diff --git a/src/shared/settings/properties.ts b/src/shared/settings/properties.ts deleted file mode 100644 index 7d037df..0000000 --- a/src/shared/settings/properties.ts +++ /dev/null @@ -1,24 +0,0 @@ -// describe types of a propety as: -// mystr: 'string', -// mynum: 'number', -// mybool: 'boolean', -const types: { [key: string]: string } = { - hintchars: 'string', - smoothscroll: 'boolean', - complete: 'string', -}; - -// describe default values of a property -const defaults: { [key: string]: string | number | boolean } = { - hintchars: 'abcdefghijklmnopqrstuvwxyz', - smoothscroll: false, - complete: 'sbh', -}; - -const docs: { [key: string]: string } = { - hintchars: 'hint characters on follow mode', - smoothscroll: 'smooth scroll', - complete: 'which are completed at the open page', -}; - -export { types, defaults, docs }; diff --git a/src/shared/settings/storage.ts b/src/shared/settings/storage.ts deleted file mode 100644 index 90a3a66..0000000 --- a/src/shared/settings/storage.ts +++ /dev/null @@ -1,32 +0,0 @@ -import DefaultSettings from './default'; -import * as settingsValues from './values'; - -const loadRaw = async(): Promise => { - let { settings } = await browser.storage.local.get('settings'); - if (!settings) { - return DefaultSettings; - } - return { ...DefaultSettings, ...settings as object }; -}; - -const loadValue = async() => { - let settings = await loadRaw(); - let value = JSON.parse(DefaultSettings.json); - if (settings.source === 'json') { - value = settingsValues.valueFromJson(settings.json); - } else if (settings.source === 'form') { - value = settingsValues.valueFromForm(settings.form); - } - if (!value.properties) { - value.properties = {}; - } - return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; -}; - -const save = (settings: any): Promise => { - return browser.storage.local.set({ - settings, - }); -}; - -export { loadRaw, loadValue, save }; diff --git a/src/shared/settings/validator.ts b/src/shared/settings/validator.ts deleted file mode 100644 index 71cc466..0000000 --- a/src/shared/settings/validator.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as operations from '../operations'; -import * as properties from './properties'; - -const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; -const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { - return operations[key]; -}); - -const validateInvalidTopKeys = (settings: any): void => { - let invalidKey = Object.keys(settings).find((key) => { - return !VALID_TOP_KEYS.includes(key); - }); - if (invalidKey) { - throw Error(`Unknown key: "${invalidKey}"`); - } -}; - -const validateKeymaps = (keymaps: any): void => { - 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 validateSearch = (search: any): void => { - let engines = search.engines; - for (let key of Object.keys(engines)) { - if ((/\s/).test(key)) { - throw new Error( - `While space in search engine name is not allowed: "${key}"` - ); - } - let url = engines[key]; - if (!url.match(/{}/)) { - throw new Error(`No {}-placeholders in URL of "${key}"`); - } - if (url.match(/{}/g).length > 1) { - throw new Error(`Multiple {}-placeholders in URL of "${key}"`); - } - } - - if (!search.default) { - throw new Error(`Default engine is not set`); - } - if (!Object.keys(engines).includes(search.default)) { - throw new Error(`Default engine "${search.default}" not found`); - } -}; - -const validateProperties = (props: any): void => { - for (let name of Object.keys(props)) { - if (!properties.types[name]) { - throw new Error(`Unknown property name: "${name}"`); - } - if (typeof props[name] !== properties.types[name]) { - throw new Error(`Invalid type for property: "${name}"`); - } - } -}; - -const validate = (settings: any): void => { - validateInvalidTopKeys(settings); - if (settings.keymaps) { - validateKeymaps(settings.keymaps); - } - if (settings.search) { - validateSearch(settings.search); - } - if (settings.properties) { - validateProperties(settings.properties); - } -}; - -export { validate }; diff --git a/src/shared/settings/values.ts b/src/shared/settings/values.ts deleted file mode 100644 index cb6a668..0000000 --- a/src/shared/settings/values.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as properties from './properties'; - -const operationFromFormName = (name: string): any => { - let [type, argStr] = name.split('?'); - let args = {}; - if (argStr) { - args = JSON.parse(argStr); - } - return { type, ...args }; -}; - -const operationToFormName = (op: any): string => { - let type = op.type; - let args = { ...op }; - delete args.type; - - if (Object.keys(args).length === 0) { - return type; - } - return op.type + '?' + JSON.stringify(args); -}; - -const valueFromJson = (json: string): object => { - return JSON.parse(json); -}; - -const valueFromForm = (form: any): object => { - let keymaps: any = undefined; - if (form.keymaps) { - keymaps = {}; - for (let name of Object.keys(form.keymaps)) { - let keys = form.keymaps[name]; - keymaps[keys] = operationFromFormName(name); - } - } - - let search: any = undefined; - if (form.search) { - search = { default: form.search.default }; - - if (form.search.engines) { - search.engines = {}; - for (let [name, url] of form.search.engines) { - search.engines[name] = url; - } - } - } - - return { - keymaps, - search, - blacklist: form.blacklist, - properties: form.properties - }; -}; - -const jsonFromValue = (value: any): string => { - return JSON.stringify(value, undefined, 2); -}; - -const formFromValue = (value: any, allowedOps: any[]): any => { - let keymaps: any = undefined; - - if (value.keymaps) { - let allowedSet = new Set(allowedOps); - - keymaps = {}; - for (let keys of Object.keys(value.keymaps)) { - let op = operationToFormName(value.keymaps[keys]); - if (allowedSet.has(op)) { - keymaps[op] = keys; - } - } - } - - let search: any = undefined; - if (value.search) { - search = { default: value.search.default }; - if (value.search.engines) { - search.engines = Object.keys(value.search.engines).map((name) => { - return [name, value.search.engines[name]]; - }); - } - } - - let formProperties = { ...properties.defaults, ...value.properties }; - - return { - keymaps, - search, - blacklist: value.blacklist, - properties: formProperties, - }; -}; - -const jsonFromForm = (form: any): string => { - return jsonFromValue(valueFromForm(form)); -}; - -const formFromJson = (json: string, allowedOps: any[]): any => { - let value = valueFromJson(json); - return formFromValue(value, allowedOps); -}; - -export { - valueFromJson, valueFromForm, jsonFromValue, formFromValue, - jsonFromForm, formFromJson -}; diff --git a/test/background/usecases/parsers.test.ts b/test/background/usecases/parsers.test.ts index 17b034b..f3a64eb 100644 --- a/test/background/usecases/parsers.test.ts +++ b/test/background/usecases/parsers.test.ts @@ -3,45 +3,32 @@ import * as parsers from 'background/usecases/parsers'; describe("shared/commands/parsers", () => { describe("#parsers.parseSetOption", () => { it('parse set string', () => { - let [key, value] = parsers.parseSetOption('encoding=utf-8', { encoding: 'string' }); - expect(key).to.equal('encoding'); - expect(value).to.equal('utf-8'); + let [key, value] = parsers.parseSetOption('hintchars=abcdefgh'); + expect(key).to.equal('hintchars'); + expect(value).to.equal('abcdefgh'); }); it('parse set empty string', () => { - let [key, value] = parsers.parseSetOption('encoding=', { encoding: 'string' }); - expect(key).to.equal('encoding'); + let [key, value] = parsers.parseSetOption('hintchars='); + expect(key).to.equal('hintchars'); expect(value).to.equal(''); }); - it('parse set string', () => { - let [key, value] = parsers.parseSetOption('history=50', { history: 'number' }); - expect(key).to.equal('history'); - expect(value).to.equal(50); - }); - it('parse set boolean', () => { - let [key, value] = parsers.parseSetOption('paste', { paste: 'boolean' }); - expect(key).to.equal('paste'); + let [key, value] = parsers.parseSetOption('smoothscroll'); + expect(key).to.equal('smoothscroll'); expect(value).to.be.true; - [key, value] = parsers.parseSetOption('nopaste', { paste: 'boolean' }); - expect(key).to.equal('paste'); + [key, value] = parsers.parseSetOption('nosmoothscroll'); + expect(key).to.equal('smoothscroll'); expect(value).to.be.false; }); it('throws error on unknown property', () => { - expect(() => parsers.parseSetOption('charset=utf-8', {})).to.throw(Error, 'Unknown'); - expect(() => parsers.parseSetOption('smoothscroll', {})).to.throw(Error, 'Unknown'); - expect(() => parsers.parseSetOption('nosmoothscroll', {})).to.throw(Error, 'Unknown'); - }) - - it('throws error on invalid property', () => { - expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); - expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('charset=', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); - }) + expect(() => parsers.parseSetOption('encoding=utf-8')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('paste')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('nopaste')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('smoothscroll=yes')).to.throw(Error, 'Invalid argument'); + }); }); }); diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts index 0721d5d..c831433 100644 --- a/test/content/actions/setting.test.ts +++ b/test/content/actions/setting.test.ts @@ -4,32 +4,40 @@ import * as settingActions from 'content/actions/setting'; describe("setting actions", () => { describe("set", () => { it('create SETTING_SET action', () => { - let action = settingActions.set({ red: 'apple', yellow: 'banana' }); + let action = settingActions.set({ + keymaps: { + 'dd': 'remove current tab', + 'z': 'increment', + }, + search: { + default: "google", + engines: { + google: 'https://google.com/search?q={}', + } + }, + properties: { + hintchars: 'abcd1234', + }, + blacklist: [], + }); expect(action.type).to.equal(actions.SETTING_SET); - expect(action.value.red).to.equal('apple'); - expect(action.value.yellow).to.equal('banana'); - expect(action.value.keymaps).to.be.empty; + expect(action.settings.properties.hintchars).to.equal('abcd1234'); }); - it('converts keymaps', () => { + it('overrides cancel keys', () => { let action = settingActions.set({ keymaps: { - 'dd': 'remove current tab', - 'z': 'increment', + "k": { "type": "scroll.vertically", "count": -1 }, + "j": { "type": "scroll.vertically", "count": 1 }, } }); - let keymaps = action.value.keymaps; - let map = new Map(keymaps); - expect(map).to.have.deep.all.keys( - [ - [{ key: 'Esc', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], - [{ key: '[', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], - [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, - { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], - [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, - { key: 'a', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], - ] - ); + let keymaps = action.settings.keymaps; + expect(action.settings.keymaps).to.deep.equals({ + "k": { type: "scroll.vertically", count: -1 }, + "j": { type: "scroll.vertically", count: 1 }, + '': { type: 'cancel' }, + '': { type: 'cancel' }, + }); }); }); }); diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts index 226fc58..fe23006 100644 --- a/test/content/reducers/setting.test.ts +++ b/test/content/reducers/setting.test.ts @@ -9,9 +9,24 @@ describe("content setting reducer", () => { it('return next state for SETTING_SET', () => { let newSettings = { red: 'apple', yellow: 'banana' }; - let action = { type: actions.SETTING_SET, value: newSettings }; + let action = { + type: actions.SETTING_SET, + settings: { + keymaps: { + "zz": { type: "zoom.neutral" }, + "": { "type": "addon.toggle.enabled" } + }, + "blacklist": [] + } + } let state = settingReducer(undefined, action); - expect(state).to.deep.equal(newSettings); - expect(state).not.to.equal(newSettings); // assert deep copy + console.log(JSON.stringify(state.keymaps)); + expect(state.keymaps).to.have.deep.all.members([ + { key: [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + { key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], + op: { type: 'zoom.neutral' }}, + { key: [{ key: 'Esc', shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }], + op: { type: 'addon.toggle.enabled' }}, + ]); }); }); diff --git a/test/settings/components/form/KeymapsForm.test.tsx b/test/settings/components/form/KeymapsForm.test.tsx index 6ac57c9..dc2322b 100644 --- a/test/settings/components/form/KeymapsForm.test.tsx +++ b/test/settings/components/form/KeymapsForm.test.tsx @@ -2,15 +2,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestRenderer from 'react-test-renderer'; import ReactTestUtils from 'react-dom/test-utils'; -import KeymapsForm from 'settings/components/form/KeymapsForm' +import KeymapsForm from '../../../../src/settings/components/form/KeymapsForm' +import { FormKeymaps } from 'shared/SettingData'; +import { expect } from 'chai'; describe("settings/form/KeymapsForm", () => { describe('render', () => { it('renders keymap fields', () => { - let root = ReactTestRenderer.create().root + })} />).root let inputj = root.findByProps({ id: 'scroll.vertically?{"count":1}' }); let inputk = root.findByProps({ id: 'scroll.vertically?{"count":-1}' }); @@ -46,12 +48,12 @@ describe("settings/form/KeymapsForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render( { - expect(value['scroll.vertically?{"count":1}']).to.equal('jjj'); + expect(value.toJSON()['scroll.vertically?{"count":1}']).to.equal('jjj'); done(); }} />, container); }); diff --git a/test/settings/components/form/SearchEngineForm.test.tsx b/test/settings/components/form/SearchEngineForm.test.tsx index 06822f2..0e6b17d 100644 --- a/test/settings/components/form/SearchEngineForm.test.tsx +++ b/test/settings/components/form/SearchEngineForm.test.tsx @@ -3,14 +3,15 @@ import ReactDOM from 'react-dom'; import ReactTestRenderer from 'react-test-renderer'; import ReactTestUtils from 'react-dom/test-utils'; import SearchForm from 'settings/components/form/SearchForm' +import { FormSearch } from 'shared/SettingData'; describe("settings/form/SearchForm", () => { describe('render', () => { it('renders SearchForm', () => { - let root = ReactTestRenderer.create().root; + })} />).root; let names = root.findAllByProps({ name: 'name' }); expect(names).to.have.lengthOf(2); @@ -22,28 +23,6 @@ describe("settings/form/SearchForm", () => { expect(urls[0].props.value).to.equal('google.com'); expect(urls[1].props.value).to.equal('yahoo.com'); }); - - it('renders blank value', () => { - let root = ReactTestRenderer.create().root; - - let names = root.findAllByProps({ name: 'name' }); - expect(names).to.be.empty; - - let urls = root.findAllByProps({ name: 'url' }); - expect(urls).to.be.empty; - }); - - it('renders blank engines', () => { - let root = ReactTestRenderer.create( - , - ).root; - - let names = root.findAllByProps({ name: 'name' }); - expect(names).to.be.empty; - - let urls = root.findAllByProps({ name: 'url' }); - expect(urls).to.be.empty; - }); }); describe('onChange event', () => { @@ -62,14 +41,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render( { - expect(value.default).to.equal('louvre'); - expect(value.engines).to.have.lengthOf(2) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('louvre'); + expect(json.engines).to.have.lengthOf(2) + expect(json.engines).to.have.deep.members( [['louvre', 'google.com'], ['yahoo', 'yahoo.com']] ); done(); @@ -87,14 +67,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on delete', (done) => { ReactTestUtils.act(() => { - ReactDOM.render( { - expect(value.default).to.equal('yahoo'); - expect(value.engines).to.have.lengthOf(1) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('yahoo'); + expect(json.engines).to.have.lengthOf(1) + expect(json.engines).to.have.deep.members( [['yahoo', 'yahoo.com']] ); done(); @@ -107,14 +88,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on add', (done) => { ReactTestUtils.act(() => { - ReactDOM.render( { - expect(value.default).to.equal('yahoo'); - expect(value.engines).to.have.lengthOf(2) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('yahoo'); + expect(json.engines).to.have.lengthOf(2) + expect(json.engines).to.have.deep.members( [['google', 'google.com'], ['', '']], ); done(); diff --git a/test/settings/reducers/setting.test.ts b/test/settings/reducers/setting.test.ts index 6a874e8..376d66e 100644 --- a/test/settings/reducers/setting.test.ts +++ b/test/settings/reducers/setting.test.ts @@ -4,8 +4,7 @@ import settingReducer from 'settings/reducers/setting'; describe("settings setting reducer", () => { it('return the initial state', () => { let state = settingReducer(undefined, {}); - expect(state).to.have.deep.property('json', ''); - expect(state).to.have.deep.property('form', null); + expect(state).to.have.deep.property('source', 'json'); expect(state).to.have.deep.property('error', ''); }); diff --git a/test/shared/SettingData.test.ts b/test/shared/SettingData.test.ts new file mode 100644 index 0000000..8736ecb --- /dev/null +++ b/test/shared/SettingData.test.ts @@ -0,0 +1,293 @@ +import SettingData, { + FormKeymaps, JSONSettings, FormSettings, +} from '../../src/shared/SettingData'; +import Settings, { Keymaps } from '../../src/shared/Settings'; +import { expect } from 'chai'; + +describe('shared/SettingData', () => { + describe('FormKeymaps', () => { + describe('#valueOF to #toKeymaps', () => { + it('parses form keymaps and convert to operations', () => { + let data = { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + } + + let keymaps = FormKeymaps.valueOf(data).toKeymaps(); + expect(keymaps).to.deep.equal({ + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }); + }); + }); + + describe('#fromKeymaps to #toJSON', () => { + it('create from a Keymaps and create a JSON object', () => { + let data: Keymaps = { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + } + + let keymaps = FormKeymaps.fromKeymaps(data).toJSON(); + expect(keymaps).to.deep.equal({ + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }); + }); + }); + }); + + describe('JSONSettings', () => { + describe('#valueOf to #toSettings', () => { + it('parse object and create a Settings', () => { + let o = `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`; + + let settings = JSONSettings.valueOf(o).toSettings(); + expect(settings).to.deep.equal(JSON.parse(o)); + }); + }); + + describe('#fromSettings to #toJSON', () => { + it('create from a Settings and create a JSON string', () => { + let o = { + keymaps: {}, + search: { + default: "google", + engines: { + google: "https://google.com/search?q={}", + }, + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }; + + let json = JSONSettings.fromSettings(o).toJSON(); + expect(JSON.parse(json)).to.deep.equal(o); + }); + }); + }); + + describe('FormSettings', () => { + describe('#valueOf to #toSettings', () => { + it('parse object and create a Settings', () => { + let data = { + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }, + search: { + default: "google", + engines: [ + ["google", "https://google.com/search?q={}"], + ] + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }; + + let settings = FormSettings.valueOf(data).toSettings(); + expect(settings).to.deep.equal({ + keymaps: { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }, + search: { + default: "google", + engines: { + "google": "https://google.com/search?q={}" + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }); + }); + }); + + describe('#fromSettings to #toJSON', () => { + it('create from a Settings and create a JSON string', () => { + let data: Settings = { + keymaps: { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }, + search: { + default: "google", + engines: { + "google": "https://google.com/search?q={}" + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }; + + let json = FormSettings.fromSettings(data).toJSON(); + expect(json).to.deep.equal({ + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }, + search: { + default: "google", + engines: [ + ["google", "https://google.com/search?q={}"], + ] + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }); + }); + }); + }); + + describe('SettingData', () => { + describe('#valueOf to #toJSON', () => { + it('parse object from json source', () => { + let data = { + source: 'json', + json: `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`, + }; + + let j = SettingData.valueOf(data).toJSON(); + expect(j.source).to.equal('json'); + expect(j.json).to.be.a('string'); + }); + + it('parse object from form source', () => { + let data = { + source: 'form', + form: { + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }, + }; + + let j = SettingData.valueOf(data).toJSON(); + expect(j.source).to.equal('form'); + expect(j.form).to.deep.equal({ + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }); + }); + }); + + describe('#toSettings', () => { + it('parse object from json source', () => { + let data = { + source: 'json', + json: `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`, + }; + + let settings = SettingData.valueOf(data).toSettings(); + expect(settings.search.default).to.equal('google'); + }); + + it('parse object from form source', () => { + let data = { + source: 'form', + form: { + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }, + }; + + let settings = SettingData.valueOf(data).toSettings(); + expect(settings.search.default).to.equal('yahoo'); + }); + }); + }); +}); diff --git a/test/shared/Settings.test.ts b/test/shared/Settings.test.ts new file mode 100644 index 0000000..02cd022 --- /dev/null +++ b/test/shared/Settings.test.ts @@ -0,0 +1,190 @@ +import * as settings from '../../src/shared/Settings'; +import { expect } from 'chai'; + +describe('Settings', () => { + describe('#keymapsValueOf', () => { + it('returns empty object by empty settings', () => { + let keymaps = settings.keymapsValueOf({}); + expect(keymaps).to.be.empty; + }); + + it('returns keymaps by valid settings', () => { + let keymaps = settings.keymapsValueOf({ + k: { type: "scroll.vertically", count: -1 }, + j: { type: "scroll.vertically", count: 1 }, + }); + + expect(keymaps['k']).to.deep.equal({ type: "scroll.vertically", count: -1 }); + expect(keymaps['j']).to.deep.equal({ type: "scroll.vertically", count: 1 }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + k: { type: "invalid.operation" }, + })).to.throw(TypeError); + }); + }); + + describe('#searchValueOf', () => { + it('returns search settings by valid settings', () => { + let search = settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + }); + + expect(search).to.deep.equal({ + default: "google", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.searchValueOf(null)).to.throw(TypeError); + expect(() => settings.searchValueOf({})).to.throw(TypeError); + expect(() => settings.searchValueOf([])).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: 123, + engines: {} + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": 123456, + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "wikipedia", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "g o o g l e", + engines: { + "g o o g l e": "https://google.com/search?q={}", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search?q={}&r={}", + } + })).to.throw(TypeError); + }); + }); + + describe('#propertiesValueOf', () => { + it('returns with default properties by empty settings', () => { + let props = settings.propertiesValueOf({}); + expect(props).to.deep.equal({ + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }) + }); + + it('returns properties by valid settings', () => { + let props = settings.propertiesValueOf({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + + expect(props).to.deep.equal({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + smoothscroll: 'false', + })).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + unknown: 'xyz' + })).to.throw(TypeError); + }); + }); + + describe('#blacklistValueOf', () => { + it('returns empty array by empty settings', () => { + let blacklist = settings.blacklistValueOf([]); + expect(blacklist).to.be.empty; + }); + + it('returns blacklist by valid settings', () => { + let blacklist = settings.blacklistValueOf([ + "github.com", + "circleci.com", + ]); + + expect(blacklist).to.deep.equal([ + "github.com", + "circleci.com", + ]); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.blacklistValueOf(null)).to.throw(TypeError); + expect(() => settings.blacklistValueOf({})).to.throw(TypeError); + expect(() => settings.blacklistValueOf([1,2,3])).to.throw(TypeError); + }); + }); + + describe('#valueOf', () => { + it('returns settings by valid settings', () => { + let x = settings.valueOf({ + keymaps: {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + } + }, + "properties": {}, + "blacklist": [] + }); + + expect(x).to.deep.equal({ + keymaps: {}, + search: { + default: "google", + engines: { + google: "https://google.com/search?q={}", + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }); + }); + + it('sets default settings', () => { + let value = settings.valueOf({}); + expect(value.keymaps).to.not.be.empty; + expect(value.properties).to.not.be.empty; + expect(value.search.default).to.be.a('string'); + expect(value.search.engines).to.be.an('object'); + expect(value.blacklist).to.be.empty; + }); + }); +}); diff --git a/test/shared/properties.test.js b/test/shared/properties.test.js new file mode 100644 index 0000000..37903d8 --- /dev/null +++ b/test/shared/properties.test.js @@ -0,0 +1,18 @@ +import * as settings from 'shared/settings'; + +describe('properties', () => { + describe('Def class', () => { + it('returns property definitions', () => { + let def = new proerties.Def( + 'smoothscroll', + 'smooth scroll', + false); + + expect(def.name).to.equal('smoothscroll'); + expect(def.describe).to.equal('smooth scroll'); + expect(def.defaultValue).to.equal(false); + expect(def.type).to.equal('boolean'); + }); + }); +}); + diff --git a/test/shared/property-defs.test.js b/test/shared/property-defs.test.js new file mode 100644 index 0000000..37903d8 --- /dev/null +++ b/test/shared/property-defs.test.js @@ -0,0 +1,18 @@ +import * as settings from 'shared/settings'; + +describe('properties', () => { + describe('Def class', () => { + it('returns property definitions', () => { + let def = new proerties.Def( + 'smoothscroll', + 'smooth scroll', + false); + + expect(def.name).to.equal('smoothscroll'); + expect(def.describe).to.equal('smooth scroll'); + expect(def.defaultValue).to.equal(false); + expect(def.type).to.equal('boolean'); + }); + }); +}); + diff --git a/test/shared/settings/validator.test.ts b/test/shared/settings/validator.test.ts deleted file mode 100644 index 9bbfa3e..0000000 --- a/test/shared/settings/validator.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { validate } from 'shared/settings/validator'; - -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'); - }); - }); - - describe("search settings", () => { - it('throws an error for invalid search engine name', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'cherry pie': 'https://cherypie.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'cherry pie'); - }); - - it('throws an error for no {}-placeholder', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'yahoo'); - }); - - it('throws an error for no default engines', () => { - let settings = { - search: { - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'Default engine'); - }); - - it('throws an error for invalid default engine', () => { - let settings = { - search: { - default: 'twitter', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'twitter'); - }); - }); -}); diff --git a/test/shared/settings/values.test.ts b/test/shared/settings/values.test.ts deleted file mode 100644 index c72824d..0000000 --- a/test/shared/settings/values.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import * as values from 'shared/settings/values'; - -describe("settings values", () => { - describe('valueFromJson', () => { - it('return object from json string', () => { - let json = `{ - "keymaps": { "0": {"type": "scroll.home"}}, - "search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }}, - "blacklist": [ "*.slack.com"], - "properties": { - "mystr": "value", - "mynum": 123, - "mybool": true - } - }`; - let value = values.valueFromJson(json); - - expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}}); - expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} }); - expect(value.blacklist).to.deep.equal(["*.slack.com"]); - expect(value.properties).to.have.property('mystr', 'value'); - expect(value.properties).to.have.property('mynum', 123); - expect(value.properties).to.have.property('mybool', true); - }); - }); - - describe('valueFromForm', () => { - it('returns value from form', () => { - let form = { - keymaps: { - 'scroll.vertically?{"count":1}': 'j', - 'scroll.home': '0', - }, - search: { - default: 'google', - engines: [['google', 'https://google.com/search?q={}']], - }, - blacklist: ['*.slack.com'], - "properties": { - "mystr": "value", - "mynum": 123, - "mybool": true, - } - }; - let value = values.valueFromForm(form); - - expect(value.keymaps).to.have.deep.property('j', { type: "scroll.vertically", count: 1 }); - expect(value.keymaps).to.have.deep.property('0', { type: "scroll.home" }); - expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} })); - expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} }); - expect(value.blacklist).to.deep.equal(["*.slack.com"]); - expect(value.properties).to.have.property('mystr', 'value'); - expect(value.properties).to.have.property('mynum', 123); - expect(value.properties).to.have.property('mybool', true); - }); - - it('convert from empty form', () => { - let form = {}; - let value = values.valueFromForm(form); - expect(value).to.not.have.key('keymaps'); - expect(value).to.not.have.key('search'); - expect(value).to.not.have.key('blacklist'); - expect(value).to.not.have.key('properties'); - }); - - it('override keymaps', () => { - let form = { - keymaps: { - 'scroll.vertically?{"count":1}': 'j', - 'scroll.vertically?{"count":-1}': 'j', - } - }; - let value = values.valueFromForm(form); - - expect(value.keymaps).to.have.key('j'); - }); - - it('override search engine', () => { - let form = { - search: { - default: 'google', - engines: [ - ['google', 'https://google.com/search?q={}'], - ['google', 'https://google.co.jp/search?q={}'], - ] - } - }; - let value = values.valueFromForm(form); - - expect(value.search.engines).to.have.property('google', 'https://google.co.jp/search?q={}'); - }); - }); - - describe('jsonFromValue', () => { - }); - - describe('formFromValue', () => { - it('convert empty value to form', () => { - let value = {}; - let form = values.formFromValue(value); - - expect(value).to.not.have.key('keymaps'); - expect(value).to.not.have.key('search'); - expect(value).to.not.have.key('blacklist'); - }); - - it('convert value to form', () => { - let value = { - keymaps: { - j: { type: 'scroll.vertically', count: 1 }, - JJ: { type: 'scroll.vertically', count: 100 }, - 0: { type: 'scroll.home' }, - }, - search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }}, - blacklist: [ '*.slack.com'], - properties: { - "mystr": "value", - "mynum": 123, - "mybool": true, - } - }; - let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ]; - let form = values.formFromValue(value, allowed); - - expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j'); - expect(form.keymaps).to.not.have.property('scroll.vertically?{"count":100}'); - expect(form.keymaps).to.have.property('scroll.home', '0'); - expect(Object.keys(form.keymaps)).to.have.lengthOf(2); - expect(form.search).to.have.property('default', 'google'); - expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]); - expect(form.blacklist).to.have.lengthOf(1); - expect(form.blacklist).to.include('*.slack.com'); - expect(form.properties).to.have.property('mystr', 'value'); - expect(form.properties).to.have.property('mynum', 123); - expect(form.properties).to.have.property('mybool', true); - }); - }); -}); -- cgit v1.2.3