From 5a7db96febd4e3cb2aa519610da75e0bccd69a7f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 23 May 2019 21:30:07 +0900 Subject: [PATCH 01/10] Move some navigation operations to background --- src/background/clients/NavigateClient.ts | 29 ++++++++++++++ .../controllers/OperationController.ts | 10 +++++ src/background/usecases/NavigateUseCase.ts | 40 +++++++++++++++++++ src/content/Application.ts | 10 +++++ src/content/controllers/KeymapController.ts | 12 ------ src/content/controllers/NavigateController.ts | 31 ++++++++++++++ src/shared/messages.ts | 31 +++++++++++++- 7 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 src/background/clients/NavigateClient.ts create mode 100644 src/background/usecases/NavigateUseCase.ts create mode 100644 src/content/controllers/NavigateController.ts diff --git a/src/background/clients/NavigateClient.ts b/src/background/clients/NavigateClient.ts new file mode 100644 index 0000000..bdd94ec --- /dev/null +++ b/src/background/clients/NavigateClient.ts @@ -0,0 +1,29 @@ +import { injectable } from 'tsyringe'; +import * as messages from '../../shared/messages'; + +@injectable() +export default class NavigateClient { + async historyNext(tabId: number): Promise { + await browser.tabs.sendMessage(tabId, { + type: messages.NAVIGATE_HISTORY_NEXT, + }); + } + + async historyPrev(tabId: number): Promise { + await browser.tabs.sendMessage(tabId, { + type: messages.NAVIGATE_HISTORY_PREV, + }); + } + + async linkNext(tabId: number): Promise { + await browser.tabs.sendMessage(tabId, { + type: messages.NAVIGATE_LINK_NEXT, + }); + } + + async linkPrev(tabId: number): Promise { + await browser.tabs.sendMessage(tabId, { + type: messages.NAVIGATE_LINK_PREV, + }); + } +} diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index de6f8cb..cb5c043 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -5,6 +5,7 @@ import ConsoleUseCase from '../usecases/ConsoleUseCase'; import TabUseCase from '../usecases/TabUseCase'; import TabSelectUseCase from '../usecases/TabSelectUseCase'; import ZoomUseCase from '../usecases/ZoomUseCase'; +import NavigateUseCase from '../usecases/NavigateUseCase'; @injectable() export default class OperationController { @@ -14,6 +15,7 @@ export default class OperationController { private tabUseCase: TabUseCase, private tabSelectUseCase: TabSelectUseCase, private zoomUseCase: ZoomUseCase, + private navigateUseCase: NavigateUseCase, ) { } @@ -74,6 +76,14 @@ export default class OperationController { return this.findUseCase.findStart(); case operations.CANCEL: return this.consoleUseCase.hideConsole(); + case operations.NAVIGATE_HISTORY_PREV: + return this.navigateUseCase.openHistoryPrev(); + case operations.NAVIGATE_HISTORY_NEXT: + return this.navigateUseCase.openHistoryNext(); + case operations.NAVIGATE_LINK_PREV: + return this.navigateUseCase.openLinkPrev(); + case operations.NAVIGATE_LINK_NEXT: + return this.navigateUseCase.openLinkNext(); } throw new Error('unknown operation: ' + operation.type); } diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts new file mode 100644 index 0000000..76e1c8e --- /dev/null +++ b/src/background/usecases/NavigateUseCase.ts @@ -0,0 +1,40 @@ +import { injectable } from 'tsyringe'; +import NavigateClient from '../clients/NavigateClient'; +import TabPresenter from '../presenters/TabPresenter'; + +@injectable() +export default class NavigateUseCase { + constructor( + private tabPresenter: TabPresenter, + private navigateClient: NavigateClient, + ) { + } + + async openHistoryNext(): Promise { + let tab = await this.tabPresenter.getCurrent(); + await this.navigateClient.historyNext(tab.id!!); + } + + async openHistoryPrev(): Promise { + let tab = await this.tabPresenter.getCurrent(); + await this.navigateClient.historyPrev(tab.id!!); + } + + async openLinkNext(): Promise { + let tab = await this.tabPresenter.getCurrent(); + await this.navigateClient.linkNext(tab.id!!); + } + + async openLinkPrev(): Promise { + let tab = await this.tabPresenter.getCurrent(); + await this.navigateClient.linkPrev(tab.id!!); + } + + openParent(): Promise { + throw new Error('not implemented'); + } + + openRoot(): Promise { + throw new Error('not implemented'); + } +} diff --git a/src/content/Application.ts b/src/content/Application.ts index 470bf53..1677655 100644 --- a/src/content/Application.ts +++ b/src/content/Application.ts @@ -12,6 +12,7 @@ import MarkKeyController from './controllers/MarkKeyController'; import AddonEnabledController from './controllers/AddonEnabledController'; import SettingController from './controllers/SettingController'; import ConsoleFrameController from './controllers/ConsoleFrameController'; +import NavigateController from './controllers/NavigateController'; import * as messages from '../shared/messages'; type Message = messages.Message; @@ -33,6 +34,7 @@ export default class Application { private addonEnabledController: AddonEnabledController, private settingController: SettingController, private consoleFrameController: ConsoleFrameController, + private navigateController: NavigateController, ) { } @@ -98,6 +100,14 @@ export default class Application { return this.settingController.reloadSettings(msg); case messages.ADDON_TOGGLE_ENABLED: return this.addonEnabledUseCase.toggle(); + case messages.NAVIGATE_HISTORY_NEXT: + return this.navigateController.openHistoryNext(msg); + case messages.NAVIGATE_HISTORY_PREV: + return this.navigateController.openHistoryPrev(msg); + case messages.NAVIGATE_LINK_NEXT: + return this.navigateController.openLinkNext(msg); + case messages.NAVIGATE_LINK_PREV: + return this.navigateController.openLinkPrev(msg); } }); diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index 1835546..4be8f9d 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -84,18 +84,6 @@ export default class KeymapController { case operations.MARK_JUMP_PREFIX: this.markKeyUseCase.enableJumpMode(); break; - case operations.NAVIGATE_HISTORY_PREV: - this.navigateUseCase.openHistoryPrev(); - break; - case operations.NAVIGATE_HISTORY_NEXT: - this.navigateUseCase.openHistoryNext(); - break; - case operations.NAVIGATE_LINK_PREV: - this.navigateUseCase.openLinkPrev(); - break; - case operations.NAVIGATE_LINK_NEXT: - this.navigateUseCase.openLinkNext(); - break; case operations.NAVIGATE_PARENT: this.navigateUseCase.openParent(); break; diff --git a/src/content/controllers/NavigateController.ts b/src/content/controllers/NavigateController.ts new file mode 100644 index 0000000..3f2df7a --- /dev/null +++ b/src/content/controllers/NavigateController.ts @@ -0,0 +1,31 @@ +import { injectable } from 'tsyringe'; +import { Message } from '../../shared/messages'; +import NavigateUseCase from '../usecases/NavigateUseCase'; + +@injectable() +export default class NavigateController { + constructor( + private navigateUseCase: NavigateUseCase, + ) { + } + + openHistoryNext(_m: Message): Promise { + this.navigateUseCase.openHistoryNext(); + return Promise.resolve(); + } + + openHistoryPrev(_m: Message): Promise { + this.navigateUseCase.openHistoryPrev(); + return Promise.resolve(); + } + + openLinkNext(_m: Message): Promise { + this.navigateUseCase.openLinkNext(); + return Promise.resolve(); + } + + openLinkPrev(_m: Message): Promise { + this.navigateUseCase.openLinkPrev(); + return Promise.resolve(); + } +} diff --git a/src/shared/messages.ts b/src/shared/messages.ts index fbd3478..36a23d8 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -42,6 +42,11 @@ export const SETTINGS_QUERY = 'settings.query'; export const CONSOLE_FRAME_MESSAGE = 'console.frame.message'; +export const NAVIGATE_HISTORY_NEXT = 'navigate.history.next'; +export const NAVIGATE_HISTORY_PREV = 'navigate.history.prev'; +export const NAVIGATE_LINK_NEXT = 'navigate.link.next'; +export const NAVIGATE_LINK_PREV = 'navigate.link.prev'; + export interface BackgroundOperationMessage { type: typeof BACKGROUND_OPERATION; operation: operations.Operation; @@ -204,6 +209,22 @@ export interface ConsoleFrameMessageMessage { message: any; } +export interface NavigateHistoryNextMessage { + type: typeof NAVIGATE_HISTORY_NEXT; +} + +export interface NavigateHistoryPrevMessage { + type: typeof NAVIGATE_HISTORY_PREV; +} + +export interface NavigateLinkNext { + type: typeof NAVIGATE_LINK_NEXT; +} + +export interface NavigateLinkPrev { + type: typeof NAVIGATE_LINK_PREV; +} + export type Message = BackgroundOperationMessage | ConsoleUnfocusMessage | @@ -236,7 +257,11 @@ export type Message = OpenUrlMessage | SettingsChangedMessage | SettingsQueryMessage | - ConsoleFrameMessageMessage; + ConsoleFrameMessageMessage | + NavigateHistoryNextMessage | + NavigateHistoryPrevMessage | + NavigateLinkNext | + NavigateLinkPrev; // eslint-disable-next-line complexity export const valueOf = (o: any): Message => { @@ -272,6 +297,10 @@ export const valueOf = (o: any): Message => { case SETTINGS_CHANGED: case SETTINGS_QUERY: case CONSOLE_FRAME_MESSAGE: + case NAVIGATE_HISTORY_NEXT: + case NAVIGATE_HISTORY_PREV: + case NAVIGATE_LINK_NEXT: + case NAVIGATE_LINK_PREV: return o; } throw new Error('unknown operation type: ' + o.type); From e779fb1779f33fb15857b5d20cb72a4b00d20f77 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 24 May 2019 20:19:02 +0900 Subject: [PATCH 02/10] Install sinon --- package-lock.json | 14 ++++++-------- package.json | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00792ab..9ae10bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,14 +105,6 @@ "@sinonjs/commons": "^1.0.2", "array-from": "^2.1.1", "lodash": "^4.17.11" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } } }, "@sinonjs/text-encoding": { @@ -202,6 +194,12 @@ } } }, + "@types/sinon": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.11.tgz", + "integrity": "sha512-6ee09Ugx6GyEr0opUIakmxIWFNmqYPjkqa3/BuxCBokA0klsOLPgMD5K4q40lH7/yZVuJVzOfQpd7pipwjngkQ==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.9.0.tgz", diff --git a/package.json b/package.json index 7feeea1..a556e50 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/react-dom": "^16.8.4", "@types/react-redux": "^7.0.8", "@types/redux-promise": "^0.5.28", + "@types/sinon": "^7.0.11", "@typescript-eslint/eslint-plugin": "^1.9.0", "chai": "^4.2.0", "css-loader": "^2.1.1", @@ -54,6 +55,7 @@ "redux-promise": "^0.6.0", "reflect-metadata": "^0.1.13", "sass-loader": "^7.1.0", + "sinon": "^7.3.2", "sinon-chrome": "^3.0.1", "style-loader": "^0.23.1", "ts-loader": "^6.0.1", From 8d0739463d970deae2ebdd88eedac29e9c4379ff Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 24 May 2019 21:51:18 +0900 Subject: [PATCH 03/10] Move open parent and open root to background --- .../controllers/OperationController.ts | 4 + src/background/presenters/TabPresenter.ts | 2 +- src/background/usecases/NavigateUseCase.ts | 25 +++++- src/content/controllers/KeymapController.ts | 8 -- src/content/presenters/NavigationPresenter.ts | 27 ------ src/content/usecases/NavigateUseCase.ts | 8 -- .../usecases/NavigateUseCase.test.ts | 82 +++++++++++++++++++ .../presenters/NavigationPresenter.test.ts | 9 -- 8 files changed, 108 insertions(+), 57 deletions(-) create mode 100644 test/background/usecases/NavigateUseCase.test.ts diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index cb5c043..1e80f54 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -84,6 +84,10 @@ export default class OperationController { return this.navigateUseCase.openLinkPrev(); case operations.NAVIGATE_LINK_NEXT: return this.navigateUseCase.openLinkNext(); + case operations.NAVIGATE_PARENT: + return this.navigateUseCase.openParent(); + case operations.NAVIGATE_ROOT: + return this.navigateUseCase.openRoot(); } throw new Error('unknown operation: ' + operation.type); } diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts index 5665bf0..5e6e56e 100644 --- a/src/background/presenters/TabPresenter.ts +++ b/src/background/presenters/TabPresenter.ts @@ -36,7 +36,7 @@ export default class TabPresenter { return tabId; } - async getByKeyword(keyword: string, excludePinned = false): Promise { + async getByKeyword(keyword: string, excludePinned: boolean = false): Promise { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { return t.url && t.url.toLowerCase().includes(keyword.toLowerCase()) || diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts index 76e1c8e..ced2a0b 100644 --- a/src/background/usecases/NavigateUseCase.ts +++ b/src/background/usecases/NavigateUseCase.ts @@ -30,11 +30,28 @@ export default class NavigateUseCase { await this.navigateClient.linkPrev(tab.id!!); } - openParent(): Promise { - throw new Error('not implemented'); + async openParent(): Promise { + let tab = await this.tabPresenter.getCurrent(); + let url = new URL(tab.url!!); + if (url.hash !== '') { + url.hash = ''; + } else if (url.search !== '') { + url.search = ''; + } else { + const basenamePattern = /\/[^/]+$/; + const lastDirPattern = /\/[^/]+\/$/; + if (basenamePattern.test(url.pathname)) { + url.pathname = url.pathname.replace(basenamePattern, '/'); + } else if (lastDirPattern.test(url.pathname)) { + url.pathname = url.pathname.replace(lastDirPattern, '/'); + } + } + await this.tabPresenter.open(url.href); } - openRoot(): Promise { - throw new Error('not implemented'); + async openRoot(): Promise { + let tab = await this.tabPresenter.getCurrent(); + let url = new URL(tab.url!!); + await this.tabPresenter.open(url.origin); } } diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index 4be8f9d..4eb6955 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -4,7 +4,6 @@ import KeymapUseCase from '../usecases/KeymapUseCase'; import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; import FindSlaveUseCase from '../usecases/FindSlaveUseCase'; import ScrollUseCase from '../usecases/ScrollUseCase'; -import NavigateUseCase from '../usecases/NavigateUseCase'; import FocusUseCase from '../usecases/FocusUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; import BackgroundClient from '../client/BackgroundClient'; @@ -19,7 +18,6 @@ export default class KeymapController { private addonEnabledUseCase: AddonEnabledUseCase, private findSlaveUseCase: FindSlaveUseCase, private scrollUseCase: ScrollUseCase, - private navigateUseCase: NavigateUseCase, private focusUseCase: FocusUseCase, private clipbaordUseCase: ClipboardUseCase, private backgroundClient: BackgroundClient, @@ -84,12 +82,6 @@ export default class KeymapController { case operations.MARK_JUMP_PREFIX: this.markKeyUseCase.enableJumpMode(); break; - case operations.NAVIGATE_PARENT: - this.navigateUseCase.openParent(); - break; - case operations.NAVIGATE_ROOT: - this.navigateUseCase.openRoot(); - break; case operations.FOCUS_INPUT: this.focusUseCase.focusFirstInput(); break; diff --git a/src/content/presenters/NavigationPresenter.ts b/src/content/presenters/NavigationPresenter.ts index c141112..11d96ec 100644 --- a/src/content/presenters/NavigationPresenter.ts +++ b/src/content/presenters/NavigationPresenter.ts @@ -6,10 +6,6 @@ export default interface NavigationPresenter { openLinkPrev(): void; openLinkNext(): void; - - openParent(): void; - - openRoot(): void; } const REL_PATTERN: {[key: string]: RegExp} = { @@ -51,29 +47,6 @@ export class NavigationPresenterImpl implements NavigationPresenter { 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]`); diff --git a/src/content/usecases/NavigateUseCase.ts b/src/content/usecases/NavigateUseCase.ts index 4711c5e..7adccfd 100644 --- a/src/content/usecases/NavigateUseCase.ts +++ b/src/content/usecases/NavigateUseCase.ts @@ -24,12 +24,4 @@ export default class NavigateUseCase { openLinkNext(): void { this.navigationPresenter.openLinkNext(); } - - openParent(): void { - this.navigationPresenter.openParent(); - } - - openRoot(): void { - this.navigationPresenter.openRoot(); - } } diff --git a/test/background/usecases/NavigateUseCase.test.ts b/test/background/usecases/NavigateUseCase.test.ts new file mode 100644 index 0000000..13f3e99 --- /dev/null +++ b/test/background/usecases/NavigateUseCase.test.ts @@ -0,0 +1,82 @@ +import TabPresenter from '../../../src/background/presenters/TabPresenter'; +import NavigateUseCase from '../../../src/background/usecases/NavigateUseCase'; +import NavigateClient from '../../../src/background/clients/NavigateClient'; +// import { expect } from 'chai'; +import * as sinon from 'sinon'; + +describe('NavigateUseCase', () => { + let sut: NavigateUseCase; + let tabPresenter: TabPresenter; + let navigateClient: NavigateClient; + beforeEach(() => { + tabPresenter = new TabPresenter(); + navigateClient = new NavigateClient(); + sut = new NavigateUseCase(tabPresenter, navigateClient); + }); + + describe('#openParent()', async () => { + it.only('opens parent directory of file', async() => { + var stub = sinon.stub(tabPresenter, 'getCurrent'); + stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/banana' })) + + var mock = sinon.mock(tabPresenter); + mock.expects('open').withArgs('https://google.com/fruits/yellow/'); + + await sut.openParent(); + + mock.verify(); + }); + + it.only('opens parent directory of directory', async() => { + var stub = sinon.stub(tabPresenter, 'getCurrent'); + stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/' })) + + var mock = sinon.mock(tabPresenter); + mock.expects('open').withArgs('https://google.com/fruits/'); + + await sut.openParent(); + + mock.verify(); + }); + + it.only('removes hash', async() => { + var stub = sinon.stub(tabPresenter, 'getCurrent'); + stub.returns(Promise.resolve({ url: 'https://google.com/#top' })) + + var mock = sinon.mock(tabPresenter); + mock.expects('open').withArgs('https://google.com/'); + + await sut.openParent(); + + mock.verify(); + }); + + it.only('removes search query', async() => { + var stub = sinon.stub(tabPresenter, 'getCurrent'); + stub.returns(Promise.resolve({ url: 'https://google.com/search?q=apple' })) + + var mock = sinon.mock(tabPresenter); + mock.expects('open').withArgs('https://google.com/search'); + + await sut.openParent(); + + mock.verify(); + }); + }); + + describe('#openRoot()', () => { + it.only('opens root direectory', async() => { + var stub = sinon.stub(tabPresenter, 'getCurrent'); + stub.returns(Promise.resolve({ + url: 'https://google.com/seach?q=apple', + })) + + var mock = sinon.mock(tabPresenter); + mock.expects('open').withArgs('https://google.com'); + + await sut.openRoot(); + + mock.verify(); + }); + }); +}); diff --git a/test/content/presenters/NavigationPresenter.test.ts b/test/content/presenters/NavigationPresenter.test.ts index c1aca9a..5b6a8ca 100644 --- a/test/content/presenters/NavigationPresenter.test.ts +++ b/test/content/presenters/NavigationPresenter.test.ts @@ -132,13 +132,4 @@ describe('NavigationPresenter', () => { '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; - }); - }); }); From 45a10d93c741da285937e6d13401d39c65ee609d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 24 May 2019 22:22:02 +0900 Subject: [PATCH 04/10] Fix NavigateUseCase test --- test/background/usecases/NavigateUseCase.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/background/usecases/NavigateUseCase.test.ts b/test/background/usecases/NavigateUseCase.test.ts index 13f3e99..ecbf888 100644 --- a/test/background/usecases/NavigateUseCase.test.ts +++ b/test/background/usecases/NavigateUseCase.test.ts @@ -15,7 +15,7 @@ describe('NavigateUseCase', () => { }); describe('#openParent()', async () => { - it.only('opens parent directory of file', async() => { + it('opens parent directory of file', async() => { var stub = sinon.stub(tabPresenter, 'getCurrent'); stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/banana' })) @@ -27,7 +27,7 @@ describe('NavigateUseCase', () => { mock.verify(); }); - it.only('opens parent directory of directory', async() => { + it('opens parent directory of directory', async() => { var stub = sinon.stub(tabPresenter, 'getCurrent'); stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/' })) @@ -39,7 +39,7 @@ describe('NavigateUseCase', () => { mock.verify(); }); - it.only('removes hash', async() => { + it('removes hash', async() => { var stub = sinon.stub(tabPresenter, 'getCurrent'); stub.returns(Promise.resolve({ url: 'https://google.com/#top' })) @@ -51,7 +51,7 @@ describe('NavigateUseCase', () => { mock.verify(); }); - it.only('removes search query', async() => { + it('removes search query', async() => { var stub = sinon.stub(tabPresenter, 'getCurrent'); stub.returns(Promise.resolve({ url: 'https://google.com/search?q=apple' })) @@ -65,7 +65,7 @@ describe('NavigateUseCase', () => { }); describe('#openRoot()', () => { - it.only('opens root direectory', async() => { + it('opens root direectory', async() => { var stub = sinon.stub(tabPresenter, 'getCurrent'); stub.returns(Promise.resolve({ url: 'https://google.com/seach?q=apple', From 03370301a7509af4bbc96d60269598bf89c71fe4 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 24 May 2019 22:25:42 +0900 Subject: [PATCH 05/10] NavigateUseCase TabPresenter --- src/background/presenters/TabPresenter.ts | 4 +++- src/background/usecases/NavigateUseCase.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts index 5e6e56e..ed88f26 100644 --- a/src/background/presenters/TabPresenter.ts +++ b/src/background/presenters/TabPresenter.ts @@ -36,7 +36,9 @@ export default class TabPresenter { return tabId; } - async getByKeyword(keyword: string, excludePinned: boolean = false): Promise { + async getByKeyword( + keyword: string, excludePinned: boolean = false, + ): Promise { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { return t.url && t.url.toLowerCase().includes(keyword.toLowerCase()) || diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts index ced2a0b..152339a 100644 --- a/src/background/usecases/NavigateUseCase.ts +++ b/src/background/usecases/NavigateUseCase.ts @@ -33,9 +33,9 @@ export default class NavigateUseCase { async openParent(): Promise { let tab = await this.tabPresenter.getCurrent(); let url = new URL(tab.url!!); - if (url.hash !== '') { + if (url.hash.length > 0) { url.hash = ''; - } else if (url.search !== '') { + } else if (url.search.length > 0) { url.search = ''; } else { const basenamePattern = /\/[^/]+$/; From ccbe08cf66e16084c919f0b2fa2da81258c01d41 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2019 21:33:33 +0900 Subject: [PATCH 06/10] Repeat last operation --- .../controllers/OperationController.ts | 19 ++++++- .../repositories/RepeatRepository.ts | 22 +++++++++ src/background/usecases/RepeatUseCase.ts | 49 +++++++++++++++++++ ...BackgroundClient.ts => OperationClient.ts} | 8 +-- src/content/controllers/KeymapController.ts | 6 ++- src/content/di.ts | 2 + src/shared/operations.ts | 11 ++++- 7 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 src/background/repositories/RepeatRepository.ts create mode 100644 src/background/usecases/RepeatUseCase.ts rename src/content/client/{BackgroundClient.ts => OperationClient.ts} (63%) diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 1e80f54..7233c0e 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -6,6 +6,7 @@ import TabUseCase from '../usecases/TabUseCase'; import TabSelectUseCase from '../usecases/TabSelectUseCase'; import ZoomUseCase from '../usecases/ZoomUseCase'; import NavigateUseCase from '../usecases/NavigateUseCase'; +import RepeatUseCase from '../usecases/RepeatUseCase'; @injectable() export default class OperationController { @@ -16,11 +17,19 @@ export default class OperationController { private tabSelectUseCase: TabSelectUseCase, private zoomUseCase: ZoomUseCase, private navigateUseCase: NavigateUseCase, + private repeatUseCase: RepeatUseCase, ) { } + async exec(op: operations.Operation): Promise { + await this.doOperation(op); + if (this.repeatUseCase.isRepeatable(op)) { + this.repeatUseCase.storeLastOperation(op); + } + } + // eslint-disable-next-line complexity, max-lines-per-function - exec(operation: operations.Operation): Promise { + doOperation(operation: operations.Operation): Promise { switch (operation.type) { case operations.TAB_CLOSE: return this.tabUseCase.close(false); @@ -88,6 +97,14 @@ export default class OperationController { return this.navigateUseCase.openParent(); case operations.NAVIGATE_ROOT: return this.navigateUseCase.openRoot(); + case operations.REPEAT_LAST: + { + let last = this.repeatUseCase.getLastOperation(); + if (typeof last !== 'undefined') { + return this.doOperation(last); + } + return Promise.resolve(); + } } throw new Error('unknown operation: ' + operation.type); } diff --git a/src/background/repositories/RepeatRepository.ts b/src/background/repositories/RepeatRepository.ts new file mode 100644 index 0000000..c7f7a71 --- /dev/null +++ b/src/background/repositories/RepeatRepository.ts @@ -0,0 +1,22 @@ +import { injectable } from 'tsyringe'; +import { Operation } from '../../shared/operations'; +import MemoryStorage from '../infrastructures/MemoryStorage'; + +const REPEAT_KEY = 'repeat'; + +@injectable() +export default class RepeatRepository { + private cache: MemoryStorage; + + constructor() { + this.cache = new MemoryStorage(); + } + + getLastOperation(): Operation | undefined { + return this.cache.get(REPEAT_KEY); + } + + setLastOperation(op: Operation): void { + this.cache.set(REPEAT_KEY, op); + } +} diff --git a/src/background/usecases/RepeatUseCase.ts b/src/background/usecases/RepeatUseCase.ts new file mode 100644 index 0000000..a005682 --- /dev/null +++ b/src/background/usecases/RepeatUseCase.ts @@ -0,0 +1,49 @@ +import { injectable } from 'tsyringe'; +import * as operations from '../../shared/operations'; +import RepeatRepository from '../repositories/RepeatRepository'; + +type Operation = operations.Operation; + +@injectable() +export default class RepeatUseCase { + constructor( + private repeatRepository: RepeatRepository, + ) { + } + + storeLastOperation(op: Operation): void { + this.repeatRepository.setLastOperation(op); + } + + getLastOperation(): operations.Operation | undefined { + return this.repeatRepository.getLastOperation(); + } + + // eslint-disable-next-line complexity + isRepeatable(op: Operation): boolean { + switch (op.type) { + case operations.NAVIGATE_HISTORY_PREV: + case operations.NAVIGATE_HISTORY_NEXT: + case operations.NAVIGATE_LINK_PREV: + case operations.NAVIGATE_LINK_NEXT: + case operations.NAVIGATE_PARENT: + case operations.NAVIGATE_ROOT: + case operations.PAGE_SOURCE: + case operations.PAGE_HOME: + case operations.TAB_CLOSE: + case operations.TAB_CLOSE_FORCE: + case operations.TAB_CLOSE_RIGHT: + case operations.TAB_REOPEN: + case operations.TAB_RELOAD: + case operations.TAB_PIN: + case operations.TAB_UNPIN: + case operations.TAB_TOGGLE_PINNED: + case operations.TAB_DUPLICATE: + case operations.ZOOM_IN: + case operations.ZOOM_OUT: + case operations.ZOOM_NEUTRAL: + return true; + } + return false; + } +} diff --git a/src/content/client/BackgroundClient.ts b/src/content/client/OperationClient.ts similarity index 63% rename from src/content/client/BackgroundClient.ts rename to src/content/client/OperationClient.ts index 4a41184..d699fec 100644 --- a/src/content/client/BackgroundClient.ts +++ b/src/content/client/OperationClient.ts @@ -1,9 +1,11 @@ -import { injectable } from 'tsyringe'; import * as operations from '../../shared/operations'; import * as messages from '../../shared/messages'; -@injectable() -export default class BackgroundClient { +export default interface OperationClient { + execBackgroundOp(op: operations.Operation): Promise; +} + +export class OperationClientImpl implements OperationClient { execBackgroundOp(op: operations.Operation): Promise { return browser.runtime.sendMessage({ type: messages.BACKGROUND_OPERATION, diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index 4eb6955..fcfaff1 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -6,7 +6,7 @@ import FindSlaveUseCase from '../usecases/FindSlaveUseCase'; import ScrollUseCase from '../usecases/ScrollUseCase'; import FocusUseCase from '../usecases/FocusUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; -import BackgroundClient from '../client/BackgroundClient'; +import OperationClient from '../client/OperationClient'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; import FollowMasterClient from '../client/FollowMasterClient'; import Key from '../domains/Key'; @@ -20,9 +20,11 @@ export default class KeymapController { private scrollUseCase: ScrollUseCase, private focusUseCase: FocusUseCase, private clipbaordUseCase: ClipboardUseCase, - private backgroundClient: BackgroundClient, private markKeyUseCase: MarkKeyyUseCase, + @inject('OperationClient') + private backgroundClient: OperationClient, + @inject('FollowMasterClient') private followMasterClient: FollowMasterClient, ) { diff --git a/src/content/di.ts b/src/content/di.ts index 23be027..e18806a 100644 --- a/src/content/di.ts +++ b/src/content/di.ts @@ -21,6 +21,7 @@ import { MarkClientImpl } from './client/MarkClient'; import { MarkKeyRepositoryImpl } from './repositories/MarkKeyRepository'; import { MarkRepositoryImpl } from './repositories/MarkRepository'; import { NavigationPresenterImpl } from './presenters/NavigationPresenter'; +import { OperationClientImpl } from './client/OperationClient'; import { ScrollPresenterImpl } from './presenters/ScrollPresenter'; import { SettingClientImpl } from './client/SettingClient'; import { SettingRepositoryImpl } from './repositories/SettingRepository'; @@ -48,6 +49,7 @@ container.register('MarkClient', { useClass: MarkClientImpl }); container.register('MarkKeyRepository', { useClass: MarkKeyRepositoryImpl }); container.register('MarkRepository', { useClass: MarkRepositoryImpl }); container.register('NavigationPresenter', { useClass: NavigationPresenterImpl }); +container.register('OperationClient', { useClass: OperationClientImpl }); container.register('ScrollPresenter', { useClass: ScrollPresenterImpl }); container.register('SettingClient', { useClass: SettingClientImpl }); container.register('SettingRepository', { useClass: SettingRepositoryImpl }); diff --git a/src/shared/operations.ts b/src/shared/operations.ts index 688c240..0f0d0c0 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -75,6 +75,9 @@ export const FIND_PREV = 'find.prev'; export const MARK_SET_PREFIX = 'mark.set.prefix'; export const MARK_JUMP_PREFIX = 'mark.jump.prefix'; +// Repeat +export const REPEAT_LAST = 'repeat.last'; + export interface CancelOperation { type: typeof CANCEL; } @@ -291,6 +294,10 @@ export interface MarkJumpPrefixOperation { type: typeof MARK_JUMP_PREFIX; } +export interface RepeatLastOperation { + type: typeof REPEAT_LAST; +} + export type Operation = CancelOperation | AddonEnableOperation | @@ -342,7 +349,8 @@ export type Operation = FindNextOperation | FindPrevOperation | MarkSetPrefixOperation | - MarkJumpPrefixOperation; + MarkJumpPrefixOperation | + RepeatLastOperation; const assertOptionalBoolean = (obj: any, name: string) => { if (Object.prototype.hasOwnProperty.call(obj, name) && @@ -441,6 +449,7 @@ export const valueOf = (o: any): Operation => { case FIND_PREV: case MARK_SET_PREFIX: case MARK_JUMP_PREFIX: + case REPEAT_LAST: return { type: o.type }; } throw new TypeError('unknown operation type: ' + o.type); From a2ee6897bfbb95a0cca50ab11042aaf94c5de77c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2019 21:55:22 +0900 Subject: [PATCH 07/10] Add repeat.last operation to default settings --- src/settings/keymaps.ts | 1 + src/shared/SettingData.ts | 1 + src/shared/Settings.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/settings/keymaps.ts b/src/settings/keymaps.ts index 38045ad..ffe0d48 100644 --- a/src/settings/keymaps.ts +++ b/src/settings/keymaps.ts @@ -63,6 +63,7 @@ const fields = [ ['zoom.out', 'Zoom-out'], ['zoom.neutral', 'Reset zoom level'], ['page.source', 'Open a page source'], + ['repeat.last', 'Repeat last change'], ] ]; diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts index 05e21fa..1c085cf 100644 --- a/src/shared/SettingData.ts +++ b/src/shared/SettingData.ts @@ -390,6 +390,7 @@ export const DefaultSettingData: SettingData = SettingData.valueOf({ "/": { "type": "find.start" }, "n": { "type": "find.next" }, "N": { "type": "find.prev" }, + ".": { "type": "repeat.last" }, "": { "type": "addon.toggle.enabled" } }, "search": { diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts index c1b5a51..e1e2046 100644 --- a/src/shared/Settings.ts +++ b/src/shared/Settings.ts @@ -177,6 +177,7 @@ export const DefaultSetting: Settings = { '/': { 'type': 'find.start' }, 'n': { 'type': 'find.next' }, 'N': { 'type': 'find.prev' }, + '.': { 'type': 'repeat.last' }, '': { 'type': 'addon.toggle.enabled' } }, search: { From 48e005dc825a5211b4d3e92ed06ad15e01c23d13 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2019 21:55:45 +0900 Subject: [PATCH 08/10] Repeat open, tabopen and winopen command --- .../controllers/OperationController.ts | 3 ++ src/background/usecases/CommandUseCase.ts | 17 ++++++++++ src/background/usecases/RepeatUseCase.ts | 1 + src/background/usecases/TabUseCase.ts | 15 +++++++++ src/content/client/OperationClient.ts | 18 ++++++++++ src/content/usecases/ClipboardUseCase.ts | 10 ++++-- src/shared/operations.ts | 33 ++++++++++++++++++- 7 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 7233c0e..51cff28 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -105,6 +105,9 @@ export default class OperationController { } return Promise.resolve(); } + case operations.INTERNAL_OPEN_URL: + return this.tabUseCase.openURL( + operation.url, operation.newTab, operation.newWindow); } throw new Error('unknown operation: ' + operation.type); } diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts index 921a779..a526cfc 100644 --- a/src/background/usecases/CommandUseCase.ts +++ b/src/background/usecases/CommandUseCase.ts @@ -1,4 +1,5 @@ import { injectable } from 'tsyringe'; +import * as operations from '../../shared/operations'; import * as parsers from './parsers'; import * as urls from '../../shared/urls'; import TabPresenter from '../presenters/TabPresenter'; @@ -7,6 +8,7 @@ import SettingRepository from '../repositories/SettingRepository'; import BookmarkRepository from '../repositories/BookmarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; +import RepeatUseCase from '../usecases/RepeatUseCase'; @injectable() export default class CommandIndicator { @@ -17,21 +19,36 @@ export default class CommandIndicator { private bookmarkRepository: BookmarkRepository, private consoleClient: ConsoleClient, private contentMessageClient: ContentMessageClient, + private repeatUseCase: RepeatUseCase, ) { } async open(keywords: string): Promise { let url = await this.urlOrSearch(keywords); + this.repeatUseCase.storeLastOperation({ + type: operations.INTERNAL_OPEN_URL, + url, + }); return this.tabPresenter.open(url); } async tabopen(keywords: string): Promise { let url = await this.urlOrSearch(keywords); + this.repeatUseCase.storeLastOperation({ + type: operations.INTERNAL_OPEN_URL, + url, + newTab: true, + }); return this.tabPresenter.create(url); } async winopen(keywords: string): Promise { let url = await this.urlOrSearch(keywords); + this.repeatUseCase.storeLastOperation({ + type: operations.INTERNAL_OPEN_URL, + url, + newWindow: true, + }); return this.windowPresenter.create(url); } diff --git a/src/background/usecases/RepeatUseCase.ts b/src/background/usecases/RepeatUseCase.ts index a005682..d78de34 100644 --- a/src/background/usecases/RepeatUseCase.ts +++ b/src/background/usecases/RepeatUseCase.ts @@ -42,6 +42,7 @@ export default class RepeatUseCase { case operations.ZOOM_IN: case operations.ZOOM_OUT: case operations.ZOOM_NEUTRAL: + case operations.INTERNAL_OPEN_URL: return true; } return false; diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts index 0239a87..31112a9 100644 --- a/src/background/usecases/TabUseCase.ts +++ b/src/background/usecases/TabUseCase.ts @@ -1,11 +1,13 @@ import { injectable } from 'tsyringe'; import TabPresenter from '../presenters/TabPresenter'; +import WindowPresenter from '../presenters/WindowPresenter'; import BrowserSettingRepository from '../repositories/BrowserSettingRepository'; @injectable() export default class TabUseCase { constructor( private tabPresenter: TabPresenter, + private windowPresenter: WindowPresenter, private browserSettingRepository: BrowserSettingRepository, ) { } @@ -77,4 +79,17 @@ export default class TabUseCase { this.tabPresenter.create(url); } } + + async openURL( + url: string, newTab?: boolean, newWindow?: boolean, + ): Promise { + if (newWindow) { + await this.windowPresenter.create(url); + } else if (newTab) { + await this.tabPresenter.create(url); + } else { + let tab = await this.tabPresenter.getCurrent(); + await this.tabPresenter.open(url, tab.id); + } + } } diff --git a/src/content/client/OperationClient.ts b/src/content/client/OperationClient.ts index d699fec..5dbe555 100644 --- a/src/content/client/OperationClient.ts +++ b/src/content/client/OperationClient.ts @@ -3,6 +3,10 @@ import * as messages from '../../shared/messages'; export default interface OperationClient { execBackgroundOp(op: operations.Operation): Promise; + + internalOpenUrl( + url: string, newTab?: boolean, background?: boolean, + ): Promise; } export class OperationClientImpl implements OperationClient { @@ -12,4 +16,18 @@ export class OperationClientImpl implements OperationClient { operation: op, }); } + + internalOpenUrl( + url: string, newTab?: boolean, background?: boolean, + ): Promise { + return browser.runtime.sendMessage({ + type: messages.BACKGROUND_OPERATION, + operation: { + type: operations.INTERNAL_OPEN_URL, + url, + newTab, + background, + }, + }); + } } diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts index 8c4d621..c8fe719 100644 --- a/src/content/usecases/ClipboardUseCase.ts +++ b/src/content/usecases/ClipboardUseCase.ts @@ -2,16 +2,16 @@ import { injectable, inject } from 'tsyringe'; import * as urls from '../../shared/urls'; import ClipboardRepository from '../repositories/ClipboardRepository'; import SettingRepository from '../repositories/SettingRepository'; -import TabsClient from '../client/TabsClient'; import ConsoleClient from '../client/ConsoleClient'; +import OperationClient from '../client/OperationClient'; @injectable() export default class ClipboardUseCase { constructor( @inject('ClipboardRepository') private repository: ClipboardRepository, @inject('SettingRepository') private settingRepository: SettingRepository, - @inject('TabsClient') private client: TabsClient, @inject('ConsoleClient') private consoleClient: ConsoleClient, + @inject('OperationClient') private operationClinet: OperationClient, ) { } @@ -26,6 +26,10 @@ export default class ClipboardUseCase { let search = this.settingRepository.get().search; let text = this.repository.read(); let url = urls.searchUrl(text, search); - await this.client.openUrl(url, newTab); + + // TODO: Repeat pasting from clipboard instead of opening a certain url. + // 'Repeat last' command is implemented in the background script and cannot + // access to clipboard until Firefox 63. + await this.operationClinet.internalOpenUrl(url, newTab); } } diff --git a/src/shared/operations.ts b/src/shared/operations.ts index 0f0d0c0..2b03d9d 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -78,6 +78,9 @@ export const MARK_JUMP_PREFIX = 'mark.jump.prefix'; // Repeat export const REPEAT_LAST = 'repeat.last'; +// Internal +export const INTERNAL_OPEN_URL = 'internal.open.url'; + export interface CancelOperation { type: typeof CANCEL; } @@ -298,6 +301,14 @@ export interface RepeatLastOperation { type: typeof REPEAT_LAST; } +export interface InternalOpenUrl { + type: typeof INTERNAL_OPEN_URL; + url: string; + newTab?: boolean; + newWindow?: boolean; + background?: boolean; +} + export type Operation = CancelOperation | AddonEnableOperation | @@ -350,7 +361,8 @@ export type Operation = FindPrevOperation | MarkSetPrefixOperation | MarkJumpPrefixOperation | - RepeatLastOperation; + RepeatLastOperation | + InternalOpenUrl; const assertOptionalBoolean = (obj: any, name: string) => { if (Object.prototype.hasOwnProperty.call(obj, name) && @@ -366,6 +378,13 @@ const assertRequiredNumber = (obj: any, name: string) => { } }; +const assertRequiredString = (obj: any, name: string) => { + if (!Object.prototype.hasOwnProperty.call(obj, name) || + typeof obj[name] !== 'string') { + throw new TypeError(`Missing string parameter '${name}`); + } +}; + // eslint-disable-next-line complexity, max-lines-per-function export const valueOf = (o: any): Operation => { if (!Object.prototype.hasOwnProperty.call(o, 'type')) { @@ -409,6 +428,18 @@ export const valueOf = (o: any): Operation => { type: URLS_PASTE, newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), }; + case INTERNAL_OPEN_URL: + assertOptionalBoolean(o, 'newTab'); + assertOptionalBoolean(o, 'newWindow'); + assertOptionalBoolean(o, 'background'); + assertRequiredString(o, 'url'); + return { + type: INTERNAL_OPEN_URL, + url: o.url, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + newWindow: Boolean(typeof o.newWindow === undefined ? false : o.newWindow), // eslint-disable-line max-len + background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len + }; case CANCEL: case ADDON_ENABLE: case ADDON_DISABLE: From 5133705ddeb5f9d67768897b1c4a0778d436eb45 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2019 12:35:38 +0900 Subject: [PATCH 09/10] Fix ClipboardUseCase.test --- .../content/usecases/ClipboardUseCase.test.ts | 83 ++++++++----------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/test/content/usecases/ClipboardUseCase.test.ts b/test/content/usecases/ClipboardUseCase.test.ts index 551c3f7..a863651 100644 --- a/test/content/usecases/ClipboardUseCase.test.ts +++ b/test/content/usecases/ClipboardUseCase.test.ts @@ -1,82 +1,69 @@ import ClipboardRepository from '../../../src/content/repositories/ClipboardRepository'; import { SettingRepositoryImpl } from '../../../src/content/repositories/SettingRepository'; -import TabsClient from '../../../src/content/client/TabsClient'; -import MockConsoleClient from '../mock/MockConsoleClient'; import ClipboardUseCase from '../../../src/content/usecases/ClipboardUseCase'; -import { expect } from 'chai'; - -class MockClipboardRepository implements ClipboardRepository { - public clipboard: string; - - constructor() { - this.clipboard = ''; - } - - read(): string { - return this.clipboard; - } - - write(text: string): void { - this.clipboard = text; - } -} - -class MockTabsClient implements TabsClient { - public last: string; - - constructor() { - this.last = ''; - } +import OperationClient from '../../../src/content/client/OperationClient'; +import ConsoleClient from '../../../src/content/client/ConsoleClient'; - openUrl(url: string, _newTab: boolean): Promise { - this.last = url; - return Promise.resolve(); - } -} +import * as sinon from 'sinon'; +import { expect } from 'chai'; describe('ClipboardUseCase', () => { - let repository: MockClipboardRepository; - let client: MockTabsClient; - let consoleClient: MockConsoleClient; + let clipboardRepository: ClipboardRepository; + let operationClient: OperationClient; + let consoleClient: ConsoleClient; let sut: ClipboardUseCase; beforeEach(() => { - repository = new MockClipboardRepository(); - client = new MockTabsClient(); - consoleClient = new MockConsoleClient(); + var modal = {}; + + clipboardRepository = { read() {}, write(_) {} }; + operationClient = { internalOpenUrl(_) {} }; + consoleClient = { info(_) {}}; sut = new ClipboardUseCase( - repository, + clipboardRepository, new SettingRepositoryImpl(), - client, - consoleClient + consoleClient, + operationClient, ); }); describe('#yankCurrentURL', () => { it('yanks current url', async () => { + let href = window.location.href; + var mockRepository = sinon.mock(clipboardRepository); + mockRepository.expects('write').withArgs(href); + var mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient.expects('info').withArgs('Yanked ' + href); + let yanked = await sut.yankCurrentURL(); - expect(yanked).to.equal(window.location.href); - expect(repository.clipboard).to.equal(yanked); - expect(consoleClient.text).to.equal('Yanked ' + yanked); + expect(yanked).to.equal(href); + mockRepository.verify(); + mockConsoleClient.verify(); }); }); describe('#openOrSearch', () => { it('opens url from the clipboard', async () => { let url = 'https://github.com/ueokande/vim-vixen' - repository.clipboard = url; + sinon.stub(clipboardRepository, 'read').returns(url); + let mockOperationClient = sinon.mock(operationClient); + mockOperationClient.expects('internalOpenUrl').withArgs(url, true); + await sut.openOrSearch(true); - expect(client.last).to.equal(url); + mockOperationClient.verify(); }); it('opens search results from the clipboard', async () => { - repository.clipboard = 'banana'; + let url = 'https://google.com/search?q=banana'; + sinon.stub(clipboardRepository, 'read').returns('banana'); + let mockOperationClient = sinon.mock(operationClient); + mockOperationClient.expects('internalOpenUrl').withArgs(url, true); + await sut.openOrSearch(true); - expect(client.last).to.equal('https://google.com/search?q=banana'); + mockOperationClient.verify(); }); }); }); - From 34a96cdc9c5d7c8a11c6f1ae512fbc97724f61c4 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2019 15:44:13 +0900 Subject: [PATCH 10/10] Add repeat operation test --- e2e/repeat.test.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 e2e/repeat.test.js diff --git a/e2e/repeat.test.js b/e2e/repeat.test.js new file mode 100644 index 0000000..4072005 --- /dev/null +++ b/e2e/repeat.test.js @@ -0,0 +1,92 @@ +const express = require('express'); +const lanthan = require('lanthan'); +const path = require('path'); +const assert = require('assert'); +const eventually = require('./eventually'); + +const Key = lanthan.Key; + +const newApp = () => { + let app = express(); + app.get('/', (req, res) => { + res.send('ok'); + }); + return app; +}; + +describe("tab test", () => { + + const port = 12321; + const url = `http://127.0.0.1:${port}/`; + + let http; + let firefox; + let session; + let browser; + let tabs; + + before(async() => { + firefox = await lanthan.firefox(); + await firefox.session.installAddonFromPath(path.join(__dirname, '..')); + session = firefox.session; + browser = firefox.browser; + http = newApp().listen(port); + + await session.navigateTo(`${url}`); + }); + + after(async() => { + http.close(); + if (firefox) { + await firefox.close(); + } + }); + + it('repeats last operation', async () => { + let before = await browser.tabs.query({}); + + let body = await session.findElementByCSS('body'); + await body.sendKeys(':'); + + await session.switchToFrame(0); + let input = await session.findElementByCSS('input'); + input.sendKeys(`tabopen ${url}newtab`, Key.Enter); + + await eventually(async() => { + let current = await browser.tabs.query({ url: `*://*/newtab` }); + assert.equal(current.length, 1); + }); + + body = await session.findElementByCSS('body'); + await body.sendKeys('.'); + + await eventually(async() => { + let current = await browser.tabs.query({ url: `*://*/newtab` }); + assert.equal(current.length, 2); + }); + }); + + it('repeats last operation', async () => { + for (let i = 1; i < 5; ++i) { + await browser.tabs.create({ url: `${url}#${i}` }); + } + let before = await browser.tabs.query({}); + + let body = await session.findElementByCSS('body'); + await body.sendKeys('d'); + + await eventually(async() => { + let current = await browser.tabs.query({}); + assert.equal(current.length, before.length - 1); + }); + + await browser.tabs.update(before[2].id, { active: true }); + body = await session.findElementByCSS('body'); + await body.sendKeys('.'); + + await eventually(async() => { + let current = await browser.tabs.query({}); + assert.equal(current.length, before.length - 2); + }); + }); +});