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 --- src/background/controllers/SettingController.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/background/controllers/SettingController.ts (limited to 'src/background/controllers/SettingController.ts') diff --git a/src/background/controllers/SettingController.ts b/src/background/controllers/SettingController.ts new file mode 100644 index 0000000..e895d72 --- /dev/null +++ b/src/background/controllers/SettingController.ts @@ -0,0 +1,18 @@ +import SettingUseCase from '../usecases/SettingUseCase'; +import ContentMessageClient from '../infrastructures/ContentMessageClient'; + +export default class SettingController { + constructor() { + this.settingUseCase = new SettingUseCase(); + this.contentMessageClient = new ContentMessageClient(); + } + + getSetting() { + return this.settingUseCase.get(); + } + + async reload() { + await this.settingUseCase.reload(); + this.contentMessageClient.broadcastSettingsChanged(); + } +} -- cgit v1.2.3 From 678020a3a27713e77ec8d74483122b4258fbc829 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 1 May 2019 11:04:24 +0900 Subject: Types on src/background --- .../controllers/AddonEnabledController.ts | 4 +- src/background/controllers/CommandController.ts | 14 ++- src/background/controllers/FindController.ts | 6 +- src/background/controllers/LinkController.ts | 12 ++- src/background/controllers/MarkController.ts | 10 +- src/background/controllers/OperationController.ts | 13 ++- src/background/controllers/SettingController.ts | 8 +- src/background/controllers/VersionController.ts | 6 +- src/background/controllers/version.ts | 13 --- src/background/domains/CommandDocs.ts | 3 +- src/background/domains/CompletionGroup.ts | 17 +--- src/background/domains/CompletionItem.ts | 29 ++---- src/background/domains/Completions.ts | 27 ------ src/background/domains/GlobalMark.ts | 28 +----- src/background/infrastructures/ConsoleClient.ts | 10 +- .../infrastructures/ContentMessageClient.ts | 12 +-- .../infrastructures/ContentMessageListener.ts | 78 ++++++++++----- src/background/infrastructures/MemoryStorage.ts | 6 +- src/background/presenters/IndicatorPresenter.ts | 4 +- src/background/presenters/NotifyPresenter.ts | 8 +- src/background/presenters/TabPresenter.ts | 44 +++++---- src/background/presenters/WindowPresenter.ts | 2 +- src/background/repositories/BookmarkRepository.ts | 4 +- .../repositories/BrowserSettingRepository.ts | 2 +- .../repositories/CompletionsRepository.ts | 14 ++- src/background/repositories/FindRepository.ts | 6 +- src/background/repositories/MarkRepository.ts | 8 +- .../repositories/PersistentSettingRepository.ts | 2 +- src/background/repositories/SettingRepository.ts | 8 +- src/background/repositories/VersionRepository.ts | 10 -- src/background/usecases/AddonEnabledUseCase.ts | 18 +++- src/background/usecases/CommandUseCase.ts | 52 ++++++---- src/background/usecases/CompletionsUseCase.ts | 107 ++++++++++++--------- src/background/usecases/ConsoleUseCase.ts | 40 ++++---- src/background/usecases/FindUseCase.ts | 14 ++- src/background/usecases/LinkUseCase.ts | 8 +- src/background/usecases/MarkUseCase.ts | 20 ++-- src/background/usecases/SettingUseCase.ts | 8 +- src/background/usecases/TabSelectUseCase.ts | 24 ++--- src/background/usecases/TabUseCase.ts | 36 ++++--- src/background/usecases/VersionUseCase.ts | 10 +- src/background/usecases/ZoomUseCase.ts | 26 ++--- src/background/usecases/filters.ts | 38 ++++---- src/background/usecases/parsers.ts | 10 +- src/content/scrolls.ts | 10 -- test/background/domains/GlobalMark.test.ts | 11 --- test/background/repositories/Mark.test.ts | 3 +- test/background/repositories/Version.ts | 34 ------- 48 files changed, 446 insertions(+), 431 deletions(-) delete mode 100644 src/background/controllers/version.ts delete mode 100644 src/background/domains/Completions.ts delete mode 100644 src/background/repositories/VersionRepository.ts delete mode 100644 test/background/domains/GlobalMark.test.ts delete mode 100644 test/background/repositories/Version.ts (limited to 'src/background/controllers/SettingController.ts') diff --git a/src/background/controllers/AddonEnabledController.ts b/src/background/controllers/AddonEnabledController.ts index 9a3a521..251af25 100644 --- a/src/background/controllers/AddonEnabledController.ts +++ b/src/background/controllers/AddonEnabledController.ts @@ -1,11 +1,13 @@ import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; export default class AddonEnabledController { + private addonEnabledUseCase: AddonEnabledUseCase; + constructor() { this.addonEnabledUseCase = new AddonEnabledUseCase(); } - indicate(enabled) { + indicate(enabled: boolean): Promise { return this.addonEnabledUseCase.indicate(enabled); } } diff --git a/src/background/controllers/CommandController.ts b/src/background/controllers/CommandController.ts index b113709..f3a6b7f 100644 --- a/src/background/controllers/CommandController.ts +++ b/src/background/controllers/CommandController.ts @@ -1,19 +1,23 @@ import CompletionsUseCase from '../usecases/CompletionsUseCase'; import CommandUseCase from '../usecases/CommandUseCase'; -import Completions from '../domains/Completions'; +import CompletionGroup from '../domains/CompletionGroup'; -const trimStart = (str) => { +const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 return str.replace(/^\s+/, ''); }; export default class CommandController { + private completionsUseCase: CompletionsUseCase; + + private commandIndicator: CommandUseCase; + constructor() { this.completionsUseCase = new CompletionsUseCase(); this.commandIndicator = new CommandUseCase(); } - getCompletions(line) { + getCompletions(line: string): Promise { let trimmed = trimStart(line); let words = trimmed.split(/ +/); let name = words[0]; @@ -45,11 +49,11 @@ export default class CommandController { case 'set': return this.completionsUseCase.querySet(name, keywords); } - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } // eslint-disable-next-line complexity - exec(line) { + exec(line: string): Promise { let trimmed = trimStart(line); let words = trimmed.split(/ +/); let name = words[0]; diff --git a/src/background/controllers/FindController.ts b/src/background/controllers/FindController.ts index caeff98..28959e2 100644 --- a/src/background/controllers/FindController.ts +++ b/src/background/controllers/FindController.ts @@ -1,15 +1,17 @@ import FindUseCase from '../usecases/FindUseCase'; export default class FindController { + private findUseCase: FindUseCase; + constructor() { this.findUseCase = new FindUseCase(); } - getKeyword() { + getKeyword(): Promise { return this.findUseCase.getKeyword(); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise { return this.findUseCase.setKeyword(keyword); } } diff --git a/src/background/controllers/LinkController.ts b/src/background/controllers/LinkController.ts index 7e395b1..707b28a 100644 --- a/src/background/controllers/LinkController.ts +++ b/src/background/controllers/LinkController.ts @@ -1,15 +1,19 @@ import LinkUseCase from '../usecases/LinkUseCase'; export default class LinkController { + private linkUseCase: LinkUseCase; + constructor() { this.linkUseCase = new LinkUseCase(); } - openToTab(url, tabId) { - this.linkUseCase.openToTab(url, tabId); + openToTab(url: string, tabId: number): Promise { + return this.linkUseCase.openToTab(url, tabId); } - openNewTab(url, openerId, background) { - this.linkUseCase.openNewTab(url, openerId, background); + openNewTab( + url: string, openerId: number, background: boolean, + ): Promise { + return this.linkUseCase.openNewTab(url, openerId, background); } } diff --git a/src/background/controllers/MarkController.ts b/src/background/controllers/MarkController.ts index 0478369..419a08b 100644 --- a/src/background/controllers/MarkController.ts +++ b/src/background/controllers/MarkController.ts @@ -1,15 +1,17 @@ import MarkUseCase from '../usecases/MarkUseCase'; export default class MarkController { + private markUseCase: MarkUseCase; + constructor() { this.markUseCase = new MarkUseCase(); } - setGlobal(key, x, y) { - this.markUseCase.setGlobal(key, x, y); + setGlobal(key: string, x: number, y: number): Promise { + return this.markUseCase.setGlobal(key, x, y); } - jumpGlobal(key) { - this.markUseCase.jumpGlobal(key); + jumpGlobal(key: string): Promise { + return this.markUseCase.jumpGlobal(key); } } diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 416aa9c..4e9c106 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -6,6 +6,16 @@ import TabSelectUseCase from '../usecases/TabSelectUseCase'; import ZoomUseCase from '../usecases/ZoomUseCase'; export default class OperationController { + private findUseCase: FindUseCase; + + private consoleUseCase: ConsoleUseCase; + + private tabUseCase: TabUseCase; + + private tabSelectUseCase: TabSelectUseCase; + + private zoomUseCase: ZoomUseCase; + constructor() { this.findUseCase = new FindUseCase(); this.consoleUseCase = new ConsoleUseCase(); @@ -15,7 +25,7 @@ export default class OperationController { } // eslint-disable-next-line complexity, max-lines-per-function - exec(operation) { + exec(operation: any): Promise { switch (operation.type) { case operations.TAB_CLOSE: return this.tabUseCase.close(false); @@ -72,6 +82,7 @@ export default class OperationController { case operations.CANCEL: return this.consoleUseCase.hideConsole(); } + throw new Error('unknown operation: ' + operation.type); } } diff --git a/src/background/controllers/SettingController.ts b/src/background/controllers/SettingController.ts index e895d72..f8b7302 100644 --- a/src/background/controllers/SettingController.ts +++ b/src/background/controllers/SettingController.ts @@ -2,16 +2,20 @@ import SettingUseCase from '../usecases/SettingUseCase'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class SettingController { + private settingUseCase: SettingUseCase; + + private contentMessageClient: ContentMessageClient; + constructor() { this.settingUseCase = new SettingUseCase(); this.contentMessageClient = new ContentMessageClient(); } - getSetting() { + getSetting(): any { return this.settingUseCase.get(); } - async reload() { + async reload(): Promise { await this.settingUseCase.reload(); this.contentMessageClient.broadcastSettingsChanged(); } diff --git a/src/background/controllers/VersionController.ts b/src/background/controllers/VersionController.ts index c596f9b..f402ed0 100644 --- a/src/background/controllers/VersionController.ts +++ b/src/background/controllers/VersionController.ts @@ -1,11 +1,13 @@ import VersionUseCase from '../usecases/VersionUseCase'; export default class VersionController { + private versionUseCase: VersionUseCase; + constructor() { this.versionUseCase = new VersionUseCase(); } - notify() { - this.versionUseCase.notify(); + notify(): void { + return this.versionUseCase.notify(); } } diff --git a/src/background/controllers/version.ts b/src/background/controllers/version.ts deleted file mode 100644 index ec0f634..0000000 --- a/src/background/controllers/version.ts +++ /dev/null @@ -1,13 +0,0 @@ -import VersionInteractor from '../usecases/version'; - -export default class VersionController { - constructor() { - this.versionInteractor = new VersionInteractor(); - } - - notifyIfUpdated() { - browser.runtime.onInstalled.addListener(() => { - return this.versionInteractor.notify(); - }); - } -} diff --git a/src/background/domains/CommandDocs.ts b/src/background/domains/CommandDocs.ts index 734c68e..25ea62a 100644 --- a/src/background/domains/CommandDocs.ts +++ b/src/background/domains/CommandDocs.ts @@ -8,5 +8,4 @@ export default { bdeletes: 'Close all tabs matched by keywords', quit: 'Close the current tab', quitall: 'Close all tabs', -}; - +} as {[key: string]: string}; diff --git a/src/background/domains/CompletionGroup.ts b/src/background/domains/CompletionGroup.ts index 1749d72..1eea7d8 100644 --- a/src/background/domains/CompletionGroup.ts +++ b/src/background/domains/CompletionGroup.ts @@ -1,14 +1,7 @@ -export default class CompletionGroup { - constructor(name, items) { - this.name0 = name; - this.items0 = items; - } +import CompletionItem from './CompletionItem'; - get name() { - return this.name0; - } - - get items() { - return this.items0; - } +export default interface CompletionGroup { + name: string; + items: CompletionItem[]; + // eslint-disable-next-line semi } diff --git a/src/background/domains/CompletionItem.ts b/src/background/domains/CompletionItem.ts index c7ad8a1..657efaa 100644 --- a/src/background/domains/CompletionItem.ts +++ b/src/background/domains/CompletionItem.ts @@ -1,24 +1,7 @@ -export default class CompletionItem { - constructor({ caption, content, url, icon }) { - this.caption0 = caption; - this.content0 = content; - this.url0 = url; - this.icon0 = icon; - } - - get caption() { - return this.caption0; - } - - get content() { - return this.content0; - } - - get url() { - return this.url0; - } - - get icon() { - return this.icon0; - } +export default interface CompletionItem { + readonly caption?: string; + readonly content?: string; + readonly url?: string; + readonly icon?: string; + // eslint-disable-next-line semi } diff --git a/src/background/domains/Completions.ts b/src/background/domains/Completions.ts deleted file mode 100644 index f399743..0000000 --- a/src/background/domains/Completions.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default class Completions { - constructor(groups) { - this.g = groups; - } - - get groups() { - return this.g; - } - - serialize() { - return this.groups.map(group => ({ - name: group.name, - items: group.items.map(item => ({ - caption: item.caption, - content: item.content, - url: item.url, - icon: item.icon, - })), - })); - } - - static empty() { - return EMPTY_COMPLETIONS; - } -} - -let EMPTY_COMPLETIONS = new Completions([]); diff --git a/src/background/domains/GlobalMark.ts b/src/background/domains/GlobalMark.ts index f0586f1..0964373 100644 --- a/src/background/domains/GlobalMark.ts +++ b/src/background/domains/GlobalMark.ts @@ -1,24 +1,6 @@ -export default class GlobalMark { - constructor(tabId, url, x, y) { - this.tabId0 = tabId; - this.url0 = url; - this.x0 = x; - this.y0 = y; - } - - get tabId() { - return this.tabId0; - } - - get url() { - return this.url0; - } - - get x() { - return this.x0; - } - - get y() { - return this.y0; - } +export interface GlobalMark { + readonly tabId: number; + readonly url: string; + readonly x: number; + readonly y: number; } diff --git a/src/background/infrastructures/ConsoleClient.ts b/src/background/infrastructures/ConsoleClient.ts index f691515..7ad5d24 100644 --- a/src/background/infrastructures/ConsoleClient.ts +++ b/src/background/infrastructures/ConsoleClient.ts @@ -1,34 +1,34 @@ import messages from '../../shared/messages'; export default class ConsoleClient { - showCommand(tabId, command) { + showCommand(tabId: number, command: string): Promise { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_COMMAND, command, }); } - showFind(tabId) { + showFind(tabId: number): Promise { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_FIND }); } - showInfo(tabId, message) { + showInfo(tabId: number, message: string): Promise { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_INFO, text: message, }); } - showError(tabId, message) { + showError(tabId: number, message: string): Promise { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_ERROR, text: message, }); } - hide(tabId) { + hide(tabId: number): Promise { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_HIDE, }); diff --git a/src/background/infrastructures/ContentMessageClient.ts b/src/background/infrastructures/ContentMessageClient.ts index 0fab5a3..20057c7 100644 --- a/src/background/infrastructures/ContentMessageClient.ts +++ b/src/background/infrastructures/ContentMessageClient.ts @@ -1,10 +1,10 @@ import messages from '../../shared/messages'; export default class ContentMessageClient { - async broadcastSettingsChanged() { + async broadcastSettingsChanged(): Promise { let tabs = await browser.tabs.query({}); for (let tab of tabs) { - if (tab.url.startsWith('about:')) { + if (!tab.id || tab.url && tab.url.startsWith('about:')) { continue; } browser.tabs.sendMessage(tab.id, { @@ -13,20 +13,20 @@ export default class ContentMessageClient { } } - async getAddonEnabled(tabId) { + async getAddonEnabled(tabId: number): Promise { let { enabled } = await browser.tabs.sendMessage(tabId, { type: messages.ADDON_ENABLED_QUERY, - }); + }) as { enabled: boolean }; return enabled; } - toggleAddonEnabled(tabId) { + toggleAddonEnabled(tabId: number): Promise { return browser.tabs.sendMessage(tabId, { type: messages.ADDON_TOGGLE_ENABLED, }); } - scrollTo(tabId, x, y) { + scrollTo(tabId: number, x: number, y: number): Promise { return browser.tabs.sendMessage(tabId, { type: messages.TAB_SCROLL_TO, x, diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 5b0f62e..81d3232 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -1,4 +1,5 @@ import messages from '../../shared/messages'; +import CompletionGroup from '../domains/CompletionGroup'; import CommandController from '../controllers/CommandController'; import SettingController from '../controllers/SettingController'; import FindController from '../controllers/FindController'; @@ -8,6 +9,22 @@ import OperationController from '../controllers/OperationController'; import MarkController from '../controllers/MarkController'; export default class ContentMessageListener { + private settingController: SettingController; + + private commandController: CommandController; + + private findController: FindController; + + private addonEnabledController: AddonEnabledController; + + private linkController: LinkController; + + private backgroundOperationController: OperationController; + + private markController: MarkController; + + private consolePorts: {[tabId: number]: browser.runtime.Port}; + constructor() { this.settingController = new SettingController(); this.commandController = new CommandController(); @@ -20,20 +37,28 @@ export default class ContentMessageListener { this.consolePorts = {}; } - run() { - browser.runtime.onMessage.addListener((message, sender) => { + run(): void { + browser.runtime.onMessage.addListener(( + message: any, sender: browser.runtime.MessageSender, + ) => { try { - let ret = this.onMessage(message, sender); + let ret = this.onMessage(message, sender.tab as browser.tabs.Tab); if (!(ret instanceof Promise)) { return {}; } return ret.catch((e) => { + if (!sender.tab || !sender.tab.id) { + return; + } return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, }); }); } catch (e) { + if (!sender.tab || !sender.tab.id) { + return; + } return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, @@ -43,7 +68,7 @@ export default class ContentMessageListener { browser.runtime.onConnect.addListener(this.onConnected.bind(this)); } - onMessage(message, sender) { + onMessage(message: any, senderTab: browser.tabs.Tab): Promise | any { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); @@ -59,7 +84,10 @@ export default class ContentMessageListener { return this.onAddonEnabledResponse(message.enabled); case messages.OPEN_URL: return this.onOpenUrl( - message.newTab, message.url, sender.tab.id, message.background); + message.newTab, + message.url, + senderTab.id as number, + message.background); case messages.BACKGROUND_OPERATION: return this.onBackgroundOperation(message.operation); case messages.MARK_SET_GLOBAL: @@ -67,56 +95,60 @@ export default class ContentMessageListener { case messages.MARK_JUMP_GLOBAL: return this.onMarkJumpGlobal(message.key); case messages.CONSOLE_FRAME_MESSAGE: - return this.onConsoleFrameMessage(sender.tab.id, message.message); + return this.onConsoleFrameMessage( + senderTab.id as number, message.message, + ); } + throw new Error('unsupported message: ' + message.type); } - async onConsoleQueryCompletions(line) { + async onConsoleQueryCompletions(line: string): Promise { let completions = await this.commandController.getCompletions(line); - return Promise.resolve(completions.serialize()); + return Promise.resolve(completions); } - onConsoleEnterCommand(text) { + onConsoleEnterCommand(text: string): Promise { return this.commandController.exec(text); } - - onSettingsQuery() { + onSettingsQuery(): Promise { return this.settingController.getSetting(); } - onFindGetKeyword() { + onFindGetKeyword(): Promise { return this.findController.getKeyword(); } - onFindSetKeyword(keyword) { + onFindSetKeyword(keyword: string): Promise { return this.findController.setKeyword(keyword); } - onAddonEnabledResponse(enabled) { + onAddonEnabledResponse(enabled: boolean): Promise { return this.addonEnabledController.indicate(enabled); } - onOpenUrl(newTab, url, openerId, background) { + onOpenUrl( + newTab: boolean, url: string, openerId: number, background: boolean, + ): Promise { if (newTab) { return this.linkController.openNewTab(url, openerId, background); } return this.linkController.openToTab(url, openerId); } - onBackgroundOperation(operation) { + onBackgroundOperation(operation: any): Promise { return this.backgroundOperationController.exec(operation); } - onMarkSetGlobal(key, x, y) { + onMarkSetGlobal(key: string, x: number, y: number): Promise { return this.markController.setGlobal(key, x, y); } - onMarkJumpGlobal(key) { + onMarkJumpGlobal(key: string): Promise { return this.markController.jumpGlobal(key); } - onConsoleFrameMessage(tabId, message) { + onConsoleFrameMessage(tabId: number, message: any): void { let port = this.consolePorts[tabId]; if (!port) { return; @@ -124,12 +156,14 @@ export default class ContentMessageListener { port.postMessage(message); } - onConnected(port) { + onConnected(port: browser.runtime.Port): void { if (port.name !== 'vimvixen-console') { return; } - let id = port.sender.tab.id; - this.consolePorts[id] = port; + if (port.sender && port.sender.tab && port.sender.tab.id) { + let id = port.sender.tab.id; + this.consolePorts[id] = port; + } } } diff --git a/src/background/infrastructures/MemoryStorage.ts b/src/background/infrastructures/MemoryStorage.ts index 3a7e4f2..baf9ffa 100644 --- a/src/background/infrastructures/MemoryStorage.ts +++ b/src/background/infrastructures/MemoryStorage.ts @@ -1,7 +1,7 @@ -const db = {}; +const db: {[key: string]: any} = {}; export default class MemoryStorage { - set(name, value) { + set(name: string, value: any): void { let data = JSON.stringify(value); if (typeof data === 'undefined') { throw new Error('value is not serializable'); @@ -9,7 +9,7 @@ export default class MemoryStorage { db[name] = data; } - get(name) { + get(name: string): any { let data = db[name]; if (!data) { return undefined; diff --git a/src/background/presenters/IndicatorPresenter.ts b/src/background/presenters/IndicatorPresenter.ts index 5737519..d9a615a 100644 --- a/src/background/presenters/IndicatorPresenter.ts +++ b/src/background/presenters/IndicatorPresenter.ts @@ -1,12 +1,12 @@ export default class IndicatorPresenter { - indicate(enabled) { + indicate(enabled: boolean): Promise { let path = enabled ? 'resources/enabled_32x32.png' : 'resources/disabled_32x32.png'; return browser.browserAction.setIcon({ path }); } - onClick(listener) { + onClick(listener: (arg: browser.tabs.Tab) => void): void { browser.browserAction.onClicked.addListener(listener); } } diff --git a/src/background/presenters/NotifyPresenter.ts b/src/background/presenters/NotifyPresenter.ts index a81f227..c83c205 100644 --- a/src/background/presenters/NotifyPresenter.ts +++ b/src/background/presenters/NotifyPresenter.ts @@ -1,8 +1,12 @@ const NOTIFICATION_ID = 'vimvixen-update'; export default class NotifyPresenter { - notify(title, message, onclick) { - const listener = (id) => { + notify( + title: string, + message: string, + onclick: () => void, + ): Promise { + const listener = (id: string) => { if (id !== NOTIFICATION_ID) { return; } diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts index 744be39..33c6513 100644 --- a/src/background/presenters/TabPresenter.ts +++ b/src/background/presenters/TabPresenter.ts @@ -3,27 +3,29 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; const CURRENT_SELECTED_KEY = 'tabs.current.selected'; const LAST_SELECTED_KEY = 'tabs.last.selected'; +type Tab = browser.tabs.Tab; + export default class TabPresenter { - open(url, tabId) { + open(url: string, tabId?: number): Promise { return browser.tabs.update(tabId, { url }); } - create(url, opts) { + create(url: string, opts?: object): Promise { return browser.tabs.create({ url, ...opts }); } - async getCurrent() { + async getCurrent(): Promise { let tabs = await browser.tabs.query({ active: true, currentWindow: true }); return tabs[0]; } - getAll() { + getAll(): Promise { return browser.tabs.query({ currentWindow: true }); } - async getLastSelectedId() { + async getLastSelectedId(): Promise { let cache = new MemoryStorage(); let tabId = await cache.get(LAST_SELECTED_KEY); if (tabId === null || typeof tabId === 'undefined') { @@ -32,25 +34,25 @@ export default class TabPresenter { return tabId; } - async getByKeyword(keyword, excludePinned = false) { + async getByKeyword(keyword: string, excludePinned = false): Promise { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { - return t.url.toLowerCase().includes(keyword.toLowerCase()) || + return t.url && t.url.toLowerCase().includes(keyword.toLowerCase()) || t.title && t.title.toLowerCase().includes(keyword.toLowerCase()); }).filter((t) => { return !(excludePinned && t.pinned); }); } - select(tabId) { + select(tabId: number): Promise { return browser.tabs.update(tabId, { active: true }); } - remove(ids) { + remove(ids: number[]): Promise { return browser.tabs.remove(ids); } - async reopen() { + async reopen(): Promise { let window = await browser.windows.getCurrent(); let sessions = await browser.sessions.getRecentlyClosed(); let session = sessions.find((s) => { @@ -59,39 +61,43 @@ export default class TabPresenter { if (!session) { return; } - if (session.tab) { + if (session.tab && session.tab.sessionId) { return browser.sessions.restore(session.tab.sessionId); } - return browser.sessions.restore(session.window.sessionId); + if (session.window && session.window.sessionId) { + return browser.sessions.restore(session.window.sessionId); + } } - reload(tabId, cache) { + reload(tabId: number, cache: boolean): Promise { return browser.tabs.reload(tabId, { bypassCache: cache }); } - setPinned(tabId, pinned) { + setPinned(tabId: number, pinned: boolean): Promise { return browser.tabs.update(tabId, { pinned }); } - duplicate(id) { + duplicate(id: number): Promise { return browser.tabs.duplicate(id); } - getZoom(tabId) { + getZoom(tabId: number): Promise { return browser.tabs.getZoom(tabId); } - setZoom(tabId, factor) { + setZoom(tabId: number, factor: number): Promise { return browser.tabs.setZoom(tabId, factor); } - onSelected(listener) { + onSelected( + listener: (arg: { tabId: number, windowId: number}) => void, + ): void { browser.tabs.onActivated.addListener(listener); } } let tabPresenter = new TabPresenter(); -tabPresenter.onSelected((tab) => { +tabPresenter.onSelected((tab: any) => { let cache = new MemoryStorage(); let lastId = cache.get(CURRENT_SELECTED_KEY); diff --git a/src/background/presenters/WindowPresenter.ts b/src/background/presenters/WindowPresenter.ts index a82c4a2..e04f258 100644 --- a/src/background/presenters/WindowPresenter.ts +++ b/src/background/presenters/WindowPresenter.ts @@ -1,5 +1,5 @@ export default class WindowPresenter { - create(url) { + create(url: string): Promise { return browser.windows.create({ url }); } } diff --git a/src/background/repositories/BookmarkRepository.ts b/src/background/repositories/BookmarkRepository.ts index 99f7ec4..b4da509 100644 --- a/src/background/repositories/BookmarkRepository.ts +++ b/src/background/repositories/BookmarkRepository.ts @@ -1,5 +1,7 @@ export default class BookmarkRepository { - async create(title, url) { + async create( + title: string, url: string + ): Promise { let item = await browser.bookmarks.create({ type: 'bookmark', title, diff --git a/src/background/repositories/BrowserSettingRepository.ts b/src/background/repositories/BrowserSettingRepository.ts index a9d2c06..48c72a5 100644 --- a/src/background/repositories/BrowserSettingRepository.ts +++ b/src/background/repositories/BrowserSettingRepository.ts @@ -1,7 +1,7 @@ import * as urls from '../../shared/urls'; export default class BrowserSettingRepository { - async getHomepageUrls() { + async getHomepageUrls(): Promise { let { value } = await browser.browserSettings.homepageOverride.get({}); return value.split('|').map(urls.normalizeUrl); } diff --git a/src/background/repositories/CompletionsRepository.ts b/src/background/repositories/CompletionsRepository.ts index 1318d36..18af587 100644 --- a/src/background/repositories/CompletionsRepository.ts +++ b/src/background/repositories/CompletionsRepository.ts @@ -1,7 +1,13 @@ +type Tab = browser.tabs.Tab; +type BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode; + export default class CompletionsRepository { - async queryBookmarks(keywords) { + async queryBookmarks(keywords: string): Promise { let items = await browser.bookmarks.search({ query: keywords }); return items.filter((item) => { + if (!item.url) { + return false; + } let url = undefined; try { url = new URL(item.url); @@ -12,17 +18,17 @@ export default class CompletionsRepository { }); } - queryHistories(keywords) { + queryHistories(keywords: string): Promise { return browser.history.search({ text: keywords, startTime: 0, }); } - async queryTabs(keywords, excludePinned) { + async queryTabs(keywords: string, excludePinned: boolean): Promise { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { - return t.url.toLowerCase().includes(keywords.toLowerCase()) || + return t.url && t.url.toLowerCase().includes(keywords.toLowerCase()) || t.title && t.title.toLowerCase().includes(keywords.toLowerCase()); }).filter((t) => { return !(excludePinned && t.pinned); diff --git a/src/background/repositories/FindRepository.ts b/src/background/repositories/FindRepository.ts index 74ec914..bf286e6 100644 --- a/src/background/repositories/FindRepository.ts +++ b/src/background/repositories/FindRepository.ts @@ -3,15 +3,17 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; const FIND_KEYWORD_KEY = 'find-keyword'; export default class FindRepository { + private cache: MemoryStorage; + constructor() { this.cache = new MemoryStorage(); } - getKeyword() { + getKeyword(): Promise { return Promise.resolve(this.cache.get(FIND_KEYWORD_KEY)); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise { this.cache.set(FIND_KEYWORD_KEY, keyword); return Promise.resolve(); } diff --git a/src/background/repositories/MarkRepository.ts b/src/background/repositories/MarkRepository.ts index 282c712..69c85f6 100644 --- a/src/background/repositories/MarkRepository.ts +++ b/src/background/repositories/MarkRepository.ts @@ -4,21 +4,23 @@ import GlobalMark from '../domains/GlobalMark'; const MARK_KEY = 'mark'; export default class MarkRepository { + private cache: MemoryStorage; + constructor() { this.cache = new MemoryStorage(); } - getMark(key) { + getMark(key: string): Promise { let marks = this.getOrEmptyMarks(); let data = marks[key]; if (!data) { return Promise.resolve(undefined); } - let mark = new GlobalMark(data.tabId, data.url, data.x, data.y); + let mark = { tabId: data.tabId, url: data.url, x: data.x, y: data.y }; return Promise.resolve(mark); } - setMark(key, mark) { + setMark(key: string, mark: GlobalMark): Promise { let marks = this.getOrEmptyMarks(); marks[key] = { tabId: mark.tabId, url: mark.url, x: mark.x, y: mark.y }; this.cache.set(MARK_KEY, marks); diff --git a/src/background/repositories/PersistentSettingRepository.ts b/src/background/repositories/PersistentSettingRepository.ts index 4cab107..3f2f4a1 100644 --- a/src/background/repositories/PersistentSettingRepository.ts +++ b/src/background/repositories/PersistentSettingRepository.ts @@ -1,7 +1,7 @@ import Setting from '../domains/Setting'; export default class SettingRepository { - async load() { + async load(): Promise { let { settings } = await browser.storage.local.get('settings'); if (!settings) { return null; diff --git a/src/background/repositories/SettingRepository.ts b/src/background/repositories/SettingRepository.ts index c4667a9..15355ba 100644 --- a/src/background/repositories/SettingRepository.ts +++ b/src/background/repositories/SettingRepository.ts @@ -3,19 +3,21 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; const CACHED_SETTING_KEY = 'setting'; export default class SettingRepository { + private cache: MemoryStorage; + constructor() { this.cache = new MemoryStorage(); } - get() { + get(): Promise { return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); } - update(value) { + update(value: any): any { return this.cache.set(CACHED_SETTING_KEY, value); } - async setProperty(name, value) { + async setProperty(name: string, value: string): Promise { let current = await this.get(); current.properties[name] = value; return this.update(current); diff --git a/src/background/repositories/VersionRepository.ts b/src/background/repositories/VersionRepository.ts deleted file mode 100644 index 4c71d05..0000000 --- a/src/background/repositories/VersionRepository.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default class VersionRepository { - async get() { - let { version } = await browser.storage.local.get('version'); - return version; - } - - update(version) { - return browser.storage.local.set({ version }); - } -} diff --git a/src/background/usecases/AddonEnabledUseCase.ts b/src/background/usecases/AddonEnabledUseCase.ts index bb2c347..0a6fb03 100644 --- a/src/background/usecases/AddonEnabledUseCase.ts +++ b/src/background/usecases/AddonEnabledUseCase.ts @@ -3,10 +3,20 @@ import TabPresenter from '../presenters/TabPresenter'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class AddonEnabledUseCase { + private indicatorPresentor: IndicatorPresenter; + + private tabPresenter: TabPresenter; + + private contentMessageClient: ContentMessageClient; + constructor() { this.indicatorPresentor = new IndicatorPresenter(); - this.indicatorPresentor.onClick(tab => this.onIndicatorClick(tab.id)); + this.indicatorPresentor.onClick((tab) => { + if (tab.id) { + this.onIndicatorClick(tab.id); + } + }); this.tabPresenter = new TabPresenter(); this.tabPresenter.onSelected(info => this.onTabSelected(info.tabId)); @@ -14,15 +24,15 @@ export default class AddonEnabledUseCase { this.contentMessageClient = new ContentMessageClient(); } - indicate(enabled) { + indicate(enabled: boolean): Promise { return this.indicatorPresentor.indicate(enabled); } - onIndicatorClick(tabId) { + onIndicatorClick(tabId: number): Promise { return this.contentMessageClient.toggleAddonEnabled(tabId); } - async onTabSelected(tabId) { + async onTabSelected(tabId: number): Promise { let enabled = await this.contentMessageClient.getAddonEnabled(tabId); return this.indicatorPresentor.indicate(enabled); } diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts index 9ec46fe..e0e3ada 100644 --- a/src/background/usecases/CommandUseCase.ts +++ b/src/background/usecases/CommandUseCase.ts @@ -6,9 +6,21 @@ 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'; +import * as properties from '../../shared/settings/properties'; export default class CommandIndicator { + private tabPresenter: TabPresenter; + + private windowPresenter: WindowPresenter; + + private settingRepository: SettingRepository; + + private bookmarkRepository: BookmarkRepository; + + private consoleClient: ConsoleClient; + + private contentMessageClient: ContentMessageClient; + constructor() { this.tabPresenter = new TabPresenter(); this.windowPresenter = new WindowPresenter(); @@ -19,34 +31,34 @@ export default class CommandIndicator { this.contentMessageClient = new ContentMessageClient(); } - async open(keywords) { + async open(keywords: string): Promise { let url = await this.urlOrSearch(keywords); return this.tabPresenter.open(url); } - async tabopen(keywords) { + async tabopen(keywords: string): Promise { let url = await this.urlOrSearch(keywords); return this.tabPresenter.create(url); } - async winopen(keywords) { + async winopen(keywords: string): Promise { let url = await this.urlOrSearch(keywords); return this.windowPresenter.create(url); } // eslint-disable-next-line max-statements - async buffer(keywords) { + async buffer(keywords: string): Promise { if (keywords.length === 0) { return; } - if (!isNaN(keywords)) { + if (!isNaN(Number(keywords))) { let tabs = await this.tabPresenter.getAll(); let index = parseInt(keywords, 10) - 1; if (index < 0 || tabs.length <= index) { throw new RangeError(`tab ${index + 1} does not exist`); } - return this.tabPresenter.select(tabs[index].id); + return this.tabPresenter.select(tabs[index].id as number); } else if (keywords.trim() === '%') { // Select current window return; @@ -66,13 +78,13 @@ export default class CommandIndicator { } for (let tab of tabs) { if (tab.index > current.index) { - return this.tabPresenter.select(tab.id); + return this.tabPresenter.select(tab.id as number); } } - return this.tabPresenter.select(tabs[0].id); + return this.tabPresenter.select(tabs[0].id as number); } - async bdelete(force, keywords) { + async bdelete(force: boolean, keywords: string): Promise { let excludePinned = !force; let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); if (tabs.length === 0) { @@ -80,35 +92,35 @@ export default class CommandIndicator { } else if (tabs.length > 1) { throw new Error('More than one match for ' + keywords); } - return this.tabPresenter.remove([tabs[0].id]); + return this.tabPresenter.remove([tabs[0].id as number]); } - async bdeletes(force, keywords) { + async bdeletes(force: boolean, keywords: string): Promise { let excludePinned = !force; let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); - let ids = tabs.map(tab => tab.id); + let ids = tabs.map(tab => tab.id as number); return this.tabPresenter.remove(ids); } - async quit() { + async quit(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.remove([tab.id]); + return this.tabPresenter.remove([tab.id as number]); } - async quitAll() { + async quitAll(): Promise { let tabs = await this.tabPresenter.getAll(); - let ids = tabs.map(tab => tab.id); + let ids = tabs.map(tab => tab.id as number); this.tabPresenter.remove(ids); } - async addbookmark(title) { + async addbookmark(title: string): Promise { let tab = await this.tabPresenter.getCurrent(); let item = await this.bookmarkRepository.create(title, tab.url); let message = 'Saved current page: ' + item.url; return this.consoleClient.showInfo(tab.id, message); } - async set(keywords) { + async set(keywords: string): Promise { if (keywords.length === 0) { return; } @@ -118,7 +130,7 @@ export default class CommandIndicator { return this.contentMessageClient.broadcastSettingsChanged(); } - async urlOrSearch(keywords) { + async urlOrSearch(keywords: string): Promise { let settings = await this.settingRepository.get(); return urls.searchUrl(keywords, settings.search); } diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index 7dc30ac..037d6eb 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -1,6 +1,5 @@ -import CompletionItem from '../domains/CompletionItem'; -import CompletionGroup from '../domains/CompletionGroup'; import Completions from '../domains/Completions'; +import CompletionGroup from '../domains/CompletionGroup'; import CommandDocs from '../domains/CommandDocs'; import CompletionsRepository from '../repositories/CompletionsRepository'; import * as filters from './filters'; @@ -10,14 +9,23 @@ import * as properties from '../../shared/settings/properties'; const COMPLETION_ITEM_LIMIT = 10; +type Tab = browser.tabs.Tab; +type HistoryItem = browser.history.HistoryItem; + export default class CompletionsUseCase { + private tabPresenter: TabPresenter; + + private completionsRepository: CompletionsRepository; + + private settingRepository: SettingRepository; + constructor() { this.tabPresenter = new TabPresenter(); this.completionsRepository = new CompletionsRepository(); this.settingRepository = new SettingRepository(); } - queryConsoleCommand(prefix) { + queryConsoleCommand(prefix: string): Promise { let keys = Object.keys(CommandDocs); let items = keys .filter(name => name.startsWith(prefix)) @@ -28,16 +36,14 @@ export default class CompletionsUseCase { })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return Promise.resolve( - new Completions([new CompletionGroup('Console Command', items)]) - ); + return Promise.resolve([{ name: 'Console CompletionGroup', items }]); } - async queryOpen(name, keywords) { + async queryOpen(name: string, keywords: string): Promise { let settings = await this.settingRepository.get(); - let groups = []; + let groups: CompletionGroup[] = []; let complete = settings.properties.complete || properties.defaults.complete; for (let c of complete) { @@ -45,31 +51,31 @@ export default class CompletionsUseCase { // eslint-disable-next-line no-await-in-loop let engines = await this.querySearchEngineItems(name, keywords); if (engines.length > 0) { - groups.push(new CompletionGroup('Search Engines', engines)); + groups.push({ name: 'Search Engines', items: engines }); } } else if (c === 'h') { // eslint-disable-next-line no-await-in-loop let histories = await this.queryHistoryItems(name, keywords); if (histories.length > 0) { - groups.push(new CompletionGroup('History', histories)); + groups.push({ name: 'History', items: histories }); } } else if (c === 'b') { // eslint-disable-next-line no-await-in-loop let bookmarks = await this.queryBookmarkItems(name, keywords); if (bookmarks.length > 0) { - groups.push(new CompletionGroup('Bookmarks', bookmarks)); + groups.push({ name: 'Bookmarks', items: bookmarks }); } } } - return new Completions(groups); + return groups; } // eslint-disable-next-line max-statements - async queryBuffer(name, keywords) { + async queryBuffer(name: string, keywords: string): Promise { let lastId = await this.tabPresenter.getLastSelectedId(); let trimmed = keywords.trim(); - let tabs = []; - if (trimmed.length > 0 && !isNaN(trimmed)) { + let tabs: Tab[] = []; + if (trimmed.length > 0 && !isNaN(Number(trimmed))) { let all = await this.tabPresenter.getAll(); let index = parseInt(trimmed, 10) - 1; if (index >= 0 && index < all.length) { @@ -77,18 +83,18 @@ export default class CompletionsUseCase { } } else if (trimmed === '%') { let all = await this.tabPresenter.getAll(); - let tab = all.find(t => t.active); + let tab = all.find(t => t.active) as Tab; tabs = [tab]; } else if (trimmed === '#') { if (typeof lastId !== 'undefined' && lastId !== null) { let all = await this.tabPresenter.getAll(); - let tab = all.find(t => t.id === lastId); + let tab = all.find(t => t.id === lastId) as Tab; tabs = [tab]; } } else { tabs = await this.completionsRepository.queryTabs(keywords, false); } - const flag = (tab) => { + const flag = (tab: Tab) => { if (tab.active) { return '%'; } else if (tab.id === lastId) { @@ -96,87 +102,90 @@ export default class CompletionsUseCase { } return ' '; }; - let items = tabs.map(tab => new CompletionItem({ + let items = tabs.map(tab => ({ caption: tab.index + 1 + ': ' + flag(tab) + ' ' + tab.title, content: name + ' ' + tab.title, url: tab.url, - icon: tab.favIconUrl + icon: tab.favIconUrl, })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return new Completions([new CompletionGroup('Buffers', items)]); + return [{ name: 'Buffers', items }]; } - queryBdelete(name, keywords) { + queryBdelete(name: string, keywords: string): Promise { return this.queryTabs(name, true, keywords); } - queryBdeleteForce(name, keywords) { + queryBdeleteForce( + name: string, keywords: string, + ): Promise { return this.queryTabs(name, false, keywords); } - querySet(name, keywords) { + querySet(name: string, keywords: string): Promise { let items = Object.keys(properties.docs).map((key) => { if (properties.types[key] === 'boolean') { return [ - new CompletionItem({ + { caption: key, content: name + ' ' + key, url: 'Enable ' + properties.docs[key], - }), - new CompletionItem({ + }, { caption: 'no' + key, content: name + ' no' + key, url: 'Disable ' + properties.docs[key], - }), + } ]; } return [ - new CompletionItem({ + { caption: key, content: name + ' ' + key, url: 'Set ' + properties.docs[key], - }) + } ]; }); - items = items.reduce((acc, val) => acc.concat(val), []); - items = items.filter((item) => { + let flatten = items.reduce((acc, val) => acc.concat(val), []); + flatten = flatten.filter((item) => { return item.caption.startsWith(keywords); }); - if (items.length === 0) { - return Promise.resolve(Completions.empty()); + if (flatten.length === 0) { + return Promise.resolve([]); } return Promise.resolve( - new Completions([new CompletionGroup('Properties', items)]) + [{ name: 'Properties', items: flatten }], ); } - async queryTabs(name, excludePinned, args) { + async queryTabs( + name: string, excludePinned: boolean, args: string, + ): Promise { let tabs = await this.completionsRepository.queryTabs(args, excludePinned); - let items = tabs.map(tab => new CompletionItem({ + let items = tabs.map(tab => ({ caption: tab.title, content: name + ' ' + tab.title, url: tab.url, icon: tab.favIconUrl })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return new Completions([new CompletionGroup('Buffers', items)]); + return [{ name: 'Buffers', items }]; } - async querySearchEngineItems(name, keywords) { + async querySearchEngineItems(name: string, keywords: string) { let settings = await this.settingRepository.get(); let engines = Object.keys(settings.search.engines) .filter(key => key.startsWith(keywords)); - return engines.map(key => new CompletionItem({ + return engines.map(key => ({ caption: key, content: name + ' ' + key, })); } - async queryHistoryItems(name, keywords) { + async queryHistoryItems(name: string, keywords: string) { let histories = await this.completionsRepository.queryHistories(keywords); histories = [histories] .map(filters.filterBlankTitle) @@ -184,19 +193,21 @@ 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, y) => x.visitCount < y.visitCount) + .sort((x: HistoryItem, y: HistoryItem) => { + return Number(x.visitCount) < Number(y.visitCount); + }) .slice(0, COMPLETION_ITEM_LIMIT); - return histories.map(page => new CompletionItem({ + return histories.map(page => ({ caption: page.title, content: name + ' ' + page.url, url: page.url })); } - async queryBookmarkItems(name, keywords) { + async queryBookmarkItems(name: string, keywords: string) { let bookmarks = await this.completionsRepository.queryBookmarks(keywords); return bookmarks.slice(0, COMPLETION_ITEM_LIMIT) - .map(page => new CompletionItem({ + .map(page => ({ caption: page.title, content: name + ' ' + page.url, url: page.url diff --git a/src/background/usecases/ConsoleUseCase.ts b/src/background/usecases/ConsoleUseCase.ts index e8e5d4a..60c0439 100644 --- a/src/background/usecases/ConsoleUseCase.ts +++ b/src/background/usecases/ConsoleUseCase.ts @@ -2,60 +2,64 @@ import TabPresenter from '../presenters/TabPresenter'; import ConsoleClient from '../infrastructures/ConsoleClient'; export default class ConsoleUseCase { + private tabPresenter: TabPresenter; + + private consoleClient: ConsoleClient; + constructor() { this.tabPresenter = new TabPresenter(); this.consoleClient = new ConsoleClient(); } - async showCommand() { + async showCommand(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.showCommand(tab.id, ''); + return this.consoleClient.showCommand(tab.id as number, ''); } - async showOpenCommand(alter) { + async showOpenCommand(alter: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); let command = 'open '; if (alter) { - command += tab.url; + command += tab.url || ''; } - return this.consoleClient.showCommand(tab.id, command); + return this.consoleClient.showCommand(tab.id as number, command); } - async showTabopenCommand(alter) { + async showTabopenCommand(alter: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); let command = 'tabopen '; if (alter) { - command += tab.url; + command += tab.url || ''; } - return this.consoleClient.showCommand(tab.id, command); + return this.consoleClient.showCommand(tab.id as number, command); } - async showWinopenCommand(alter) { + async showWinopenCommand(alter: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); let command = 'winopen '; if (alter) { - command += tab.url; + command += tab.url || ''; } - return this.consoleClient.showCommand(tab.id, command); + return this.consoleClient.showCommand(tab.id as number, command); } - async showBufferCommand() { + async showBufferCommand(): Promise { let tab = await this.tabPresenter.getCurrent(); let command = 'buffer '; - return this.consoleClient.showCommand(tab.id, command); + return this.consoleClient.showCommand(tab.id as number, command); } - async showAddbookmarkCommand(alter) { + async showAddbookmarkCommand(alter: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); let command = 'addbookmark '; if (alter) { - command += tab.title; + command += tab.title || ''; } - return this.consoleClient.showCommand(tab.id, command); + return this.consoleClient.showCommand(tab.id as number, command); } - async hideConsole() { + async hideConsole(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.hide(tab.id); + return this.consoleClient.hide(tab.id as number); } } diff --git a/src/background/usecases/FindUseCase.ts b/src/background/usecases/FindUseCase.ts index 224e4a9..d567800 100644 --- a/src/background/usecases/FindUseCase.ts +++ b/src/background/usecases/FindUseCase.ts @@ -3,22 +3,28 @@ import TabPresenter from '../presenters/TabPresenter'; import ConsoleClient from '../infrastructures/ConsoleClient'; export default class FindUseCase { + private tabPresenter: TabPresenter; + + private findRepository: FindRepository; + + private consoleClient: ConsoleClient; + constructor() { this.tabPresenter = new TabPresenter(); this.findRepository = new FindRepository(); this.consoleClient = new ConsoleClient(); } - getKeyword() { + getKeyword(): Promise { return this.findRepository.getKeyword(); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise { return this.findRepository.setKeyword(keyword); } - async findStart() { + async findStart(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.showFind(tab.id); + return this.consoleClient.showFind(tab.id as number); } } diff --git a/src/background/usecases/LinkUseCase.ts b/src/background/usecases/LinkUseCase.ts index 89412c5..2f4df7b 100644 --- a/src/background/usecases/LinkUseCase.ts +++ b/src/background/usecases/LinkUseCase.ts @@ -1,17 +1,17 @@ -import SettingRepository from '../repositories/SettingRepository'; import TabPresenter from '../presenters/TabPresenter'; export default class LinkUseCase { + private tabPresenter: TabPresenter; + constructor() { - this.settingRepository = new SettingRepository(); this.tabPresenter = new TabPresenter(); } - openToTab(url, tabId) { + openToTab(url: string, tabId: number): Promise { return this.tabPresenter.open(url, tabId); } - openNewTab(url, openerId, background) { + openNewTab(url: string, openerId: number, background: boolean): Promise { return this.tabPresenter.create(url, { openerTabId: openerId, active: !background }); diff --git a/src/background/usecases/MarkUseCase.ts b/src/background/usecases/MarkUseCase.ts index 39c796b..8b544aa 100644 --- a/src/background/usecases/MarkUseCase.ts +++ b/src/background/usecases/MarkUseCase.ts @@ -1,10 +1,17 @@ -import GlobalMark from '../domains/GlobalMark'; import TabPresenter from '../presenters/TabPresenter'; import MarkRepository from '../repositories/MarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class MarkUseCase { + private tabPresenter: TabPresenter; + + private markRepository: MarkRepository; + + private consoleClient: ConsoleClient; + + private contentMessageClient: ContentMessageClient; + constructor() { this.tabPresenter = new TabPresenter(); this.markRepository = new MarkRepository(); @@ -12,18 +19,19 @@ export default class MarkUseCase { this.contentMessageClient = new ContentMessageClient(); } - async setGlobal(key, x, y) { + async setGlobal(key: string, x: number, y: number): Promise { let tab = await this.tabPresenter.getCurrent(); - let mark = new GlobalMark(tab.id, tab.url, x, y); + let mark = { tabId: tab.id, url: tab.url, x, y }; return this.markRepository.setMark(key, mark); } - async jumpGlobal(key) { + async jumpGlobal(key: string): Promise { let current = await this.tabPresenter.getCurrent(); let mark = await this.markRepository.getMark(key); if (!mark) { - return this.consoleClient.showError(current.id, 'Mark is not set'); + return this.consoleClient.showError( + current.id as number, 'Mark is not set'); } return this.contentMessageClient.scrollTo( @@ -32,7 +40,7 @@ export default class MarkUseCase { return this.tabPresenter.select(mark.tabId); }).catch(async() => { let tab = await this.tabPresenter.create(mark.url); - let mark2 = new GlobalMark(tab.id, mark.url, mark.x, mark.y); + let mark2 = { tabId: tab.id, url: mark.url, x: mark.x, y: mark.y }; return this.markRepository.setMark(key, mark2); }); } diff --git a/src/background/usecases/SettingUseCase.ts b/src/background/usecases/SettingUseCase.ts index 9e17408..b66ce02 100644 --- a/src/background/usecases/SettingUseCase.ts +++ b/src/background/usecases/SettingUseCase.ts @@ -4,16 +4,20 @@ import PersistentSettingRepository from '../repositories/PersistentSettingReposi import SettingRepository from '../repositories/SettingRepository'; export default class SettingUseCase { + private persistentSettingRepository: PersistentSettingRepository; + + private settingRepository: SettingRepository; + constructor() { this.persistentSettingRepository = new PersistentSettingRepository(); this.settingRepository = new SettingRepository(); } - get() { + get(): Promise { return this.settingRepository.get(); } - async reload() { + async reload(): Promise { let settings = await this.persistentSettingRepository.load(); if (!settings) { settings = Setting.defaultSettings(); diff --git a/src/background/usecases/TabSelectUseCase.ts b/src/background/usecases/TabSelectUseCase.ts index 16b3e14..a0b52f0 100644 --- a/src/background/usecases/TabSelectUseCase.ts +++ b/src/background/usecases/TabSelectUseCase.ts @@ -1,11 +1,13 @@ import TabPresenter from '../presenters/TabPresenter'; export default class TabSelectUseCase { + private tabPresenter: TabPresenter; + constructor() { this.tabPresenter = new TabPresenter(); } - async selectPrev(count) { + async selectPrev(count: number): Promise { let tabs = await this.tabPresenter.getAll(); if (tabs.length < 2) { return; @@ -15,10 +17,10 @@ export default class TabSelectUseCase { return; } let select = (tab.index - count + tabs.length) % tabs.length; - return this.tabPresenter.select(tabs[select].id); + return this.tabPresenter.select(tabs[select].id as number); } - async selectNext(count) { + async selectNext(count: number): Promise { let tabs = await this.tabPresenter.getAll(); if (tabs.length < 2) { return; @@ -28,24 +30,24 @@ export default class TabSelectUseCase { return; } let select = (tab.index + count) % tabs.length; - return this.tabPresenter.select(tabs[select].id); + return this.tabPresenter.select(tabs[select].id as number); } - async selectFirst() { + async selectFirst(): Promise { let tabs = await this.tabPresenter.getAll(); - return this.tabPresenter.select(tabs[0].id); + return this.tabPresenter.select(tabs[0].id as number); } - async selectLast() { + async selectLast(): Promise { let tabs = await this.tabPresenter.getAll(); - return this.tabPresenter.select(tabs[tabs.length - 1].id); + return this.tabPresenter.select(tabs[tabs.length - 1].id as number); } - async selectPrevSelected() { + async selectPrevSelected(): Promise { let tabId = await this.tabPresenter.getLastSelectedId(); if (tabId === null || typeof tabId === 'undefined') { - return; + return Promise.resolve(); } - this.tabPresenter.select(tabId); + return this.tabPresenter.select(tabId); } } diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts index d930842..1615333 100644 --- a/src/background/usecases/TabUseCase.ts +++ b/src/background/usecases/TabUseCase.ts @@ -2,20 +2,24 @@ import TabPresenter from '../presenters/TabPresenter'; import BrowserSettingRepository from '../repositories/BrowserSettingRepository'; export default class TabUseCase { + private tabPresenter: TabPresenter; + + private browserSettingRepository: BrowserSettingRepository; + constructor() { this.tabPresenter = new TabPresenter(); this.browserSettingRepository = new BrowserSettingRepository(); } - async close(force) { + async close(force: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); if (!force && tab.pinned) { - return; + return Promise.resolve(); } - return this.tabPresenter.remove([tab.id]); + return this.tabPresenter.remove([tab.id as number]); } - async closeRight() { + async closeRight(): Promise { let tabs = await this.tabPresenter.getAll(); tabs.sort((t1, t2) => t1.index - t2.index); let index = tabs.findIndex(t => t.active); @@ -25,42 +29,42 @@ export default class TabUseCase { for (let i = index + 1; i < tabs.length; ++i) { let tab = tabs[i]; if (!tab.pinned) { - this.tabPresenter.remove(tab.id); + this.tabPresenter.remove([tab.id as number]); } } } - reopen() { + reopen(): Promise { return this.tabPresenter.reopen(); } - async reload(cache) { + async reload(cache: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.reload(tab.id, cache); + return this.tabPresenter.reload(tab.id as number, cache); } - async setPinned(pinned) { + async setPinned(pinned: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.setPinned(tab.id, pinned); + return this.tabPresenter.setPinned(tab.id as number, pinned); } - async togglePinned() { + async togglePinned(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.setPinned(tab.id, !tab.pinned); + return this.tabPresenter.setPinned(tab.id as number, !tab.pinned); } - async duplicate() { + async duplicate(): Promise { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.duplicate(tab.id); + return this.tabPresenter.duplicate(tab.id as number); } - async openPageSource() { + async openPageSource(): Promise { let tab = await this.tabPresenter.getCurrent(); let url = 'view-source:' + tab.url; return this.tabPresenter.create(url); } - async openHome(newTab) { + async openHome(newTab: boolean): Promise { let tab = await this.tabPresenter.getCurrent(); let urls = await this.browserSettingRepository.getHomepageUrls(); if (urls.length === 1 && urls[0] === 'about:home') { diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts index ed5112b..207f9e2 100644 --- a/src/background/usecases/VersionUseCase.ts +++ b/src/background/usecases/VersionUseCase.ts @@ -3,21 +3,25 @@ import TabPresenter from '../presenters/TabPresenter'; import NotifyPresenter from '../presenters/NotifyPresenter'; export default class VersionUseCase { + private tabPresenter: TabPresenter; + + private notifyPresenter: NotifyPresenter; + constructor() { this.tabPresenter = new TabPresenter(); this.notifyPresenter = new NotifyPresenter(); } - notify() { + 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); - this.notifyPresenter.notify(title, message, () => { + return this.notifyPresenter.notify(title, message, () => { this.tabPresenter.create(url); }); } - releaseNoteUrl(version) { + releaseNoteUrl(version?: string): string { if (version) { return `https://github.com/ueokande/vim-vixen/releases/tag/${version}`; } diff --git a/src/background/usecases/ZoomUseCase.ts b/src/background/usecases/ZoomUseCase.ts index 692d6d9..661c3cd 100644 --- a/src/background/usecases/ZoomUseCase.ts +++ b/src/background/usecases/ZoomUseCase.ts @@ -1,35 +1,39 @@ import TabPresenter from '../presenters/TabPresenter'; -const ZOOM_SETTINGS = [ +const ZOOM_SETTINGS: number[] = [ 0.33, 0.50, 0.66, 0.75, 0.80, 0.90, 1.00, 1.10, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00 ]; export default class ZoomUseCase { + private tabPresenter: TabPresenter; + constructor() { this.tabPresenter = new TabPresenter(); } - async zoomIn(tabId) { + async zoomIn(): Promise { let tab = await this.tabPresenter.getCurrent(); - let current = await this.tabPresenter.getZoom(tab.id); + let tabId = tab.id as number; + let current = await this.tabPresenter.getZoom(tabId); let factor = ZOOM_SETTINGS.find(f => f > current); if (factor) { - return this.tabPresenter.setZoom(tabId, factor); + return this.tabPresenter.setZoom(tabId as number, factor); } } - async zoomOut(tabId) { + async zoomOut(): Promise { let tab = await this.tabPresenter.getCurrent(); - let current = await this.tabPresenter.getZoom(tab.id); - let factor = [].concat(ZOOM_SETTINGS).reverse().find(f => f < current); + let tabId = tab.id as number; + let current = await this.tabPresenter.getZoom(tabId); + let factor = ZOOM_SETTINGS.slice(0).reverse().find(f => f < current); if (factor) { - return this.tabPresenter.setZoom(tabId, factor); + return this.tabPresenter.setZoom(tabId as number, factor); } } - zoomNutoral(tabId) { - return this.tabPresenter.setZoom(tabId, 1); + async zoomNutoral(): Promise { + let tab = await this.tabPresenter.getCurrent(); + return this.tabPresenter.setZoom(tab.id as number, 1); } - } diff --git a/src/background/usecases/filters.ts b/src/background/usecases/filters.ts index d057dca..44eb56f 100644 --- a/src/background/usecases/filters.ts +++ b/src/background/usecases/filters.ts @@ -1,40 +1,42 @@ -const filterHttp = (items) => { - let httpsHosts = items.map(x => new URL(x.url)) +type Item = browser.history.HistoryItem; + +const filterHttp = (items: Item[]): Item[] => { + let httpsHosts = items.map(x => new URL(x.url as string)) .filter(x => x.protocol === 'https:') .map(x => x.host); - httpsHosts = new Set(httpsHosts); + let hostsSet = new Set(httpsHosts); - return items.filter((item) => { - let url = new URL(item.url); - return url.protocol === 'https:' || !httpsHosts.has(url.host); + return items.filter((item: Item) => { + let url = new URL(item.url as string); + return url.protocol === 'https:' || !hostsSet.has(url.host); }); }; -const filterBlankTitle = (items) => { +const filterBlankTitle = (items: Item[]): Item[] => { return items.filter(item => item.title && item.title !== ''); }; -const filterByTailingSlash = (items) => { - let urls = items.map(item => new URL(item.url)); +const filterByTailingSlash = (items: Item[]): Item[] => { + let urls = items.map(item => new URL(item.url as string)); let simplePaths = urls .filter(url => url.hash === '' && url.search === '') .map(url => url.origin + url.pathname); - simplePaths = new Set(simplePaths); + let pathsSet = new Set(simplePaths); return items.filter((item) => { - let url = new URL(item.url); + let url = new URL(item.url as string); if (url.hash !== '' || url.search !== '' || url.pathname.slice(-1) !== '/') { return true; } - return !simplePaths.has(url.origin + url.pathname.slice(0, -1)); + return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); }); }; -const filterByPathname = (items, min) => { - let hash = {}; +const filterByPathname = (items: Item[], min: number): Item[] => { + let hash: {[key: string]: Item} = {}; for (let item of items) { - let url = new URL(item.url); + let url = new URL(item.url as string); let pathname = url.origin + url.pathname; if (!hash[pathname]) { hash[pathname] = item; @@ -49,10 +51,10 @@ const filterByPathname = (items, min) => { return filtered; }; -const filterByOrigin = (items, min) => { - let hash = {}; +const filterByOrigin = (items: Item[], min: number): Item[] => { + let hash: {[key: string]: Item} = {}; for (let item of items) { - let origin = new URL(item.url).origin; + let origin = new URL(item.url as string).origin; if (!hash[origin]) { hash[origin] = item; } else if (hash[origin].url.length > item.url.length) { diff --git a/src/background/usecases/parsers.ts b/src/background/usecases/parsers.ts index 43c8177..3616ac3 100644 --- a/src/background/usecases/parsers.ts +++ b/src/background/usecases/parsers.ts @@ -1,4 +1,4 @@ -const mustNumber = (v) => { +const mustNumber = (v: any): number => { let num = Number(v); if (isNaN(num)) { throw new Error('Not number: ' + v); @@ -6,8 +6,11 @@ const mustNumber = (v) => { return num; }; -const parseSetOption = (word, types) => { - let [key, value] = word.split('='); +const parseSetOption = ( + word: string, + types: { [key: string]: string }, +): any[] => { + let [key, value]: any[] = word.split('='); if (value === undefined) { value = !key.startsWith('no'); key = value ? key : key.slice(2); @@ -26,6 +29,7 @@ const parseSetOption = (word, types) => { case 'number': return [key, mustNumber(value)]; case 'boolean': return [key, value]; } + throw new Error('Unknown property type: ' + type); }; export { parseSetOption }; diff --git a/src/content/scrolls.ts b/src/content/scrolls.ts index f3124a1..bbf2491 100644 --- a/src/content/scrolls.ts +++ b/src/content/scrolls.ts @@ -90,16 +90,6 @@ class Scroller { } } -class RoughtScroller { - constructor(element) { - this.element = element; - } - - scroll(x, y) { - this.element.scrollTo(x, y); - } -} - const getScroll = () => { let target = scrollTarget(); return { x: target.scrollLeft, y: target.scrollTop }; diff --git a/test/background/domains/GlobalMark.test.ts b/test/background/domains/GlobalMark.test.ts deleted file mode 100644 index ed636e9..0000000 --- a/test/background/domains/GlobalMark.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import GlobalMark from 'background/domains/GlobalMark'; - -describe('background/domains/global-mark', () => { - describe('constructor and getter', () => { - let mark = new GlobalMark(1, 'http://example.com', 10, 30); - expect(mark.tabId).to.equal(1); - expect(mark.url).to.equal('http://example.com'); - expect(mark.x).to.equal(10); - expect(mark.y).to.equal(30); - }); -}); diff --git a/test/background/repositories/Mark.test.ts b/test/background/repositories/Mark.test.ts index 2a5b099..167e512 100644 --- a/test/background/repositories/Mark.test.ts +++ b/test/background/repositories/Mark.test.ts @@ -9,12 +9,11 @@ describe('background/repositories/mark', () => { }); it('get and set', async() => { - let mark = new GlobalMark(1, 'http://example.com', 10, 30); + let mark = { tabId: 1, url: 'http://example.com', x: 10, y: 30 }; repository.setMark('A', mark); let got = await repository.getMark('A'); - expect(got).to.be.a('object'); expect(got.tabId).to.equal(1); expect(got.url).to.equal('http://example.com'); expect(got.x).to.equal(10); diff --git a/test/background/repositories/Version.ts b/test/background/repositories/Version.ts deleted file mode 100644 index c7fa88b..0000000 --- a/test/background/repositories/Version.ts +++ /dev/null @@ -1,34 +0,0 @@ -import VersionRepository from 'background/repositories/Version'; - -describe("background/repositories/version", () => { - let versionRepository; - - beforeEach(() => { - versionRepository = new VersionRepository; - }); - - describe('#get', () => { - beforeEach(() => { - return browser.storage.local.remove('version'); - }); - - it('loads saved version', async() => { - await browser.storage.local.set({ version: '1.2.3' }); - let version = await this.versionRepository.get(); - expect(version).to.equal('1.2.3'); - }); - - it('returns undefined if no versions in storage', async() => { - let version = await storage.load(); - expect(version).to.be.a('undefined'); - }); - }); - - describe('#update', () => { - it('saves version string', async() => { - await versionRepository.update('2.3.4'); - let { version } = await browser.storage.local.get('version'); - expect(version).to.equal('2.3.4'); - }); - }); -}); -- 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 'src/background/controllers/SettingController.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