Make addon-enabled as a clean architecture

jh-changes
Shin'ya Ueoka 6 years ago
parent 05ef6a8ca3
commit e76ca380f7
  1. 6
      src/background/infrastructures/ContentMessageClient.ts
  2. 2
      src/content/MessageListener.ts
  3. 19
      src/content/actions/addon.ts
  4. 10
      src/content/actions/index.ts
  5. 19
      src/content/actions/operation.ts
  6. 16
      src/content/client/AddonIndicatorClient.ts
  7. 14
      src/content/components/common/index.ts
  8. 8
      src/content/components/common/keymapper.ts
  9. 13
      src/content/components/top-content/index.ts
  10. 22
      src/content/reducers/addon.ts
  11. 4
      src/content/reducers/index.ts
  12. 19
      src/content/repositories/AddonEnabledRepository.ts
  13. 40
      src/content/usecases/AddonEnabledUseCase.ts
  14. 17
      test/content/reducers/addon.test.ts
  15. 15
      test/content/repositories/AddonEnabledRepository.test.ts
  16. 90
      test/content/usecases/AddonEnabledUseCase.test.ts

@ -14,10 +14,10 @@ export default class ContentMessageClient {
} }
async getAddonEnabled(tabId: number): Promise<boolean> { async getAddonEnabled(tabId: number): Promise<boolean> {
let { enabled } = await browser.tabs.sendMessage(tabId, { let enabled = await browser.tabs.sendMessage(tabId, {
type: messages.ADDON_ENABLED_QUERY, type: messages.ADDON_ENABLED_QUERY,
}) as { enabled: boolean }; });
return enabled; return enabled as any as boolean;
} }
toggleAddonEnabled(tabId: number): Promise<void> { toggleAddonEnabled(tabId: number): Promise<void> {

@ -25,7 +25,7 @@ export default class MessageListener {
) { ) {
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(
(msg: any, sender: WebExtMessageSender) => { (msg: any, sender: WebExtMessageSender) => {
listener(valueOf(msg), sender); return listener(valueOf(msg), sender);
}, },
); );
} }

@ -1,19 +0,0 @@
import * as messages from '../../shared/messages';
import * as actions from './index';
const enable = (): Promise<actions.AddonAction> => setEnabled(true);
const disable = (): Promise<actions.AddonAction> => setEnabled(false);
const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => {
await browser.runtime.sendMessage({
type: messages.ADDON_ENABLED_RESPONSE,
enabled,
});
return {
type: actions.ADDON_SET_ENABLED,
enabled,
};
};
export { enable, disable, setEnabled };

@ -2,9 +2,6 @@ import Redux from 'redux';
import Settings from '../../shared/Settings'; import Settings from '../../shared/Settings';
import * as keyUtils from '../../shared/utils/keys'; import * as keyUtils from '../../shared/utils/keys';
// Enable/disable
export const ADDON_SET_ENABLED = 'addon.set.enabled';
// Find // Find
export const FIND_SET_KEYWORD = 'find.set.keyword'; export const FIND_SET_KEYWORD = 'find.set.keyword';
@ -34,11 +31,6 @@ export const MARK_SET_LOCAL = 'mark.set.local';
export const NOOP = 'noop'; export const NOOP = 'noop';
export interface AddonSetEnabledAction extends Redux.Action {
type: typeof ADDON_SET_ENABLED;
enabled: boolean;
}
export interface FindSetKeywordAction extends Redux.Action { export interface FindSetKeywordAction extends Redux.Action {
type: typeof FIND_SET_KEYWORD; type: typeof FIND_SET_KEYWORD;
keyword: string; keyword: string;
@ -101,7 +93,6 @@ export interface NoopAction extends Redux.Action {
type: typeof NOOP; type: typeof NOOP;
} }
export type AddonAction = AddonSetEnabledAction;
export type FindAction = FindSetKeywordAction | NoopAction; export type FindAction = FindSetKeywordAction | NoopAction;
export type SettingAction = SettingSetAction; export type SettingAction = SettingSetAction;
export type InputAction = InputKeyPressAction | InputClearKeysAction; export type InputAction = InputKeyPressAction | InputClearKeysAction;
@ -113,7 +104,6 @@ export type MarkAction =
MarkCancelAction | MarkSetLocalAction | NoopAction; MarkCancelAction | MarkSetLocalAction | NoopAction;
export type Action = export type Action =
AddonAction |
FindAction | FindAction |
SettingAction | SettingAction |
InputAction | InputAction |

@ -6,23 +6,28 @@ import * as navigates from '../navigates';
import * as focuses from '../focuses'; import * as focuses from '../focuses';
import * as urls from '../urls'; import * as urls from '../urls';
import * as consoleFrames from '../console-frames'; import * as consoleFrames from '../console-frames';
import * as addonActions from './addon';
import * as markActions from './mark'; import * as markActions from './mark';
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase';
let addonEnabledUseCase = new AddonEnabledUseCase();
// eslint-disable-next-line complexity, max-lines-per-function // eslint-disable-next-line complexity, max-lines-per-function
const exec = ( const exec = async(
operation: operations.Operation, operation: operations.Operation,
settings: any, settings: any,
addonEnabled: boolean, ): Promise<actions.Action> => {
): Promise<actions.Action> | actions.Action => {
let smoothscroll = settings.properties.smoothscroll; let smoothscroll = settings.properties.smoothscroll;
switch (operation.type) { switch (operation.type) {
case operations.ADDON_ENABLE: case operations.ADDON_ENABLE:
return addonActions.enable(); await addonEnabledUseCase.enable();
return { type: actions.NOOP };
case operations.ADDON_DISABLE: case operations.ADDON_DISABLE:
return addonActions.disable(); await addonEnabledUseCase.disable();
return { type: actions.NOOP };
case operations.ADDON_TOGGLE_ENABLED: case operations.ADDON_TOGGLE_ENABLED:
return addonActions.setEnabled(!addonEnabled); await addonEnabledUseCase.toggle();
return { type: actions.NOOP };
case operations.FIND_NEXT: case operations.FIND_NEXT:
window.top.postMessage(JSON.stringify({ window.top.postMessage(JSON.stringify({
type: messages.FIND_NEXT, type: messages.FIND_NEXT,

@ -0,0 +1,16 @@
import * as messages from '../../shared/messages';
export default interface AddonIndicatorClient {
setEnabled(enabled: boolean): Promise<void>;
// eslint-disable-next-line semi
}
export class AddonIndicatorClientImpl implements AddonIndicatorClient {
setEnabled(enabled: boolean): Promise<void> {
return browser.runtime.sendMessage({
type: messages.ADDON_ENABLED_RESPONSE,
enabled,
});
}
}

@ -5,11 +5,14 @@ import KeymapperComponent from './keymapper';
import * as settingActions from '../../actions/setting'; import * as settingActions from '../../actions/setting';
import * as messages from '../../../shared/messages'; import * as messages from '../../../shared/messages';
import MessageListener from '../../MessageListener'; import MessageListener from '../../MessageListener';
import * as addonActions from '../../actions/addon';
import * as blacklists from '../../../shared/blacklists'; import * as blacklists from '../../../shared/blacklists';
import * as keys from '../../../shared/utils/keys'; import * as keys from '../../../shared/utils/keys';
import * as actions from '../../actions'; import * as actions from '../../actions';
import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase';
let addonEnabledUseCase = new AddonEnabledUseCase();
export default class Common { export default class Common {
private win: Window; private win: Window;
@ -34,12 +37,11 @@ export default class Common {
} }
onMessage(message: messages.Message) { onMessage(message: messages.Message) {
let { enabled } = this.store.getState().addon;
switch (message.type) { switch (message.type) {
case messages.SETTINGS_CHANGED: case messages.SETTINGS_CHANGED:
return this.reloadSettings(); return this.reloadSettings();
case messages.ADDON_TOGGLE_ENABLED: case messages.ADDON_TOGGLE_ENABLED:
this.store.dispatch(addonActions.setEnabled(!enabled)); addonEnabledUseCase.toggle();
} }
} }
@ -50,7 +52,11 @@ export default class Common {
let enabled = !blacklists.includes( let enabled = !blacklists.includes(
action.settings.blacklist, this.win.location.href action.settings.blacklist, this.win.location.href
); );
this.store.dispatch(addonActions.setEnabled(enabled)); if (enabled) {
addonEnabledUseCase.enable();
} else {
addonEnabledUseCase.disable();
}
}); });
} catch (e) { } catch (e) {
// Sometime sendMessage fails when background script is not ready. // Sometime sendMessage fails when background script is not ready.

@ -3,6 +3,10 @@ import * as operationActions from '../../actions/operation';
import * as operations from '../../../shared/operations'; import * as operations from '../../../shared/operations';
import * as keyUtils from '../../../shared/utils/keys'; import * as keyUtils from '../../../shared/utils/keys';
import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase';
let addonEnabledUseCase = new AddonEnabledUseCase();
const mapStartsWith = ( const mapStartsWith = (
mapping: keyUtils.Key[], mapping: keyUtils.Key[],
keys: keyUtils.Key[], keys: keyUtils.Key[],
@ -41,7 +45,7 @@ export default class KeymapperComponent {
(mapping: keyUtils.Key[]) => { (mapping: keyUtils.Key[]) => {
return mapStartsWith(mapping, input.keys); return mapStartsWith(mapping, input.keys);
}); });
if (!state.addon.enabled) { if (!addonEnabledUseCase.getEnabled()) {
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
// the addon disabled // the addon disabled
matched = matched.filter((keys) => { matched = matched.filter((keys) => {
@ -59,7 +63,7 @@ export default class KeymapperComponent {
} }
let operation = keymaps.get(matched[0]) as operations.Operation; let operation = keymaps.get(matched[0]) as operations.Operation;
let act = operationActions.exec( let act = operationActions.exec(
operation, state.setting, state.addon.enabled operation, state.setting,
); );
this.store.dispatch(act); this.store.dispatch(act);
this.store.dispatch(inputActions.clearKeys()); this.store.dispatch(inputActions.clearKeys());

@ -5,15 +5,15 @@ import * as consoleFrames from '../../console-frames';
import * as messages from '../../../shared/messages'; import * as messages from '../../../shared/messages';
import MessageListener from '../../MessageListener'; import MessageListener from '../../MessageListener';
import * as scrolls from '../../scrolls'; import * as scrolls from '../../scrolls';
import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase';
let addonEnabledUseCase = new AddonEnabledUseCase();
export default class TopContent { export default class TopContent {
private win: Window; private win: Window;
private store: any;
constructor(win: Window, store: any) { constructor(win: Window, store: any) {
this.win = win; this.win = win;
this.store = store;
new CommonComponent(win, store); // eslint-disable-line no-new new CommonComponent(win, store); // eslint-disable-line no-new
new FollowController(win, store); // eslint-disable-line no-new new FollowController(win, store); // eslint-disable-line no-new
@ -36,14 +36,11 @@ export default class TopContent {
} }
onBackgroundMessage(message: messages.Message) { onBackgroundMessage(message: messages.Message) {
let addonState = this.store.getState().addon; let addonEnabled = addonEnabledUseCase.getEnabled();
switch (message.type) { switch (message.type) {
case messages.ADDON_ENABLED_QUERY: case messages.ADDON_ENABLED_QUERY:
return Promise.resolve({ return Promise.resolve(addonEnabled);
type: messages.ADDON_ENABLED_RESPONSE,
enabled: addonState.enabled,
});
case messages.TAB_SCROLL_TO: case messages.TAB_SCROLL_TO:
return scrolls.scrollTo(message.x, message.y, false); return scrolls.scrollTo(message.x, message.y, false);
} }

@ -1,22 +0,0 @@
import * as actions from '../actions';
export interface State {
enabled: boolean;
}
const defaultState: State = {
enabled: true,
};
export default function reducer(
state: State = defaultState,
action: actions.AddonAction,
): State {
switch (action.type) {
case actions.ADDON_SET_ENABLED:
return { ...state,
enabled: action.enabled, };
default:
return state;
}
}

@ -1,5 +1,4 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import addon, { State as AddonState } from './addon';
import find, { State as FindState } from './find'; import find, { State as FindState } from './find';
import setting, { State as SettingState } from './setting'; import setting, { State as SettingState } from './setting';
import input, { State as InputState } from './input'; import input, { State as InputState } from './input';
@ -8,7 +7,6 @@ import followController, { State as FollowControllerState }
import mark, { State as MarkState } from './mark'; import mark, { State as MarkState } from './mark';
export interface State { export interface State {
addon: AddonState;
find: FindState; find: FindState;
setting: SettingState; setting: SettingState;
input: InputState; input: InputState;
@ -17,5 +15,5 @@ export interface State {
} }
export default combineReducers({ export default combineReducers({
addon, find, setting, input, followController, mark, find, setting, input, followController, mark,
}); });

@ -0,0 +1,19 @@
let enabled: boolean = false;
export default interface AddonEnabledRepository {
set(on: boolean): void;
get(): boolean;
// eslint-disable-next-line semi
}
export class AddonEnabledRepositoryImpl implements AddonEnabledRepository {
set(on: boolean): void {
enabled = on;
}
get(): boolean {
return enabled;
}
}

@ -0,0 +1,40 @@
import AddonIndicatorClient, { AddonIndicatorClientImpl }
from '../client/AddonIndicatorClient';
import AddonEnabledRepository, { AddonEnabledRepositoryImpl }
from '../repositories/AddonEnabledRepository';
export default class AddonEnabledUseCase {
private indicator: AddonIndicatorClient;
private repository: AddonEnabledRepository;
constructor({
indicator = new AddonIndicatorClientImpl(),
repository = new AddonEnabledRepositoryImpl(),
} = {}) {
this.indicator = indicator;
this.repository = repository;
}
async enable(): Promise<void> {
await this.setEnabled(true);
}
async disable(): Promise<void> {
await this.setEnabled(false);
}
async toggle(): Promise<void> {
let current = this.repository.get();
await this.setEnabled(!current);
}
getEnabled(): boolean {
return this.repository.get();
}
private async setEnabled(on: boolean): Promise<void> {
this.repository.set(on);
await this.indicator.setEnabled(on);
}
}

@ -1,17 +0,0 @@
import * as actions from 'content/actions';
import addonReducer from 'content/reducers/addon';
describe("addon reducer", () => {
it('return the initial state', () => {
let state = addonReducer(undefined, {});
expect(state).to.have.property('enabled', true);
});
it('return next state for ADDON_SET_ENABLED', () => {
let action = { type: actions.ADDON_SET_ENABLED, enabled: true };
let prev = { enabled: false };
let state = addonReducer(prev, action);
expect(state.enabled).is.equal(true);
});
});

@ -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;
});
});

@ -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<void> {
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;
});
});
});