Mark set/jump as a clean architecture

jh-changes
Shin'ya Ueoka 6 years ago
parent ebfb172520
commit c6288f19d9
  1. 11
      src/content/actions/index.ts
  2. 31
      src/content/actions/mark.ts
  3. 28
      src/content/client/MarkClient.ts
  4. 48
      src/content/components/common/mark.ts
  5. 0
      src/content/domains/Mark.ts
  6. 2
      src/content/presenters/ScrollPresenter.ts
  7. 8
      src/content/reducers/mark.ts
  8. 25
      src/content/repositories/MarkRepository.ts
  9. 62
      src/content/usecases/MarkUseCase.ts
  10. 10
      test/content/actions/mark.test.ts
  11. 26
      test/content/mock/MockConsoleClient.ts
  12. 47
      test/content/mock/MockScrollPresenter.ts
  13. 10
      test/content/reducers/mark.test.ts
  14. 13
      test/content/repositories/MarkRepository.test.ts
  15. 25
      test/content/usecases/FindUseCase.test.ts
  16. 107
      test/content/usecases/MarkUseCase.test.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_SET = 'mark.start.set';
export const MARK_START_JUMP = 'mark.start.jump'; export const MARK_START_JUMP = 'mark.start.jump';
export const MARK_CANCEL = 'mark.cancel'; export const MARK_CANCEL = 'mark.cancel';
export const MARK_SET_LOCAL = 'mark.set.local';
export const NOOP = 'noop'; export const NOOP = 'noop';
@ -64,13 +63,6 @@ export interface MarkCancelAction extends Redux.Action {
type: typeof MARK_CANCEL; 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 { export interface NoopAction extends Redux.Action {
type: typeof NOOP; type: typeof NOOP;
} }
@ -80,8 +72,7 @@ export type FollowAction =
FollowControllerEnableAction | FollowControllerDisableAction | FollowControllerEnableAction | FollowControllerDisableAction |
FollowControllerKeyPressAction | FollowControllerBackspaceAction; FollowControllerKeyPressAction | FollowControllerBackspaceAction;
export type MarkAction = export type MarkAction =
MarkStartSetAction | MarkStartJumpAction | MarkStartSetAction | MarkStartJumpAction | MarkCancelAction | NoopAction;
MarkCancelAction | MarkSetLocalAction | NoopAction;
export type Action = export type Action =
InputAction | InputAction |

@ -1,5 +1,4 @@
import * as actions from './index'; import * as actions from './index';
import * as messages from '../../shared/messages';
const startSet = (): actions.MarkAction => { const startSet = (): actions.MarkAction => {
return { type: actions.MARK_START_SET }; return { type: actions.MARK_START_SET };
@ -13,34 +12,6 @@ const cancel = (): actions.MarkAction => {
return { type: actions.MARK_CANCEL }; 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 { export {
startSet, startJump, cancel, setLocal, startSet, startJump, cancel,
setGlobal, jumpGlobal,
}; };

@ -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<void>;
jumpGlobalMark(key: string): Promise<void>;
// eslint-disable-next-line semi
}
export class MarkClientImpl implements MarkClient {
async setGloablMark(key: string, mark: Mark): Promise<void> {
await browser.runtime.sendMessage({
type: messages.MARK_SET_GLOBAL,
key,
x: mark.x,
y: mark.y,
});
}
async jumpGlobalMark(key: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.MARK_JUMP_GLOBAL,
key,
});
}
}

@ -1,22 +1,15 @@
import * as markActions from '../../actions/mark'; import * as markActions from '../../actions/mark';
import * as consoleFrames from '../..//console-frames'; import * as consoleFrames from '../..//console-frames';
import * as keyUtils from '../../../shared/utils/keys'; import * as keyUtils from '../../../shared/utils/keys';
import Mark from '../../Mark';
import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; import MarkUseCase from '../../usecases/MarkUseCase';
import { ScrollPresenterImpl } from '../../presenters/ScrollPresenter';
let settingRepository = new SettingRepositoryImpl(); let markUseCase = new MarkUseCase();
let scrollPresenter = new ScrollPresenterImpl();
const cancelKey = (key: keyUtils.Key): boolean => { const cancelKey = (key: keyUtils.Key): boolean => {
return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey);
}; };
const globalKey = (key: string): boolean => {
return (/^[A-Z0-9]$/).test(key);
};
export default class MarkComponent { export default class MarkComponent {
private store: any; private store: any;
@ -26,7 +19,6 @@ export default class MarkComponent {
// eslint-disable-next-line max-statements // eslint-disable-next-line max-statements
key(key: keyUtils.Key) { key(key: keyUtils.Key) {
let smoothscroll = settingRepository.get().properties.smoothscroll;
let { mark: markState } = this.store.getState(); let { mark: markState } = this.store.getState();
if (!markState.setMode && !markState.jumpMode) { if (!markState.setMode && !markState.jumpMode) {
@ -40,45 +32,13 @@ export default class MarkComponent {
if (key.ctrlKey || key.metaKey || key.altKey) { if (key.ctrlKey || key.metaKey || key.altKey) {
consoleFrames.postError('Unknown mark'); 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) { } else if (markState.setMode) {
this.doSet(key); markUseCase.set(key.key);
} else if (markState.jumpMode) { } else if (markState.jumpMode) {
this.doJump(markState.marks, key, smoothscroll); markUseCase.jump(key.key);
} }
this.store.dispatch(markActions.cancel()); this.store.dispatch(markActions.cancel());
return true; 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));
}
} }

@ -94,7 +94,7 @@ class Scroller {
} }
} }
type Point = { x: number, y: number }; export type Point = { x: number, y: number };
export default interface ScrollPresenter { export default interface ScrollPresenter {
getScroll(): Point; getScroll(): Point;

@ -1,16 +1,13 @@
import Mark from '../Mark';
import * as actions from '../actions'; import * as actions from '../actions';
export interface State { export interface State {
setMode: boolean; setMode: boolean;
jumpMode: boolean; jumpMode: boolean;
marks: { [key: string]: Mark };
} }
const defaultState: State = { const defaultState: State = {
setMode: false, setMode: false,
jumpMode: false, jumpMode: false,
marks: {},
}; };
export default function reducer( export default function reducer(
@ -24,11 +21,6 @@ export default function reducer(
return { ...state, jumpMode: true }; return { ...state, jumpMode: true };
case actions.MARK_CANCEL: case actions.MARK_CANCEL:
return { ...state, setMode: false, jumpMode: false }; 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: default:
return state; return state;
} }

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

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

@ -22,14 +22,4 @@ describe('mark actions', () => {
expect(action.type).to.equal(actions.MARK_CANCEL); 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);
});
});
}); });

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

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

@ -6,7 +6,6 @@ describe("mark reducer", () => {
let state = reducer(undefined, {}); let state = reducer(undefined, {});
expect(state.setMode).to.be.false; expect(state.setMode).to.be.false;
expect(state.jumpMode).to.be.false; expect(state.jumpMode).to.be.false;
expect(state.marks).to.be.empty;
}); });
it('starts set mode', () => { it('starts set mode', () => {
@ -29,13 +28,4 @@ describe("mark reducer", () => {
state = reducer({ jumpMode: true }, action); state = reducer({ jumpMode: true }, action);
expect(state.jumpMode).to.be.false; 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)
});
}); });

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

@ -1,8 +1,8 @@
import FindRepository from '../../../src/content/repositories/FindRepository'; import FindRepository from '../../../src/content/repositories/FindRepository';
import FindPresenter from '../../../src/content/presenters/FindPresenter'; import FindPresenter from '../../../src/content/presenters/FindPresenter';
import ConsoleClient from '../../../src/content/client/ConsoleClient';
import FindClient from '../../../src/content/client/FindClient'; import FindClient from '../../../src/content/client/FindClient';
import FindUseCase from '../../../src/content/usecases/FindUseCase'; import FindUseCase from '../../../src/content/usecases/FindUseCase';
import MockConsoleClient from '../mock/MockConsoleClient';
import { expect } from 'chai'; import { expect } from 'chai';
class MockFindRepository implements FindRepository { 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<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', () => { describe('FindUseCase', () => {
let repository: MockFindRepository; let repository: MockFindRepository;
let presenter: MockFindPresenter; let presenter: MockFindPresenter;

@ -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<void> {
this.marks[key] = mark;
return Promise.resolve();
}
jumpGlobalMark(key: string): Promise<void> {
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')
});
});
});