Make find as a clean architecture

jh-changes
Shin'ya Ueoka 6 years ago
parent bacf83a320
commit 1ba1660269
  1. 100
      src/content/actions/find.ts
  2. 11
      src/content/actions/index.ts
  3. 30
      src/content/client/ConsoleClient.ts
  4. 25
      src/content/client/FindClient.ts
  5. 26
      src/content/components/top-content/find.ts
  6. 2
      src/content/components/top-content/index.ts
  7. 59
      src/content/presenters/FindPresenter.ts
  8. 25
      src/content/reducers/find.ts
  9. 4
      src/content/reducers/index.ts
  10. 19
      src/content/repositories/FindRepository.ts
  11. 1
      src/content/repositories/SettingRepository.ts
  12. 81
      src/content/usecases/FindUseCase.ts
  13. 22
      test/content/reducers/find.test.ts
  14. 15
      test/content/repositories/FindRepository.test.ts
  15. 184
      test/content/usecases/FindUseCase.test.ts

@ -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<actions.FindAction> => {
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<actions.FindAction> => {
return findNext(currentKeyword, reset, false);
};
const prev = (
currentKeyword: string, reset: boolean,
): Promise<actions.FindAction> => {
return findNext(currentKeyword, reset, true);
};
export { next, prev };

@ -1,9 +1,6 @@
import Redux from 'redux'; import Redux from 'redux';
import * as keyUtils from '../../shared/utils/keys'; import * as keyUtils from '../../shared/utils/keys';
// Find
export const FIND_SET_KEYWORD = 'find.set.keyword';
// User input // User input
export const INPUT_KEY_PRESS = 'input.key.press'; export const INPUT_KEY_PRESS = 'input.key.press';
export const INPUT_CLEAR_KEYS = 'input.clear.keys'; 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 const NOOP = 'noop';
export interface FindSetKeywordAction extends Redux.Action {
type: typeof FIND_SET_KEYWORD;
keyword: string;
found: boolean;
}
export interface InputKeyPressAction extends Redux.Action { export interface InputKeyPressAction extends Redux.Action {
type: typeof INPUT_KEY_PRESS; type: typeof INPUT_KEY_PRESS;
key: keyUtils.Key; key: keyUtils.Key;
@ -84,7 +75,6 @@ export interface NoopAction extends Redux.Action {
type: typeof NOOP; type: typeof NOOP;
} }
export type FindAction = FindSetKeywordAction | NoopAction;
export type InputAction = InputKeyPressAction | InputClearKeysAction; export type InputAction = InputKeyPressAction | InputClearKeysAction;
export type FollowAction = export type FollowAction =
FollowControllerEnableAction | FollowControllerDisableAction | FollowControllerEnableAction | FollowControllerDisableAction |
@ -94,7 +84,6 @@ export type MarkAction =
MarkCancelAction | MarkSetLocalAction | NoopAction; MarkCancelAction | MarkSetLocalAction | NoopAction;
export type Action = export type Action =
FindAction |
InputAction | InputAction |
FollowAction | FollowAction |
MarkAction | MarkAction |

@ -0,0 +1,30 @@
import * as messages from '../../shared/messages';
export default interface ConsoleClient {
info(text: string): Promise<void>;
error(text: string): Promise<void>;
// eslint-disable-next-line semi
}
export class ConsoleClientImpl implements ConsoleClient {
async info(text: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.CONSOLE_FRAME_MESSAGE,
message: {
type: messages.CONSOLE_SHOW_INFO,
text,
},
});
}
async error(text: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.CONSOLE_FRAME_MESSAGE,
message: {
type: messages.CONSOLE_SHOW_ERROR,
text,
},
});
}
}

@ -0,0 +1,25 @@
import * as messages from '../../shared/messages';
export default interface FindClient {
getGlobalLastKeyword(): Promise<string | null>;
setGlobalLastKeyword(keyword: string): Promise<void>;
// eslint-disable-next-line semi
}
export class FindClientImpl implements FindClient {
async getGlobalLastKeyword(): Promise<string | null> {
let keyword = await browser.runtime.sendMessage({
type: messages.FIND_GET_KEYWORD,
});
return keyword as string;
}
async setGlobalLastKeyword(keyword: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.FIND_SET_KEYWORD,
keyword: keyword,
});
}
}

@ -1,13 +1,12 @@
import * as findActions from '../../actions/find';
import * as messages from '../../../shared/messages'; import * as messages from '../../../shared/messages';
import MessageListener from '../../MessageListener'; import MessageListener from '../../MessageListener';
export default class FindComponent { import FindUseCase from '../../usecases/FindUseCase';
private store: any;
constructor(store: any) { let findUseCase = new FindUseCase();
this.store = store;
export default class FindComponent {
constructor() {
new MessageListener().onWebMessage(this.onMessage.bind(this)); new MessageListener().onWebMessage(this.onMessage.bind(this));
} }
@ -20,27 +19,18 @@ export default class FindComponent {
case messages.FIND_PREV: case messages.FIND_PREV:
return this.prev(); return this.prev();
} }
return Promise.resolve();
} }
start(text: string) { start(text: string) {
let state = this.store.getState().find; return findUseCase.startFind(text.length === 0 ? null : text);
if (text.length === 0) {
return this.store.dispatch(
findActions.next(state.keyword as string, true));
}
return this.store.dispatch(findActions.next(text, true));
} }
next() { next() {
let state = this.store.getState().find; return findUseCase.findNext();
return this.store.dispatch(
findActions.next(state.keyword as string, false));
} }
prev() { prev() {
let state = this.store.getState().find; return findUseCase.findPrev();
return this.store.dispatch(
findActions.prev(state.keyword as string, false));
} }
} }

@ -17,7 +17,7 @@ export default class TopContent {
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
new FindComponent(store); // eslint-disable-line no-new new FindComponent(); // eslint-disable-line no-new
// TODO make component // TODO make component
consoleFrames.initialize(this.win.document); consoleFrames.initialize(this.win.document);

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

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

@ -1,17 +1,15 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import find, { State as FindState } from './find';
import input, { State as InputState } from './input'; import input, { State as InputState } from './input';
import followController, { State as FollowControllerState } import followController, { State as FollowControllerState }
from './follow-controller'; from './follow-controller';
import mark, { State as MarkState } from './mark'; import mark, { State as MarkState } from './mark';
export interface State { export interface State {
find: FindState;
input: InputState; input: InputState;
followController: FollowControllerState; followController: FollowControllerState;
mark: MarkState; mark: MarkState;
} }
export default combineReducers({ export default combineReducers({
find, input, followController, mark, input, followController, mark,
}); });

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

@ -18,5 +18,4 @@ export class SettingRepositoryImpl implements SettingRepository {
get(): Settings { get(): Settings {
return current; return current;
} }
} }

@ -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<void> {
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<void> {
return this.findNextPrev(false);
}
findPrev(): Promise<void> {
return this.findNextPrev(true);
}
private async findNextPrev(
backwards: boolean,
): Promise<void> {
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<string | null> {
let keyword = this.repository.getLastKeyword();
if (!keyword) {
keyword = await this.client.getGlobalLastKeyword();
}
return keyword;
}
private async saveKeyword(keyword: string): Promise<void> {
this.repository.setLastKeyword(keyword);
await this.client.setGlobalLastKeyword(keyword);
}
private async showNoLastKeywordError(): Promise<void> {
await this.consoleClient.error('No previous search keywords');
}
}

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

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

@ -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<string | null> {
return Promise.resolve(this.keyword);
}
setGlobalLastKeyword(keyword: string): Promise<void> {
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<void> {
this.isError = false;
this.text = text;
return Promise.resolve();
}
error(text: string): Promise<void> {
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', () => {
});
});