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/repositories/AddonEnabledRepository.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/content/repositories/AddonEnabledRepository.test.ts (limited to 'test/content/repositories') diff --git a/test/content/repositories/AddonEnabledRepository.test.ts b/test/content/repositories/AddonEnabledRepository.test.ts new file mode 100644 index 0000000..3cea897 --- /dev/null +++ b/test/content/repositories/AddonEnabledRepository.test.ts @@ -0,0 +1,15 @@ +import { AddonEnabledRepositoryImpl } from '../../../src/content/repositories/AddonEnabledRepository'; +import { expect } from 'chai'; + +describe('AddonEnabledRepositoryImpl', () => { + it('updates and gets current value', () => { + let sut = new AddonEnabledRepositoryImpl(); + + sut.set(true); + expect(sut.get()).to.be.true; + + sut.set(false); + expect(sut.get()).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/repositories') 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/repositories') 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 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/repositories') 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 6d9aaef18c9f48684c8bb99e53c586e9781a69f0 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 19 May 2019 15:36:14 +0900 Subject: Add NavigationPresenter --- src/content/navigates.ts | 83 ------------ src/content/presenters/NavigationPresenter.ts | 98 ++++++++++++++ src/content/usecases/FocusUseCase.ts | 1 + src/content/usecases/NavigateUseCase.ts | 25 ++-- test/content/navigates.test.ts | 137 -------------------- .../content/presenters/NavigationPresenter.test.ts | 144 +++++++++++++++++++++ .../repositories/FollowKeyRepository.test.ts | 31 +++++ .../repositories/FollowMasterRepository.test.ts | 49 +++++++ .../repositories/FollowSlaveRepository.test.ts | 24 ++++ test/content/repositories/KeymapRepository.test.ts | 37 ++++++ .../content/repositories/MarkKeyRepository.test.ts | 36 ++++++ 11 files changed, 437 insertions(+), 228 deletions(-) delete mode 100644 src/content/navigates.ts create mode 100644 src/content/presenters/NavigationPresenter.ts delete mode 100644 test/content/navigates.test.ts create mode 100644 test/content/presenters/NavigationPresenter.test.ts create mode 100644 test/content/repositories/FollowKeyRepository.test.ts create mode 100644 test/content/repositories/FollowMasterRepository.test.ts create mode 100644 test/content/repositories/FollowSlaveRepository.test.ts create mode 100644 test/content/repositories/KeymapRepository.test.ts create mode 100644 test/content/repositories/MarkKeyRepository.test.ts (limited to 'test/content/repositories') diff --git a/src/content/navigates.ts b/src/content/navigates.ts deleted file mode 100644 index a2007a6..0000000 --- a/src/content/navigates.ts +++ /dev/null @@ -1,83 +0,0 @@ -const REL_PATTERN: {[key: string]: RegExp} = { - prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<>/i, -}; - -// Return the last element in the document matching the supplied selector -// and the optional filter, or null if there are no matches. -// eslint-disable-next-line func-style -function selectLast( - win: Window, - selector: string, - filter?: (e: E) => boolean, -): E | null { - let nodes = Array.from( - win.document.querySelectorAll(selector) as NodeListOf - ); - - if (filter) { - nodes = nodes.filter(filter); - } - return nodes.length ? nodes[nodes.length - 1] : null; -} - -const historyPrev = (win: Window): void => { - win.history.back(); -}; - -const historyNext = (win: Window): void => { - win.history.forward(); -}; - -// Code common to linkPrev and linkNext which navigates to the specified page. -const linkRel = (win: Window, rel: string): void => { - let link = selectLast(win, `link[rel~=${rel}][href]`); - if (link) { - win.location.href = link.href; - return; - } - - const pattern = REL_PATTERN[rel]; - - let a = selectLast(win, `a[rel~=${rel}][href]`) || - // `innerText` is much slower than `textContent`, but produces much better - // (i.e. less unexpected) results - selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText)); - - if (a) { - a.click(); - } -}; - -const linkPrev = (win: Window): void => { - linkRel(win, 'prev'); -}; - -const linkNext = (win: Window): void => { - linkRel(win, 'next'); -}; - -const parent = (win: Window): void => { - const loc = win.location; - if (loc.hash !== '') { - loc.hash = ''; - return; - } else if (loc.search !== '') { - loc.search = ''; - return; - } - - const basenamePattern = /\/[^/]+$/; - const lastDirPattern = /\/[^/]+\/$/; - if (basenamePattern.test(loc.pathname)) { - loc.pathname = loc.pathname.replace(basenamePattern, '/'); - } else if (lastDirPattern.test(loc.pathname)) { - loc.pathname = loc.pathname.replace(lastDirPattern, '/'); - } -}; - -const root = (win: Window): void => { - win.location.href = win.location.origin; -}; - -export { historyPrev, historyNext, linkPrev, linkNext, parent, root }; diff --git a/src/content/presenters/NavigationPresenter.ts b/src/content/presenters/NavigationPresenter.ts new file mode 100644 index 0000000..66110e5 --- /dev/null +++ b/src/content/presenters/NavigationPresenter.ts @@ -0,0 +1,98 @@ +export default interface NavigationPresenter { + openHistoryPrev(): void; + + openHistoryNext(): void; + + openLinkPrev(): void; + + openLinkNext(): void; + + openParent(): void; + + openRoot(): void; + + // eslint-disable-next-line semi +} + +const REL_PATTERN: {[key: string]: RegExp} = { + prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<>/i, +}; + +// Return the last element in the document matching the supplied selector +// and the optional filter, or null if there are no matches. +// eslint-disable-next-line func-style +function selectLast( + selector: string, + filter?: (e: E) => boolean, +): E | null { + let nodes = Array.from( + window.document.querySelectorAll(selector) as NodeListOf + ); + + if (filter) { + nodes = nodes.filter(filter); + } + return nodes.length ? nodes[nodes.length - 1] : null; +} + +export class NavigationPresenterImpl implements NavigationPresenter { + openHistoryPrev(): void { + window.history.back(); + } + + openHistoryNext(): void { + window.history.forward(); + } + + openLinkPrev(): void { + this.linkRel('prev'); + } + + openLinkNext(): void { + this.linkRel('next'); + } + + openParent(): void { + const loc = window.location; + if (loc.hash !== '') { + loc.hash = ''; + return; + } else if (loc.search !== '') { + loc.search = ''; + return; + } + + const basenamePattern = /\/[^/]+$/; + const lastDirPattern = /\/[^/]+\/$/; + if (basenamePattern.test(loc.pathname)) { + loc.pathname = loc.pathname.replace(basenamePattern, '/'); + } else if (lastDirPattern.test(loc.pathname)) { + loc.pathname = loc.pathname.replace(lastDirPattern, '/'); + } + } + + openRoot(): void { + window.location.href = window.location.origin; + } + + // Code common to linkPrev and linkNext which navigates to the specified page. + private linkRel(rel: 'prev' | 'next'): void { + let link = selectLast(`link[rel~=${rel}][href]`); + if (link) { + window.location.href = link.href; + return; + } + + const pattern = REL_PATTERN[rel]; + + let a = selectLast(`a[rel~=${rel}][href]`) || + // `innerText` is much slower than `textContent`, but produces much better + // (i.e. less unexpected) results + selectLast('a[href]', lnk => pattern.test(lnk.innerText)); + + if (a) { + a.click(); + } + } +} diff --git a/src/content/usecases/FocusUseCase.ts b/src/content/usecases/FocusUseCase.ts index 615442d..0ad4021 100644 --- a/src/content/usecases/FocusUseCase.ts +++ b/src/content/usecases/FocusUseCase.ts @@ -1,5 +1,6 @@ import FocusPresenter, { FocusPresenterImpl } from '../presenters/FocusPresenter'; + export default class FocusUseCases { private presenter: FocusPresenter; diff --git a/src/content/usecases/NavigateUseCase.ts b/src/content/usecases/NavigateUseCase.ts index f790212..6f82d3f 100644 --- a/src/content/usecases/NavigateUseCase.ts +++ b/src/content/usecases/NavigateUseCase.ts @@ -1,27 +1,36 @@ -import * as navigates from '../navigates'; +import NavigationPresenter, { NavigationPresenterImpl } + from '../presenters/NavigationPresenter'; + +export default class NavigateUseCase { + private navigationPresenter: NavigationPresenter; + + constructor({ + navigationPresenter = new NavigationPresenterImpl(), + } = {}) { + this.navigationPresenter = navigationPresenter; + } -export default class NavigateClass { openHistoryPrev(): void { - navigates.historyPrev(window); + this.navigationPresenter.openHistoryPrev(); } openHistoryNext(): void { - navigates.historyNext(window); + this.navigationPresenter.openHistoryNext(); } openLinkPrev(): void { - navigates.linkPrev(window); + this.navigationPresenter.openLinkPrev(); } openLinkNext(): void { - navigates.linkNext(window); + this.navigationPresenter.openLinkNext(); } openParent(): void { - navigates.parent(window); + this.navigationPresenter.openParent(); } openRoot(): void { - navigates.root(window); + this.navigationPresenter.openRoot(); } } diff --git a/test/content/navigates.test.ts b/test/content/navigates.test.ts deleted file mode 100644 index 1d73344..0000000 --- a/test/content/navigates.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import * as navigates from 'content/navigates'; - -const testRel = (done, rel, html) => { - const method = rel === 'prev' ? 'linkPrev' : 'linkNext'; - document.body.innerHTML = html; - navigates[method](window); - setTimeout(() => { - expect(document.location.hash).to.equal(`#${rel}`); - done(); - }, 0); -}; - -const testPrev = html => done => testRel(done, 'prev', html); -const testNext = html => done => testRel(done, 'next', html); - -describe('navigates module', () => { - describe('#linkPrev', () => { - it('navigates to elements whose rel attribute is "prev"', testPrev( - '' - )); - - it('navigates to elements whose rel attribute starts with "prev"', testPrev( - '' - )); - - it('navigates to elements whose rel attribute ends with "prev"', testPrev( - '' - )); - - it('navigates to elements whose rel attribute contains "prev"', testPrev( - '' - )); - - it('navigates to elements whose rel attribute is "prev"', testPrev( - '' - )); - - it('navigates to elements whose rel attribute starts with "prev"', testPrev( - 'click me' - )); - - it('navigates to elements whose rel attribute ends with "prev"', testPrev( - 'click me' - )); - - it('navigates to elements whose rel attribute contains "prev"', testPrev( - 'click me' - )); - - it('navigates to elements whose text matches "prev"', testPrev( - 'previewgo to prev' - )); - - it('navigates to elements whose text matches "previous"', testPrev( - 'previouslyprevious page' - )); - - it('navigates to elements whose decoded text matches "<<"', testPrev( - 'click me<<' - )); - - it('navigates to matching elements by clicking', testPrev( - `` - )); - - it('prefers link[rel~=prev] to a[rel~=prev]', testPrev( - '' - )); - - it('prefers a[rel~=prev] to a::text(pattern)', testPrev( - 'go to prev' - )); - }); - - describe('#linkNext', () => { - it('navigates to elements whose rel attribute is "next"', testNext( - '' - )); - - it('navigates to elements whose rel attribute starts with "next"', testNext( - '' - )); - - it('navigates to elements whose rel attribute ends with "next"', testNext( - '' - )); - - it('navigates to elements whose rel attribute contains "next"', testNext( - '' - )); - - it('navigates to elements whose rel attribute is "next"', testNext( - '' - )); - - it('navigates to elements whose rel attribute starts with "next"', testNext( - 'click me' - )); - - it('navigates to elements whose rel attribute ends with "next"', testNext( - 'click me' - )); - - it('navigates to elements whose rel attribute contains "next"', testNext( - 'click me' - )); - - it('navigates to elements whose text matches "next"', testNext( - 'inextricablego to next' - )); - - it('navigates to elements whose decoded text matches ">>"', testNext( - 'click me>>' - )); - - it('navigates to matching elements by clicking', testNext( - `` - )); - - it('prefers link[rel~=next] to a[rel~=next]', testNext( - '' - )); - - it('prefers a[rel~=next] to a::text(pattern)', testNext( - 'next page' - )); - }); - - describe('#parent', () => { - // NOTE: not able to test location - it('removes hash', () => { - window.location.hash = '#section-1'; - navigates.parent(window); - expect(document.location.hash).to.be.empty; - }); - }); -}); diff --git a/test/content/presenters/NavigationPresenter.test.ts b/test/content/presenters/NavigationPresenter.test.ts new file mode 100644 index 0000000..c1aca9a --- /dev/null +++ b/test/content/presenters/NavigationPresenter.test.ts @@ -0,0 +1,144 @@ +import NavigationPresenter, { NavigationPresenterImpl } + from '../../../src/content/presenters/NavigationPresenter'; +import { expect } from 'chai'; + +describe('NavigationPresenter', () => { + let sut; + + const testRel = (done, rel, html) => { + const method = rel === 'prev' ? sut.openLinkPrev.bind(sut) : sut.openLinkNext.bind(sut); + document.body.innerHTML = html; + method(); + setTimeout(() => { + expect(document.location.hash).to.equal(`#${rel}`); + done(); + }, 0); + }; + const testPrev = html => done => testRel(done, 'prev', html); + const testNext = html => done => testRel(done, 'next', html); + + before(() => { + sut = new NavigationPresenterImpl(); + }); + + describe('#linkPrev', () => { + it('navigates to elements whose rel attribute is "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute starts with "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute ends with "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute contains "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute is "prev"', testPrev( + '' + )); + + it('navigates to elements whose rel attribute starts with "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose rel attribute ends with "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose rel attribute contains "prev"', testPrev( + 'click me' + )); + + it('navigates to elements whose text matches "prev"', testPrev( + 'previewgo to prev' + )); + + it('navigates to elements whose text matches "previous"', testPrev( + 'previouslyprevious page' + )); + + it('navigates to elements whose decoded text matches "<<"', testPrev( + 'click me<<' + )); + + it('navigates to matching elements by clicking', testPrev( + `` + )); + + it('prefers link[rel~=prev] to a[rel~=prev]', testPrev( + '' + )); + + it('prefers a[rel~=prev] to a::text(pattern)', testPrev( + 'go to prev' + )); + }); + + describe('#linkNext', () => { + it('navigates to elements whose rel attribute is "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute starts with "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute ends with "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute contains "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute is "next"', testNext( + '' + )); + + it('navigates to elements whose rel attribute starts with "next"', testNext( + 'click me' + )); + + it('navigates to elements whose rel attribute ends with "next"', testNext( + 'click me' + )); + + it('navigates to elements whose rel attribute contains "next"', testNext( + 'click me' + )); + + it('navigates to elements whose text matches "next"', testNext( + 'inextricablego to next' + )); + + it('navigates to elements whose decoded text matches ">>"', testNext( + 'click me>>' + )); + + it('navigates to matching elements by clicking', testNext( + `` + )); + + it('prefers link[rel~=next] to a[rel~=next]', testNext( + '' + )); + + it('prefers a[rel~=next] to a::text(pattern)', testNext( + 'next page' + )); + }); + + describe('#parent', () => { + // NOTE: not able to test location + it('removes hash', () => { + window.location.hash = '#section-1'; + sut.openParent(); + expect(document.location.hash).to.be.empty; + }); + }); +}); diff --git a/test/content/repositories/FollowKeyRepository.test.ts b/test/content/repositories/FollowKeyRepository.test.ts new file mode 100644 index 0000000..eae58b9 --- /dev/null +++ b/test/content/repositories/FollowKeyRepository.test.ts @@ -0,0 +1,31 @@ +import FollowKeyRepository, { FollowKeyRepositoryImpl } + from '../../../src/content/repositories/FollowKeyRepository'; +import { expect } from 'chai'; + +describe('FollowKeyRepositoryImpl', () => { + let sut: FollowKeyRepository; + + before(() => { + sut = new FollowKeyRepositoryImpl(); + }); + + describe('#getKeys()/#pushKey()/#popKey()', () => { + it('enqueues keys', () => { + expect(sut.getKeys()).to.be.empty; + + sut.pushKey('a'); + sut.pushKey('b'); + sut.pushKey('c'); + expect(sut.getKeys()).to.deep.equal(['a', 'b', 'c']); + + sut.popKey(); + expect(sut.getKeys()).to.deep.equal(['a', 'b']); + + sut.clearKeys(); + expect(sut.getKeys()).to.be.empty; + }); + }); +}); + + + diff --git a/test/content/repositories/FollowMasterRepository.test.ts b/test/content/repositories/FollowMasterRepository.test.ts new file mode 100644 index 0000000..8c3f34e --- /dev/null +++ b/test/content/repositories/FollowMasterRepository.test.ts @@ -0,0 +1,49 @@ +import FollowMasterRepository, { FollowMasterRepositoryImpl } + from '../../../src/content/repositories/FollowMasterRepository'; +import { expect } from 'chai'; + +describe('FollowMasterRepositoryImpl', () => { + let sut: FollowMasterRepository; + + before(() => { + sut = new FollowMasterRepositoryImpl(); + }); + + describe('#getTags()/#addTag()/#clearTags()', () => { + it('gets, adds and clears tags', () => { + expect(sut.getTags()).to.be.empty; + + sut.addTag('a'); + sut.addTag('b'); + sut.addTag('c'); + expect(sut.getTags()).to.deep.equal(['a', 'b', 'c']); + + sut.clearTags(); + expect(sut.getTags()).to.be.empty; + }); + }); + + describe('#getTagsByPrefix', () => { + it('gets tags matched by prefix', () => { + for (let tag of ['a', 'aa', 'ab', 'b', 'ba', 'bb']) { + sut.addTag(tag); + } + expect(sut.getTagsByPrefix('a')).to.deep.equal(['a', 'aa', 'ab']); + expect(sut.getTagsByPrefix('aa')).to.deep.equal(['aa']); + expect(sut.getTagsByPrefix('b')).to.deep.equal(['b', 'ba', 'bb']); + expect(sut.getTagsByPrefix('c')).to.be.empty; + }); + }); + + describe('#setCurrentFollowMode()/#getCurrentNewTabMode()/#getCurrentBackgroundMode', () => { + it('updates and gets follow mode', () => { + sut.setCurrentFollowMode(false, true); + expect(sut.getCurrentNewTabMode()).to.be.false; + expect(sut.getCurrentBackgroundMode()).to.be.true; + + sut.setCurrentFollowMode(true, false); + expect(sut.getCurrentNewTabMode()).to.be.true; + expect(sut.getCurrentBackgroundMode()).to.be.false; + }); + }); +}); diff --git a/test/content/repositories/FollowSlaveRepository.test.ts b/test/content/repositories/FollowSlaveRepository.test.ts new file mode 100644 index 0000000..10cf094 --- /dev/null +++ b/test/content/repositories/FollowSlaveRepository.test.ts @@ -0,0 +1,24 @@ +import FollowSlaveRepository, { FollowSlaveRepositoryImpl } + from '../../../src/content/repositories/FollowSlaveRepository'; +import { expect } from 'chai'; + +describe('FollowSlaveRepository', () => { + let sut: FollowSlaveRepository; + + before(() => { + sut = new FollowSlaveRepositoryImpl(); + }); + + describe('#isFollowMode()/#enableFollowMode()/#disableFollowMode()', () => { + it('gets, adds updates follow mode', () => { + expect(sut.isFollowMode()).to.be.false; + + sut.enableFollowMode(); + expect(sut.isFollowMode()).to.be.true; + + sut.disableFollowMode(); + expect(sut.isFollowMode()).to.be.false; + }); + }); +}); + diff --git a/test/content/repositories/KeymapRepository.test.ts b/test/content/repositories/KeymapRepository.test.ts new file mode 100644 index 0000000..34704d9 --- /dev/null +++ b/test/content/repositories/KeymapRepository.test.ts @@ -0,0 +1,37 @@ +import KeymapRepository, { KeymapRepositoryImpl } + from '../../../src/content/repositories/KeymapRepository'; +import { expect } from 'chai'; + +describe('KeymapRepositoryImpl', () => { + let sut: KeymapRepository; + + before(() => { + sut = new KeymapRepositoryImpl(); + }); + + describe('#enqueueKey()', () => { + it('enqueues keys', () => { + sut.enqueueKey({ key: 'a' }); + sut.enqueueKey({ key: 'b' }); + let sequence = sut.enqueueKey({ key: 'c' }); + + expect(sequence.getKeyArray()).deep.equals([ + { key: 'a' }, { key: 'b' }, { key: 'c' }, + ]); + }); + }); + + describe('#clear()', () => { + it('clears keys', () => { + sut.enqueueKey({ key: 'a' }); + sut.enqueueKey({ key: 'b' }); + sut.enqueueKey({ key: 'c' }); + sut.clear(); + + let sequence = sut.enqueueKey({ key: 'a' }); + expect(sequence.length()).to.equal(1); + }); + }); +}); + + diff --git a/test/content/repositories/MarkKeyRepository.test.ts b/test/content/repositories/MarkKeyRepository.test.ts new file mode 100644 index 0000000..8592332 --- /dev/null +++ b/test/content/repositories/MarkKeyRepository.test.ts @@ -0,0 +1,36 @@ +import MarkRepository, { MarkKeyRepositoryImpl } + from '../../../src/content/repositories/MarkKeyRepository'; +import { expect } from 'chai'; + +describe('MarkKeyRepositoryImpl', () => { + let sut: MarkRepository; + + before(() => { + sut = new MarkKeyRepositoryImpl(); + }) + + describe('#isSetMode/#enableSetMode/#disabeSetMode', () => { + it('enables and disables set mode', () => { + expect(sut.isSetMode()).to.be.false; + + sut.enableSetMode(); + expect(sut.isSetMode()).to.be.true; + + sut.disabeSetMode(); + expect(sut.isSetMode()).to.be.false; + }); + }); + + describe('#isJumpMode/#enableJumpMode/#disabeJumpMode', () => { + it('enables and disables jump mode', () => { + expect(sut.isJumpMode()).to.be.false; + + sut.enableJumpMode(); + expect(sut.isJumpMode()).to.be.true; + + sut.disabeJumpMode(); + expect(sut.isJumpMode()).to.be.false; + }); + }); +}); + -- cgit v1.2.3