From e76ca380f733b515c31297a285d8bea44e074a1b Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 10 May 2019 22:27:20 +0900 Subject: Make addon-enabled as a clean architecture --- test/content/usecases/AddonEnabledUseCase.test.ts | 90 +++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/content/usecases/AddonEnabledUseCase.test.ts (limited to 'test/content/usecases') diff --git a/test/content/usecases/AddonEnabledUseCase.test.ts b/test/content/usecases/AddonEnabledUseCase.test.ts new file mode 100644 index 0000000..912bddf --- /dev/null +++ b/test/content/usecases/AddonEnabledUseCase.test.ts @@ -0,0 +1,90 @@ +import AddonEnabledRepository from '../../../src/content/repositories/AddonEnabledRepository'; +import AddonEnabledUseCase from '../../../src/content/usecases/AddonEnabledUseCase'; +import AddonIndicatorClient from '../../../src/content/client/AddonIndicatorClient'; +import { expect } from 'chai'; + +class MockAddonEnabledRepository implements AddonEnabledRepository { + private enabled: boolean; + + constructor(init: boolean) { + this.enabled = init; + } + + set(on: boolean): void { + this.enabled = on; + } + + get(): boolean { + return this.enabled; + } +} + +class MockAddonIndicatorClient implements AddonIndicatorClient { + public enabled: boolean; + + constructor(init: boolean) { + this.enabled = init; + } + + async setEnabled(enabled: boolean): Promise { + this.enabled = enabled; + return + } +} + +describe('AddonEnabledUseCase', () => { + let repository: MockAddonEnabledRepository; + let indicator: MockAddonIndicatorClient; + let sut: AddonEnabledUseCase; + + beforeEach(() => { + repository = new MockAddonEnabledRepository(true); + indicator = new MockAddonIndicatorClient(false); + sut = new AddonEnabledUseCase({ repository, indicator }); + }); + + describe('#enable', () => { + it('store and indicate as enabled', async() => { + await sut.enable(); + + expect(repository.get()).to.be.true; + expect(indicator.enabled).to.be.true; + }); + }); + + describe('#disable', async() => { + it('store and indicate as disabled', async() => { + await sut.disable(); + + expect(repository.get()).to.be.false; + expect(indicator.enabled).to.be.false; + }); + }); + + describe('#toggle', () => { + it('toggled enabled and disabled', async() => { + repository.set(true); + await sut.toggle(); + + expect(repository.get()).to.be.false; + expect(indicator.enabled).to.be.false; + + repository.set(false); + + await sut.toggle(); + + expect(repository.get()).to.be.true; + expect(indicator.enabled).to.be.true; + }); + }); + + describe('#getEnabled', () => { + it('returns current addon enabled', () => { + repository.set(true); + expect(sut.getEnabled()).to.be.true; + + repository.set(false); + expect(sut.getEnabled()).to.be.false; + }); + }); +}); -- cgit v1.2.3 From bacf83a32083c5a4c4a45c061288081423bbf18a Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 08:04:01 +0900 Subject: Make settings as a clean architecture --- src/content/actions/index.ts | 11 ---- src/content/actions/operation.ts | 7 ++- src/content/actions/setting.ts | 28 --------- src/content/client/SettingClient.ts | 17 ++++++ src/content/components/common/index.ts | 36 +++++------ src/content/components/common/keymapper.ts | 39 ++++++++---- src/content/components/common/mark.ts | 8 ++- .../components/top-content/follow-controller.ts | 8 ++- src/content/reducers/index.ts | 4 +- src/content/reducers/setting.ts | 40 ------------ src/content/repositories/SettingRepository.ts | 22 +++++++ src/content/usecases/SettingUseCase.ts | 24 ++++++++ test/content/actions/setting.test.ts | 43 ------------- test/content/reducers/setting.test.ts | 31 ---------- .../content/repositories/SettingRepository.test.ts | 30 +++++++++ test/content/usecases/SettingUseCaase.test.ts | 71 ++++++++++++++++++++++ 16 files changed, 223 insertions(+), 196 deletions(-) delete mode 100644 src/content/actions/setting.ts create mode 100644 src/content/client/SettingClient.ts delete mode 100644 src/content/reducers/setting.ts create mode 100644 src/content/repositories/SettingRepository.ts create mode 100644 src/content/usecases/SettingUseCase.ts delete mode 100644 test/content/actions/setting.test.ts delete mode 100644 test/content/reducers/setting.test.ts create mode 100644 test/content/repositories/SettingRepository.test.ts create mode 100644 test/content/usecases/SettingUseCaase.test.ts (limited to 'test/content/usecases') diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index 74353fb..4e395c5 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,13 +1,9 @@ import Redux from 'redux'; -import Settings from '../../shared/Settings'; import * as keyUtils from '../../shared/utils/keys'; // Find export const FIND_SET_KEYWORD = 'find.set.keyword'; -// Settings -export const SETTING_SET = 'setting.set'; - // User input export const INPUT_KEY_PRESS = 'input.key.press'; export const INPUT_CLEAR_KEYS = 'input.clear.keys'; @@ -37,11 +33,6 @@ export interface FindSetKeywordAction extends Redux.Action { found: boolean; } -export interface SettingSetAction extends Redux.Action { - type: typeof SETTING_SET; - settings: Settings, -} - export interface InputKeyPressAction extends Redux.Action { type: typeof INPUT_KEY_PRESS; key: keyUtils.Key; @@ -94,7 +85,6 @@ export interface NoopAction extends Redux.Action { } export type FindAction = FindSetKeywordAction | NoopAction; -export type SettingAction = SettingSetAction; export type InputAction = InputKeyPressAction | InputClearKeysAction; export type FollowAction = FollowControllerEnableAction | FollowControllerDisableAction | @@ -105,7 +95,6 @@ export type MarkAction = export type Action = FindAction | - SettingAction | InputAction | FollowAction | MarkAction | diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index 949f69f..f65d0bd 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -9,14 +9,16 @@ import * as consoleFrames from '../console-frames'; import * as markActions from './mark'; import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; +import { SettingRepositoryImpl } from '../repositories/SettingRepository'; let addonEnabledUseCase = new AddonEnabledUseCase(); +let settingRepository = new SettingRepositoryImpl(); // eslint-disable-next-line complexity, max-lines-per-function const exec = async( operation: operations.Operation, - settings: any, ): Promise => { + let settings = settingRepository.get(); let smoothscroll = settings.properties.smoothscroll; switch (operation.type) { case operations.ADDON_ENABLE: @@ -97,7 +99,8 @@ const exec = async( break; case operations.URLS_PASTE: urls.paste( - window, operation.newTab ? operation.newTab : false, settings.search + window, operation.newTab ? operation.newTab : false, + settings.search, ); break; default: diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts deleted file mode 100644 index 92f8559..0000000 --- a/src/content/actions/setting.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as actions from './index'; -import * as operations from '../../shared/operations'; -import * as messages from '../../shared/messages'; -import Settings, { Keymaps } from '../../shared/Settings'; - -const reservedKeymaps: Keymaps = { - '': { type: operations.CANCEL }, - '': { type: operations.CANCEL }, -}; - -const set = (settings: Settings): actions.SettingAction => { - return { - type: actions.SETTING_SET, - settings: { - ...settings, - keymaps: { ...settings.keymaps, ...reservedKeymaps }, - } - }; -}; - -const load = async(): Promise => { - let settings = await browser.runtime.sendMessage({ - type: messages.SETTINGS_QUERY, - }); - return set(settings); -}; - -export { set, load }; diff --git a/src/content/client/SettingClient.ts b/src/content/client/SettingClient.ts new file mode 100644 index 0000000..c67f544 --- /dev/null +++ b/src/content/client/SettingClient.ts @@ -0,0 +1,17 @@ +import Settings from '../../shared/Settings'; +import * as messages from '../../shared/messages'; + +export default interface SettingClient { + load(): Promise; + + // eslint-disable-next-line semi +} + +export class SettingClientImpl { + async load(): Promise { + let settings = await browser.runtime.sendMessage({ + type: messages.SETTINGS_QUERY, + }); + return settings as Settings; + } +} diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index be77812..899953d 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -2,22 +2,18 @@ import InputComponent from './input'; import FollowComponent from './follow'; import MarkComponent from './mark'; import KeymapperComponent from './keymapper'; -import * as settingActions from '../../actions/setting'; import * as messages from '../../../shared/messages'; import MessageListener from '../../MessageListener'; import * as blacklists from '../../../shared/blacklists'; import * as keys from '../../../shared/utils/keys'; -import * as actions from '../../actions'; import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; +import SettingUseCase from '../../usecases/SettingUseCase'; let addonEnabledUseCase = new AddonEnabledUseCase(); +let settingUseCase = new SettingUseCase(); export default class Common { - private win: Window; - - private store: any; - constructor(win: Window, store: any) { const input = new InputComponent(win.document.body); const follow = new FollowComponent(win); @@ -28,9 +24,6 @@ export default class Common { input.onKey((key: keys.Key) => mark.key(key)); input.onKey((key: keys.Key) => keymapper.key(key)); - this.win = win; - this.store = store; - this.reloadSettings(); new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); @@ -41,23 +34,22 @@ export default class Common { case messages.SETTINGS_CHANGED: return this.reloadSettings(); case messages.ADDON_TOGGLE_ENABLED: - addonEnabledUseCase.toggle(); + return addonEnabledUseCase.toggle(); } + return undefined; } - reloadSettings() { + async reloadSettings() { try { - this.store.dispatch(settingActions.load()) - .then((action: actions.SettingAction) => { - let enabled = !blacklists.includes( - action.settings.blacklist, this.win.location.href - ); - if (enabled) { - addonEnabledUseCase.enable(); - } else { - addonEnabledUseCase.disable(); - } - }); + let current = await settingUseCase.reload(); + let disabled = blacklists.includes( + current.blacklist, window.location.href, + ); + if (disabled) { + addonEnabledUseCase.disable(); + } else { + addonEnabledUseCase.enable(); + } } catch (e) { // Sometime sendMessage fails when background script is not ready. console.warn(e); diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts index 02579ec..c901ffe 100644 --- a/src/content/components/common/keymapper.ts +++ b/src/content/components/common/keymapper.ts @@ -4,8 +4,18 @@ import * as operations from '../../../shared/operations'; import * as keyUtils from '../../../shared/utils/keys'; import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; +import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; +import { Keymaps } from '../../../shared/Settings'; + +type KeymapEntityMap = Map; let addonEnabledUseCase = new AddonEnabledUseCase(); +let settingRepository = new SettingRepositoryImpl(); + +const reservedKeymaps: Keymaps = { + '': { type: operations.CANCEL }, + '': { type: operations.CANCEL }, +}; const mapStartsWith = ( mapping: keyUtils.Key[], @@ -29,18 +39,11 @@ export default class KeymapperComponent { this.store = store; } - // eslint-disable-next-line max-statements 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.map( - (e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op], - ) - ); - + let input = this.store.getState().input; + let keymaps = this.keymapEntityMap(); let matched = Array.from(keymaps.keys()).filter( (mapping: keyUtils.Key[]) => { return mapStartsWith(mapping, input.keys); @@ -62,11 +65,23 @@ export default class KeymapperComponent { return true; } let operation = keymaps.get(matched[0]) as operations.Operation; - let act = operationActions.exec( - operation, state.setting, - ); + let act = operationActions.exec(operation); this.store.dispatch(act); this.store.dispatch(inputActions.clearKeys()); return true; } + + private keymapEntityMap(): KeymapEntityMap { + let keymaps = { + ...settingRepository.get().keymaps, + ...reservedKeymaps, + }; + let entries = Object.entries(keymaps).map((entry) => { + return [ + keyUtils.fromMapKeys(entry[0]), + entry[1], + ]; + }) as [keyUtils.Key[], operations.Operation][]; + return new Map(entries); + } } diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index 1237385..77aa15d 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -4,6 +4,10 @@ import * as consoleFrames from '../..//console-frames'; import * as keyUtils from '../../../shared/utils/keys'; import Mark from '../../Mark'; +import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; + +let settingRepository = new SettingRepositoryImpl(); + const cancelKey = (key: keyUtils.Key): boolean => { return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); }; @@ -21,8 +25,8 @@ export default class MarkComponent { // eslint-disable-next-line max-statements key(key: keyUtils.Key) { - let { mark: markState, setting } = this.store.getState(); - let smoothscroll = setting.properties.smoothscroll; + let smoothscroll = settingRepository.get().properties.smoothscroll; + let { mark: markState } = this.store.getState(); if (!markState.setMode && !markState.jumpMode) { return false; diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index d49b22a..2fcf365 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -3,6 +3,10 @@ import * as messages from '../../../shared/messages'; import MessageListener, { WebMessageSender } from '../../MessageListener'; import HintKeyProducer from '../../hint-key-producer'; +import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; + +let settingRepository = new SettingRepositoryImpl(); + const broadcastMessage = (win: Window, message: messages.Message): void => { let json = JSON.stringify(message); let frames = [win.self].concat(Array.from(win.frames as any)); @@ -160,7 +164,7 @@ export default class FollowController { }); } - hintchars() { - return this.store.getState().setting.properties.hintchars; + private hintchars() { + return settingRepository.get().properties.hintchars; } } diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts index 6f11512..21e8918 100644 --- a/src/content/reducers/index.ts +++ b/src/content/reducers/index.ts @@ -1,6 +1,5 @@ import { combineReducers } from 'redux'; import find, { State as FindState } from './find'; -import setting, { State as SettingState } from './setting'; import input, { State as InputState } from './input'; import followController, { State as FollowControllerState } from './follow-controller'; @@ -8,12 +7,11 @@ import mark, { State as MarkState } from './mark'; export interface State { find: FindState; - setting: SettingState; input: InputState; followController: FollowControllerState; mark: MarkState; } export default combineReducers({ - find, setting, input, followController, mark, + find, input, followController, mark, }); diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts deleted file mode 100644 index 9ca1380..0000000 --- a/src/content/reducers/setting.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as actions from '../actions'; -import * as keyUtils from '../../shared/utils/keys'; -import * as operations from '../../shared/operations'; -import { Search, Properties, DefaultSetting } from '../../shared/Settings'; - -export interface State { - keymaps: { key: keyUtils.Key[], op: operations.Operation }[]; - search: Search; - properties: Properties; -} - -// defaultState does not refer due to the state is load from -// background on load. -const defaultState: State = { - keymaps: [], - search: DefaultSetting.search, - properties: DefaultSetting.properties, -}; - -export default function reducer( - state: State = defaultState, - action: actions.SettingAction, -): State { - switch (action.type) { - case actions.SETTING_SET: - return { - keymaps: Object.entries(action.settings.keymaps).map((entry) => { - return { - key: keyUtils.fromMapKeys(entry[0]), - op: entry[1], - }; - }), - properties: action.settings.properties, - search: action.settings.search, - }; - default: - return state; - } -} - diff --git a/src/content/repositories/SettingRepository.ts b/src/content/repositories/SettingRepository.ts new file mode 100644 index 0000000..ce13c25 --- /dev/null +++ b/src/content/repositories/SettingRepository.ts @@ -0,0 +1,22 @@ +import Settings, { DefaultSetting } from '../../shared/Settings'; + +let current: Settings = DefaultSetting; + +export default interface SettingRepository { + set(setting: Settings): void; + + get(): Settings; + + // eslint-disable-next-line semi +} + +export class SettingRepositoryImpl implements SettingRepository { + set(setting: Settings): void { + current = setting; + } + + get(): Settings { + return current; + } + +} diff --git a/src/content/usecases/SettingUseCase.ts b/src/content/usecases/SettingUseCase.ts new file mode 100644 index 0000000..765cb45 --- /dev/null +++ b/src/content/usecases/SettingUseCase.ts @@ -0,0 +1,24 @@ +import SettingRepository, { SettingRepositoryImpl } + from '../repositories/SettingRepository'; +import SettingClient, { SettingClientImpl } from '../client/SettingClient'; +import Settings from '../../shared/Settings'; + +export default class SettingUseCase { + private repository: SettingRepository; + + private client: SettingClient; + + constructor({ + repository = new SettingRepositoryImpl(), + client = new SettingClientImpl(), + } = {}) { + this.repository = repository; + this.client = client; + } + + async reload(): Promise { + let settings = await this.client.load(); + this.repository.set(settings); + return settings; + } +} diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts deleted file mode 100644 index c831433..0000000 --- a/test/content/actions/setting.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as actions from 'content/actions'; -import * as settingActions from 'content/actions/setting'; - -describe("setting actions", () => { - describe("set", () => { - it('create SETTING_SET action', () => { - let action = settingActions.set({ - 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.settings.properties.hintchars).to.equal('abcd1234'); - }); - - it('overrides cancel keys', () => { - let action = settingActions.set({ - keymaps: { - "k": { "type": "scroll.vertically", "count": -1 }, - "j": { "type": "scroll.vertically", "count": 1 }, - } - }); - 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 deleted file mode 100644 index 9b332aa..0000000 --- a/test/content/reducers/setting.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as actions from 'content/actions'; -import settingReducer from 'content/reducers/setting'; - -describe("content setting reducer", () => { - it('return the initial state', () => { - let state = settingReducer(undefined, {}); - expect(state.keymaps).to.be.empty; - }); - - it('return next state for SETTING_SET', () => { - let newSettings = { red: 'apple', yellow: 'banana' }; - let action = { - type: actions.SETTING_SET, - settings: { - keymaps: { - "zz": { type: "zoom.neutral" }, - "": { "type": "addon.toggle.enabled" } - }, - "blacklist": [] - } - } - let state = settingReducer(undefined, action); - 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/content/repositories/SettingRepository.test.ts b/test/content/repositories/SettingRepository.test.ts new file mode 100644 index 0000000..fea70b7 --- /dev/null +++ b/test/content/repositories/SettingRepository.test.ts @@ -0,0 +1,30 @@ +import { SettingRepositoryImpl } from '../../../src/content/repositories/SettingRepository'; +import { expect } from 'chai'; + +describe('SettingRepositoryImpl', () => { + it('updates and gets current value', () => { + let sut = new SettingRepositoryImpl(); + + let settings = { + keymaps: {}, + search: { + default: 'google', + engines: { + google: 'https://google.com/?q={}', + } + }, + properties: { + hintchars: 'abcd1234', + smoothscroll: false, + complete: 'sbh', + }, + blacklist: [], + } + + sut.set(settings); + + let actual = sut.get(); + expect(actual.properties.hintchars).to.equal('abcd1234'); + }); +}); + diff --git a/test/content/usecases/SettingUseCaase.test.ts b/test/content/usecases/SettingUseCaase.test.ts new file mode 100644 index 0000000..02cef78 --- /dev/null +++ b/test/content/usecases/SettingUseCaase.test.ts @@ -0,0 +1,71 @@ +import SettingRepository from '../../../src/content/repositories/SettingRepository'; +import SettingClient from '../../../src/content/client/SettingClient'; +import SettingUseCase from '../../../src/content/usecases/SettingUseCase'; +import Settings, { DefaultSetting } from '../../../src/shared/Settings'; +import { expect } from 'chai'; + +class MockSettingRepository implements SettingRepository { + private current: Settings; + + constructor() { + this.current = DefaultSetting; + } + + set(settings: Settings): void { + this.current= settings; + } + + get(): Settings { + return this.current; + } +} + +class MockSettingClient implements SettingClient { + private data: Settings; + + constructor(data: Settings) { + this.data = data; + } + + load(): Promise { + return Promise.resolve(this.data); + } +} + +describe('AddonEnabledUseCase', () => { + let repository: MockSettingRepository; + let client: MockSettingClient; + let sut: SettingUseCase; + + beforeEach(() => { + let testSettings = { + keymaps: {}, + search: { + default: 'google', + engines: { + google: 'https://google.com/?q={}', + } + }, + properties: { + hintchars: 'abcd1234', + smoothscroll: false, + complete: 'sbh', + }, + blacklist: [], + }; + + repository = new MockSettingRepository(); + client = new MockSettingClient(testSettings); + sut = new SettingUseCase({ repository, client }); + }); + + describe('#reload', () => { + it('loads settings and store to repository', async() => { + let settings = await sut.reload(); + expect(settings.properties.hintchars).to.equal('abcd1234'); + + let saved = repository.get(); + expect(saved.properties.hintchars).to.equal('abcd1234'); + }); + }); +}); -- cgit v1.2.3 From 1ba1660269b24446e9df7df0016de8c3e5596c8f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 11:37:18 +0900 Subject: Make find as a clean architecture --- src/content/actions/find.ts | 100 ------------ src/content/actions/index.ts | 11 -- src/content/client/ConsoleClient.ts | 30 ++++ src/content/client/FindClient.ts | 25 +++ src/content/components/top-content/find.ts | 26 +--- src/content/components/top-content/index.ts | 2 +- src/content/presenters/FindPresenter.ts | 59 ++++++++ src/content/reducers/find.ts | 25 --- src/content/reducers/index.ts | 4 +- src/content/repositories/FindRepository.ts | 19 +++ src/content/repositories/SettingRepository.ts | 1 - src/content/usecases/FindUseCase.ts | 81 ++++++++++ test/content/reducers/find.test.ts | 22 --- test/content/repositories/FindRepository.test.ts | 15 ++ test/content/usecases/FindUseCase.test.ts | 184 +++++++++++++++++++++++ 15 files changed, 423 insertions(+), 181 deletions(-) delete mode 100644 src/content/actions/find.ts create mode 100644 src/content/client/ConsoleClient.ts create mode 100644 src/content/client/FindClient.ts create mode 100644 src/content/presenters/FindPresenter.ts delete mode 100644 src/content/reducers/find.ts create mode 100644 src/content/repositories/FindRepository.ts create mode 100644 src/content/usecases/FindUseCase.ts delete mode 100644 test/content/reducers/find.test.ts create mode 100644 test/content/repositories/FindRepository.test.ts create mode 100644 test/content/usecases/FindUseCase.test.ts (limited to 'test/content/usecases') diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts deleted file mode 100644 index 53e03ae..0000000 --- a/src/content/actions/find.ts +++ /dev/null @@ -1,100 +0,0 @@ -// -// window.find(aString, aCaseSensitive, aBackwards, aWrapAround, -// aWholeWord, aSearchInFrames); -// -// NOTE: window.find is not standard API -// https://developer.mozilla.org/en-US/docs/Web/API/Window/find - -import * as messages from '../../shared/messages'; -import * as actions from './index'; -import * as consoleFrames from '../console-frames'; - -interface MyWindow extends Window { - find( - aString: string, - aCaseSensitive?: boolean, - aBackwards?: boolean, - aWrapAround?: boolean, - aWholeWord?: boolean, - aSearchInFrames?: boolean, - aShowDialog?: boolean): boolean; -} - -// eslint-disable-next-line no-var, vars-on-top, init-declarations -declare var window: MyWindow; - -const find = (str: string, backwards: boolean): boolean => { - let caseSensitive = false; - let wrapScan = true; - - - // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work - // because of same origin policy - - // eslint-disable-next-line no-extra-parens - let found = window.find(str, caseSensitive, backwards, wrapScan); - if (found) { - return found; - } - let sel = window.getSelection(); - if (sel) { - sel.removeAllRanges(); - } - - // eslint-disable-next-line no-extra-parens - return window.find(str, caseSensitive, backwards, wrapScan); -}; - -// eslint-disable-next-line max-statements -const findNext = async( - currentKeyword: string, reset: boolean, backwards: boolean, -): Promise => { - if (reset) { - let sel = window.getSelection(); - if (sel) { - sel.removeAllRanges(); - } - } - - let keyword = currentKeyword; - if (currentKeyword) { - browser.runtime.sendMessage({ - type: messages.FIND_SET_KEYWORD, - keyword: currentKeyword, - }); - } else { - keyword = await browser.runtime.sendMessage({ - type: messages.FIND_GET_KEYWORD, - }); - } - if (!keyword) { - await consoleFrames.postError('No previous search keywords'); - return { type: actions.NOOP }; - } - let found = find(keyword, backwards); - if (found) { - consoleFrames.postInfo('Pattern found: ' + keyword); - } else { - consoleFrames.postError('Pattern not found: ' + keyword); - } - - return { - type: actions.FIND_SET_KEYWORD, - keyword, - found, - }; -}; - -const next = ( - currentKeyword: string, reset: boolean, -): Promise => { - return findNext(currentKeyword, reset, false); -}; - -const prev = ( - currentKeyword: string, reset: boolean, -): Promise => { - return findNext(currentKeyword, reset, true); -}; - -export { next, prev }; diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index 4e395c5..f6d19aa 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,9 +1,6 @@ import Redux from 'redux'; import * as keyUtils from '../../shared/utils/keys'; -// Find -export const FIND_SET_KEYWORD = 'find.set.keyword'; - // User input export const INPUT_KEY_PRESS = 'input.key.press'; export const INPUT_CLEAR_KEYS = 'input.clear.keys'; @@ -27,12 +24,6 @@ export const MARK_SET_LOCAL = 'mark.set.local'; export const NOOP = 'noop'; -export interface FindSetKeywordAction extends Redux.Action { - type: typeof FIND_SET_KEYWORD; - keyword: string; - found: boolean; -} - export interface InputKeyPressAction extends Redux.Action { type: typeof INPUT_KEY_PRESS; key: keyUtils.Key; @@ -84,7 +75,6 @@ export interface NoopAction extends Redux.Action { type: typeof NOOP; } -export type FindAction = FindSetKeywordAction | NoopAction; export type InputAction = InputKeyPressAction | InputClearKeysAction; export type FollowAction = FollowControllerEnableAction | FollowControllerDisableAction | @@ -94,7 +84,6 @@ export type MarkAction = MarkCancelAction | MarkSetLocalAction | NoopAction; export type Action = - FindAction | InputAction | FollowAction | MarkAction | diff --git a/src/content/client/ConsoleClient.ts b/src/content/client/ConsoleClient.ts new file mode 100644 index 0000000..e7046e5 --- /dev/null +++ b/src/content/client/ConsoleClient.ts @@ -0,0 +1,30 @@ +import * as messages from '../../shared/messages'; + +export default interface ConsoleClient { + info(text: string): Promise; + error(text: string): Promise; + + // eslint-disable-next-line semi +} + +export class ConsoleClientImpl implements ConsoleClient { + async info(text: string): Promise { + await browser.runtime.sendMessage({ + type: messages.CONSOLE_FRAME_MESSAGE, + message: { + type: messages.CONSOLE_SHOW_INFO, + text, + }, + }); + } + + async error(text: string): Promise { + await browser.runtime.sendMessage({ + type: messages.CONSOLE_FRAME_MESSAGE, + message: { + type: messages.CONSOLE_SHOW_ERROR, + text, + }, + }); + } +} diff --git a/src/content/client/FindClient.ts b/src/content/client/FindClient.ts new file mode 100644 index 0000000..22cd3cb --- /dev/null +++ b/src/content/client/FindClient.ts @@ -0,0 +1,25 @@ +import * as messages from '../../shared/messages'; + +export default interface FindClient { + getGlobalLastKeyword(): Promise; + + setGlobalLastKeyword(keyword: string): Promise; + + // eslint-disable-next-line semi +} + +export class FindClientImpl implements FindClient { + async getGlobalLastKeyword(): Promise { + let keyword = await browser.runtime.sendMessage({ + type: messages.FIND_GET_KEYWORD, + }); + return keyword as string; + } + + async setGlobalLastKeyword(keyword: string): Promise { + await browser.runtime.sendMessage({ + type: messages.FIND_SET_KEYWORD, + keyword: keyword, + }); + } +} diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts index 74b95bc..c25cbeb 100644 --- a/src/content/components/top-content/find.ts +++ b/src/content/components/top-content/find.ts @@ -1,13 +1,12 @@ -import * as findActions from '../../actions/find'; import * as messages from '../../../shared/messages'; import MessageListener from '../../MessageListener'; -export default class FindComponent { - private store: any; +import FindUseCase from '../../usecases/FindUseCase'; - constructor(store: any) { - this.store = store; +let findUseCase = new FindUseCase(); +export default class FindComponent { + constructor() { new MessageListener().onWebMessage(this.onMessage.bind(this)); } @@ -20,27 +19,18 @@ export default class FindComponent { case messages.FIND_PREV: return this.prev(); } + return Promise.resolve(); } start(text: string) { - let state = this.store.getState().find; - - if (text.length === 0) { - return this.store.dispatch( - findActions.next(state.keyword as string, true)); - } - return this.store.dispatch(findActions.next(text, true)); + return findUseCase.startFind(text.length === 0 ? null : text); } next() { - let state = this.store.getState().find; - return this.store.dispatch( - findActions.next(state.keyword as string, false)); + return findUseCase.findNext(); } prev() { - let state = this.store.getState().find; - return this.store.dispatch( - findActions.prev(state.keyword as string, false)); + return findUseCase.findPrev(); } } diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts index 101edca..b9ef2dd 100644 --- a/src/content/components/top-content/index.ts +++ b/src/content/components/top-content/index.ts @@ -17,7 +17,7 @@ export default class TopContent { new CommonComponent(win, store); // eslint-disable-line no-new new FollowController(win, store); // eslint-disable-line no-new - new FindComponent(store); // eslint-disable-line no-new + new FindComponent(); // eslint-disable-line no-new // TODO make component consoleFrames.initialize(this.win.document); diff --git a/src/content/presenters/FindPresenter.ts b/src/content/presenters/FindPresenter.ts new file mode 100644 index 0000000..6dd03f8 --- /dev/null +++ b/src/content/presenters/FindPresenter.ts @@ -0,0 +1,59 @@ +import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; + +export default interface FindPresenter { + find(keyword: string, backwards: boolean): boolean; + + clearSelection(): void; + + // eslint-disable-next-line semi +} + +// window.find(aString, aCaseSensitive, aBackwards, aWrapAround, +// aWholeWord, aSearchInFrames); +// +// NOTE: window.find is not standard API +// https://developer.mozilla.org/en-US/docs/Web/API/Window/find +interface MyWindow extends Window { + find( + aString: string, + aCaseSensitive?: boolean, + aBackwards?: boolean, + aWrapAround?: boolean, + aWholeWord?: boolean, + aSearchInFrames?: boolean, + aShowDialog?: boolean): boolean; +} + +// eslint-disable-next-line no-var, vars-on-top, init-declarations +declare var window: MyWindow; + +export class FindPresenterImpl implements FindPresenter { + private consoleClient: ConsoleClient; + + constructor({ consoleClient = new ConsoleClientImpl() } = {}) { + this.consoleClient = consoleClient; + } + + find(keyword: string, backwards: boolean): boolean { + let caseSensitive = false; + let wrapScan = true; + + + // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work + // because of same origin policy + let found = window.find(keyword, caseSensitive, backwards, wrapScan); + if (found) { + return found; + } + this.clearSelection(); + + return window.find(keyword, caseSensitive, backwards, wrapScan); + } + + clearSelection(): void { + let sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + } + } +} diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts deleted file mode 100644 index 8c3e637..0000000 --- a/src/content/reducers/find.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as actions from '../actions'; - -export interface State { - keyword: string | null; - found: boolean; -} - -const defaultState: State = { - keyword: null, - found: false, -}; - -export default function reducer( - state: State = defaultState, - action: actions.FindAction, -): State { - switch (action.type) { - case actions.FIND_SET_KEYWORD: - return { ...state, - keyword: action.keyword, - found: action.found, }; - default: - return state; - } -} diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts index 21e8918..812a404 100644 --- a/src/content/reducers/index.ts +++ b/src/content/reducers/index.ts @@ -1,17 +1,15 @@ import { combineReducers } from 'redux'; -import find, { State as FindState } from './find'; import input, { State as InputState } from './input'; import followController, { State as FollowControllerState } from './follow-controller'; import mark, { State as MarkState } from './mark'; export interface State { - find: FindState; input: InputState; followController: FollowControllerState; mark: MarkState; } export default combineReducers({ - find, input, followController, mark, + input, followController, mark, }); diff --git a/src/content/repositories/FindRepository.ts b/src/content/repositories/FindRepository.ts new file mode 100644 index 0000000..85eca40 --- /dev/null +++ b/src/content/repositories/FindRepository.ts @@ -0,0 +1,19 @@ +export default interface FindRepository { + getLastKeyword(): string | null; + + setLastKeyword(keyword: string): void; + + // eslint-disable-next-line semi +} + +let current: string | null = null; + +export class FindRepositoryImpl implements FindRepository { + getLastKeyword(): string | null { + return current; + } + + setLastKeyword(keyword: string): void { + current = keyword; + } +} diff --git a/src/content/repositories/SettingRepository.ts b/src/content/repositories/SettingRepository.ts index ce13c25..711b2a2 100644 --- a/src/content/repositories/SettingRepository.ts +++ b/src/content/repositories/SettingRepository.ts @@ -18,5 +18,4 @@ export class SettingRepositoryImpl implements SettingRepository { get(): Settings { return current; } - } diff --git a/src/content/usecases/FindUseCase.ts b/src/content/usecases/FindUseCase.ts new file mode 100644 index 0000000..4fda323 --- /dev/null +++ b/src/content/usecases/FindUseCase.ts @@ -0,0 +1,81 @@ +import FindPresenter, { FindPresenterImpl } from '../presenters/FindPresenter'; +import FindRepository, { FindRepositoryImpl } + from '../repositories/FindRepository'; +import FindClient, { FindClientImpl } from '../client/FindClient'; +import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; + +export default class FindUseCase { + private presenter: FindPresenter; + + private repository: FindRepository; + + private client: FindClient; + + private consoleClient: ConsoleClient; + + constructor({ + presenter = new FindPresenterImpl() as FindPresenter, + repository = new FindRepositoryImpl(), + client = new FindClientImpl(), + consoleClient = new ConsoleClientImpl(), + } = {}) { + this.presenter = presenter; + this.repository = repository; + this.client = client; + this.consoleClient = consoleClient; + } + + async startFind(keyword: string | null): Promise { + this.presenter.clearSelection(); + if (keyword) { + this.saveKeyword(keyword); + } else { + let lastKeyword = await this.getKeyword(); + if (!lastKeyword) { + return this.showNoLastKeywordError(); + } + this.saveKeyword(lastKeyword); + } + return this.findNext(); + } + + findNext(): Promise { + return this.findNextPrev(false); + } + + findPrev(): Promise { + return this.findNextPrev(true); + } + + private async findNextPrev( + backwards: boolean, + ): Promise { + let keyword = await this.getKeyword(); + if (!keyword) { + return this.showNoLastKeywordError(); + } + let found = this.presenter.find(keyword, backwards); + if (found) { + this.consoleClient.info('Pattern found: ' + keyword); + } else { + this.consoleClient.error('Pattern not found: ' + keyword); + } + } + + private async getKeyword(): Promise { + let keyword = this.repository.getLastKeyword(); + if (!keyword) { + keyword = await this.client.getGlobalLastKeyword(); + } + return keyword; + } + + private async saveKeyword(keyword: string): Promise { + this.repository.setLastKeyword(keyword); + await this.client.setGlobalLastKeyword(keyword); + } + + private async showNoLastKeywordError(): Promise { + await this.consoleClient.error('No previous search keywords'); + } +} diff --git a/test/content/reducers/find.test.ts b/test/content/reducers/find.test.ts deleted file mode 100644 index 66a2c67..0000000 --- a/test/content/reducers/find.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as actions from 'content/actions'; -import findReducer from 'content/reducers/find'; - -describe("find reducer", () => { - it('return the initial state', () => { - let state = findReducer(undefined, {}); - expect(state).to.have.property('keyword', null); - expect(state).to.have.property('found', false); - }); - - it('return next state for FIND_SET_KEYWORD', () => { - let action = { - type: actions.FIND_SET_KEYWORD, - keyword: 'xyz', - found: true, - }; - let state = findReducer({}, action); - - expect(state.keyword).is.equal('xyz'); - expect(state.found).to.be.true; - }); -}); diff --git a/test/content/repositories/FindRepository.test.ts b/test/content/repositories/FindRepository.test.ts new file mode 100644 index 0000000..dcb2dff --- /dev/null +++ b/test/content/repositories/FindRepository.test.ts @@ -0,0 +1,15 @@ +import { FindRepositoryImpl } from '../../../src/content/repositories/FindRepository'; +import { expect } from 'chai'; + +describe('FindRepositoryImpl', () => { + it('updates and gets last keyword', () => { + let sut = new FindRepositoryImpl(); + + expect(sut.getLastKeyword()).to.be.null; + + sut.setLastKeyword('monkey'); + + expect(sut.getLastKeyword()).to.equal('monkey'); + }); +}); + diff --git a/test/content/usecases/FindUseCase.test.ts b/test/content/usecases/FindUseCase.test.ts new file mode 100644 index 0000000..347b817 --- /dev/null +++ b/test/content/usecases/FindUseCase.test.ts @@ -0,0 +1,184 @@ +import FindRepository from '../../../src/content/repositories/FindRepository'; +import FindPresenter from '../../../src/content/presenters/FindPresenter'; +import ConsoleClient from '../../../src/content/client/ConsoleClient'; +import FindClient from '../../../src/content/client/FindClient'; +import FindUseCase from '../../../src/content/usecases/FindUseCase'; +import { expect } from 'chai'; + +class MockFindRepository implements FindRepository { + public keyword: string | null; + + constructor() { + this.keyword = null; + } + + getLastKeyword(): string | null { + return this.keyword; + } + + setLastKeyword(keyword: string): void { + this.keyword = keyword; + } +} + +class MockFindPresenter implements FindPresenter { + public document: string; + + public highlighted: boolean; + + constructor() { + this.document = ''; + this.highlighted = false; + } + + find(keyword: string, _backward: boolean): boolean { + let found = this.document.includes(keyword); + this.highlighted = found; + return found; + } + + clearSelection(): void { + this.highlighted = false; + } +} + +class MockFindClient implements FindClient { + public keyword: string | null; + + constructor() { + this.keyword = null; + } + + getGlobalLastKeyword(): Promise { + return Promise.resolve(this.keyword); + } + + setGlobalLastKeyword(keyword: string): Promise { + this.keyword = keyword; + return Promise.resolve(); + } +} + +class MockConsoleClient implements ConsoleClient { + public isError: boolean; + + public text: string; + + constructor() { + this.isError = false; + this.text = ''; + } + + info(text: string): Promise { + this.isError = false; + this.text = text; + return Promise.resolve(); + } + + error(text: string): Promise { + this.isError = true; + this.text = text; + return Promise.resolve(); + } +} + +describe('FindUseCase', () => { + let repository: MockFindRepository; + let presenter: MockFindPresenter; + let client: MockFindClient; + let consoleClient: MockConsoleClient; + let sut: FindUseCase; + + beforeEach(() => { + repository = new MockFindRepository(); + presenter = new MockFindPresenter(); + client = new MockFindClient(); + consoleClient = new MockConsoleClient(); + sut = new FindUseCase({ repository, presenter, client, consoleClient }); + }); + + describe('#startFind', () => { + it('find next by ketword', async() => { + presenter.document = 'monkey punch'; + + await sut.startFind('monkey'); + + expect(await presenter.highlighted).to.be.true; + expect(await consoleClient.text).to.equal('Pattern found: monkey'); + expect(await repository.getLastKeyword()).to.equal('monkey'); + expect(await client.getGlobalLastKeyword()).to.equal('monkey'); + }); + + it('find next by last keyword', async() => { + presenter.document = 'gorilla kick'; + repository.keyword = 'gorilla'; + + await sut.startFind(null); + + expect(await presenter.highlighted).to.be.true; + expect(await consoleClient.text).to.equal('Pattern found: gorilla'); + expect(await repository.getLastKeyword()).to.equal('gorilla'); + expect(await client.getGlobalLastKeyword()).to.equal('gorilla'); + }); + + it('find next by global last keyword', async() => { + presenter.document = 'chimpanzee typing'; + + repository.keyword = null; + client.keyword = 'chimpanzee'; + + await sut.startFind(null); + + expect(await presenter.highlighted).to.be.true; + expect(await consoleClient.text).to.equal('Pattern found: chimpanzee'); + expect(await repository.getLastKeyword()).to.equal('chimpanzee'); + expect(await client.getGlobalLastKeyword()).to.equal('chimpanzee'); + }); + + it('find not found error', async() => { + presenter.document = 'zoo'; + + await sut.startFind('giraffe'); + + expect(await presenter.highlighted).to.be.false; + expect(await consoleClient.text).to.equal('Pattern not found: giraffe'); + expect(await repository.getLastKeyword()).to.equal('giraffe'); + expect(await client.getGlobalLastKeyword()).to.equal('giraffe'); + }); + + it('show errors when no last keywords', async() => { + repository.keyword = null; + client.keyword = null; + + await sut.startFind(null); + + expect(await consoleClient.text).to.equal('No previous search keywords'); + expect(await consoleClient.isError).to.be.true; + }); + }); + + describe('#findNext', () => { + it('finds by last keyword', async() => { + presenter.document = 'monkey punch'; + repository.keyword = 'monkey'; + + await sut.findNext(); + + expect(await presenter.highlighted).to.be.true; + expect(await consoleClient.text).to.equal('Pattern found: monkey'); + }); + + it('show errors when no last keywords', async() => { + repository.keyword = null; + client.keyword = null; + + await sut.findNext(); + + expect(await consoleClient.text).to.equal('No previous search keywords'); + expect(await consoleClient.isError).to.be.true; + }); + }); + + describe('#findPrev', () => { + }); +}); -- cgit v1.2.3 From ebfb172520f7077a15cd5d4e865e5d86593c55ac Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 15:56:18 +0900 Subject: find --- test/content/usecases/FindUseCase.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'test/content/usecases') diff --git a/test/content/usecases/FindUseCase.test.ts b/test/content/usecases/FindUseCase.test.ts index 347b817..2f966ae 100644 --- a/test/content/usecases/FindUseCase.test.ts +++ b/test/content/usecases/FindUseCase.test.ts @@ -113,7 +113,7 @@ describe('FindUseCase', () => { presenter.document = 'gorilla kick'; repository.keyword = 'gorilla'; - await sut.startFind(null); + await sut.startFind(undefined); expect(await presenter.highlighted).to.be.true; expect(await consoleClient.text).to.equal('Pattern found: gorilla'); @@ -127,7 +127,7 @@ describe('FindUseCase', () => { repository.keyword = null; client.keyword = 'chimpanzee'; - await sut.startFind(null); + await sut.startFind(undefined); expect(await presenter.highlighted).to.be.true; expect(await consoleClient.text).to.equal('Pattern found: chimpanzee'); @@ -150,7 +150,7 @@ describe('FindUseCase', () => { repository.keyword = null; client.keyword = null; - await sut.startFind(null); + await sut.startFind(undefined); expect(await consoleClient.text).to.equal('No previous search keywords'); expect(await consoleClient.isError).to.be.true; -- cgit v1.2.3 From c6288f19d93a05f96274dd172450b8350389c39f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 16:38:08 +0900 Subject: Mark set/jump as a clean architecture --- src/content/Mark.ts | 6 -- src/content/actions/index.ts | 11 +-- src/content/actions/mark.ts | 31 +------ src/content/client/MarkClient.ts | 28 ++++++ src/content/components/common/mark.ts | 48 +--------- src/content/domains/Mark.ts | 6 ++ src/content/presenters/ScrollPresenter.ts | 2 +- src/content/reducers/mark.ts | 8 -- src/content/repositories/MarkRepository.ts | 25 ++++++ src/content/usecases/MarkUseCase.ts | 62 +++++++++++++ test/content/actions/mark.test.ts | 10 --- test/content/mock/MockConsoleClient.ts | 26 ++++++ test/content/mock/MockScrollPresenter.ts | 47 ++++++++++ test/content/reducers/mark.test.ts | 10 --- test/content/repositories/MarkRepository.test.ts | 13 +++ test/content/usecases/FindUseCase.test.ts | 25 +----- test/content/usecases/MarkUseCase.test.ts | 107 +++++++++++++++++++++++ 17 files changed, 322 insertions(+), 143 deletions(-) delete mode 100644 src/content/Mark.ts create mode 100644 src/content/client/MarkClient.ts create mode 100644 src/content/domains/Mark.ts create mode 100644 src/content/repositories/MarkRepository.ts create mode 100644 src/content/usecases/MarkUseCase.ts create mode 100644 test/content/mock/MockConsoleClient.ts create mode 100644 test/content/mock/MockScrollPresenter.ts create mode 100644 test/content/repositories/MarkRepository.test.ts create mode 100644 test/content/usecases/MarkUseCase.test.ts (limited to 'test/content/usecases') diff --git a/src/content/Mark.ts b/src/content/Mark.ts deleted file mode 100644 index f1282fc..0000000 --- a/src/content/Mark.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface Mark { - x: number; - y: number; - // eslint-disable-next-line semi -} - diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index f6d19aa..eb826fc 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -20,7 +20,6 @@ export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace'; export const MARK_START_SET = 'mark.start.set'; export const MARK_START_JUMP = 'mark.start.jump'; export const MARK_CANCEL = 'mark.cancel'; -export const MARK_SET_LOCAL = 'mark.set.local'; export const NOOP = 'noop'; @@ -64,13 +63,6 @@ export interface MarkCancelAction extends Redux.Action { type: typeof MARK_CANCEL; } -export interface MarkSetLocalAction extends Redux.Action { - type: typeof MARK_SET_LOCAL; - key: string; - x: number; - y: number; -} - export interface NoopAction extends Redux.Action { type: typeof NOOP; } @@ -80,8 +72,7 @@ export type FollowAction = FollowControllerEnableAction | FollowControllerDisableAction | FollowControllerKeyPressAction | FollowControllerBackspaceAction; export type MarkAction = - MarkStartSetAction | MarkStartJumpAction | - MarkCancelAction | MarkSetLocalAction | NoopAction; + MarkStartSetAction | MarkStartJumpAction | MarkCancelAction | NoopAction; export type Action = InputAction | diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts index 5eb9554..1068507 100644 --- a/src/content/actions/mark.ts +++ b/src/content/actions/mark.ts @@ -1,5 +1,4 @@ import * as actions from './index'; -import * as messages from '../../shared/messages'; const startSet = (): actions.MarkAction => { return { type: actions.MARK_START_SET }; @@ -13,34 +12,6 @@ const cancel = (): actions.MarkAction => { return { type: actions.MARK_CANCEL }; }; -const setLocal = (key: string, x: number, y: number): actions.MarkAction => { - return { - type: actions.MARK_SET_LOCAL, - key, - x, - y, - }; -}; - -const setGlobal = (key: string, x: number, y: number): actions.MarkAction => { - browser.runtime.sendMessage({ - type: messages.MARK_SET_GLOBAL, - key, - x, - y, - }); - return { type: actions.NOOP }; -}; - -const jumpGlobal = (key: string): actions.MarkAction => { - browser.runtime.sendMessage({ - type: messages.MARK_JUMP_GLOBAL, - key, - }); - return { type: actions.NOOP }; -}; - export { - startSet, startJump, cancel, setLocal, - setGlobal, jumpGlobal, + startSet, startJump, cancel, }; diff --git a/src/content/client/MarkClient.ts b/src/content/client/MarkClient.ts new file mode 100644 index 0000000..b7cf535 --- /dev/null +++ b/src/content/client/MarkClient.ts @@ -0,0 +1,28 @@ +import Mark from '../domains/Mark'; +import * as messages from '../../shared/messages'; + +export default interface MarkClient { + setGloablMark(key: string, mark: Mark): Promise; + + jumpGlobalMark(key: string): Promise; + + // eslint-disable-next-line semi +} + +export class MarkClientImpl implements MarkClient { + async setGloablMark(key: string, mark: Mark): Promise { + await browser.runtime.sendMessage({ + type: messages.MARK_SET_GLOBAL, + key, + x: mark.x, + y: mark.y, + }); + } + + async jumpGlobalMark(key: string): Promise { + await browser.runtime.sendMessage({ + type: messages.MARK_JUMP_GLOBAL, + key, + }); + } +} diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index ddd1a38..eec95d6 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -1,22 +1,15 @@ import * as markActions from '../../actions/mark'; import * as consoleFrames from '../..//console-frames'; import * as keyUtils from '../../../shared/utils/keys'; -import Mark from '../../Mark'; -import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; -import { ScrollPresenterImpl } from '../../presenters/ScrollPresenter'; +import MarkUseCase from '../../usecases/MarkUseCase'; -let settingRepository = new SettingRepositoryImpl(); -let scrollPresenter = new ScrollPresenterImpl(); +let markUseCase = new MarkUseCase(); const cancelKey = (key: keyUtils.Key): boolean => { return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); }; -const globalKey = (key: string): boolean => { - return (/^[A-Z0-9]$/).test(key); -}; - export default class MarkComponent { private store: any; @@ -26,7 +19,6 @@ export default class MarkComponent { // eslint-disable-next-line max-statements key(key: keyUtils.Key) { - let smoothscroll = settingRepository.get().properties.smoothscroll; let { mark: markState } = this.store.getState(); if (!markState.setMode && !markState.jumpMode) { @@ -40,45 +32,13 @@ export default class MarkComponent { if (key.ctrlKey || key.metaKey || key.altKey) { consoleFrames.postError('Unknown mark'); - } else if (globalKey(key.key) && markState.setMode) { - this.doSetGlobal(key); - } else if (globalKey(key.key) && markState.jumpMode) { - this.doJumpGlobal(key); } else if (markState.setMode) { - this.doSet(key); + markUseCase.set(key.key); } else if (markState.jumpMode) { - this.doJump(markState.marks, key, smoothscroll); + markUseCase.jump(key.key); } this.store.dispatch(markActions.cancel()); return true; } - - doSet(key: keyUtils.Key) { - let { x, y } = scrollPresenter.getScroll(); - this.store.dispatch(markActions.setLocal(key.key, x, y)); - } - - doJump( - marks: { [key: string]: Mark }, - key: keyUtils.Key, - smoothscroll: boolean, - ) { - if (!marks[key.key]) { - consoleFrames.postError('Mark is not set'); - return; - } - - let { x, y } = marks[key.key]; - scrollPresenter.scrollTo(x, y, smoothscroll); - } - - doSetGlobal(key: keyUtils.Key) { - let { x, y } = scrollPresenter.getScroll(); - this.store.dispatch(markActions.setGlobal(key.key, x, y)); - } - - doJumpGlobal(key: keyUtils.Key) { - this.store.dispatch(markActions.jumpGlobal(key.key)); - } } diff --git a/src/content/domains/Mark.ts b/src/content/domains/Mark.ts new file mode 100644 index 0000000..f1282fc --- /dev/null +++ b/src/content/domains/Mark.ts @@ -0,0 +1,6 @@ +export default interface Mark { + x: number; + y: number; + // eslint-disable-next-line semi +} + diff --git a/src/content/presenters/ScrollPresenter.ts b/src/content/presenters/ScrollPresenter.ts index 9f47394..9286fb0 100644 --- a/src/content/presenters/ScrollPresenter.ts +++ b/src/content/presenters/ScrollPresenter.ts @@ -94,7 +94,7 @@ class Scroller { } } -type Point = { x: number, y: number }; +export type Point = { x: number, y: number }; export default interface ScrollPresenter { getScroll(): Point; diff --git a/src/content/reducers/mark.ts b/src/content/reducers/mark.ts index 7409938..a8f2f1b 100644 --- a/src/content/reducers/mark.ts +++ b/src/content/reducers/mark.ts @@ -1,16 +1,13 @@ -import Mark from '../Mark'; import * as actions from '../actions'; export interface State { setMode: boolean; jumpMode: boolean; - marks: { [key: string]: Mark }; } const defaultState: State = { setMode: false, jumpMode: false, - marks: {}, }; export default function reducer( @@ -24,11 +21,6 @@ export default function reducer( return { ...state, jumpMode: true }; case actions.MARK_CANCEL: return { ...state, setMode: false, jumpMode: false }; - case actions.MARK_SET_LOCAL: { - let marks = { ...state.marks }; - marks[action.key] = { x: action.x, y: action.y }; - return { ...state, setMode: false, marks }; - } default: return state; } diff --git a/src/content/repositories/MarkRepository.ts b/src/content/repositories/MarkRepository.ts new file mode 100644 index 0000000..ed5afe2 --- /dev/null +++ b/src/content/repositories/MarkRepository.ts @@ -0,0 +1,25 @@ +import Mark from '../domains/Mark'; + +export default interface MarkRepository { + set(key: string, mark: Mark): void; + + get(key: string): Mark | null; + + // eslint-disable-next-line semi +} + +const saved: {[key: string]: Mark} = {}; + +export class MarkRepositoryImpl implements MarkRepository { + set(key: string, mark: Mark): void { + saved[key] = mark; + } + + get(key: string): Mark | null { + let v = saved[key]; + if (!v) { + return null; + } + return { ...v }; + } +} diff --git a/src/content/usecases/MarkUseCase.ts b/src/content/usecases/MarkUseCase.ts new file mode 100644 index 0000000..ec63f2b --- /dev/null +++ b/src/content/usecases/MarkUseCase.ts @@ -0,0 +1,62 @@ +import ScrollPresenter, { ScrollPresenterImpl } + from '../presenters/ScrollPresenter'; +import MarkClient, { MarkClientImpl } from '../client/MarkClient'; +import MarkRepository, { MarkRepositoryImpl } + from '../repositories/MarkRepository'; +import SettingRepository, { SettingRepositoryImpl } + from '../repositories/SettingRepository'; +import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; + +export default class MarkUseCase { + private scrollPresenter: ScrollPresenter; + + private client: MarkClient; + + private repository: MarkRepository; + + private settingRepository: SettingRepository; + + private consoleClient: ConsoleClient; + + constructor({ + scrollPresenter = new ScrollPresenterImpl(), + client = new MarkClientImpl(), + repository = new MarkRepositoryImpl(), + settingRepository = new SettingRepositoryImpl(), + consoleClient = new ConsoleClientImpl(), + } = {}) { + this.scrollPresenter = scrollPresenter; + this.client = client; + this.repository = repository; + this.settingRepository = settingRepository; + this.consoleClient = consoleClient; + } + + async set(key: string): Promise { + let pos = this.scrollPresenter.getScroll(); + if (this.globalKey(key)) { + this.client.setGloablMark(key, pos); + await this.consoleClient.info(`Set global mark to '${key}'`); + } else { + this.repository.set(key, pos); + await this.consoleClient.info(`Set local mark to '${key}'`); + } + } + + async jump(key: string): Promise { + if (this.globalKey(key)) { + await this.client.jumpGlobalMark(key); + } else { + let pos = this.repository.get(key); + if (!pos) { + throw new Error('Mark is not set'); + } + let smooth = this.settingRepository.get().properties.smoothscroll; + this.scrollPresenter.scrollTo(pos.x, pos.y, smooth); + } + } + + private globalKey(key: string) { + return (/^[A-Z0-9]$/).test(key); + } +} diff --git a/test/content/actions/mark.test.ts b/test/content/actions/mark.test.ts index 6c6d59e..f2df367 100644 --- a/test/content/actions/mark.test.ts +++ b/test/content/actions/mark.test.ts @@ -22,14 +22,4 @@ describe('mark actions', () => { expect(action.type).to.equal(actions.MARK_CANCEL); }); }); - - describe('setLocal', () => { - it('create setLocal action', () => { - let action = markActions.setLocal('a', 20, 30); - expect(action.type).to.equal(actions.MARK_SET_LOCAL); - expect(action.key).to.equal('a'); - expect(action.x).to.equal(20); - expect(action.y).to.equal(30); - }); - }); }); diff --git a/test/content/mock/MockConsoleClient.ts b/test/content/mock/MockConsoleClient.ts new file mode 100644 index 0000000..8de2d83 --- /dev/null +++ b/test/content/mock/MockConsoleClient.ts @@ -0,0 +1,26 @@ +import ConsoleClient from '../../../src/content/client/ConsoleClient'; + +export default class MockConsoleClient implements ConsoleClient { + public isError: boolean; + + public text: string; + + constructor() { + this.isError = false; + this.text = ''; + } + + info(text: string): Promise { + this.isError = false; + this.text = text; + return Promise.resolve(); + } + + error(text: string): Promise { + this.isError = true; + this.text = text; + return Promise.resolve(); + } +} + + diff --git a/test/content/mock/MockScrollPresenter.ts b/test/content/mock/MockScrollPresenter.ts new file mode 100644 index 0000000..819569a --- /dev/null +++ b/test/content/mock/MockScrollPresenter.ts @@ -0,0 +1,47 @@ +import ScrollPresenter, { Point } from '../../../src/content/presenters/ScrollPresenter'; + +export default class MockScrollPresenter implements ScrollPresenter { + private pos: Point; + + constructor() { + this.pos = { x: 0, y: 0 }; + } + + getScroll(): Point { + return this.pos; + } + + scrollVertically(amount: number, _smooth: boolean): void { + this.pos.y += amount; + } + + scrollHorizonally(amount: number, _smooth: boolean): void { + this.pos.x += amount; + } + + scrollPages(amount: number, _smooth: boolean): void { + this.pos.x += amount; + } + + scrollTo(x: number, y: number, _smooth: boolean): void { + this.pos.x = x; + this.pos.y = y; + } + + scrollToTop(_smooth: boolean): void { + this.pos.y = 0; + } + + scrollToBottom(_smooth: boolean): void { + this.pos.y = Infinity; + } + + scrollToHome(_smooth: boolean): void { + this.pos.x = 0; + } + + scrollToEnd(_smooth: boolean): void { + this.pos.x = Infinity; + } +} + diff --git a/test/content/reducers/mark.test.ts b/test/content/reducers/mark.test.ts index 1a51c3e..918a560 100644 --- a/test/content/reducers/mark.test.ts +++ b/test/content/reducers/mark.test.ts @@ -6,7 +6,6 @@ describe("mark reducer", () => { let state = reducer(undefined, {}); expect(state.setMode).to.be.false; expect(state.jumpMode).to.be.false; - expect(state.marks).to.be.empty; }); it('starts set mode', () => { @@ -29,13 +28,4 @@ describe("mark reducer", () => { state = reducer({ jumpMode: true }, action); expect(state.jumpMode).to.be.false; }); - - it('stores local mark', () => { - let action = { type: actions.MARK_SET_LOCAL, key: 'a', x: 20, y: 30}; - let state = reducer({ setMode: true }, action); - expect(state.setMode).to.be.false; - expect(state.marks['a']).to.be.an('object') - expect(state.marks['a'].x).to.equal(20) - expect(state.marks['a'].y).to.equal(30) - }); }); diff --git a/test/content/repositories/MarkRepository.test.ts b/test/content/repositories/MarkRepository.test.ts new file mode 100644 index 0000000..7fced5f --- /dev/null +++ b/test/content/repositories/MarkRepository.test.ts @@ -0,0 +1,13 @@ +import { MarkRepositoryImpl } from '../../../src/content/repositories/MarkRepository'; +import { expect } from 'chai'; + +describe('MarkRepositoryImpl', () => { + it('save and load marks', () => { + let sut = new MarkRepositoryImpl(); + + sut.set('a', { x: 10, y: 20 }); + expect(sut.get('a')).to.deep.equal({ x: 10, y: 20 }); + expect(sut.get('b')).to.be.null; + }); +}); + diff --git a/test/content/usecases/FindUseCase.test.ts b/test/content/usecases/FindUseCase.test.ts index 2f966ae..c7bfd39 100644 --- a/test/content/usecases/FindUseCase.test.ts +++ b/test/content/usecases/FindUseCase.test.ts @@ -1,8 +1,8 @@ import FindRepository from '../../../src/content/repositories/FindRepository'; import FindPresenter from '../../../src/content/presenters/FindPresenter'; -import ConsoleClient from '../../../src/content/client/ConsoleClient'; import FindClient from '../../../src/content/client/FindClient'; import FindUseCase from '../../../src/content/usecases/FindUseCase'; +import MockConsoleClient from '../mock/MockConsoleClient'; import { expect } from 'chai'; class MockFindRepository implements FindRepository { @@ -59,29 +59,6 @@ class MockFindClient implements FindClient { } } -class MockConsoleClient implements ConsoleClient { - public isError: boolean; - - public text: string; - - constructor() { - this.isError = false; - this.text = ''; - } - - info(text: string): Promise { - this.isError = false; - this.text = text; - return Promise.resolve(); - } - - error(text: string): Promise { - this.isError = true; - this.text = text; - return Promise.resolve(); - } -} - describe('FindUseCase', () => { let repository: MockFindRepository; let presenter: MockFindPresenter; diff --git a/test/content/usecases/MarkUseCase.test.ts b/test/content/usecases/MarkUseCase.test.ts new file mode 100644 index 0000000..4f2dee4 --- /dev/null +++ b/test/content/usecases/MarkUseCase.test.ts @@ -0,0 +1,107 @@ +import MarkRepository from '../../../src/content/repositories/MarkRepository'; +import MarkUseCase from '../../../src/content/usecases/MarkUseCase'; +import MarkClient from '../../../src/content/client/MarkClient'; +import MockConsoleClient from '../mock/MockConsoleClient'; +import MockScrollPresenter from '../mock/MockScrollPresenter'; +import Mark from '../../../src/content/domains/Mark'; +import { expect } from 'chai'; + +class MockMarkRepository implements MarkRepository { + private current: {[key: string]: Mark}; + + constructor() { + this.current = {}; + } + + set(key: string, mark: Mark): void { + this.current[key] = mark; + } + + get(key: string): Mark | null { + return this.current[key]; + } +} + +class MockMarkClient implements MarkClient { + public marks: {[key: string]: Mark}; + public last: string; + + constructor() { + this.marks = {}; + this.last = ''; + } + + setGloablMark(key: string, mark: Mark): Promise { + this.marks[key] = mark; + return Promise.resolve(); + } + + jumpGlobalMark(key: string): Promise { + this.last = key + return Promise.resolve(); + } +} + +describe('MarkUseCase', () => { + let repository: MockMarkRepository; + let client: MockMarkClient; + let consoleClient: MockConsoleClient; + let scrollPresenter: MockScrollPresenter; + let sut: MarkUseCase; + + beforeEach(() => { + repository = new MockMarkRepository(); + client = new MockMarkClient(); + consoleClient = new MockConsoleClient(); + scrollPresenter = new MockScrollPresenter(); + sut = new MarkUseCase({ + repository, client, consoleClient, scrollPresenter, + }); + }); + + describe('#set', () => { + it('sets local mark', async() => { + scrollPresenter.scrollTo(10, 20, false); + + await sut.set('x'); + + expect(repository.get('x')).to.deep.equals({ x: 10, y: 20 }); + expect(consoleClient.text).to.equal("Set local mark to 'x'"); + }); + + it('sets global mark', async() => { + scrollPresenter.scrollTo(30, 40, false); + + await sut.set('Z'); + + expect(client.marks['Z']).to.deep.equals({ x: 30, y: 40 }); + expect(consoleClient.text).to.equal("Set global mark to 'Z'"); + }); + }); + + describe('#jump', () => { + it('jumps to local mark', async() => { + repository.set('x', { x: 20, y: 40 }); + + await sut.jump('x'); + + expect(scrollPresenter.getScroll()).to.deep.equals({ x: 20, y: 40 }); + }); + + it('throws an error when no local marks', () => { + return sut.jump('a').then(() => { + throw new Error('error'); + }).catch((e) => { + expect(e).to.be.instanceof(Error); + }) + }) + + it('jumps to global mark', async() => { + client.marks['Z'] = { x: 20, y: 0 }; + + await sut.jump('Z'); + + expect(client.last).to.equal('Z') + }); + }); +}); -- cgit v1.2.3 From 8cef5981b808bc1713170627c88dc26ca81063c1 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 17:45:58 +0900 Subject: Clipbaord as a clean architecture --- src/content/actions/operation.ts | 12 ++-- src/content/client/TabsClient.ts | 18 ++++++ src/content/repositories/ClipboardRepository.ts | 46 +++++++++++++++ src/content/urls.ts | 41 ------------- src/content/usecases/ClipboardUseCase.ts | 44 ++++++++++++++ src/shared/urls.ts | 10 ++-- test/content/usecases/ClipboardUseCase.test.ts | 76 +++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 52 deletions(-) create mode 100644 src/content/client/TabsClient.ts create mode 100644 src/content/repositories/ClipboardRepository.ts delete mode 100644 src/content/urls.ts create mode 100644 src/content/usecases/ClipboardUseCase.ts create mode 100644 test/content/usecases/ClipboardUseCase.test.ts (limited to 'test/content/usecases') diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index b264e36..28192d7 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -3,15 +3,15 @@ import * as actions from './index'; import * as messages from '../../shared/messages'; import * as navigates from '../navigates'; import * as focuses from '../focuses'; -import * as urls from '../urls'; -import * as consoleFrames from '../console-frames'; import * as markActions from './mark'; import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; +import ClipboardUseCase from '../usecases/ClipboardUseCase'; import { SettingRepositoryImpl } from '../repositories/SettingRepository'; import { ScrollPresenterImpl } from '../presenters/ScrollPresenter'; let addonEnabledUseCase = new AddonEnabledUseCase(); +let clipbaordUseCase = new ClipboardUseCase(); let settingRepository = new SettingRepositoryImpl(); let scrollPresenter = new ScrollPresenterImpl(); @@ -95,13 +95,11 @@ const exec = async( focuses.focusInput(); break; case operations.URLS_YANK: - urls.yank(window); - consoleFrames.postInfo('Yanked ' + window.location.href); + await clipbaordUseCase.yankCurrentURL(); break; case operations.URLS_PASTE: - urls.paste( - window, operation.newTab ? operation.newTab : false, - settings.search, + await clipbaordUseCase.openOrSearch( + operation.newTab ? operation.newTab : false, ); break; default: diff --git a/src/content/client/TabsClient.ts b/src/content/client/TabsClient.ts new file mode 100644 index 0000000..fe72e11 --- /dev/null +++ b/src/content/client/TabsClient.ts @@ -0,0 +1,18 @@ +import * as messages from '../../shared/messages'; + +export default interface TabsClient { + openUrl(url: string, newTab: boolean): Promise; + + // eslint-disable-next-line semi +} + +export class TabsClientImpl { + async openUrl(url: string, newTab: boolean): Promise { + await browser.runtime.sendMessage({ + type: messages.OPEN_URL, + url, + newTab, + }); + } +} + diff --git a/src/content/repositories/ClipboardRepository.ts b/src/content/repositories/ClipboardRepository.ts new file mode 100644 index 0000000..747ae6a --- /dev/null +++ b/src/content/repositories/ClipboardRepository.ts @@ -0,0 +1,46 @@ +export default interface ClipboardRepository { + read(): string; + + write(text: string): void; + + // eslint-disable-next-line semi +} + +export class ClipboardRepositoryImpl { + read(): string { + let textarea = window.document.createElement('textarea'); + window.document.body.append(textarea); + + textarea.style.position = 'fixed'; + textarea.style.top = '-100px'; + textarea.contentEditable = 'true'; + textarea.focus(); + + let ok = window.document.execCommand('paste'); + let value = textarea.textContent!!; + textarea.remove(); + + if (!ok) { + throw new Error('failed to access clipbaord'); + } + + return value; + } + + write(text: string): void { + let input = window.document.createElement('input'); + window.document.body.append(input); + + input.style.position = 'fixed'; + input.style.top = '-100px'; + input.value = text; + input.select(); + + let ok = window.document.execCommand('copy'); + input.remove(); + + if (!ok) { + throw new Error('failed to access clipbaord'); + } + } +} diff --git a/src/content/urls.ts b/src/content/urls.ts deleted file mode 100644 index 035b9bb..0000000 --- a/src/content/urls.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as messages from '../shared/messages'; -import * as urls from '../shared/urls'; -import { Search } from '../shared/Settings'; - -const yank = (win: Window) => { - let input = win.document.createElement('input'); - win.document.body.append(input); - - input.style.position = 'fixed'; - input.style.top = '-100px'; - input.value = win.location.href; - input.select(); - - win.document.execCommand('copy'); - - input.remove(); -}; - -const paste = (win: Window, newTab: boolean, search: Search) => { - let textarea = win.document.createElement('textarea'); - win.document.body.append(textarea); - - textarea.style.position = 'fixed'; - textarea.style.top = '-100px'; - textarea.contentEditable = 'true'; - textarea.focus(); - - if (win.document.execCommand('paste')) { - let value = textarea.textContent as string; - let url = urls.searchUrl(value, search); - browser.runtime.sendMessage({ - type: messages.OPEN_URL, - url, - newTab, - }); - } - - textarea.remove(); -}; - -export { yank, paste }; diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts new file mode 100644 index 0000000..b2ece2f --- /dev/null +++ b/src/content/usecases/ClipboardUseCase.ts @@ -0,0 +1,44 @@ +import * as urls from '../../shared/urls'; +import ClipboardRepository, { ClipboardRepositoryImpl } + from '../repositories/ClipboardRepository'; +import SettingRepository, { SettingRepositoryImpl } + from '../repositories/SettingRepository'; +import TabsClient, { TabsClientImpl } + from '../client/TabsClient'; +import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; + +export default class ClipboardUseCase { + private repository: ClipboardRepository; + + private settingRepository: SettingRepository; + + private client: TabsClient; + + private consoleClient: ConsoleClient; + + constructor({ + repository = new ClipboardRepositoryImpl(), + settingRepository = new SettingRepositoryImpl(), + client = new TabsClientImpl(), + consoleClient = new ConsoleClientImpl(), + } = {}) { + this.repository = repository; + this.settingRepository = settingRepository; + this.client = client; + this.consoleClient = consoleClient; + } + + async yankCurrentURL(): Promise { + let url = window.location.href; + this.repository.write(url); + await this.consoleClient.info('Yanked ' + url); + return Promise.resolve(url); + } + + async openOrSearch(newTab: boolean): Promise { + let search = this.settingRepository.get().search; + let text = this.repository.read(); + let url = urls.searchUrl(text, search); + await this.client.openUrl(url, newTab); + } +} diff --git a/src/shared/urls.ts b/src/shared/urls.ts index 18349c8..bbdb1ea 100644 --- a/src/shared/urls.ts +++ b/src/shared/urls.ts @@ -1,3 +1,5 @@ +import { Search } from './Settings'; + const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 return str.replace(/^\s+/, ''); @@ -5,7 +7,7 @@ const trimStart = (str: string): string => { const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:', 'mailto:', 'about:']; -const searchUrl = (keywords: string, searchSettings: any): string => { +const searchUrl = (keywords: string, search: Search): string => { try { let u = new URL(keywords); if (SUPPORTED_PROTOCOLS.includes(u.protocol.toLowerCase())) { @@ -17,12 +19,12 @@ const searchUrl = (keywords: string, searchSettings: any): string => { if (keywords.includes('.') && !keywords.includes(' ')) { return 'http://' + keywords; } - let template = searchSettings.engines[searchSettings.default]; + let template = search.engines[search.default]; let query = keywords; let first = trimStart(keywords).split(' ')[0]; - if (Object.keys(searchSettings.engines).includes(first)) { - template = searchSettings.engines[first]; + if (Object.keys(search.engines).includes(first)) { + template = search.engines[first]; query = trimStart(trimStart(keywords).slice(first.length)); } return template.replace('{}', encodeURIComponent(query)); diff --git a/test/content/usecases/ClipboardUseCase.test.ts b/test/content/usecases/ClipboardUseCase.test.ts new file mode 100644 index 0000000..862ee8a --- /dev/null +++ b/test/content/usecases/ClipboardUseCase.test.ts @@ -0,0 +1,76 @@ +import ClipboardRepository from '../../../src/content/repositories/ClipboardRepository'; +import TabsClient from '../../../src/content/client/TabsClient'; +import MockConsoleClient from '../mock/MockConsoleClient'; +import ClipboardUseCase from '../../../src/content/usecases/ClipboardUseCase'; +import { expect } from 'chai'; + +class MockClipboardRepository implements ClipboardRepository { + public clipboard: string; + + constructor() { + this.clipboard = ''; + } + + read(): string { + return this.clipboard; + } + + write(text: string): void { + this.clipboard = text; + } +} + +class MockTabsClient implements TabsClient { + public last: string; + + constructor() { + this.last = ''; + } + + openUrl(url: string, _newTab: boolean): Promise { + this.last = url; + return Promise.resolve(); + } +} + +describe('ClipboardUseCase', () => { + let repository: MockClipboardRepository; + let client: MockTabsClient; + let consoleClient: MockConsoleClient; + let sut: ClipboardUseCase; + + beforeEach(() => { + repository = new MockClipboardRepository(); + client = new MockTabsClient(); + consoleClient = new MockConsoleClient(); + sut = new ClipboardUseCase({ repository, client: client, consoleClient }); + }); + + describe('#yankCurrentURL', () => { + it('yanks current url', async () => { + let yanked = await sut.yankCurrentURL(); + + expect(yanked).to.equal(window.location.href); + expect(repository.clipboard).to.equal(yanked); + expect(consoleClient.text).to.equal('Yanked ' + yanked); + }); + }); + + describe('#openOrSearch', () => { + it('opens url from the clipboard', async () => { + let url = 'https://github.com/ueokande/vim-vixen' + repository.clipboard = url; + await sut.openOrSearch(true); + + expect(client.last).to.equal(url); + }); + + it('opens search results from the clipboard', async () => { + repository.clipboard = 'banana'; + await sut.openOrSearch(true); + + expect(client.last).to.equal('https://google.com/search?q=banana'); + }); + }); +}); + -- cgit v1.2.3 From c4dcdff9844e2404e3bc035f4cea9fce2f7770ab Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 19 May 2019 15:40:23 +0900 Subject: Add HintKeyProducer --- src/content/usecases/FollowMasterUseCase.ts | 2 +- src/content/usecases/HintKeyProducer.ts | 38 +++++++++++++++++++++++++++ test/content/hint-key-producer.test.ts | 24 ----------------- test/content/usecases/HintKeyProducer.test.ts | 25 ++++++++++++++++++ 4 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 src/content/usecases/HintKeyProducer.ts delete mode 100644 test/content/hint-key-producer.test.ts create mode 100644 test/content/usecases/HintKeyProducer.test.ts (limited to 'test/content/usecases') diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts index c2c6835..9cbb790 100644 --- a/src/content/usecases/FollowMasterUseCase.ts +++ b/src/content/usecases/FollowMasterUseCase.ts @@ -4,7 +4,7 @@ import FollowMasterRepository, { FollowMasterRepositoryImpl } from '../repositories/FollowMasterRepository'; import FollowSlaveClient, { FollowSlaveClientImpl } from '../client/FollowSlaveClient'; -import HintKeyProducer from '../hint-key-producer'; +import HintKeyProducer from './HintKeyProducer'; import SettingRepository, { SettingRepositoryImpl } from '../repositories/SettingRepository'; diff --git a/src/content/usecases/HintKeyProducer.ts b/src/content/usecases/HintKeyProducer.ts new file mode 100644 index 0000000..241cd56 --- /dev/null +++ b/src/content/usecases/HintKeyProducer.ts @@ -0,0 +1,38 @@ +export default class HintKeyProducer { + private charset: string; + + private counter: number[]; + + constructor(charset: string) { + if (charset.length === 0) { + throw new TypeError('charset is empty'); + } + + this.charset = charset; + this.counter = []; + } + + produce(): string { + this.increment(); + + return this.counter.map(x => this.charset[x]).join(''); + } + + private increment(): void { + let max = this.charset.length - 1; + if (this.counter.every(x => x === max)) { + this.counter = new Array(this.counter.length + 1).fill(0); + return; + } + + this.counter.reverse(); + let len = this.charset.length; + let num = this.counter.reduce((x, y, index) => x + y * len ** index) + 1; + for (let i = 0; i < this.counter.length; ++i) { + this.counter[i] = num % len; + num = ~~(num / len); + } + this.counter.reverse(); + } +} + diff --git a/test/content/hint-key-producer.test.ts b/test/content/hint-key-producer.test.ts deleted file mode 100644 index dcf477d..0000000 --- a/test/content/hint-key-producer.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import HintKeyProducer from 'content/hint-key-producer'; - -describe('HintKeyProducer class', () => { - describe('#constructor', () => { - it('throws an exception on empty charset', () => { - expect(() => new HintKeyProducer([])).to.throw(TypeError); - }); - }); - - describe('#produce', () => { - it('produce incremented keys', () => { - let charset = 'abc'; - let sequences = [ - 'a', 'b', 'c', - 'aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc', - 'aaa', 'aab', 'aac', 'aba'] - - let producer = new HintKeyProducer(charset); - for (let i = 0; i < sequences.length; ++i) { - expect(producer.produce()).to.equal(sequences[i]); - } - }); - }); -}); diff --git a/test/content/usecases/HintKeyProducer.test.ts b/test/content/usecases/HintKeyProducer.test.ts new file mode 100644 index 0000000..feafffb --- /dev/null +++ b/test/content/usecases/HintKeyProducer.test.ts @@ -0,0 +1,25 @@ +import HintKeyProducer from '../../../src/content/usecases/HintKeyProducer'; +import { expect } from 'chai'; + +describe('HintKeyProducer class', () => { + describe('#constructor', () => { + it('throws an exception on empty charset', () => { + expect(() => new HintKeyProducer('')).to.throw(TypeError); + }); + }); + + describe('#produce', () => { + it('produce incremented keys', () => { + let charset = 'abc'; + let sequences = [ + 'a', 'b', 'c', + 'aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc', + 'aaa', 'aab', 'aac', 'aba'] + + let producer = new HintKeyProducer(charset); + for (let i = 0; i < sequences.length; ++i) { + expect(producer.produce()).to.equal(sequences[i]); + } + }); + }); +}); -- cgit v1.2.3