aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2020-12-10 12:52:17 +0000
committerGitHub <noreply@github.com>2020-12-10 12:52:17 +0000
commit5a0444d7bb7eae27fdca5c2be8fc3ec6c36d53bd (patch)
tree46d70e19f9720d237f4423c1debfcacdd088ce0b
parenta3c34a309c4b1421eb4914c3fbeba327a5400021 (diff)
parentd2fb674566393d9a8b88d71dba9f5081786b118c (diff)
Merge pull request #917 from ueokande/operation-as-a-operator
refactor: Make each operation as an operator
-rw-r--r--e2e/tab.test.ts6
-rw-r--r--src/background/clients/NavigateClient.ts14
-rw-r--r--src/background/controllers/OperationController.ts116
-rw-r--r--src/background/di.ts16
-rw-r--r--src/background/infrastructures/ConsoleClient.ts14
-rw-r--r--src/background/operators/Operator.ts5
-rw-r--r--src/background/operators/OperatorFactory.ts6
-rw-r--r--src/background/operators/OperatorFactoryChain.ts6
-rw-r--r--src/background/operators/impls/CancelOperator.ts15
-rw-r--r--src/background/operators/impls/CloseTabOperator.ts22
-rw-r--r--src/background/operators/impls/CloseTabRightOperator.ts21
-rw-r--r--src/background/operators/impls/CommandOperatorFactoryChain.ts63
-rw-r--r--src/background/operators/impls/DuplicateTabOperator.ts11
-rw-r--r--src/background/operators/impls/InternalOpenURLOperator.ts24
-rw-r--r--src/background/operators/impls/InternalOperatorFactoryChain.ts38
-rw-r--r--src/background/operators/impls/NavigateHistoryNextOperator.ts15
-rw-r--r--src/background/operators/impls/NavigateHistoryPrevOperator.ts15
-rw-r--r--src/background/operators/impls/NavigateLinkNextOperator.ts15
-rw-r--r--src/background/operators/impls/NavigateLinkPrevOperator.ts15
-rw-r--r--src/background/operators/impls/NavigateOperatorFactoryChain.ts66
-rw-r--r--src/background/operators/impls/NavigateParentOperator.ts25
-rw-r--r--src/background/operators/impls/NavigateRootOperator.ts12
-rw-r--r--src/background/operators/impls/OpenHomeOperator.ts29
-rw-r--r--src/background/operators/impls/OpenSourceOperator.ts12
-rw-r--r--src/background/operators/impls/OperatorFactoryImpl.ts45
-rw-r--r--src/background/operators/impls/PinTabOperator.ts11
-rw-r--r--src/background/operators/impls/ReloadTabOperator.ts14
-rw-r--r--src/background/operators/impls/ReopenTabOperator.ts10
-rw-r--r--src/background/operators/impls/RepeatLastOperator.ts18
-rw-r--r--src/background/operators/impls/RepeatOperatorFactoryChain.ts29
-rw-r--r--src/background/operators/impls/ResetZoomOperator.ts10
-rw-r--r--src/background/operators/impls/SelectFirstTabOperator.ts11
-rw-r--r--src/background/operators/impls/SelectLastTabOperator.ts11
-rw-r--r--src/background/operators/impls/SelectPreviousSelectedTabOperator.ts14
-rw-r--r--src/background/operators/impls/SelectTabNextOperator.ts19
-rw-r--r--src/background/operators/impls/SelectTabPrevOperator.ts19
-rw-r--r--src/background/operators/impls/ShowAddBookmarkOperator.ts20
-rw-r--r--src/background/operators/impls/ShowBufferCommandOperator.ts16
-rw-r--r--src/background/operators/impls/ShowCommandOperator.ts15
-rw-r--r--src/background/operators/impls/ShowOpenCommandOperator.ts20
-rw-r--r--src/background/operators/impls/ShowTabOpenCommandOperator.ts20
-rw-r--r--src/background/operators/impls/ShowWinOpenCommandOperator.ts20
-rw-r--r--src/background/operators/impls/StartFindOperator.ts15
-rw-r--r--src/background/operators/impls/TabOperatorFactoryChain.ts64
-rw-r--r--src/background/operators/impls/TogglePinnedTabOperator.ts11
-rw-r--r--src/background/operators/impls/UnpinTabOperator.ts11
-rw-r--r--src/background/operators/impls/ZoomInOperator.ts10
-rw-r--r--src/background/operators/impls/ZoomOperatorFactoryChain.ts28
-rw-r--r--src/background/operators/impls/ZoomOutOperator.ts10
-rw-r--r--src/background/presenters/WindowPresenter.ts11
-rw-r--r--src/background/presenters/ZoomPresenter.ts60
-rw-r--r--src/background/repositories/BrowserSettingRepository.ts6
-rw-r--r--src/background/repositories/RepeatRepository.ts8
-rw-r--r--src/background/usecases/AddonEnabledUseCase.ts2
-rw-r--r--src/background/usecases/CommandUseCase.ts23
-rw-r--r--src/background/usecases/ConsoleUseCase.ts63
-rw-r--r--src/background/usecases/FindUseCase.ts15
-rw-r--r--src/background/usecases/MarkUseCase.ts12
-rw-r--r--src/background/usecases/NavigateUseCase.ts56
-rw-r--r--src/background/usecases/RepeatUseCase.ts7
-rw-r--r--src/background/usecases/TabSelectUseCase.ts51
-rw-r--r--src/background/usecases/TabUseCase.ts102
-rw-r--r--src/background/usecases/ZoomUseCase.ts51
-rw-r--r--src/content/controllers/KeymapController.ts88
-rw-r--r--src/content/di.ts8
-rw-r--r--src/content/hint-key-producer.ts37
-rw-r--r--src/content/operators/Operator.ts5
-rw-r--r--src/content/operators/OperatorFactory.ts6
-rw-r--r--src/content/operators/OperatorFactoryChain.ts6
-rw-r--r--src/content/operators/impls/AbstractScrollOperator.ts10
-rw-r--r--src/content/operators/impls/AddonOperatorFactoryChain.ts40
-rw-r--r--src/content/operators/impls/BackgroundOperationOperator.ts15
-rw-r--r--src/content/operators/impls/ClipboardOperatorFactoryChain.ts47
-rw-r--r--src/content/operators/impls/DisableAddonOperator.ts15
-rw-r--r--src/content/operators/impls/EnableAddonOperator.ts15
-rw-r--r--src/content/operators/impls/EnableJumpMarkOperator.ts10
-rw-r--r--src/content/operators/impls/EnableSetMarkOperator.ts10
-rw-r--r--src/content/operators/impls/FindNextOperator.ts15
-rw-r--r--src/content/operators/impls/FindOperatorFactoryChain.ts25
-rw-r--r--src/content/operators/impls/FindPrevOperator.ts15
-rw-r--r--src/content/operators/impls/FocusOperator.ts10
-rw-r--r--src/content/operators/impls/FocusOperatorFactoryChain.ts22
-rw-r--r--src/content/operators/impls/FollowOperatorFactoryChain.ts27
-rw-r--r--src/content/operators/impls/HorizontalScrollOperator.ts21
-rw-r--r--src/content/operators/impls/MarkOperatorFactoryChain.ts25
-rw-r--r--src/content/operators/impls/OperatorFactoryImpl.ts51
-rw-r--r--src/content/operators/impls/PageScrollOperator.ts21
-rw-r--r--src/content/operators/impls/PasteOperator.ts25
-rw-r--r--src/content/operators/impls/ScrollOperatorFactoryChain.ts68
-rw-r--r--src/content/operators/impls/ScrollToBottomOperator.ts20
-rw-r--r--src/content/operators/impls/ScrollToEndOperator.ts20
-rw-r--r--src/content/operators/impls/ScrollToHomeOperator.ts20
-rw-r--r--src/content/operators/impls/ScrollToTopOperator.ts20
-rw-r--r--src/content/operators/impls/StartFollowOperator.ts14
-rw-r--r--src/content/operators/impls/ToggleAddonOperator.ts16
-rw-r--r--src/content/operators/impls/URLRepository.ts9
-rw-r--r--src/content/operators/impls/VerticalScrollOperator.ts21
-rw-r--r--src/content/operators/impls/YankURLOperator.ts18
-rw-r--r--src/content/repositories/HintKeyRepository.ts49
-rw-r--r--src/content/usecases/ClipboardUseCase.ts34
-rw-r--r--src/content/usecases/FindSlaveUseCase.ts17
-rw-r--r--src/content/usecases/FocusUseCase.ts11
-rw-r--r--src/content/usecases/FollowMasterUseCase.ts18
-rw-r--r--src/content/usecases/HintKeyProducer.ts37
-rw-r--r--src/content/usecases/MarkKeyUseCase.ts8
-rw-r--r--src/content/usecases/ScrollUseCase.ts51
-rw-r--r--test/background/mock/MockBrowserSettingRepository.ts10
-rw-r--r--test/background/mock/MockConsoleClient.ts23
-rw-r--r--test/background/mock/MockNavigateClient.ts19
-rw-r--r--test/background/mock/MockRepeatRepository.ts14
-rw-r--r--test/background/mock/MockTabPresenter.ts179
-rw-r--r--test/background/mock/MockWindowPresenter.ts7
-rw-r--r--test/background/mock/MockZoomPresenter.ts15
-rw-r--r--test/background/operators/impls/CancelOperator.test.ts24
-rw-r--r--test/background/operators/impls/CloseTabOperator.test.ts61
-rw-r--r--test/background/operators/impls/CloseTabRightOperator.test.ts24
-rw-r--r--test/background/operators/impls/CommandOperatorFactoryChain.test.ts42
-rw-r--r--test/background/operators/impls/DuplicateTabOperator.test.ts25
-rw-r--r--test/background/operators/impls/InternalOpenURLOperator.test.ts0
-rw-r--r--test/background/operators/impls/InternalOperatorFactoryChain.test.ts37
-rw-r--r--test/background/operators/impls/NavigateHistoryNextOperator.test.ts25
-rw-r--r--test/background/operators/impls/NavigateHistoryPrevOperator.test.ts25
-rw-r--r--test/background/operators/impls/NavigateLinkNextOperator.test.ts22
-rw-r--r--test/background/operators/impls/NavigateLinkPrevOperator.test.ts22
-rw-r--r--test/background/operators/impls/NavigateOperatorFactoryChain.test.ts56
-rw-r--r--test/background/operators/impls/NavigateParentOperator.test.ts53
-rw-r--r--test/background/operators/impls/NavigateRootOperator.test.ts18
-rw-r--r--test/background/operators/impls/OpenHomeOperator.test.ts70
-rw-r--r--test/background/operators/impls/OpenSourceOperator.test.ts21
-rw-r--r--test/background/operators/impls/PinTabOperator.test.ts25
-rw-r--r--test/background/operators/impls/ReloadTabOperator.test.ts34
-rw-r--r--test/background/operators/impls/ReopenTabOperator.test.ts17
-rw-r--r--test/background/operators/impls/RepeatLastOperator.test.ts56
-rw-r--r--test/background/operators/impls/RepeatOperatorFactoryChain.test.ts32
-rw-r--r--test/background/operators/impls/ResetZoomOperator.test.ts17
-rw-r--r--test/background/operators/impls/SelectFirstTabOperator.test.ts20
-rw-r--r--test/background/operators/impls/SelectLastTabOperator.test.ts20
-rw-r--r--test/background/operators/impls/SelectPreviousSelectedTabOperator.test.ts38
-rw-r--r--test/background/operators/impls/SelectTabNextOperator.test.ts35
-rw-r--r--test/background/operators/impls/SelectTabPrevOperator.test.ts35
-rw-r--r--test/background/operators/impls/ShowAddBookmarkOperator.test.ts50
-rw-r--r--test/background/operators/impls/ShowBufferCommandOperator.test.ts25
-rw-r--r--test/background/operators/impls/ShowCommandOperator.test.ts25
-rw-r--r--test/background/operators/impls/ShowOpenCommandOperator.test.ts50
-rw-r--r--test/background/operators/impls/ShowTabOpenCommandOperator.test.ts50
-rw-r--r--test/background/operators/impls/ShowWinOpenCommandOperator.test.ts50
-rw-r--r--test/background/operators/impls/StartFindOperator.test.ts22
-rw-r--r--test/background/operators/impls/TabOperatorFactoryChain.test.ts71
-rw-r--r--test/background/operators/impls/TogglePinnedTabOperator.test.ts32
-rw-r--r--test/background/operators/impls/UnpinTabOperator.test.ts25
-rw-r--r--test/background/operators/impls/ZoomInOperator.test.ts17
-rw-r--r--test/background/operators/impls/ZoomOperatorFactoryChain.test.ts28
-rw-r--r--test/background/operators/impls/ZoomOutOperator.test.ts17
-rw-r--r--test/background/usecases/NavigateUseCase.test.ts183
-rw-r--r--test/content/mock/MockAddonEnabledRepository.ts18
-rw-r--r--test/content/mock/MockAddonIndicatorClient.ts13
-rw-r--r--test/content/mock/MockClipboardRepository.ts16
-rw-r--r--test/content/mock/MockFindMasterClient.ts11
-rw-r--r--test/content/mock/MockFocusPresenter.ts7
-rw-r--r--test/content/mock/MockFollowMasterClient.ts16
-rw-r--r--test/content/mock/MockMarkKeyRepository.ts43
-rw-r--r--test/content/mock/MockOperationClient.ts16
-rw-r--r--test/content/mock/MockScrollPresenter.ts6
-rw-r--r--test/content/mock/MockSettingRepository.ts20
-rw-r--r--test/content/mock/MockURLRepository.ts9
-rw-r--r--test/content/operators/impls/AddonOperatorFactoryChain.test.ts29
-rw-r--r--test/content/operators/impls/BackgroundOperationOperator.test.ts38
-rw-r--r--test/content/operators/impls/ClipboardOperatorFactoryChain.test.ts31
-rw-r--r--test/content/operators/impls/DisableAddonOperator.test.ts19
-rw-r--r--test/content/operators/impls/EnableAddonOperator.test.ts19
-rw-r--r--test/content/operators/impls/EnableJumpMarkOperator.test.ts19
-rw-r--r--test/content/operators/impls/EnableSetMarkOperator.test.ts19
-rw-r--r--test/content/operators/impls/FindNextOperator.test.ts17
-rw-r--r--test/content/operators/impls/FindOperatorFactoryChain.test.ts21
-rw-r--r--test/content/operators/impls/FindPrevOperator.test.ts17
-rw-r--r--test/content/operators/impls/FocusOperator.test.ts17
-rw-r--r--test/content/operators/impls/FocusOperatorFactoryChain.test.ts17
-rw-r--r--test/content/operators/impls/FollowOperatorFactoryChain.test.ts20
-rw-r--r--test/content/operators/impls/HorizontalScrollOperator.test.ts28
-rw-r--r--test/content/operators/impls/MarkOperatorFactoryChain.test.ts21
-rw-r--r--test/content/operators/impls/PageScrollOperator.test.ts28
-rw-r--r--test/content/operators/impls/PasteOperator.test.ts51
-rw-r--r--test/content/operators/impls/ScrollOperatorFactoryChain.test.ts46
-rw-r--r--test/content/operators/impls/ScrollToBottomOperator.test.ts18
-rw-r--r--test/content/operators/impls/ScrollToEndOperator.test.ts18
-rw-r--r--test/content/operators/impls/ScrollToHomeOperator.test.ts18
-rw-r--r--test/content/operators/impls/ScrollToTopOperator.test.ts18
-rw-r--r--test/content/operators/impls/StartFollowOperator.test.ts20
-rw-r--r--test/content/operators/impls/ToggleAddonOperator.test.ts24
-rw-r--r--test/content/operators/impls/VerticalScrollOperator.test.ts28
-rw-r--r--test/content/operators/impls/YankURLOperator.test.ts26
-rw-r--r--test/content/usecases/ClipboardUseCase.test.ts95
-rw-r--r--test/content/usecases/HintKeyProducer.test.ts33
193 files changed, 4186 insertions, 1049 deletions
diff --git a/e2e/tab.test.ts b/e2e/tab.test.ts
index 98ec9f8..49d1c03 100644
--- a/e2e/tab.test.ts
+++ b/e2e/tab.test.ts
@@ -79,8 +79,10 @@ describe("tab test", () => {
const page = await Page.currentContext(webdriver);
await page.sendKeys("x", "$");
- const current = await browser.tabs.query({ windowId: win.id });
- assert.strictEqual(current.length, 2);
+ await eventually(async () => {
+ const current = await browser.tabs.query({ windowId: win.id });
+ assert.strictEqual(current.length, 2);
+ });
});
it("duplicates tab by zd", async () => {
diff --git a/src/background/clients/NavigateClient.ts b/src/background/clients/NavigateClient.ts
index 40ceb45..af8688e 100644
--- a/src/background/clients/NavigateClient.ts
+++ b/src/background/clients/NavigateClient.ts
@@ -1,8 +1,16 @@
-import { injectable } from "tsyringe";
import * as messages from "../../shared/messages";
-@injectable()
-export default class NavigateClient {
+export default interface NavigateClient {
+ historyNext(tabId: number): Promise<void>;
+
+ historyPrev(tabId: number): Promise<void>;
+
+ linkNext(tabId: number): Promise<void>;
+
+ linkPrev(tabId: number): Promise<void>;
+}
+
+export class NavigateClientImpl implements NavigateClient {
async historyNext(tabId: number): Promise<void> {
await browser.tabs.sendMessage(tabId, {
type: messages.NAVIGATE_HISTORY_NEXT,
diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts
index 5a7047d..d606ac0 100644
--- a/src/background/controllers/OperationController.ts
+++ b/src/background/controllers/OperationController.ts
@@ -1,23 +1,14 @@
-import { injectable } from "tsyringe";
+import { inject, injectable } from "tsyringe";
import * as operations from "../../shared/operations";
-import FindUseCase from "../usecases/FindUseCase";
-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";
+import OperatorFactory from "../operators/OperatorFactory";
import RepeatUseCase from "../usecases/RepeatUseCase";
@injectable()
export default class OperationController {
constructor(
- private findUseCase: FindUseCase,
- private consoleUseCase: ConsoleUseCase,
- private tabUseCase: TabUseCase,
- private tabSelectUseCase: TabSelectUseCase,
- private zoomUseCase: ZoomUseCase,
- private navigateUseCase: NavigateUseCase,
- private repeatUseCase: RepeatUseCase
+ private readonly repeatUseCase: RepeatUseCase,
+ @inject("OperatorFactory")
+ private readonly operatorFactory: OperatorFactory
) {}
async exec(repeat: number, op: operations.Operation): Promise<any> {
@@ -27,105 +18,14 @@ export default class OperationController {
}
}
- // eslint-disable-next-line complexity, max-lines-per-function
- async doOperation(
+ private async doOperation(
repeat: number,
operation: operations.Operation
): Promise<any> {
- // eslint-disable-next-line complexity, max-lines-per-function
- const opFunc = (() => {
- switch (operation.type) {
- case operations.TAB_CLOSE:
- return () =>
- this.tabUseCase.close(false, operation.select === "left");
- case operations.TAB_CLOSE_RIGHT:
- return () => this.tabUseCase.closeRight();
- case operations.TAB_CLOSE_FORCE:
- return () => this.tabUseCase.close(true);
- case operations.TAB_REOPEN:
- return () => this.tabUseCase.reopen();
- case operations.TAB_PREV:
- return () => this.tabSelectUseCase.selectPrev(1);
- case operations.TAB_NEXT:
- return () => this.tabSelectUseCase.selectNext(1);
- case operations.TAB_FIRST:
- return () => this.tabSelectUseCase.selectFirst();
- case operations.TAB_LAST:
- return () => this.tabSelectUseCase.selectLast();
- case operations.TAB_PREV_SEL:
- return () => this.tabSelectUseCase.selectPrevSelected();
- case operations.TAB_RELOAD:
- return () => this.tabUseCase.reload(operation.cache);
- case operations.TAB_PIN:
- return () => this.tabUseCase.setPinned(true);
- case operations.TAB_UNPIN:
- return () => this.tabUseCase.setPinned(false);
- case operations.TAB_TOGGLE_PINNED:
- return () => this.tabUseCase.togglePinned();
- case operations.TAB_DUPLICATE:
- return () => this.tabUseCase.duplicate();
- case operations.PAGE_SOURCE:
- return () => this.tabUseCase.openPageSource();
- case operations.PAGE_HOME:
- return () => this.tabUseCase.openHome(operation.newTab);
- case operations.ZOOM_IN:
- return () => this.zoomUseCase.zoomIn();
- case operations.ZOOM_OUT:
- return () => this.zoomUseCase.zoomOut();
- case operations.ZOOM_NEUTRAL:
- return () => this.zoomUseCase.zoomNutoral();
- case operations.COMMAND_SHOW:
- return () => this.consoleUseCase.showCommand();
- case operations.COMMAND_SHOW_OPEN:
- return () => this.consoleUseCase.showOpenCommand(operation.alter);
- case operations.COMMAND_SHOW_TABOPEN:
- return () => this.consoleUseCase.showTabopenCommand(operation.alter);
- case operations.COMMAND_SHOW_WINOPEN:
- return () => this.consoleUseCase.showWinopenCommand(operation.alter);
- case operations.COMMAND_SHOW_BUFFER:
- return () => this.consoleUseCase.showBufferCommand();
- case operations.COMMAND_SHOW_ADDBOOKMARK:
- return () =>
- this.consoleUseCase.showAddbookmarkCommand(operation.alter);
- case operations.FIND_START:
- 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();
- case operations.NAVIGATE_PARENT:
- return () => this.navigateUseCase.openParent();
- case operations.NAVIGATE_ROOT:
- return () => this.navigateUseCase.openRoot();
- case operations.REPEAT_LAST:
- return () => {
- const last = this.repeatUseCase.getLastOperation();
- if (typeof last !== "undefined") {
- return this.doOperation(1, last);
- }
- return Promise.resolve();
- };
- case operations.INTERNAL_OPEN_URL:
- return () =>
- this.tabUseCase.openURL(
- operation.url,
- operation.newTab,
- operation.newWindow
- );
- default:
- throw new Error("unknown operation: " + operation.type);
- }
- })();
-
+ const operator = this.operatorFactory.create(operation);
for (let i = 0; i < repeat; ++i) {
// eslint-disable-next-line no-await-in-loop
- await opFunc();
+ await operator.run();
}
}
}
diff --git a/src/background/di.ts b/src/background/di.ts
index 5e6ad03..efdf363 100644
--- a/src/background/di.ts
+++ b/src/background/di.ts
@@ -11,6 +11,13 @@ import HistoryRepositoryImpl from "./completion/impl/HistoryRepositoryImpl";
import BookmarkRepositoryImpl from "./completion/impl/BookmarkRepositoryImpl";
import TabRepositoryImpl from "./completion/impl/TabRepositoryImpl";
import { TabPresenterImpl } from "./presenters/TabPresenter";
+import { OperatorFactoryImpl } from "./operators/impls/OperatorFactoryImpl";
+import { NavigateClientImpl } from "./clients/NavigateClient";
+import { ConsoleClientImpl } from "./infrastructures/ConsoleClient";
+import { BrowserSettingRepositoryImpl } from "./repositories/BrowserSettingRepository";
+import { RepeatRepositoryImpl } from "./repositories/RepeatRepository";
+import { ZoomPresenterImpl } from "./presenters/ZoomPresenter";
+import { WindowPresenterImpl } from "./presenters/WindowPresenter";
container.register("LocalSettingRepository", {
useValue: LocalSettingRepository,
@@ -24,5 +31,14 @@ container.register("CachedSettingRepository", {
container.register("Notifier", { useClass: NotifierImpl });
container.register("HistoryRepository", { useClass: HistoryRepositoryImpl });
container.register("BookmarkRepository", { useClass: BookmarkRepositoryImpl });
+container.register("BrowserSettingRepository", {
+ useClass: BrowserSettingRepositoryImpl,
+});
+container.register("RepeatRepository", { useClass: RepeatRepositoryImpl });
container.register("TabRepository", { useClass: TabRepositoryImpl });
+container.register("ZoomPresenter", { useClass: ZoomPresenterImpl });
container.register("TabPresenter", { useClass: TabPresenterImpl });
+container.register("WindowPresenter", { useClass: WindowPresenterImpl });
+container.register("NavigateClient", { useClass: NavigateClientImpl });
+container.register("ConsoleClient", { useClass: ConsoleClientImpl });
+container.register("OperatorFactory", { useClass: OperatorFactoryImpl });
diff --git a/src/background/infrastructures/ConsoleClient.ts b/src/background/infrastructures/ConsoleClient.ts
index 8d0af89..2a1df5b 100644
--- a/src/background/infrastructures/ConsoleClient.ts
+++ b/src/background/infrastructures/ConsoleClient.ts
@@ -1,8 +1,20 @@
import { injectable } from "tsyringe";
import * as messages from "../../shared/messages";
+export default interface ConsoleClient {
+ showCommand(tabId: number, command: string): Promise<any>;
+
+ showFind(tabId: number): Promise<any>;
+
+ showInfo(tabId: number, message: string): Promise<any>;
+
+ showError(tabId: number, message: string): Promise<any>;
+
+ hide(tabId: number): Promise<any>;
+}
+
@injectable()
-export default class ConsoleClient {
+export class ConsoleClientImpl implements ConsoleClient {
showCommand(tabId: number, command: string): Promise<any> {
return browser.tabs.sendMessage(tabId, {
type: messages.CONSOLE_SHOW_COMMAND,
diff --git a/src/background/operators/Operator.ts b/src/background/operators/Operator.ts
new file mode 100644
index 0000000..3b1fe03
--- /dev/null
+++ b/src/background/operators/Operator.ts
@@ -0,0 +1,5 @@
+interface Operator {
+ run(): Promise<void>;
+}
+
+export default Operator;
diff --git a/src/background/operators/OperatorFactory.ts b/src/background/operators/OperatorFactory.ts
new file mode 100644
index 0000000..9a5234c
--- /dev/null
+++ b/src/background/operators/OperatorFactory.ts
@@ -0,0 +1,6 @@
+import Operator from "./Operator";
+import { Operation } from "../../shared/operations";
+
+export default interface OperatorFactory {
+ create(op: Operation): Operator;
+}
diff --git a/src/background/operators/OperatorFactoryChain.ts b/src/background/operators/OperatorFactoryChain.ts
new file mode 100644
index 0000000..046ed42
--- /dev/null
+++ b/src/background/operators/OperatorFactoryChain.ts
@@ -0,0 +1,6 @@
+import Operator from "./Operator";
+import { Operation } from "../../shared/operations";
+
+export default interface OperatorFactoryChain {
+ create(op: Operation): Operator | null;
+}
diff --git a/src/background/operators/impls/CancelOperator.ts b/src/background/operators/impls/CancelOperator.ts
new file mode 100644
index 0000000..7ab09fb
--- /dev/null
+++ b/src/background/operators/impls/CancelOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class CancelOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.consoleClient.hide(tab.id as number);
+ }
+}
diff --git a/src/background/operators/impls/CloseTabOperator.ts b/src/background/operators/impls/CloseTabOperator.ts
new file mode 100644
index 0000000..5d8e80b
--- /dev/null
+++ b/src/background/operators/impls/CloseTabOperator.ts
@@ -0,0 +1,22 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class CloseTabOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly force: boolean = false,
+ private readonly selectLeft: boolean = false
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ if (!this.force && tab.pinned) {
+ return Promise.resolve();
+ }
+ if (this.selectLeft && tab.index > 0) {
+ const tabs = await this.tabPresenter.getAll();
+ await this.tabPresenter.select(tabs[tab.index - 1].id as number);
+ }
+ return this.tabPresenter.remove([tab.id as number]);
+ }
+}
diff --git a/src/background/operators/impls/CloseTabRightOperator.ts b/src/background/operators/impls/CloseTabRightOperator.ts
new file mode 100644
index 0000000..f36930e
--- /dev/null
+++ b/src/background/operators/impls/CloseTabRightOperator.ts
@@ -0,0 +1,21 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class CloseTabRightOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabs = await this.tabPresenter.getAll();
+ tabs.sort((t1, t2) => t1.index - t2.index);
+ const index = tabs.findIndex((t) => t.active);
+ if (index < 0) {
+ return;
+ }
+ for (let i = index + 1; i < tabs.length; ++i) {
+ const tab = tabs[i];
+ if (!tab.pinned) {
+ await this.tabPresenter.remove([tab.id as number]);
+ }
+ }
+ }
+}
diff --git a/src/background/operators/impls/CommandOperatorFactoryChain.ts b/src/background/operators/impls/CommandOperatorFactoryChain.ts
new file mode 100644
index 0000000..680a384
--- /dev/null
+++ b/src/background/operators/impls/CommandOperatorFactoryChain.ts
@@ -0,0 +1,63 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import ShowCommandOperator from "./ShowCommandOperator";
+import ShowOpenCommandOperator from "./ShowOpenCommandOperator";
+import ShowTabOpenCommandOperator from "./ShowTabOpenCommandOperator";
+import ShowWinOpenCommandOperator from "./ShowWinOpenCommandOperator";
+import ShowBufferCommandOperator from "./ShowBufferCommandOperator";
+import ShowAddBookmarkOperator from "./ShowAddBookmarkOperator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+import * as operations from "../../../shared/operations";
+import StartFindOperator from "./StartFindOperator";
+
+@injectable()
+export default class CommandOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter,
+ @inject("ConsoleClient")
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.COMMAND_SHOW:
+ return new ShowCommandOperator(this.tabPresenter, this.consoleClient);
+ case operations.COMMAND_SHOW_OPEN:
+ return new ShowOpenCommandOperator(
+ this.tabPresenter,
+ this.consoleClient,
+ op.alter
+ );
+ case operations.COMMAND_SHOW_TABOPEN:
+ return new ShowTabOpenCommandOperator(
+ this.tabPresenter,
+ this.consoleClient,
+ op.alter
+ );
+ case operations.COMMAND_SHOW_WINOPEN:
+ return new ShowWinOpenCommandOperator(
+ this.tabPresenter,
+ this.consoleClient,
+ op.alter
+ );
+ case operations.COMMAND_SHOW_BUFFER:
+ return new ShowBufferCommandOperator(
+ this.tabPresenter,
+ this.consoleClient
+ );
+ case operations.COMMAND_SHOW_ADDBOOKMARK:
+ return new ShowAddBookmarkOperator(
+ this.tabPresenter,
+ this.consoleClient,
+ op.alter
+ );
+ case operations.FIND_START:
+ return new StartFindOperator(this.tabPresenter, this.consoleClient);
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/DuplicateTabOperator.ts b/src/background/operators/impls/DuplicateTabOperator.ts
new file mode 100644
index 0000000..7737cfa
--- /dev/null
+++ b/src/background/operators/impls/DuplicateTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class DuplicateTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.tabPresenter.duplicate(tab.id as number);
+ }
+}
diff --git a/src/background/operators/impls/InternalOpenURLOperator.ts b/src/background/operators/impls/InternalOpenURLOperator.ts
new file mode 100644
index 0000000..6bf513b
--- /dev/null
+++ b/src/background/operators/impls/InternalOpenURLOperator.ts
@@ -0,0 +1,24 @@
+import Operator from "../Operator";
+import WindowPresenter from "../../presenters/WindowPresenter";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class InternalOpenURLOperator implements Operator {
+ constructor(
+ private readonly windowPresenter: WindowPresenter,
+ private readonly tabPresenter: TabPresenter,
+ private readonly url: string,
+ private readonly newTab?: boolean,
+ private readonly newWindow?: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ if (this.newWindow) {
+ await this.windowPresenter.create(this.url);
+ } else if (this.newTab) {
+ await this.tabPresenter.create(this.url);
+ } else {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.tabPresenter.open(this.url, tab.id);
+ }
+ }
+}
diff --git a/src/background/operators/impls/InternalOperatorFactoryChain.ts b/src/background/operators/impls/InternalOperatorFactoryChain.ts
new file mode 100644
index 0000000..05615f6
--- /dev/null
+++ b/src/background/operators/impls/InternalOperatorFactoryChain.ts
@@ -0,0 +1,38 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import CancelOperator from "./CancelOperator";
+import InternalOpenURLOperator from "./InternalOpenURLOperator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+import WindowPresenter from "../../presenters/WindowPresenter";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class InternalOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("WindowPresenter")
+ private readonly windowPresenter: WindowPresenter,
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter,
+ @inject("ConsoleClient")
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.CANCEL:
+ return new CancelOperator(this.tabPresenter, this.consoleClient);
+ case operations.INTERNAL_OPEN_URL:
+ return new InternalOpenURLOperator(
+ this.windowPresenter,
+ this.tabPresenter,
+ op.url,
+ op.newTab,
+ op.newWindow
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/NavigateHistoryNextOperator.ts b/src/background/operators/impls/NavigateHistoryNextOperator.ts
new file mode 100644
index 0000000..b092c48
--- /dev/null
+++ b/src/background/operators/impls/NavigateHistoryNextOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import NavigateClient from "../../clients/NavigateClient";
+
+export default class NavigateHistoryNextOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly navigateClient: NavigateClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.navigateClient.historyNext(tab.id!);
+ }
+}
diff --git a/src/background/operators/impls/NavigateHistoryPrevOperator.ts b/src/background/operators/impls/NavigateHistoryPrevOperator.ts
new file mode 100644
index 0000000..27d4ee9
--- /dev/null
+++ b/src/background/operators/impls/NavigateHistoryPrevOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import NavigateClient from "../../clients/NavigateClient";
+
+export default class NavigateHistoryPrevOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly navigateClient: NavigateClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.navigateClient.historyPrev(tab.id!);
+ }
+}
diff --git a/src/background/operators/impls/NavigateLinkNextOperator.ts b/src/background/operators/impls/NavigateLinkNextOperator.ts
new file mode 100644
index 0000000..dbbcc45
--- /dev/null
+++ b/src/background/operators/impls/NavigateLinkNextOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import NavigateClient from "../../clients/NavigateClient";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class NavigateLinkNextOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly navigateClient: NavigateClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.navigateClient.linkNext(tab.id!);
+ }
+}
diff --git a/src/background/operators/impls/NavigateLinkPrevOperator.ts b/src/background/operators/impls/NavigateLinkPrevOperator.ts
new file mode 100644
index 0000000..fe41ee6
--- /dev/null
+++ b/src/background/operators/impls/NavigateLinkPrevOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import NavigateClient from "../../clients/NavigateClient";
+
+export default class NavigateLinkPrevOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly navigateClient: NavigateClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ await this.navigateClient.linkPrev(tab.id!);
+ }
+}
diff --git a/src/background/operators/impls/NavigateOperatorFactoryChain.ts b/src/background/operators/impls/NavigateOperatorFactoryChain.ts
new file mode 100644
index 0000000..618db45
--- /dev/null
+++ b/src/background/operators/impls/NavigateOperatorFactoryChain.ts
@@ -0,0 +1,66 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import NavigateHistoryPrevOperator from "./NavigateHistoryPrevOperator";
+import NavigateHistoryNextOperator from "./NavigateHistoryNextOperator";
+import NavigateLinkPrevOperator from "./NavigateLinkPrevOperator";
+import NavigateLinkNextOperator from "./NavigateLinkNextOperator";
+import NavigateParentOperator from "./NavigateParentOperator";
+import NavigateRootOperator from "./NavigateRootOperator";
+import OpenSourceOperator from "./OpenSourceOperator";
+import OpenHomeOperator from "./OpenHomeOperator";
+import TabPresenter from "../../presenters/TabPresenter";
+import NavigateClient from "../../clients/NavigateClient";
+import BrowserSettingRepository from "../../repositories/BrowserSettingRepository";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class NavigateOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter,
+ @inject("NavigateClient")
+ private readonly navigateClient: NavigateClient,
+ @inject("BrowserSettingRepository")
+ private readonly browserSettingRepository: BrowserSettingRepository
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.NAVIGATE_HISTORY_PREV:
+ return new NavigateHistoryPrevOperator(
+ this.tabPresenter,
+ this.navigateClient
+ );
+ case operations.NAVIGATE_HISTORY_NEXT:
+ return new NavigateHistoryNextOperator(
+ this.tabPresenter,
+ this.navigateClient
+ );
+ case operations.NAVIGATE_LINK_PREV:
+ return new NavigateLinkPrevOperator(
+ this.tabPresenter,
+ this.navigateClient
+ );
+ case operations.NAVIGATE_LINK_NEXT:
+ return new NavigateLinkNextOperator(
+ this.tabPresenter,
+ this.navigateClient
+ );
+ case operations.NAVIGATE_PARENT:
+ return new NavigateParentOperator(this.tabPresenter);
+ case operations.NAVIGATE_ROOT:
+ return new NavigateRootOperator(this.tabPresenter);
+ case operations.PAGE_SOURCE:
+ return new OpenSourceOperator(this.tabPresenter);
+ case operations.PAGE_HOME:
+ return new OpenHomeOperator(
+ this.tabPresenter,
+ this.browserSettingRepository,
+ op.newTab
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/NavigateParentOperator.ts b/src/background/operators/impls/NavigateParentOperator.ts
new file mode 100644
index 0000000..652cfb8
--- /dev/null
+++ b/src/background/operators/impls/NavigateParentOperator.ts
@@ -0,0 +1,25 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class NavigateParentOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ const url = new URL(tab.url!);
+ if (url.hash.length > 0) {
+ url.hash = "";
+ } else if (url.search.length > 0) {
+ 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);
+ }
+}
diff --git a/src/background/operators/impls/NavigateRootOperator.ts b/src/background/operators/impls/NavigateRootOperator.ts
new file mode 100644
index 0000000..b140156
--- /dev/null
+++ b/src/background/operators/impls/NavigateRootOperator.ts
@@ -0,0 +1,12 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class NavigateRootOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ const url = new URL(tab.url!);
+ await this.tabPresenter.open(url.origin);
+ }
+}
diff --git a/src/background/operators/impls/OpenHomeOperator.ts b/src/background/operators/impls/OpenHomeOperator.ts
new file mode 100644
index 0000000..4773be6
--- /dev/null
+++ b/src/background/operators/impls/OpenHomeOperator.ts
@@ -0,0 +1,29 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import BrowserSettingRepository from "../../repositories/BrowserSettingRepository";
+
+export default class OpenHomeOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly browserSettingRepository: BrowserSettingRepository,
+ private readonly newTab: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ const urls = await this.browserSettingRepository.getHomepageUrls();
+ if (urls.length === 1 && urls[0] === "about:home") {
+ // eslint-disable-next-line max-len
+ throw new Error(
+ "Cannot open Firefox Home (about:home) by WebExtensions, set your custom URLs"
+ );
+ }
+ if (urls.length === 1 && !this.newTab) {
+ await this.tabPresenter.open(urls[0], tab.id);
+ return;
+ }
+ for (const url of urls) {
+ await this.tabPresenter.create(url);
+ }
+ }
+}
diff --git a/src/background/operators/impls/OpenSourceOperator.ts b/src/background/operators/impls/OpenSourceOperator.ts
new file mode 100644
index 0000000..9185ba5
--- /dev/null
+++ b/src/background/operators/impls/OpenSourceOperator.ts
@@ -0,0 +1,12 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class OpenSourceOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ const url = "view-source:" + tab.url;
+ await this.tabPresenter.create(url);
+ }
+}
diff --git a/src/background/operators/impls/OperatorFactoryImpl.ts b/src/background/operators/impls/OperatorFactoryImpl.ts
new file mode 100644
index 0000000..34e7bb5
--- /dev/null
+++ b/src/background/operators/impls/OperatorFactoryImpl.ts
@@ -0,0 +1,45 @@
+import { inject, delay, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactory from "../OperatorFactory";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import CommandOperatorFactoryChain from "./CommandOperatorFactoryChain";
+import InternalOperatorFactoryChain from "./InternalOperatorFactoryChain";
+import NavigateOperatorFactoryChain from "./NavigateOperatorFactoryChain";
+import RepeatOperatorFactoryChain from "./RepeatOperatorFactoryChain";
+import TabOperatorFactoryChain from "./TabOperatorFactoryChain";
+import ZoomOperatorFactoryChain from "./ZoomOperatorFactoryChain";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export class OperatorFactoryImpl implements OperatorFactory {
+ private readonly factoryChains: OperatorFactoryChain[];
+
+ constructor(
+ commandOperatorFactoryChain: CommandOperatorFactoryChain,
+ internalOperatorFactoryChain: InternalOperatorFactoryChain,
+ navigateOperatorFactoryChain: NavigateOperatorFactoryChain,
+ tabOperatorFactoryChain: TabOperatorFactoryChain,
+ zoomOperatorFactoryChain: ZoomOperatorFactoryChain,
+ @inject(delay(() => RepeatOperatorFactoryChain))
+ repeatOperatorFactoryChain: RepeatOperatorFactoryChain
+ ) {
+ this.factoryChains = [
+ commandOperatorFactoryChain,
+ internalOperatorFactoryChain,
+ navigateOperatorFactoryChain,
+ repeatOperatorFactoryChain,
+ tabOperatorFactoryChain,
+ zoomOperatorFactoryChain,
+ ];
+ }
+
+ create(op: operations.Operation): Operator {
+ for (const chain of this.factoryChains) {
+ const operator = chain.create(op);
+ if (operator !== null) {
+ return operator;
+ }
+ }
+ throw new Error("unknown operation: " + op.type);
+ }
+}
diff --git a/src/background/operators/impls/PinTabOperator.ts b/src/background/operators/impls/PinTabOperator.ts
new file mode 100644
index 0000000..8121725
--- /dev/null
+++ b/src/background/operators/impls/PinTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class PinTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.tabPresenter.setPinned(tab.id as number, true);
+ }
+}
diff --git a/src/background/operators/impls/ReloadTabOperator.ts b/src/background/operators/impls/ReloadTabOperator.ts
new file mode 100644
index 0000000..db3389e
--- /dev/null
+++ b/src/background/operators/impls/ReloadTabOperator.ts
@@ -0,0 +1,14 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class ReloadTabOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly cache: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.tabPresenter.reload(tab.id as number, this.cache);
+ }
+}
diff --git a/src/background/operators/impls/ReopenTabOperator.ts b/src/background/operators/impls/ReopenTabOperator.ts
new file mode 100644
index 0000000..507b4a8
--- /dev/null
+++ b/src/background/operators/impls/ReopenTabOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class ReopenTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ run(): Promise<void> {
+ return this.tabPresenter.reopen();
+ }
+}
diff --git a/src/background/operators/impls/RepeatLastOperator.ts b/src/background/operators/impls/RepeatLastOperator.ts
new file mode 100644
index 0000000..d46daab
--- /dev/null
+++ b/src/background/operators/impls/RepeatLastOperator.ts
@@ -0,0 +1,18 @@
+import Operator from "../Operator";
+import RepeatRepository from "../../repositories/RepeatRepository";
+import OperatorFactory from "../OperatorFactory";
+
+export default class RepeatLastOperator implements Operator {
+ constructor(
+ private readonly repeatRepository: RepeatRepository,
+ private readonly operatorFactory: OperatorFactory
+ ) {}
+
+ run(): Promise<void> {
+ const op = this.repeatRepository.getLastOperation();
+ if (typeof op === "undefined") {
+ return Promise.resolve();
+ }
+ return this.operatorFactory.create(op).run();
+ }
+}
diff --git a/src/background/operators/impls/RepeatOperatorFactoryChain.ts b/src/background/operators/impls/RepeatOperatorFactoryChain.ts
new file mode 100644
index 0000000..5038d48
--- /dev/null
+++ b/src/background/operators/impls/RepeatOperatorFactoryChain.ts
@@ -0,0 +1,29 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import RepeatLastOperator from "./RepeatLastOperator";
+import RepeatRepository from "../../repositories/RepeatRepository";
+import OperatorFactory from "../OperatorFactory";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class RepeatOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("RepeatRepository")
+ private readonly repeatRepository: RepeatRepository,
+ @inject("OperatorFactory")
+ private readonly operatorFactory: OperatorFactory
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.REPEAT_LAST:
+ return new RepeatLastOperator(
+ this.repeatRepository,
+ this.operatorFactory
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/ResetZoomOperator.ts b/src/background/operators/impls/ResetZoomOperator.ts
new file mode 100644
index 0000000..96af733
--- /dev/null
+++ b/src/background/operators/impls/ResetZoomOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import ZoomPresenter from "../../presenters/ZoomPresenter";
+
+export default class ResetZoomOperator implements Operator {
+ constructor(private readonly zoomPresenter: ZoomPresenter) {}
+
+ run(): Promise<void> {
+ return this.zoomPresenter.resetZoom();
+ }
+}
diff --git a/src/background/operators/impls/SelectFirstTabOperator.ts b/src/background/operators/impls/SelectFirstTabOperator.ts
new file mode 100644
index 0000000..c04b8a9
--- /dev/null
+++ b/src/background/operators/impls/SelectFirstTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class SelectFirstTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabs = await this.tabPresenter.getAll();
+ return this.tabPresenter.select(tabs[0].id as number);
+ }
+}
diff --git a/src/background/operators/impls/SelectLastTabOperator.ts b/src/background/operators/impls/SelectLastTabOperator.ts
new file mode 100644
index 0000000..e16d406
--- /dev/null
+++ b/src/background/operators/impls/SelectLastTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class SelectLastTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabs = await this.tabPresenter.getAll();
+ return this.tabPresenter.select(tabs[tabs.length - 1].id as number);
+ }
+}
diff --git a/src/background/operators/impls/SelectPreviousSelectedTabOperator.ts b/src/background/operators/impls/SelectPreviousSelectedTabOperator.ts
new file mode 100644
index 0000000..03a778d
--- /dev/null
+++ b/src/background/operators/impls/SelectPreviousSelectedTabOperator.ts
@@ -0,0 +1,14 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class SelectPreviousSelectedTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabId = await this.tabPresenter.getLastSelectedId();
+ if (tabId === null || typeof tabId === "undefined") {
+ return Promise.resolve();
+ }
+ return this.tabPresenter.select(tabId);
+ }
+}
diff --git a/src/background/operators/impls/SelectTabNextOperator.ts b/src/background/operators/impls/SelectTabNextOperator.ts
new file mode 100644
index 0000000..34d37ff
--- /dev/null
+++ b/src/background/operators/impls/SelectTabNextOperator.ts
@@ -0,0 +1,19 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class SelectTabNextOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabs = await this.tabPresenter.getAll();
+ if (tabs.length < 2) {
+ return;
+ }
+ const tab = tabs.find((t) => t.active);
+ if (!tab) {
+ return;
+ }
+ const select = (tab.index + 1) % tabs.length;
+ return this.tabPresenter.select(tabs[select].id as number);
+ }
+}
diff --git a/src/background/operators/impls/SelectTabPrevOperator.ts b/src/background/operators/impls/SelectTabPrevOperator.ts
new file mode 100644
index 0000000..3776c1a
--- /dev/null
+++ b/src/background/operators/impls/SelectTabPrevOperator.ts
@@ -0,0 +1,19 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class SelectTabPrevOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tabs = await this.tabPresenter.getAll();
+ if (tabs.length < 2) {
+ return;
+ }
+ const tab = tabs.find((t) => t.active);
+ if (!tab) {
+ return;
+ }
+ const select = (tab.index - 1 + tabs.length) % tabs.length;
+ return this.tabPresenter.select(tabs[select].id as number);
+ }
+}
diff --git a/src/background/operators/impls/ShowAddBookmarkOperator.ts b/src/background/operators/impls/ShowAddBookmarkOperator.ts
new file mode 100644
index 0000000..cce4879
--- /dev/null
+++ b/src/background/operators/impls/ShowAddBookmarkOperator.ts
@@ -0,0 +1,20 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowAddBookmarkOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient,
+ private readonly alter: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ let command = "addbookmark ";
+ if (this.alter) {
+ command += tab.title || "";
+ }
+ return this.consoleClient.showCommand(tab.id as number, command);
+ }
+}
diff --git a/src/background/operators/impls/ShowBufferCommandOperator.ts b/src/background/operators/impls/ShowBufferCommandOperator.ts
new file mode 100644
index 0000000..e1246e4
--- /dev/null
+++ b/src/background/operators/impls/ShowBufferCommandOperator.ts
@@ -0,0 +1,16 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowBufferCommandOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ const command = "buffer ";
+ return this.consoleClient.showCommand(tab.id as number, command);
+ }
+}
diff --git a/src/background/operators/impls/ShowCommandOperator.ts b/src/background/operators/impls/ShowCommandOperator.ts
new file mode 100644
index 0000000..a5e1765
--- /dev/null
+++ b/src/background/operators/impls/ShowCommandOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowCommandOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.consoleClient.showCommand(tab.id as number, "");
+ }
+}
diff --git a/src/background/operators/impls/ShowOpenCommandOperator.ts b/src/background/operators/impls/ShowOpenCommandOperator.ts
new file mode 100644
index 0000000..1e78e5f
--- /dev/null
+++ b/src/background/operators/impls/ShowOpenCommandOperator.ts
@@ -0,0 +1,20 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowOpenCommandOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient,
+ private readonly alter: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ let command = "open ";
+ if (this.alter) {
+ command += tab.url || "";
+ }
+ return this.consoleClient.showCommand(tab.id as number, command);
+ }
+}
diff --git a/src/background/operators/impls/ShowTabOpenCommandOperator.ts b/src/background/operators/impls/ShowTabOpenCommandOperator.ts
new file mode 100644
index 0000000..d734da1
--- /dev/null
+++ b/src/background/operators/impls/ShowTabOpenCommandOperator.ts
@@ -0,0 +1,20 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowTabOpenCommandOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient,
+ private readonly alter: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ let command = "tabopen ";
+ if (this.alter) {
+ command += tab.url || "";
+ }
+ return this.consoleClient.showCommand(tab.id as number, command);
+ }
+}
diff --git a/src/background/operators/impls/ShowWinOpenCommandOperator.ts b/src/background/operators/impls/ShowWinOpenCommandOperator.ts
new file mode 100644
index 0000000..3c5e639
--- /dev/null
+++ b/src/background/operators/impls/ShowWinOpenCommandOperator.ts
@@ -0,0 +1,20 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class ShowWinOpenCommandOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient,
+ private readonly alter: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ let command = "winopen ";
+ if (this.alter) {
+ command += tab.url || "";
+ }
+ return this.consoleClient.showCommand(tab.id as number, command);
+ }
+}
diff --git a/src/background/operators/impls/StartFindOperator.ts b/src/background/operators/impls/StartFindOperator.ts
new file mode 100644
index 0000000..284ac9b
--- /dev/null
+++ b/src/background/operators/impls/StartFindOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import ConsoleClient from "../../infrastructures/ConsoleClient";
+
+export default class StartFindOperator implements Operator {
+ constructor(
+ private readonly tabPresenter: TabPresenter,
+ private readonly consoleClient: ConsoleClient
+ ) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.consoleClient.showFind(tab.id as number);
+ }
+}
diff --git a/src/background/operators/impls/TabOperatorFactoryChain.ts b/src/background/operators/impls/TabOperatorFactoryChain.ts
new file mode 100644
index 0000000..edd3eaf
--- /dev/null
+++ b/src/background/operators/impls/TabOperatorFactoryChain.ts
@@ -0,0 +1,64 @@
+import { inject, injectable } from "tsyringe";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import * as operations from "../../../shared/operations";
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+import CloseTabOperator from "./CloseTabOperator";
+import CloseTabRightOperator from "./CloseTabRightOperator";
+import ReopenTabOperator from "./ReopenTabOperator";
+import SelectTabPrevOperator from "./SelectTabPrevOperator";
+import SelectTabNextOperator from "./SelectTabNextOperator";
+import SelectFirstTabOperator from "./SelectFirstTabOperator";
+import SelectLastTabOperator from "./SelectLastTabOperator";
+import SelectPreviousSelectedTabOperator from "./SelectPreviousSelectedTabOperator";
+import ReloadTabOperator from "./ReloadTabOperator";
+import PinTabOperator from "./PinTabOperator";
+import UnpinTabOperator from "./UnpinTabOperator";
+import TogglePinnedTabOperator from "./TogglePinnedTabOperator";
+import DuplicateTabOperator from "./DuplicateTabOperator";
+
+@injectable()
+export default class TabOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.TAB_CLOSE:
+ return new CloseTabOperator(
+ this.tabPresenter,
+ false,
+ op.select === "left"
+ );
+ case operations.TAB_CLOSE_RIGHT:
+ return new CloseTabRightOperator(this.tabPresenter);
+ case operations.TAB_CLOSE_FORCE:
+ return new CloseTabOperator(this.tabPresenter, true, false);
+ case operations.TAB_REOPEN:
+ return new ReopenTabOperator(this.tabPresenter);
+ case operations.TAB_PREV:
+ return new SelectTabPrevOperator(this.tabPresenter);
+ case operations.TAB_NEXT:
+ return new SelectTabNextOperator(this.tabPresenter);
+ case operations.TAB_FIRST:
+ return new SelectFirstTabOperator(this.tabPresenter);
+ case operations.TAB_LAST:
+ return new SelectLastTabOperator(this.tabPresenter);
+ case operations.TAB_PREV_SEL:
+ return new SelectPreviousSelectedTabOperator(this.tabPresenter);
+ case operations.TAB_RELOAD:
+ return new ReloadTabOperator(this.tabPresenter, op.cache);
+ case operations.TAB_PIN:
+ return new PinTabOperator(this.tabPresenter);
+ case operations.TAB_UNPIN:
+ return new UnpinTabOperator(this.tabPresenter);
+ case operations.TAB_TOGGLE_PINNED:
+ return new TogglePinnedTabOperator(this.tabPresenter);
+ case operations.TAB_DUPLICATE:
+ return new DuplicateTabOperator(this.tabPresenter);
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/TogglePinnedTabOperator.ts b/src/background/operators/impls/TogglePinnedTabOperator.ts
new file mode 100644
index 0000000..c4fd4ba
--- /dev/null
+++ b/src/background/operators/impls/TogglePinnedTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class TogglePinnedTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.tabPresenter.setPinned(tab.id as number, !tab.pinned);
+ }
+}
diff --git a/src/background/operators/impls/UnpinTabOperator.ts b/src/background/operators/impls/UnpinTabOperator.ts
new file mode 100644
index 0000000..fa50145
--- /dev/null
+++ b/src/background/operators/impls/UnpinTabOperator.ts
@@ -0,0 +1,11 @@
+import Operator from "../Operator";
+import TabPresenter from "../../presenters/TabPresenter";
+
+export default class UnpinTabOperator implements Operator {
+ constructor(private readonly tabPresenter: TabPresenter) {}
+
+ async run(): Promise<void> {
+ const tab = await this.tabPresenter.getCurrent();
+ return this.tabPresenter.setPinned(tab.id as number, false);
+ }
+}
diff --git a/src/background/operators/impls/ZoomInOperator.ts b/src/background/operators/impls/ZoomInOperator.ts
new file mode 100644
index 0000000..e900f0d
--- /dev/null
+++ b/src/background/operators/impls/ZoomInOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import ZoomPresenter from "../../presenters/ZoomPresenter";
+
+export default class ZoomInOperator implements Operator {
+ constructor(private readonly zoomPresenter: ZoomPresenter) {}
+
+ run(): Promise<void> {
+ return this.zoomPresenter.zoomIn();
+ }
+}
diff --git a/src/background/operators/impls/ZoomOperatorFactoryChain.ts b/src/background/operators/impls/ZoomOperatorFactoryChain.ts
new file mode 100644
index 0000000..bf930a7
--- /dev/null
+++ b/src/background/operators/impls/ZoomOperatorFactoryChain.ts
@@ -0,0 +1,28 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import ZoomInOperator from "./ZoomInOperator";
+import ZoomOutOperator from "./ZoomOutOperator";
+import ResetZoomOperator from "./ResetZoomOperator";
+import ZoomPresenter from "../../presenters/ZoomPresenter";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class ZoomOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("ZoomPresenter")
+ private readonly zoomPresenter: ZoomPresenter
+ ) {}
+
+ create(op: operations.Operation): Operator | null {
+ switch (op.type) {
+ case operations.ZOOM_IN:
+ return new ZoomInOperator(this.zoomPresenter);
+ case operations.ZOOM_OUT:
+ return new ZoomOutOperator(this.zoomPresenter);
+ case operations.ZOOM_NEUTRAL:
+ return new ResetZoomOperator(this.zoomPresenter);
+ }
+ return null;
+ }
+}
diff --git a/src/background/operators/impls/ZoomOutOperator.ts b/src/background/operators/impls/ZoomOutOperator.ts
new file mode 100644
index 0000000..0c0389e
--- /dev/null
+++ b/src/background/operators/impls/ZoomOutOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import ZoomPresenter from "../../presenters/ZoomPresenter";
+
+export default class ZoomOutOperator implements Operator {
+ constructor(private readonly zoomPresenter: ZoomPresenter) {}
+
+ run(): Promise<void> {
+ return this.zoomPresenter.zoomOut();
+ }
+}
diff --git a/src/background/presenters/WindowPresenter.ts b/src/background/presenters/WindowPresenter.ts
index 4f37f5d..5eb0f22 100644
--- a/src/background/presenters/WindowPresenter.ts
+++ b/src/background/presenters/WindowPresenter.ts
@@ -1,8 +1,9 @@
-import { injectable } from "tsyringe";
+export default interface WindowPresenter {
+ create(url: string): Promise<void>;
+}
-@injectable()
-export default class WindowPresenter {
- create(url: string): Promise<browser.windows.Window> {
- return browser.windows.create({ url });
+export class WindowPresenterImpl implements WindowPresenter {
+ async create(url: string): Promise<void> {
+ await browser.windows.create({ url });
}
}
diff --git a/src/background/presenters/ZoomPresenter.ts b/src/background/presenters/ZoomPresenter.ts
new file mode 100644
index 0000000..5a3c64d
--- /dev/null
+++ b/src/background/presenters/ZoomPresenter.ts
@@ -0,0 +1,60 @@
+const ZOOM_SETTINGS = [
+ 0.33,
+ 0.5,
+ 0.66,
+ 0.75,
+ 0.8,
+ 0.9,
+ 1.0,
+ 1.1,
+ 1.25,
+ 1.5,
+ 1.75,
+ 2.0,
+ 2.5,
+ 3.0,
+] as const;
+
+export default interface ZoomPresenter {
+ zoomIn(): Promise<void>;
+ zoomOut(): Promise<void>;
+ resetZoom(): Promise<void>;
+}
+
+export class ZoomPresenterImpl implements ZoomPresenter {
+ async zoomIn(): Promise<void> {
+ const tab = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ const tabId = tab[0].id as number;
+ const current = await browser.tabs.getZoom(tabId);
+ const factor = ZOOM_SETTINGS.find((f) => f > current);
+ if (factor) {
+ return browser.tabs.setZoom(tabId, factor);
+ }
+ }
+
+ async zoomOut(): Promise<void> {
+ const tab = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ const tabId = tab[0].id as number;
+ const current = await browser.tabs.getZoom(tabId);
+ const factor = ZOOM_SETTINGS.slice(0)
+ .reverse()
+ .find((f) => f < current);
+ if (factor) {
+ return browser.tabs.setZoom(tabId, factor);
+ }
+ }
+
+ async resetZoom(): Promise<void> {
+ const tab = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ return browser.tabs.setZoom(tab[0].id, 1);
+ }
+}
diff --git a/src/background/repositories/BrowserSettingRepository.ts b/src/background/repositories/BrowserSettingRepository.ts
index e24874b..1dde190 100644
--- a/src/background/repositories/BrowserSettingRepository.ts
+++ b/src/background/repositories/BrowserSettingRepository.ts
@@ -1,8 +1,12 @@
import { injectable } from "tsyringe";
import * as urls from "../../shared/urls";
+export default interface BrowserSettingRepository {
+ getHomepageUrls(): Promise<string[]>;
+}
+
@injectable()
-export default class BrowserSettingRepository {
+export class BrowserSettingRepositoryImpl implements BrowserSettingRepository {
async getHomepageUrls(): Promise<string[]> {
const { value } = await browser.browserSettings.homepageOverride.get({});
return value.split("|").map(urls.normalizeUrl);
diff --git a/src/background/repositories/RepeatRepository.ts b/src/background/repositories/RepeatRepository.ts
index e3ab43d..00098d3 100644
--- a/src/background/repositories/RepeatRepository.ts
+++ b/src/background/repositories/RepeatRepository.ts
@@ -4,8 +4,14 @@ import MemoryStorage from "../infrastructures/MemoryStorage";
const REPEAT_KEY = "repeat";
+export default interface RepeatRepository {
+ getLastOperation(): Operation | undefined;
+
+ setLastOperation(op: Operation): void;
+}
+
@injectable()
-export default class RepeatRepository {
+export class RepeatRepositoryImpl implements RepeatRepository {
private cache: MemoryStorage;
constructor() {
diff --git a/src/background/usecases/AddonEnabledUseCase.ts b/src/background/usecases/AddonEnabledUseCase.ts
index f563ab0..f9bafde 100644
--- a/src/background/usecases/AddonEnabledUseCase.ts
+++ b/src/background/usecases/AddonEnabledUseCase.ts
@@ -22,7 +22,7 @@ export default class AddonEnabledUseCase {
return this.indicatorPresentor.indicate(enabled);
}
- onIndicatorClick(tabId: number): Promise<void> {
+ private onIndicatorClick(tabId: number): Promise<void> {
return this.contentMessageClient.toggleAddonEnabled(tabId);
}
diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts
index 811ec77..18ddd4d 100644
--- a/src/background/usecases/CommandUseCase.ts
+++ b/src/background/usecases/CommandUseCase.ts
@@ -12,17 +12,20 @@ import ContentMessageClient from "../infrastructures/ContentMessageClient";
import RepeatUseCase from "../usecases/RepeatUseCase";
@injectable()
-export default class CommandIndicator {
+export default class CommandUseCase {
constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private windowPresenter: WindowPresenter,
- private helpPresenter: HelpPresenter,
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter,
+ @inject("WindowPresenter")
+ private readonly windowPresenter: WindowPresenter,
+ private readonly helpPresenter: HelpPresenter,
@inject("CachedSettingRepository")
- private cachedSettingRepository: CachedSettingRepository,
- private bookmarkRepository: BookmarkRepository,
- private consoleClient: ConsoleClient,
- private contentMessageClient: ContentMessageClient,
- private repeatUseCase: RepeatUseCase
+ private readonly cachedSettingRepository: CachedSettingRepository,
+ private readonly bookmarkRepository: BookmarkRepository,
+ @inject("ConsoleClient")
+ private readonly consoleClient: ConsoleClient,
+ private readonly contentMessageClient: ContentMessageClient,
+ private readonly repeatUseCase: RepeatUseCase
) {}
async open(keywords: string): Promise<browser.tabs.Tab> {
@@ -44,7 +47,7 @@ export default class CommandIndicator {
return this.tabPresenter.create(url);
}
- async winopen(keywords: string): Promise<browser.windows.Window> {
+ async winopen(keywords: string): Promise<void> {
const url = await this.urlOrSearch(keywords);
this.repeatUseCase.storeLastOperation({
type: operations.INTERNAL_OPEN_URL,
diff --git a/src/background/usecases/ConsoleUseCase.ts b/src/background/usecases/ConsoleUseCase.ts
deleted file mode 100644
index 195c70f..0000000
--- a/src/background/usecases/ConsoleUseCase.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { inject, injectable } from "tsyringe";
-import TabPresenter from "../presenters/TabPresenter";
-import ConsoleClient from "../infrastructures/ConsoleClient";
-
-@injectable()
-export default class ConsoleUseCase {
- constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private consoleClient: ConsoleClient
- ) {}
-
- async showCommand(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.consoleClient.showCommand(tab.id as number, "");
- }
-
- async showOpenCommand(alter: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- let command = "open ";
- if (alter) {
- command += tab.url || "";
- }
- return this.consoleClient.showCommand(tab.id as number, command);
- }
-
- async showTabopenCommand(alter: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- let command = "tabopen ";
- if (alter) {
- command += tab.url || "";
- }
- return this.consoleClient.showCommand(tab.id as number, command);
- }
-
- async showWinopenCommand(alter: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- let command = "winopen ";
- if (alter) {
- command += tab.url || "";
- }
- return this.consoleClient.showCommand(tab.id as number, command);
- }
-
- async showBufferCommand(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- const command = "buffer ";
- return this.consoleClient.showCommand(tab.id as number, command);
- }
-
- async showAddbookmarkCommand(alter: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- let command = "addbookmark ";
- if (alter) {
- command += tab.title || "";
- }
- return this.consoleClient.showCommand(tab.id as number, command);
- }
-
- async hideConsole(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.consoleClient.hide(tab.id as number);
- }
-}
diff --git a/src/background/usecases/FindUseCase.ts b/src/background/usecases/FindUseCase.ts
index facc461..ce96e03 100644
--- a/src/background/usecases/FindUseCase.ts
+++ b/src/background/usecases/FindUseCase.ts
@@ -1,15 +1,9 @@
-import { inject, injectable } from "tsyringe";
+import { injectable } from "tsyringe";
import FindRepository from "../repositories/FindRepository";
-import TabPresenter from "../presenters/TabPresenter";
-import ConsoleClient from "../infrastructures/ConsoleClient";
@injectable()
export default class FindUseCase {
- constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private findRepository: FindRepository,
- private consoleClient: ConsoleClient
- ) {}
+ constructor(private readonly findRepository: FindRepository) {}
getKeyword(): Promise<string> {
return this.findRepository.getKeyword();
@@ -18,9 +12,4 @@ export default class FindUseCase {
setKeyword(keyword: string): Promise<any> {
return this.findRepository.setKeyword(keyword);
}
-
- async findStart(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.consoleClient.showFind(tab.id as number);
- }
}
diff --git a/src/background/usecases/MarkUseCase.ts b/src/background/usecases/MarkUseCase.ts
index 9da9a21..57e04b4 100644
--- a/src/background/usecases/MarkUseCase.ts
+++ b/src/background/usecases/MarkUseCase.ts
@@ -1,16 +1,18 @@
import { inject, injectable } from "tsyringe";
import TabPresenter from "../presenters/TabPresenter";
import MarkRepository from "../repositories/MarkRepository";
-import ConsoleClient from "../infrastructures/ConsoleClient";
import ContentMessageClient from "../infrastructures/ContentMessageClient";
+import ConsoleClient from "../infrastructures/ConsoleClient";
@injectable()
export default class MarkUseCase {
constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private markRepository: MarkRepository,
- private consoleClient: ConsoleClient,
- private contentMessageClient: ContentMessageClient
+ @inject("TabPresenter")
+ private readonly tabPresenter: TabPresenter,
+ private readonly markRepository: MarkRepository,
+ @inject("ConsoleClient")
+ private readonly consoleClient: ConsoleClient,
+ private readonly contentMessageClient: ContentMessageClient
) {}
async setGlobal(key: string, x: number, y: number): Promise<any> {
diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts
deleted file mode 100644
index 974606c..0000000
--- a/src/background/usecases/NavigateUseCase.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { inject, injectable } from "tsyringe";
-import NavigateClient from "../clients/NavigateClient";
-import TabPresenter from "../presenters/TabPresenter";
-
-@injectable()
-export default class NavigateUseCase {
- constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private navigateClient: NavigateClient
- ) {}
-
- async openHistoryNext(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- await this.navigateClient.historyNext(tab.id!);
- }
-
- async openHistoryPrev(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- await this.navigateClient.historyPrev(tab.id!);
- }
-
- async openLinkNext(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- await this.navigateClient.linkNext(tab.id!);
- }
-
- async openLinkPrev(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- await this.navigateClient.linkPrev(tab.id!);
- }
-
- async openParent(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- const url = new URL(tab.url!);
- if (url.hash.length > 0) {
- url.hash = "";
- } else if (url.search.length > 0) {
- 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);
- }
-
- async openRoot(): Promise<void> {
- const tab = await this.tabPresenter.getCurrent();
- const url = new URL(tab.url!);
- await this.tabPresenter.open(url.origin);
- }
-}
diff --git a/src/background/usecases/RepeatUseCase.ts b/src/background/usecases/RepeatUseCase.ts
index d7235ee..992e76b 100644
--- a/src/background/usecases/RepeatUseCase.ts
+++ b/src/background/usecases/RepeatUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from "tsyringe";
+import { inject, injectable } from "tsyringe";
import * as operations from "../../shared/operations";
import RepeatRepository from "../repositories/RepeatRepository";
@@ -6,7 +6,10 @@ type Operation = operations.Operation;
@injectable()
export default class RepeatUseCase {
- constructor(private repeatRepository: RepeatRepository) {}
+ constructor(
+ @inject("RepeatRepository")
+ private readonly repeatRepository: RepeatRepository
+ ) {}
storeLastOperation(op: Operation): void {
this.repeatRepository.setLastOperation(op);
diff --git a/src/background/usecases/TabSelectUseCase.ts b/src/background/usecases/TabSelectUseCase.ts
deleted file mode 100644
index 663ceb8..0000000
--- a/src/background/usecases/TabSelectUseCase.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { inject, injectable } from "tsyringe";
-import TabPresenter from "../presenters/TabPresenter";
-
-@injectable()
-export default class TabSelectUseCase {
- constructor(@inject("TabPresenter") private tabPresenter: TabPresenter) {}
-
- async selectPrev(count: number): Promise<any> {
- const tabs = await this.tabPresenter.getAll();
- if (tabs.length < 2) {
- return;
- }
- const tab = tabs.find((t) => t.active);
- if (!tab) {
- return;
- }
- const select = (tab.index - count + tabs.length) % tabs.length;
- return this.tabPresenter.select(tabs[select].id as number);
- }
-
- async selectNext(count: number): Promise<any> {
- const tabs = await this.tabPresenter.getAll();
- if (tabs.length < 2) {
- return;
- }
- const tab = tabs.find((t) => t.active);
- if (!tab) {
- return;
- }
- const select = (tab.index + count) % tabs.length;
- return this.tabPresenter.select(tabs[select].id as number);
- }
-
- async selectFirst(): Promise<any> {
- const tabs = await this.tabPresenter.getAll();
- return this.tabPresenter.select(tabs[0].id as number);
- }
-
- async selectLast(): Promise<any> {
- const tabs = await this.tabPresenter.getAll();
- return this.tabPresenter.select(tabs[tabs.length - 1].id as number);
- }
-
- async selectPrevSelected(): Promise<any> {
- const tabId = await this.tabPresenter.getLastSelectedId();
- if (tabId === null || typeof tabId === "undefined") {
- return Promise.resolve();
- }
- return this.tabPresenter.select(tabId);
- }
-}
diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts
deleted file mode 100644
index 1439107..0000000
--- a/src/background/usecases/TabUseCase.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { inject, injectable } from "tsyringe";
-import TabPresenter from "../presenters/TabPresenter";
-import WindowPresenter from "../presenters/WindowPresenter";
-import BrowserSettingRepository from "../repositories/BrowserSettingRepository";
-
-@injectable()
-export default class TabUseCase {
- constructor(
- @inject("TabPresenter") private tabPresenter: TabPresenter,
- private windowPresenter: WindowPresenter,
- private browserSettingRepository: BrowserSettingRepository
- ) {}
-
- async close(force: boolean, selectLeft = false): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- if (!force && tab.pinned) {
- return Promise.resolve();
- }
- if (selectLeft && tab.index > 0) {
- const tabs = await this.tabPresenter.getAll();
- await this.tabPresenter.select(tabs[tab.index - 1].id as number);
- }
- return this.tabPresenter.remove([tab.id as number]);
- }
-
- async closeRight(): Promise<any> {
- const tabs = await this.tabPresenter.getAll();
- tabs.sort((t1, t2) => t1.index - t2.index);
- const index = tabs.findIndex((t) => t.active);
- if (index < 0) {
- return;
- }
- for (let i = index + 1; i < tabs.length; ++i) {
- const tab = tabs[i];
- if (!tab.pinned) {
- this.tabPresenter.remove([tab.id as number]);
- }
- }
- }
-
- reopen(): Promise<any> {
- return this.tabPresenter.reopen();
- }
-
- async reload(cache: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.tabPresenter.reload(tab.id as number, cache);
- }
-
- async setPinned(pinned: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.tabPresenter.setPinned(tab.id as number, pinned);
- }
-
- async togglePinned(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.tabPresenter.setPinned(tab.id as number, !tab.pinned);
- }
-
- async duplicate(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.tabPresenter.duplicate(tab.id as number);
- }
-
- async openPageSource(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- const url = "view-source:" + tab.url;
- return this.tabPresenter.create(url);
- }
-
- async openHome(newTab: boolean): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- const urls = await this.browserSettingRepository.getHomepageUrls();
- if (urls.length === 1 && urls[0] === "about:home") {
- // eslint-disable-next-line max-len
- throw new Error(
- "Cannot open Firefox Home (about:home) by WebExtensions, set your custom URLs"
- );
- }
- if (urls.length === 1 && !newTab) {
- return this.tabPresenter.open(urls[0], tab.id);
- }
- for (const url of urls) {
- this.tabPresenter.create(url);
- }
- }
-
- async openURL(
- url: string,
- newTab?: boolean,
- newWindow?: boolean
- ): Promise<void> {
- if (newWindow) {
- await this.windowPresenter.create(url);
- } else if (newTab) {
- await this.tabPresenter.create(url);
- } else {
- const tab = await this.tabPresenter.getCurrent();
- await this.tabPresenter.open(url, tab.id);
- }
- }
-}
diff --git a/src/background/usecases/ZoomUseCase.ts b/src/background/usecases/ZoomUseCase.ts
deleted file mode 100644
index 173e4d7..0000000
--- a/src/background/usecases/ZoomUseCase.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { inject, injectable } from "tsyringe";
-import TabPresenter from "../presenters/TabPresenter";
-
-const ZOOM_SETTINGS: number[] = [
- 0.33,
- 0.5,
- 0.66,
- 0.75,
- 0.8,
- 0.9,
- 1.0,
- 1.1,
- 1.25,
- 1.5,
- 1.75,
- 2.0,
- 2.5,
- 3.0,
-];
-
-@injectable()
-export default class ZoomUseCase {
- constructor(@inject("TabPresenter") private tabPresenter: TabPresenter) {}
-
- async zoomIn(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- const tabId = tab.id as number;
- const current = await this.tabPresenter.getZoom(tabId);
- const factor = ZOOM_SETTINGS.find((f) => f > current);
- if (factor) {
- return this.tabPresenter.setZoom(tabId as number, factor);
- }
- }
-
- async zoomOut(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- const tabId = tab.id as number;
- const current = await this.tabPresenter.getZoom(tabId);
- const factor = ZOOM_SETTINGS.slice(0)
- .reverse()
- .find((f) => f < current);
- if (factor) {
- return this.tabPresenter.setZoom(tabId as number, factor);
- }
- }
-
- async zoomNutoral(): Promise<any> {
- const tab = await this.tabPresenter.getCurrent();
- return this.tabPresenter.setZoom(tab.id as number, 1);
- }
-}
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
index 092e55c..01ac8b5 100644
--- a/src/content/controllers/KeymapController.ts
+++ b/src/content/controllers/KeymapController.ts
@@ -1,32 +1,15 @@
import { injectable, inject } from "tsyringe";
-import * as operations from "../../shared/operations";
import KeymapUseCase from "../usecases/KeymapUseCase";
-import AddonEnabledUseCase from "../usecases/AddonEnabledUseCase";
-import FindSlaveUseCase from "../usecases/FindSlaveUseCase";
-import ScrollUseCase from "../usecases/ScrollUseCase";
-import FocusUseCase from "../usecases/FocusUseCase";
-import ClipboardUseCase from "../usecases/ClipboardUseCase";
-import OperationClient from "../client/OperationClient";
-import MarkKeyyUseCase from "../usecases/MarkKeyUseCase";
-import FollowMasterClient from "../client/FollowMasterClient";
import Key from "../../shared/settings/Key";
+import OperatorFactory from "../operators/OperatorFactory";
@injectable()
export default class KeymapController {
constructor(
private keymapUseCase: KeymapUseCase,
- private addonEnabledUseCase: AddonEnabledUseCase,
- private findSlaveUseCase: FindSlaveUseCase,
- private scrollUseCase: ScrollUseCase,
- private focusUseCase: FocusUseCase,
- private clipbaordUseCase: ClipboardUseCase,
- private markKeyUseCase: MarkKeyyUseCase,
- @inject("OperationClient")
- private operationClient: OperationClient,
-
- @inject("FollowMasterClient")
- private followMasterClient: FollowMasterClient
+ @inject("OperatorFactory")
+ private readonly operatorFactory: OperatorFactory
) {}
// eslint-disable-next-line complexity, max-lines-per-function
@@ -36,65 +19,14 @@ export default class KeymapController {
return false;
}
- if (!operations.isNRepeatable(nextOp.op.type)) {
- nextOp.repeat = 1;
- }
-
- const doFunc = ((op: operations.Operation) => {
- switch (op.type) {
- case operations.ADDON_ENABLE:
- return () => this.addonEnabledUseCase.enable();
- case operations.ADDON_DISABLE:
- return () => this.addonEnabledUseCase.disable();
- case operations.ADDON_TOGGLE_ENABLED:
- return () => this.addonEnabledUseCase.toggle();
- case operations.FIND_NEXT:
- return () => this.findSlaveUseCase.findNext();
- case operations.FIND_PREV:
- return () => this.findSlaveUseCase.findPrev();
- case operations.SCROLL_VERTICALLY:
- return () => this.scrollUseCase.scrollVertically(op.count);
- case operations.SCROLL_HORIZONALLY:
- return () => this.scrollUseCase.scrollHorizonally(op.count);
- case operations.SCROLL_PAGES:
- return () => this.scrollUseCase.scrollPages(op.count);
- case operations.SCROLL_TOP:
- return () => this.scrollUseCase.scrollToTop();
- case operations.SCROLL_BOTTOM:
- return () => this.scrollUseCase.scrollToBottom();
- case operations.SCROLL_HOME:
- return () => this.scrollUseCase.scrollToHome();
- case operations.SCROLL_END:
- return () => this.scrollUseCase.scrollToEnd();
- case operations.FOLLOW_START:
- return () =>
- this.followMasterClient.startFollow(op.newTab, op.background);
- case operations.MARK_SET_PREFIX:
- return () => this.markKeyUseCase.enableSetMode();
- case operations.MARK_JUMP_PREFIX:
- return () => this.markKeyUseCase.enableJumpMode();
- case operations.FOCUS_INPUT:
- return () => this.focusUseCase.focusFirstInput();
- case operations.URLS_YANK:
- return () => this.clipbaordUseCase.yankCurrentURL();
- case operations.URLS_PASTE:
- return () =>
- this.clipbaordUseCase.openOrSearch(op.newTab ? op.newTab : false);
- default:
- return null;
- }
- })(nextOp.op);
+ // Do not await asynchronous methods to return a boolean immidiately. The
+ // caller requires the synchronous response from the callback to identify
+ // to continue of abandon the event propagation.
+ this.operatorFactory
+ .create(nextOp.op, nextOp.repeat)
+ .run()
+ .catch(console.error);
- if (doFunc === null) {
- // Do not await asynchronous methods to return a boolean immidiately. The
- // caller requires the synchronous response from the callback to identify
- // to continue of abandon the event propagations.
- this.operationClient.execBackgroundOp(nextOp.repeat, nextOp.op);
- } else {
- for (let i = 0; i < nextOp.repeat; ++i) {
- doFunc();
- }
- }
return true;
}
diff --git a/src/content/di.ts b/src/content/di.ts
index cc10c78..e74d7ac 100644
--- a/src/content/di.ts
+++ b/src/content/di.ts
@@ -17,6 +17,7 @@ import { FollowMasterRepositoryImpl } from "./repositories/FollowMasterRepositor
import { FollowPresenterImpl } from "./presenters/FollowPresenter";
import { FollowSlaveClientFactoryImpl } from "./client/FollowSlaveClientFactory";
import { FollowSlaveRepositoryImpl } from "./repositories/FollowSlaveRepository";
+import { HintKeyRepositoryImpl } from "./repositories/HintKeyRepository";
import { KeymapRepositoryImpl } from "./repositories/KeymapRepository";
import { MarkClientImpl } from "./client/MarkClient";
import { MarkKeyRepositoryImpl } from "./repositories/MarkKeyRepository";
@@ -28,6 +29,8 @@ import { SettingClientImpl } from "./client/SettingClient";
import { SettingRepositoryImpl } from "./repositories/SettingRepository";
import { TabsClientImpl } from "./client/TabsClient";
import { container } from "tsyringe";
+import OperatorFactoryImpl from "./operators/impls/OperatorFactoryImpl";
+import { URLRepositoryImpl } from "./operators/impls/URLRepository";
container.register("FollowMasterClient", {
useValue: new FollowMasterClientImpl(window.top),
@@ -64,6 +67,9 @@ container.register("FollowSlaveClientFactory", {
container.register("FollowSlaveRepository", {
useClass: FollowSlaveRepositoryImpl,
});
+container.register("HintKeyRepository", {
+ useClass: HintKeyRepositoryImpl,
+});
container.register("KeymapRepository", { useClass: KeymapRepositoryImpl });
container.register("MarkClient", { useClass: MarkClientImpl });
container.register("MarkKeyRepository", { useClass: MarkKeyRepositoryImpl });
@@ -75,4 +81,6 @@ container.register("OperationClient", { useClass: OperationClientImpl });
container.register("ScrollPresenter", { useClass: ScrollPresenterImpl });
container.register("SettingClient", { useClass: SettingClientImpl });
container.register("SettingRepository", { useClass: SettingRepositoryImpl });
+container.register("URLRepository", { useClass: URLRepositoryImpl });
container.register("TabsClient", { useClass: TabsClientImpl });
+container.register("OperatorFactory", { useClass: OperatorFactoryImpl });
diff --git a/src/content/hint-key-producer.ts b/src/content/hint-key-producer.ts
deleted file mode 100644
index a5e2877..0000000
--- a/src/content/hint-key-producer.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export default class HintKeyProducer {
- private charset: string;
-
- private counter: number[];
-
- constructor(charset: string) {
- if (charset.length === 0) {
- throw new TypeError("charset is empty");
- }
-
- this.charset = charset;
- this.counter = [];
- }
-
- produce(): string {
- this.increment();
-
- return this.counter.map((x) => this.charset[x]).join("");
- }
-
- private increment(): void {
- const max = this.charset.length - 1;
- if (this.counter.every((x) => x === max)) {
- this.counter = new Array(this.counter.length + 1).fill(0);
- return;
- }
-
- this.counter.reverse();
- const len = this.charset.length;
- let num = this.counter.reduce((x, y, index) => x + y * len ** index) + 1;
- for (let i = 0; i < this.counter.length; ++i) {
- this.counter[i] = num % len;
- num = ~~(num / len);
- }
- this.counter.reverse();
- }
-}
diff --git a/src/content/operators/Operator.ts b/src/content/operators/Operator.ts
new file mode 100644
index 0000000..3b1fe03
--- /dev/null
+++ b/src/content/operators/Operator.ts
@@ -0,0 +1,5 @@
+interface Operator {
+ run(): Promise<void>;
+}
+
+export default Operator;
diff --git a/src/content/operators/OperatorFactory.ts b/src/content/operators/OperatorFactory.ts
new file mode 100644
index 0000000..f45973b
--- /dev/null
+++ b/src/content/operators/OperatorFactory.ts
@@ -0,0 +1,6 @@
+import * as operations from "../../shared/operations";
+import Operator from "./Operator";
+
+export default interface OperatorFactory {
+ create(op: operations.Operation, repeat: number): Operator;
+}
diff --git a/src/content/operators/OperatorFactoryChain.ts b/src/content/operators/OperatorFactoryChain.ts
new file mode 100644
index 0000000..98dedb5
--- /dev/null
+++ b/src/content/operators/OperatorFactoryChain.ts
@@ -0,0 +1,6 @@
+import * as operations from "../../shared/operations";
+import Operator from "./Operator";
+
+export default interface OperatorFactoryChain {
+ create(op: operations.Operation, repeat: number): Operator | null;
+}
diff --git a/src/content/operators/impls/AbstractScrollOperator.ts b/src/content/operators/impls/AbstractScrollOperator.ts
new file mode 100644
index 0000000..f8d9f70
--- /dev/null
+++ b/src/content/operators/impls/AbstractScrollOperator.ts
@@ -0,0 +1,10 @@
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class AbstractScrollOperator {
+ constructor(private readonly settingRepository: SettingRepository) {}
+
+ protected getSmoothScroll(): boolean {
+ const settings = this.settingRepository.get();
+ return settings.properties.smoothscroll;
+ }
+}
diff --git a/src/content/operators/impls/AddonOperatorFactoryChain.ts b/src/content/operators/impls/AddonOperatorFactoryChain.ts
new file mode 100644
index 0000000..54880c4
--- /dev/null
+++ b/src/content/operators/impls/AddonOperatorFactoryChain.ts
@@ -0,0 +1,40 @@
+import { inject, injectable } from "tsyringe";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import AddonIndicatorClient from "../../client/AddonIndicatorClient";
+import AddonEnabledRepository from "../../repositories/AddonEnabledRepository";
+import * as operations from "../../../shared/operations";
+import Operator from "../Operator";
+import EnableAddonOperator from "./EnableAddonOperator";
+import DisableAddonOperator from "./DisableAddonOperator";
+import ToggleAddonOperator from "./ToggleAddonOperator";
+
+@injectable()
+export default class AddonOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("AddonIndicatorClient")
+ private readonly addonIndicatorClient: AddonIndicatorClient,
+ @inject("AddonEnabledRepository")
+ private readonly addonEnabledRepository: AddonEnabledRepository
+ ) {}
+
+ create(op: operations.Operation, _repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.ADDON_ENABLE:
+ return new EnableAddonOperator(
+ this.addonIndicatorClient,
+ this.addonEnabledRepository
+ );
+ case operations.ADDON_DISABLE:
+ return new DisableAddonOperator(
+ this.addonIndicatorClient,
+ this.addonEnabledRepository
+ );
+ case operations.ADDON_TOGGLE_ENABLED:
+ return new ToggleAddonOperator(
+ this.addonIndicatorClient,
+ this.addonEnabledRepository
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/BackgroundOperationOperator.ts b/src/content/operators/impls/BackgroundOperationOperator.ts
new file mode 100644
index 0000000..dd86559
--- /dev/null
+++ b/src/content/operators/impls/BackgroundOperationOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import OperationClient from "../../client/OperationClient";
+import * as operations from "../../../shared/operations";
+
+export default class BackgroundOperationOperator implements Operator {
+ constructor(
+ private readonly operationClient: OperationClient,
+ private readonly repeat: number,
+ private readonly op: operations.Operation
+ ) {}
+
+ async run(): Promise<void> {
+ await this.operationClient.execBackgroundOp(this.repeat, this.op);
+ }
+}
diff --git a/src/content/operators/impls/ClipboardOperatorFactoryChain.ts b/src/content/operators/impls/ClipboardOperatorFactoryChain.ts
new file mode 100644
index 0000000..454aea1
--- /dev/null
+++ b/src/content/operators/impls/ClipboardOperatorFactoryChain.ts
@@ -0,0 +1,47 @@
+import { inject, injectable } from "tsyringe";
+import YankURLOperator from "./YankURLOperator";
+import PasteOperator from "./PasteOperator";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import ClipboardRepository from "../../repositories/ClipboardRepository";
+import ConsoleClient from "../../client/ConsoleClient";
+import OperationClient from "../../client/OperationClient";
+import SettingRepository from "../../repositories/SettingRepository";
+import * as operations from "../../../shared/operations";
+import URLRepository from "./URLRepository";
+
+@injectable()
+export default class ClipboardOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("ClipboardRepository")
+ private readonly clipboardRepository: ClipboardRepository,
+ @inject("ConsoleClient")
+ private readonly consoleClient: ConsoleClient,
+ @inject("OperationClient")
+ private readonly operationClinet: OperationClient,
+ @inject("SettingRepository")
+ private readonly settingRepository: SettingRepository,
+ @inject("URLRepository")
+ private readonly urlRepository: URLRepository
+ ) {}
+
+ create(op: operations.Operation, _repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.URLS_YANK:
+ return new YankURLOperator(
+ this.clipboardRepository,
+ this.consoleClient,
+ this.urlRepository
+ );
+ case operations.URLS_PASTE:
+ return new PasteOperator(
+ this.clipboardRepository,
+ this.settingRepository,
+ this.operationClinet,
+ op.newTab
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/DisableAddonOperator.ts b/src/content/operators/impls/DisableAddonOperator.ts
new file mode 100644
index 0000000..28811fe
--- /dev/null
+++ b/src/content/operators/impls/DisableAddonOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import AddonIndicatorClient from "../../client/AddonIndicatorClient";
+import AddonEnabledRepository from "../../repositories/AddonEnabledRepository";
+
+export default class DisableAddonOperator implements Operator {
+ constructor(
+ private readonly indicator: AddonIndicatorClient,
+ private readonly repository: AddonEnabledRepository
+ ) {}
+
+ async run(): Promise<void> {
+ this.repository.set(false);
+ await this.indicator.setEnabled(false);
+ }
+}
diff --git a/src/content/operators/impls/EnableAddonOperator.ts b/src/content/operators/impls/EnableAddonOperator.ts
new file mode 100644
index 0000000..b5b1d79
--- /dev/null
+++ b/src/content/operators/impls/EnableAddonOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import AddonIndicatorClient from "../../client/AddonIndicatorClient";
+import AddonEnabledRepository from "../../repositories/AddonEnabledRepository";
+
+export default class EnableAddonOperator implements Operator {
+ constructor(
+ private readonly indicator: AddonIndicatorClient,
+ private readonly repository: AddonEnabledRepository
+ ) {}
+
+ async run(): Promise<void> {
+ this.repository.set(true);
+ await this.indicator.setEnabled(true);
+ }
+}
diff --git a/src/content/operators/impls/EnableJumpMarkOperator.ts b/src/content/operators/impls/EnableJumpMarkOperator.ts
new file mode 100644
index 0000000..42ca8ee
--- /dev/null
+++ b/src/content/operators/impls/EnableJumpMarkOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import MarkKeyRepository from "../../repositories/MarkKeyRepository";
+
+export default class EnableJumpMarkOperator implements Operator {
+ constructor(private readonly repository: MarkKeyRepository) {}
+
+ async run(): Promise<void> {
+ this.repository.enableJumpMode();
+ }
+}
diff --git a/src/content/operators/impls/EnableSetMarkOperator.ts b/src/content/operators/impls/EnableSetMarkOperator.ts
new file mode 100644
index 0000000..3d0daf4
--- /dev/null
+++ b/src/content/operators/impls/EnableSetMarkOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import MarkKeyRepository from "../../repositories/MarkKeyRepository";
+
+export default class EnableSetMarkOperator implements Operator {
+ constructor(private readonly repository: MarkKeyRepository) {}
+
+ async run(): Promise<void> {
+ this.repository.enableSetMode();
+ }
+}
diff --git a/src/content/operators/impls/FindNextOperator.ts b/src/content/operators/impls/FindNextOperator.ts
new file mode 100644
index 0000000..c67f6d9
--- /dev/null
+++ b/src/content/operators/impls/FindNextOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import FindMasterClient from "../../client/FindMasterClient";
+
+export default class FindNextOperator implements Operator {
+ constructor(
+ private readonly findMasterClient: FindMasterClient,
+ private readonly repeat: number
+ ) {}
+
+ async run(): Promise<void> {
+ for (let i = 0; i < this.repeat; ++i) {
+ this.findMasterClient.findNext();
+ }
+ }
+}
diff --git a/src/content/operators/impls/FindOperatorFactoryChain.ts b/src/content/operators/impls/FindOperatorFactoryChain.ts
new file mode 100644
index 0000000..b3524c1
--- /dev/null
+++ b/src/content/operators/impls/FindOperatorFactoryChain.ts
@@ -0,0 +1,25 @@
+import { inject, injectable } from "tsyringe";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import FindNextOperator from "./FindNextOperator";
+import FindPrevOperator from "./FindPrevOperator";
+import FindMasterClient from "../../client/FindMasterClient";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class FindOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("FindMasterClient")
+ private readonly findMasterClient: FindMasterClient
+ ) {}
+
+ create(op: operations.Operation, repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.FIND_NEXT:
+ return new FindNextOperator(this.findMasterClient, repeat);
+ case operations.FIND_PREV:
+ return new FindPrevOperator(this.findMasterClient, repeat);
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/FindPrevOperator.ts b/src/content/operators/impls/FindPrevOperator.ts
new file mode 100644
index 0000000..f73e605
--- /dev/null
+++ b/src/content/operators/impls/FindPrevOperator.ts
@@ -0,0 +1,15 @@
+import Operator from "../Operator";
+import FindMasterClient from "../../client/FindMasterClient";
+
+export default class FindPrevOperator implements Operator {
+ constructor(
+ private readonly findMasterClient: FindMasterClient,
+ private readonly repeat: number
+ ) {}
+
+ async run(): Promise<void> {
+ for (let i = 0; i < this.repeat; ++i) {
+ this.findMasterClient.findPrev();
+ }
+ }
+}
diff --git a/src/content/operators/impls/FocusOperator.ts b/src/content/operators/impls/FocusOperator.ts
new file mode 100644
index 0000000..51d6fec
--- /dev/null
+++ b/src/content/operators/impls/FocusOperator.ts
@@ -0,0 +1,10 @@
+import Operator from "../Operator";
+import FocusPresenter from "../../presenters/FocusPresenter";
+
+export default class FocusOperator implements Operator {
+ constructor(private readonly presenter: FocusPresenter) {}
+
+ async run(): Promise<void> {
+ this.presenter.focusFirstElement();
+ }
+}
diff --git a/src/content/operators/impls/FocusOperatorFactoryChain.ts b/src/content/operators/impls/FocusOperatorFactoryChain.ts
new file mode 100644
index 0000000..c89c1e5
--- /dev/null
+++ b/src/content/operators/impls/FocusOperatorFactoryChain.ts
@@ -0,0 +1,22 @@
+import { inject, injectable } from "tsyringe";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import Operator from "../Operator";
+import FocusOperator from "./FocusOperator";
+import FocusPresenter from "../../presenters/FocusPresenter";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class FocusOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("FocusPresenter")
+ private readonly focusPresenter: FocusPresenter
+ ) {}
+
+ create(op: operations.Operation, _repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.FOCUS_INPUT:
+ return new FocusOperator(this.focusPresenter);
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/FollowOperatorFactoryChain.ts b/src/content/operators/impls/FollowOperatorFactoryChain.ts
new file mode 100644
index 0000000..588e1a4
--- /dev/null
+++ b/src/content/operators/impls/FollowOperatorFactoryChain.ts
@@ -0,0 +1,27 @@
+import { inject, injectable } from "tsyringe";
+import StartFollowOperator from "./StartFollowOperator";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import FollowMasterClient from "../../client/FollowMasterClient";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class FollowOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("FollowMasterClient")
+ private followMasterClient: FollowMasterClient
+ ) {}
+
+ create(op: operations.Operation, _repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.FOLLOW_START:
+ return new StartFollowOperator(
+ this.followMasterClient,
+ op.newTab,
+ op.background
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/HorizontalScrollOperator.ts b/src/content/operators/impls/HorizontalScrollOperator.ts
new file mode 100644
index 0000000..f813f85
--- /dev/null
+++ b/src/content/operators/impls/HorizontalScrollOperator.ts
@@ -0,0 +1,21 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class HorizontalScrollOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository,
+ private readonly count: number
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollHorizonally(this.count, smooth);
+ }
+}
diff --git a/src/content/operators/impls/MarkOperatorFactoryChain.ts b/src/content/operators/impls/MarkOperatorFactoryChain.ts
new file mode 100644
index 0000000..7e6c513
--- /dev/null
+++ b/src/content/operators/impls/MarkOperatorFactoryChain.ts
@@ -0,0 +1,25 @@
+import { inject, injectable } from "tsyringe";
+import EnableSetMarkOperator from "./EnableSetMarkOperator";
+import EnableJumpMarkOperator from "./EnableJumpMarkOperator";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import MarkKeyRepository from "../../repositories/MarkKeyRepository";
+import * as operations from "../../../shared/operations";
+
+@injectable()
+export default class MarkOperatorFactoryChain implements OperatorFactoryChain {
+ constructor(
+ @inject("MarkKeyRepository")
+ private readonly markKeyRepository: MarkKeyRepository
+ ) {}
+
+ create(op: operations.Operation, _repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.MARK_SET_PREFIX:
+ return new EnableSetMarkOperator(this.markKeyRepository);
+ case operations.MARK_JUMP_PREFIX:
+ return new EnableJumpMarkOperator(this.markKeyRepository);
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/OperatorFactoryImpl.ts b/src/content/operators/impls/OperatorFactoryImpl.ts
new file mode 100644
index 0000000..22b35c8
--- /dev/null
+++ b/src/content/operators/impls/OperatorFactoryImpl.ts
@@ -0,0 +1,51 @@
+import { inject, injectable } from "tsyringe";
+import OperatorFactory from "../OperatorFactory";
+import BackgroundOperationOperator from "./BackgroundOperationOperator";
+import Operator from "../Operator";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import { Operation } from "../../../shared/operations";
+import OperationClient from "../../client/OperationClient";
+import AddonOperatorFactoryChain from "./AddonOperatorFactoryChain";
+import ClipboardOperatorFactoryChain from "./ClipboardOperatorFactoryChain";
+import FindOperatorFactoryChain from "./FindOperatorFactoryChain";
+import FocusOperatorFactoryChain from "./FocusOperatorFactoryChain";
+import FollowOperatorFactoryChain from "./FollowOperatorFactoryChain";
+import MarkOperatorFactoryChain from "./MarkOperatorFactoryChain";
+import ScrollOperatorFactoryChain from "./ScrollOperatorFactoryChain";
+
+@injectable()
+export default class OperatorFactoryImpl implements OperatorFactory {
+ private readonly factoryChains: OperatorFactoryChain[];
+
+ constructor(
+ addonOperatorFactoryChain: AddonOperatorFactoryChain,
+ clipboardOperatorFactoryChain: ClipboardOperatorFactoryChain,
+ findOperatorFactoryChain: FindOperatorFactoryChain,
+ focusOperatorFactoryChain: FocusOperatorFactoryChain,
+ followOperatorFactoryChain: FollowOperatorFactoryChain,
+ markOperatorFactoryChain: MarkOperatorFactoryChain,
+ scrollOperatorFactoryChain: ScrollOperatorFactoryChain,
+ @inject("OperationClient")
+ private readonly operationClient: OperationClient
+ ) {
+ this.factoryChains = [
+ addonOperatorFactoryChain,
+ clipboardOperatorFactoryChain,
+ findOperatorFactoryChain,
+ focusOperatorFactoryChain,
+ followOperatorFactoryChain,
+ markOperatorFactoryChain,
+ scrollOperatorFactoryChain,
+ ];
+ }
+
+ create(op: Operation, repeat: number): Operator {
+ for (const chain of this.factoryChains) {
+ const operator = chain.create(op, repeat);
+ if (operator != null) {
+ return operator;
+ }
+ }
+ return new BackgroundOperationOperator(this.operationClient, repeat, op);
+ }
+}
diff --git a/src/content/operators/impls/PageScrollOperator.ts b/src/content/operators/impls/PageScrollOperator.ts
new file mode 100644
index 0000000..377bf92
--- /dev/null
+++ b/src/content/operators/impls/PageScrollOperator.ts
@@ -0,0 +1,21 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class PageScrollOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository,
+ private readonly count: number
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollPages(this.count, smooth);
+ }
+}
diff --git a/src/content/operators/impls/PasteOperator.ts b/src/content/operators/impls/PasteOperator.ts
new file mode 100644
index 0000000..592da66
--- /dev/null
+++ b/src/content/operators/impls/PasteOperator.ts
@@ -0,0 +1,25 @@
+import Operator from "../Operator";
+import ClipboardRepository from "../../repositories/ClipboardRepository";
+import SettingRepository from "../../repositories/SettingRepository";
+import OperationClient from "../../client/OperationClient";
+import * as urls from "../../../shared/urls";
+
+export default class PasteOperator implements Operator {
+ constructor(
+ private readonly repository: ClipboardRepository,
+ private readonly settingRepository: SettingRepository,
+ private readonly operationClient: OperationClient,
+ private readonly newTab: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ const search = this.settingRepository.get().search;
+ const text = this.repository.read();
+ const url = urls.searchUrl(text, search);
+
+ // NOTE: 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.operationClient.internalOpenUrl(url, this.newTab);
+ }
+}
diff --git a/src/content/operators/impls/ScrollOperatorFactoryChain.ts b/src/content/operators/impls/ScrollOperatorFactoryChain.ts
new file mode 100644
index 0000000..6847aea
--- /dev/null
+++ b/src/content/operators/impls/ScrollOperatorFactoryChain.ts
@@ -0,0 +1,68 @@
+import { inject, injectable } from "tsyringe";
+import OperatorFactoryChain from "../OperatorFactoryChain";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+import * as operations from "../../../shared/operations";
+import Operator from "../Operator";
+import VerticalScrollOperator from "./VerticalScrollOperator";
+import HorizontalScrollOperator from "./HorizontalScrollOperator";
+import PageScrollOperator from "./PageScrollOperator";
+import ScrollToTopOperator from "./ScrollToTopOperator";
+import ScrollToBottomOperator from "./ScrollToBottomOperator";
+import ScrollToHomeOperator from "./ScrollToHomeOperator";
+import ScrollToEndOperator from "./ScrollToEndOperator";
+
+@injectable()
+export default class ScrollOperatorFactoryChain
+ implements OperatorFactoryChain {
+ constructor(
+ @inject("ScrollPresenter")
+ private readonly scrollPresenter: ScrollPresenter,
+ @inject("SettingRepository")
+ private readonly settingRepository: SettingRepository
+ ) {}
+
+ create(op: operations.Operation, repeat: number): Operator | null {
+ switch (op.type) {
+ case operations.SCROLL_VERTICALLY:
+ return new VerticalScrollOperator(
+ this.scrollPresenter,
+ this.settingRepository,
+ op.count * repeat
+ );
+ case operations.SCROLL_HORIZONALLY:
+ return new HorizontalScrollOperator(
+ this.scrollPresenter,
+ this.settingRepository,
+ op.count * repeat
+ );
+ case operations.SCROLL_PAGES:
+ return new PageScrollOperator(
+ this.scrollPresenter,
+ this.settingRepository,
+ op.count * repeat
+ );
+ case operations.SCROLL_TOP:
+ return new ScrollToTopOperator(
+ this.scrollPresenter,
+ this.settingRepository
+ );
+ case operations.SCROLL_BOTTOM:
+ return new ScrollToBottomOperator(
+ this.scrollPresenter,
+ this.settingRepository
+ );
+ case operations.SCROLL_HOME:
+ return new ScrollToHomeOperator(
+ this.scrollPresenter,
+ this.settingRepository
+ );
+ case operations.SCROLL_END:
+ return new ScrollToEndOperator(
+ this.scrollPresenter,
+ this.settingRepository
+ );
+ }
+ return null;
+ }
+}
diff --git a/src/content/operators/impls/ScrollToBottomOperator.ts b/src/content/operators/impls/ScrollToBottomOperator.ts
new file mode 100644
index 0000000..4db521b
--- /dev/null
+++ b/src/content/operators/impls/ScrollToBottomOperator.ts
@@ -0,0 +1,20 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class ScrollToBottomOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollToBottom(smooth);
+ }
+}
diff --git a/src/content/operators/impls/ScrollToEndOperator.ts b/src/content/operators/impls/ScrollToEndOperator.ts
new file mode 100644
index 0000000..8217e15
--- /dev/null
+++ b/src/content/operators/impls/ScrollToEndOperator.ts
@@ -0,0 +1,20 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class ScrollToEndOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollToEnd(smooth);
+ }
+}
diff --git a/src/content/operators/impls/ScrollToHomeOperator.ts b/src/content/operators/impls/ScrollToHomeOperator.ts
new file mode 100644
index 0000000..a0d7701
--- /dev/null
+++ b/src/content/operators/impls/ScrollToHomeOperator.ts
@@ -0,0 +1,20 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class ScrollToHomeOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollToHome(smooth);
+ }
+}
diff --git a/src/content/operators/impls/ScrollToTopOperator.ts b/src/content/operators/impls/ScrollToTopOperator.ts
new file mode 100644
index 0000000..6075758
--- /dev/null
+++ b/src/content/operators/impls/ScrollToTopOperator.ts
@@ -0,0 +1,20 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class ScrollToTopOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollToTop(smooth);
+ }
+}
diff --git a/src/content/operators/impls/StartFollowOperator.ts b/src/content/operators/impls/StartFollowOperator.ts
new file mode 100644
index 0000000..6f30058
--- /dev/null
+++ b/src/content/operators/impls/StartFollowOperator.ts
@@ -0,0 +1,14 @@
+import Operator from "../Operator";
+import FollowMasterClient from "../../client/FollowMasterClient";
+
+export default class StartFollowOperator implements Operator {
+ constructor(
+ private readonly followMasterClient: FollowMasterClient,
+ private readonly newTab: boolean,
+ private readonly background: boolean
+ ) {}
+
+ async run(): Promise<void> {
+ this.followMasterClient.startFollow(this.newTab, this.background);
+ }
+}
diff --git a/src/content/operators/impls/ToggleAddonOperator.ts b/src/content/operators/impls/ToggleAddonOperator.ts
new file mode 100644
index 0000000..2a249d6
--- /dev/null
+++ b/src/content/operators/impls/ToggleAddonOperator.ts
@@ -0,0 +1,16 @@
+import Operator from "../Operator";
+import AddonIndicatorClient from "../../client/AddonIndicatorClient";
+import AddonEnabledRepository from "../../repositories/AddonEnabledRepository";
+
+export default class ToggleAddonOperator implements Operator {
+ constructor(
+ private readonly indicator: AddonIndicatorClient,
+ private readonly repository: AddonEnabledRepository
+ ) {}
+
+ async run(): Promise<void> {
+ const current = this.repository.get();
+ this.repository.set(!current);
+ await this.indicator.setEnabled(!current);
+ }
+}
diff --git a/src/content/operators/impls/URLRepository.ts b/src/content/operators/impls/URLRepository.ts
new file mode 100644
index 0000000..a1efc2e
--- /dev/null
+++ b/src/content/operators/impls/URLRepository.ts
@@ -0,0 +1,9 @@
+export default interface URLRepository {
+ getCurrentURL(): string;
+}
+
+export class URLRepositoryImpl implements URLRepository {
+ getCurrentURL(): string {
+ return window.location.href;
+ }
+}
diff --git a/src/content/operators/impls/VerticalScrollOperator.ts b/src/content/operators/impls/VerticalScrollOperator.ts
new file mode 100644
index 0000000..4ab336c
--- /dev/null
+++ b/src/content/operators/impls/VerticalScrollOperator.ts
@@ -0,0 +1,21 @@
+import AbstractScrollOperator from "./AbstractScrollOperator";
+import Operator from "../Operator";
+import ScrollPresenter from "../../presenters/ScrollPresenter";
+import SettingRepository from "../../repositories/SettingRepository";
+
+export default class VerticalScrollOperator
+ extends AbstractScrollOperator
+ implements Operator {
+ constructor(
+ private readonly presenter: ScrollPresenter,
+ settingRepository: SettingRepository,
+ private readonly count: number
+ ) {
+ super(settingRepository);
+ }
+
+ async run(): Promise<void> {
+ const smooth = this.getSmoothScroll();
+ this.presenter.scrollVertically(this.count, smooth);
+ }
+}
diff --git a/src/content/operators/impls/YankURLOperator.ts b/src/content/operators/impls/YankURLOperator.ts
new file mode 100644
index 0000000..2e774eb
--- /dev/null
+++ b/src/content/operators/impls/YankURLOperator.ts
@@ -0,0 +1,18 @@
+import Operator from "../Operator";
+import ClipboardRepository from "../../repositories/ClipboardRepository";
+import ConsoleClient from "../../client/ConsoleClient";
+import URLRepository from "./URLRepository";
+
+export default class YankURLOperator implements Operator {
+ constructor(
+ private readonly repository: ClipboardRepository,
+ private readonly consoleClient: ConsoleClient,
+ private readonly urlRepository: URLRepository
+ ) {}
+
+ async run(): Promise<void> {
+ const url = this.urlRepository.getCurrentURL();
+ this.repository.write(url);
+ await this.consoleClient.info("Yanked " + url);
+ }
+}
diff --git a/src/content/repositories/HintKeyRepository.ts b/src/content/repositories/HintKeyRepository.ts
new file mode 100644
index 0000000..342b003
--- /dev/null
+++ b/src/content/repositories/HintKeyRepository.ts
@@ -0,0 +1,49 @@
+export default interface HintKeyRepository {
+ reset(charset: string): void;
+
+ produce(): string;
+}
+
+const current: {
+ charset: string;
+ counter: number[];
+} = {
+ charset: "",
+ counter: [],
+};
+
+export class HintKeyRepositoryImpl implements HintKeyRepository {
+ reset(charset: string): void {
+ if (charset.length === 0) {
+ throw new TypeError("charset is empty");
+ }
+ current.charset = charset;
+ current.counter = [];
+ }
+
+ produce(): string {
+ if (current.charset === "") {
+ throw new Error("charset is not set");
+ }
+ this.increment();
+
+ return current.counter.map((x) => current.charset[x]).join("");
+ }
+
+ private increment(): void {
+ const max = current.charset.length - 1;
+ if (current.counter.every((x) => x === max)) {
+ current.counter = new Array(current.counter.length + 1).fill(0);
+ return;
+ }
+
+ current.counter.reverse();
+ const len = current.charset.length;
+ let num = current.counter.reduce((x, y, index) => x + y * len ** index) + 1;
+ for (let i = 0; i < current.counter.length; ++i) {
+ current.counter[i] = num % len;
+ num = ~~(num / len);
+ }
+ current.counter.reverse();
+ }
+}
diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts
deleted file mode 100644
index 875fc11..0000000
--- a/src/content/usecases/ClipboardUseCase.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { injectable, inject } from "tsyringe";
-import * as urls from "../../shared/urls";
-import ClipboardRepository from "../repositories/ClipboardRepository";
-import SettingRepository from "../repositories/SettingRepository";
-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("ConsoleClient") private consoleClient: ConsoleClient,
- @inject("OperationClient") private operationClinet: OperationClient
- ) {}
-
- async yankCurrentURL(): Promise<string> {
- const url = window.location.href;
- this.repository.write(url);
- await this.consoleClient.info("Yanked " + url);
- return Promise.resolve(url);
- }
-
- async openOrSearch(newTab: boolean): Promise<void> {
- const search = this.settingRepository.get().search;
- const text = this.repository.read();
- const url = urls.searchUrl(text, search);
-
- // 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/content/usecases/FindSlaveUseCase.ts b/src/content/usecases/FindSlaveUseCase.ts
deleted file mode 100644
index 3b8c4b4..0000000
--- a/src/content/usecases/FindSlaveUseCase.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { injectable, inject } from "tsyringe";
-import FindMasterClient from "../client/FindMasterClient";
-
-@injectable()
-export default class FindSlaveUseCase {
- constructor(
- @inject("FindMasterClient") private findMasterClient: FindMasterClient
- ) {}
-
- findNext() {
- this.findMasterClient.findNext();
- }
-
- findPrev() {
- this.findMasterClient.findPrev();
- }
-}
diff --git a/src/content/usecases/FocusUseCase.ts b/src/content/usecases/FocusUseCase.ts
deleted file mode 100644
index 8c62003..0000000
--- a/src/content/usecases/FocusUseCase.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { injectable, inject } from "tsyringe";
-import FocusPresenter from "../presenters/FocusPresenter";
-
-@injectable()
-export default class FocusUseCases {
- constructor(@inject("FocusPresenter") private presenter: FocusPresenter) {}
-
- focusFirstInput() {
- this.presenter.focusFirstElement();
- }
-}
diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts
index 88c682e..f4705e1 100644
--- a/src/content/usecases/FollowMasterUseCase.ts
+++ b/src/content/usecases/FollowMasterUseCase.ts
@@ -4,13 +4,10 @@ import FollowMasterRepository from "../repositories/FollowMasterRepository";
import FollowSlaveClient from "../client/FollowSlaveClient";
import FollowSlaveClientFactory from "../client/FollowSlaveClientFactory";
import SettingRepository from "../repositories/SettingRepository";
-import HintKeyProducer from "./HintKeyProducer";
+import HintKeyRepository from "../repositories/HintKeyRepository";
@injectable()
export default class FollowMasterUseCase {
- // TODO Make repository
- private producer: HintKeyProducer | null;
-
constructor(
@inject("FollowKeyRepository")
private followKeyRepository: FollowKeyRepository,
@@ -22,14 +19,15 @@ export default class FollowMasterUseCase {
private settingRepository: SettingRepository,
@inject("FollowSlaveClientFactory")
- private followSlaveClientFactory: FollowSlaveClientFactory
- ) {
- this.producer = null;
- }
+ private followSlaveClientFactory: FollowSlaveClientFactory,
+
+ @inject("HintKeyRepository")
+ private hintKeyRepository: HintKeyRepository
+ ) {}
startFollow(newTab: boolean, background: boolean): void {
const hintchars = this.settingRepository.get().properties.hintchars;
- this.producer = new HintKeyProducer(hintchars);
+ this.hintKeyRepository.reset(hintchars);
this.followKeyRepository.clearKeys();
this.followMasterRepository.setCurrentFollowMode(newTab, background);
@@ -59,7 +57,7 @@ export default class FollowMasterUseCase {
createSlaveHints(count: number, sender: Window): void {
const produced = [];
for (let i = 0; i < count; ++i) {
- const tag = this.producer!.produce();
+ const tag = this.hintKeyRepository.produce();
produced.push(tag);
this.followMasterRepository.addTag(tag);
}
diff --git a/src/content/usecases/HintKeyProducer.ts b/src/content/usecases/HintKeyProducer.ts
deleted file mode 100644
index a5e2877..0000000
--- a/src/content/usecases/HintKeyProducer.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export default class HintKeyProducer {
- private charset: string;
-
- private counter: number[];
-
- constructor(charset: string) {
- if (charset.length === 0) {
- throw new TypeError("charset is empty");
- }
-
- this.charset = charset;
- this.counter = [];
- }
-
- produce(): string {
- this.increment();
-
- return this.counter.map((x) => this.charset[x]).join("");
- }
-
- private increment(): void {
- const max = this.charset.length - 1;
- if (this.counter.every((x) => x === max)) {
- this.counter = new Array(this.counter.length + 1).fill(0);
- return;
- }
-
- this.counter.reverse();
- const len = this.charset.length;
- let num = this.counter.reduce((x, y, index) => x + y * len ** index) + 1;
- for (let i = 0; i < this.counter.length; ++i) {
- this.counter[i] = num % len;
- num = ~~(num / len);
- }
- this.counter.reverse();
- }
-}
diff --git a/src/content/usecases/MarkKeyUseCase.ts b/src/content/usecases/MarkKeyUseCase.ts
index b807c74..61ed135 100644
--- a/src/content/usecases/MarkKeyUseCase.ts
+++ b/src/content/usecases/MarkKeyUseCase.ts
@@ -15,18 +15,10 @@ export default class MarkKeyUseCase {
return this.repository.isJumpMode();
}
- enableSetMode(): void {
- this.repository.enableSetMode();
- }
-
disableSetMode(): void {
this.repository.disabeSetMode();
}
- enableJumpMode(): void {
- this.repository.enableJumpMode();
- }
-
disableJumpMode(): void {
this.repository.disabeJumpMode();
}
diff --git a/src/content/usecases/ScrollUseCase.ts b/src/content/usecases/ScrollUseCase.ts
deleted file mode 100644
index 319c8b4..0000000
--- a/src/content/usecases/ScrollUseCase.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { injectable, inject } from "tsyringe";
-import ScrollPresenter from "../presenters/ScrollPresenter";
-import SettingRepository from "../repositories/SettingRepository";
-
-@injectable()
-export default class ScrollUseCase {
- constructor(
- @inject("ScrollPresenter") private presenter: ScrollPresenter,
- @inject("SettingRepository") private settingRepository: SettingRepository
- ) {}
-
- scrollVertically(count: number): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollVertically(count, smooth);
- }
-
- scrollHorizonally(count: number): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollHorizonally(count, smooth);
- }
-
- scrollPages(count: number): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollPages(count, smooth);
- }
-
- scrollToTop(): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollToTop(smooth);
- }
-
- scrollToBottom(): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollToBottom(smooth);
- }
-
- scrollToHome(): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollToHome(smooth);
- }
-
- scrollToEnd(): void {
- const smooth = this.getSmoothScroll();
- this.presenter.scrollToEnd(smooth);
- }
-
- private getSmoothScroll(): boolean {
- const settings = this.settingRepository.get();
- return settings.properties.smoothscroll;
- }
-}
diff --git a/test/background/mock/MockBrowserSettingRepository.ts b/test/background/mock/MockBrowserSettingRepository.ts
new file mode 100644
index 0000000..22e7084
--- /dev/null
+++ b/test/background/mock/MockBrowserSettingRepository.ts
@@ -0,0 +1,10 @@
+import BrowserSettingRepository from "../../../src/background/repositories/BrowserSettingRepository";
+
+export default class MockBrowserSettingRepository
+ implements BrowserSettingRepository {
+ constructor(private readonly homepageUrls: string[]) {}
+
+ getHomepageUrls(): Promise<string[]> {
+ return Promise.resolve(this.homepageUrls);
+ }
+}
diff --git a/test/background/mock/MockConsoleClient.ts b/test/background/mock/MockConsoleClient.ts
new file mode 100644
index 0000000..d1f8fc3
--- /dev/null
+++ b/test/background/mock/MockConsoleClient.ts
@@ -0,0 +1,23 @@
+import ConsoleClient from "../../../src/background/infrastructures/ConsoleClient";
+
+export default class MockConsoleClient implements ConsoleClient {
+ hide(_tabId: number): Promise<any> {
+ throw new Error("not implemented");
+ }
+
+ showCommand(_tabId: number, _command: string): Promise<any> {
+ throw new Error("not implemented");
+ }
+
+ showError(_tabId: number, _message: string): Promise<any> {
+ throw new Error("not implemented");
+ }
+
+ showFind(_tabId: number): Promise<any> {
+ throw new Error("not implemented");
+ }
+
+ showInfo(_tabId: number, _message: string): Promise<any> {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/background/mock/MockNavigateClient.ts b/test/background/mock/MockNavigateClient.ts
new file mode 100644
index 0000000..d9442a4
--- /dev/null
+++ b/test/background/mock/MockNavigateClient.ts
@@ -0,0 +1,19 @@
+import NavigateClient from "../../../src/background/clients/NavigateClient";
+
+export default class MockNavigateClient implements NavigateClient {
+ historyNext(_tabId: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ historyPrev(_tabId: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ linkNext(_tabId: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ linkPrev(_tabId: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/background/mock/MockRepeatRepository.ts b/test/background/mock/MockRepeatRepository.ts
new file mode 100644
index 0000000..1a686c8
--- /dev/null
+++ b/test/background/mock/MockRepeatRepository.ts
@@ -0,0 +1,14 @@
+import RepeatRepository from "../../../src/background/repositories/RepeatRepository";
+import { Operation } from "../../../src/shared/operations";
+
+export default class MockRepeatRepository implements RepeatRepository {
+ private op: Operation | undefined = undefined;
+
+ getLastOperation(): Operation | undefined {
+ return this.op;
+ }
+
+ setLastOperation(op: Operation): void {
+ this.op = op;
+ }
+}
diff --git a/test/background/mock/MockTabPresenter.ts b/test/background/mock/MockTabPresenter.ts
new file mode 100644
index 0000000..22fb947
--- /dev/null
+++ b/test/background/mock/MockTabPresenter.ts
@@ -0,0 +1,179 @@
+import TabPresenter from "../../../src/background/presenters/TabPresenter";
+
+export default class MockTabPresenter implements TabPresenter {
+ private readonly tabs: browser.tabs.Tab[] = [];
+ private readonly zooms: number[] = [];
+ private nextid = 0;
+
+ private readonly lastSelectedId: number | undefined;
+
+ private static defaultTabOptions = {
+ hidden: false,
+ highlighted: false,
+ incognito: false,
+ isArticle: false,
+ isInReaderMode: false,
+ lastAccessed: 0,
+ pinned: false,
+ selected: false,
+ windowId: 0,
+ };
+
+ create(
+ url: string,
+ opts?: {
+ active?: boolean;
+ cookieStoreId?: string;
+ index?: number;
+ openerTabId?: number;
+ pinned?: boolean;
+ windowId?: number;
+ }
+ ): Promise<browser.tabs.Tab> {
+ const tab = {
+ ...MockTabPresenter.defaultTabOptions,
+ ...opts,
+ id: this.nextid++,
+ active: false,
+ title: "welcome, world",
+ url,
+ index: this.tabs.length,
+ };
+ if (opts?.active || this.tabs.length === 0) {
+ this.tabs.forEach((t) => (t.active = false));
+ tab.active = true;
+ }
+ this.tabs.push(tab);
+ this.zooms.push(1);
+ return Promise.resolve(tab);
+ }
+
+ duplicate(id: number): Promise<browser.tabs.Tab> {
+ const src = this.tabs.find((t) => t.id === id);
+ if (!src) {
+ throw new Error(`tab ${id} not found`);
+ }
+ this.tabs.forEach((t) => (t.active = false));
+ const tab = { ...src, id: this.nextid++, active: true };
+ this.tabs.push(tab);
+ this.zooms.push(1);
+
+ return Promise.resolve(tab);
+ }
+
+ getAll(): Promise<browser.tabs.Tab[]> {
+ return Promise.resolve([...this.tabs]);
+ }
+
+ getByKeyword(
+ keyword: string,
+ excludePinned: boolean
+ ): Promise<browser.tabs.Tab[]> {
+ const tabs = this.tabs
+
+ .filter((t) => {
+ return (
+ (t.url && t.url.toLowerCase().includes(keyword.toLowerCase())) ||
+ (t.title && t.title.toLowerCase().includes(keyword.toLowerCase()))
+ );
+ })
+ .filter((t) => {
+ return !(excludePinned && t.pinned);
+ });
+ return Promise.resolve(tabs);
+ }
+
+ getCurrent(): Promise<browser.tabs.Tab> {
+ const tab = this.tabs.find((t) => t.active);
+ if (!tab) {
+ throw new Error("active tab not found");
+ }
+ return Promise.resolve(tab);
+ }
+
+ getLastSelectedId(): Promise<number | undefined> {
+ return Promise.resolve(this.lastSelectedId);
+ }
+
+ getZoom(tabId: number): Promise<number> {
+ const index = this.tabs.findIndex((t) => t.id === tabId);
+ if (index === -1) {
+ throw new Error(`tab ${tabId} not found`);
+ }
+ return Promise.resolve(this.zooms[index]);
+ }
+
+ onSelected(
+ _listener: (arg: { tabId: number; windowId: number }) => void
+ ): void {
+ throw new Error("not implemented");
+ }
+
+ open(url: string, tabId?: number): Promise<browser.tabs.Tab> {
+ let tab = this.tabs.find((t) => t.active);
+ if (!tab) {
+ throw new Error(`active tab not found`);
+ }
+ if (tabId !== undefined) {
+ tab = this.tabs.find((t) => t.id === tabId);
+ }
+ if (!tab) {
+ throw new Error(`tab ${tabId} not found`);
+ }
+ tab.url = url;
+ return Promise.resolve(tab);
+ }
+
+ reload(_tabId: number, _cache: boolean): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ remove(ids: number[]): Promise<void> {
+ for (const id of ids) {
+ const index = this.tabs.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error(`tab ${id} not found`);
+ }
+ const tab = this.tabs[index];
+ this.tabs.splice(index, 1);
+ this.zooms.splice(index, 1);
+ if (tab.active) {
+ this.tabs[Math.min(index, this.tabs.length - 1)].active = true;
+ }
+ }
+
+ return Promise.resolve(undefined);
+ }
+
+ reopen(): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ select(tabId: number): Promise<void> {
+ const tab = this.tabs.find((t) => t.id === tabId);
+ if (!tab) {
+ throw new Error(`tab ${tabId} not found`);
+ }
+ this.tabs.forEach((t) => (t.active = false));
+ tab.active = true;
+ return Promise.resolve(undefined);
+ }
+
+ setPinned(tabId: number, pinned: boolean): Promise<void> {
+ const tab = this.tabs.find((t) => t.id === tabId);
+ if (!tab) {
+ throw new Error(`tab ${tabId} not found`);
+ }
+ tab.pinned = pinned;
+ return Promise.resolve();
+ }
+
+ setZoom(tabId: number, factor: number): Promise<void> {
+ const index = this.tabs.findIndex((t) => t.id === tabId);
+ if (index === -1) {
+ throw new Error(`tab ${tabId} not found`);
+ }
+ this.zooms[index] = factor;
+ return Promise.resolve();
+ }
+}
diff --git a/test/background/mock/MockWindowPresenter.ts b/test/background/mock/MockWindowPresenter.ts
new file mode 100644
index 0000000..420ae8b
--- /dev/null
+++ b/test/background/mock/MockWindowPresenter.ts
@@ -0,0 +1,7 @@
+import WindowPresenter from "../../../src/background/presenters/WindowPresenter";
+
+export default class MockWindowPresenter implements WindowPresenter {
+ create(_url: string): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/background/mock/MockZoomPresenter.ts b/test/background/mock/MockZoomPresenter.ts
new file mode 100644
index 0000000..53d1980
--- /dev/null
+++ b/test/background/mock/MockZoomPresenter.ts
@@ -0,0 +1,15 @@
+import ZoomPresenter from "../../../src/background/presenters/ZoomPresenter";
+
+export default class MockZoomPresenter implements ZoomPresenter {
+ resetZoom(): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ zoomIn(): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ zoomOut(): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/background/operators/impls/CancelOperator.test.ts b/test/background/operators/impls/CancelOperator.test.ts
new file mode 100644
index 0000000..915becf
--- /dev/null
+++ b/test/background/operators/impls/CancelOperator.test.ts
@@ -0,0 +1,24 @@
+import sinon from "sinon";
+import CancelOperator from "../../../../src/background/operators/impls/CancelOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("CancelOperator", () => {
+ describe("#run", () => {
+ it("hides console", async () => {
+ const tabPresenter = new MockTabPresenter();
+ const currenTab = await tabPresenter.create("https://example.com/");
+
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("hide")
+ .withArgs(currenTab?.id);
+ const sut = new CancelOperator(tabPresenter, consoleClient);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/CloseTabOperator.test.ts b/test/background/operators/impls/CloseTabOperator.test.ts
new file mode 100644
index 0000000..ba9cbfe
--- /dev/null
+++ b/test/background/operators/impls/CloseTabOperator.test.ts
@@ -0,0 +1,61 @@
+import { expect } from "chai";
+import CloseTabOperator from "../../../../src/background/operators/impls/CloseTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("CloseTabOperator", () => {
+ describe("#run", () => {
+ it("close a current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new CloseTabOperator(tabPresenter);
+
+ await sut.run();
+
+ const tabs = await tabPresenter.getAll();
+ expect(tabs.map((t) => t.url)).to.deep.equal([
+ "https://example.com/1",
+ "https://example.com/3",
+ ]);
+ });
+
+ it("close a current tab forcely", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", {
+ pinned: true,
+ active: false,
+ });
+ await tabPresenter.create("https://example.com/2", {
+ pinned: true,
+ active: true,
+ });
+ await tabPresenter.create("https://example.com/3", {
+ pinned: true,
+ active: false,
+ });
+ const sut = new CloseTabOperator(tabPresenter, true);
+
+ await sut.run();
+
+ const tabs = await tabPresenter.getAll();
+ expect(tabs.map((t) => t.url)).to.deep.equal([
+ "https://example.com/1",
+ "https://example.com/3",
+ ]);
+ });
+
+ it("close a current tab and select left of the closed tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new CloseTabOperator(tabPresenter, false, true);
+
+ await sut.run();
+
+ const tab = await tabPresenter.getCurrent();
+ expect(tab.url).to.equal("https://example.com/1");
+ });
+ });
+});
diff --git a/test/background/operators/impls/CloseTabRightOperator.test.ts b/test/background/operators/impls/CloseTabRightOperator.test.ts
new file mode 100644
index 0000000..c2a106c
--- /dev/null
+++ b/test/background/operators/impls/CloseTabRightOperator.test.ts
@@ -0,0 +1,24 @@
+import { expect } from "chai";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import CloseTabRightOperator from "../../../../src/background/operators/impls/CloseTabRightOperator";
+
+describe("CloseTabRightOperator", () => {
+ describe("#run", () => {
+ it("close the right of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ await tabPresenter.create("https://example.com/4", { active: false });
+ const sut = new CloseTabRightOperator(tabPresenter);
+
+ await sut.run();
+
+ const tabs = await tabPresenter.getAll();
+ expect(tabs.map((t) => t.url)).to.deep.equal([
+ "https://example.com/1",
+ "https://example.com/2",
+ ]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/CommandOperatorFactoryChain.test.ts b/test/background/operators/impls/CommandOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..e481c5a
--- /dev/null
+++ b/test/background/operators/impls/CommandOperatorFactoryChain.test.ts
@@ -0,0 +1,42 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import CommandOperatorFactoryChain from "../../../../src/background/operators/impls/CommandOperatorFactoryChain";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+import * as operations from "../../../../src/shared/operations";
+import ShowCommandOperator from "../../../../src/background/operators/impls/ShowCommandOperator";
+import ShowTabOpenCommandOperator from "../../../../src/background/operators/impls/ShowTabOpenCommandOperator";
+import ShowWinOpenCommandOperator from "../../../../src/background/operators/impls/ShowWinOpenCommandOperator";
+import ShowBufferCommandOperator from "../../../../src/background/operators/impls/ShowBufferCommandOperator";
+import ShowAddBookmarkOperator from "../../../../src/background/operators/impls/ShowAddBookmarkOperator";
+import StartFindOperator from "../../../../src/background/operators/impls/StartFindOperator";
+
+describe("CommandOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const tabPresenter = new MockTabPresenter();
+ const consoleClient = new MockConsoleClient();
+ const sut = new CommandOperatorFactoryChain(tabPresenter, consoleClient);
+
+ expect(sut.create({ type: operations.COMMAND_SHOW })).to.be.instanceOf(
+ ShowCommandOperator
+ );
+ expect(
+ sut.create({ type: operations.COMMAND_SHOW_TABOPEN, alter: true })
+ ).to.be.instanceOf(ShowTabOpenCommandOperator);
+ expect(
+ sut.create({ type: operations.COMMAND_SHOW_WINOPEN, alter: true })
+ ).to.be.instanceOf(ShowWinOpenCommandOperator);
+ expect(
+ sut.create({ type: operations.COMMAND_SHOW_BUFFER })
+ ).to.be.instanceOf(ShowBufferCommandOperator);
+ expect(
+ sut.create({ type: operations.COMMAND_SHOW_ADDBOOKMARK, alter: true })
+ ).to.be.instanceOf(ShowAddBookmarkOperator);
+ expect(sut.create({ type: operations.FIND_START })).to.be.instanceOf(
+ StartFindOperator
+ );
+ expect(sut.create({ type: operations.CANCEL })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/DuplicateTabOperator.test.ts b/test/background/operators/impls/DuplicateTabOperator.test.ts
new file mode 100644
index 0000000..ce2c19d
--- /dev/null
+++ b/test/background/operators/impls/DuplicateTabOperator.test.ts
@@ -0,0 +1,25 @@
+import { expect } from "chai";
+import DuplicateTabOperator from "../../../../src/background/operators/impls/DuplicateTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("DuplicateTabOperator", () => {
+ describe("#run", () => {
+ it("duplicate a tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new DuplicateTabOperator(tabPresenter);
+
+ await sut.run();
+
+ const tabs = await tabPresenter.getAll();
+ expect(tabs.map((t) => t.url)).to.deep.equal([
+ "https://example.com/1",
+ "https://example.com/2",
+ "https://example.com/3",
+ "https://example.com/2",
+ ]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/InternalOpenURLOperator.test.ts b/test/background/operators/impls/InternalOpenURLOperator.test.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/background/operators/impls/InternalOpenURLOperator.test.ts
diff --git a/test/background/operators/impls/InternalOperatorFactoryChain.test.ts b/test/background/operators/impls/InternalOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..09029db
--- /dev/null
+++ b/test/background/operators/impls/InternalOperatorFactoryChain.test.ts
@@ -0,0 +1,37 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import InternalOperatorFactoryChain from "../../../../src/background/operators/impls/InternalOperatorFactoryChain";
+import MockWindowPresenter from "../../mock/MockWindowPresenter";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+import CancelOperator from "../../../../src/background/operators/impls/CancelOperator";
+import InternalOpenURLOperator from "../../../../src/background/operators/impls/InternalOpenURLOperator";
+import * as operations from "../../../../src/shared/operations";
+
+describe("InternalOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const windowPresenter = new MockWindowPresenter();
+ const tabPresenter = new MockTabPresenter();
+ const consoleClient = new MockConsoleClient();
+ const sut = new InternalOperatorFactoryChain(
+ windowPresenter,
+ tabPresenter,
+ consoleClient
+ );
+
+ expect(sut.create({ type: operations.CANCEL })).to.be.instanceOf(
+ CancelOperator
+ );
+ expect(
+ sut.create({
+ type: operations.INTERNAL_OPEN_URL,
+ url: "https://example.com",
+ newTab: false,
+ newWindow: false,
+ })
+ ).to.be.instanceOf(InternalOpenURLOperator);
+ expect(sut.create({ type: operations.COMMAND_SHOW })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateHistoryNextOperator.test.ts b/test/background/operators/impls/NavigateHistoryNextOperator.test.ts
new file mode 100644
index 0000000..de8f597
--- /dev/null
+++ b/test/background/operators/impls/NavigateHistoryNextOperator.test.ts
@@ -0,0 +1,25 @@
+import sinon from "sinon";
+import NavigateHistoryNextOperator from "../../../../src/background/operators/impls/NavigateHistoryNextOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockNavigateClient from "../../mock/MockNavigateClient";
+
+describe("NavigateHistoryNextOperator", () => {
+ describe("#run", () => {
+ it("send a message to navigate next in the history", async () => {
+ const navigateClient = new MockNavigateClient();
+ const mock = sinon
+ .mock(navigateClient)
+ .expects("historyNext")
+ .withArgs(1);
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new NavigateHistoryNextOperator(tabPresenter, navigateClient);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateHistoryPrevOperator.test.ts b/test/background/operators/impls/NavigateHistoryPrevOperator.test.ts
new file mode 100644
index 0000000..6ebe71e
--- /dev/null
+++ b/test/background/operators/impls/NavigateHistoryPrevOperator.test.ts
@@ -0,0 +1,25 @@
+import sinon from "sinon";
+import NavigateHistoryPrevOperator from "../../../../src/background/operators/impls/NavigateHistoryPrevOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockNavigateClient from "../../mock/MockNavigateClient";
+
+describe("NavigateHistoryPrevOperator", () => {
+ describe("#run", () => {
+ it("send a message to navigate previous in the history", async () => {
+ const navigateClient = new MockNavigateClient();
+ const mock = sinon
+ .mock(navigateClient)
+ .expects("historyPrev")
+ .withArgs(1);
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new NavigateHistoryPrevOperator(tabPresenter, navigateClient);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateLinkNextOperator.test.ts b/test/background/operators/impls/NavigateLinkNextOperator.test.ts
new file mode 100644
index 0000000..09c4907
--- /dev/null
+++ b/test/background/operators/impls/NavigateLinkNextOperator.test.ts
@@ -0,0 +1,22 @@
+import sinon from "sinon";
+import NavigateLinkNextOperator from "../../../../src/background/operators/impls/NavigateLinkNextOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockNavigateClient from "../../mock/MockNavigateClient";
+
+describe("NavigateLinkNextOperator", () => {
+ describe("#run", () => {
+ it("send a message to navigate next page", async () => {
+ const navigateClient = new MockNavigateClient();
+ const mock = sinon.mock(navigateClient).expects("linkNext").withArgs(1);
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new NavigateLinkNextOperator(tabPresenter, navigateClient);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateLinkPrevOperator.test.ts b/test/background/operators/impls/NavigateLinkPrevOperator.test.ts
new file mode 100644
index 0000000..6b7f791
--- /dev/null
+++ b/test/background/operators/impls/NavigateLinkPrevOperator.test.ts
@@ -0,0 +1,22 @@
+import sinon from "sinon";
+import NavigateLinkPrevOperator from "../../../../src/background/operators/impls/NavigateLinkPrevOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockNavigateClient from "../../mock/MockNavigateClient";
+
+describe("NavigateLinkPrevOperator", () => {
+ describe("#run", () => {
+ it("send a message to navigate next page", async () => {
+ const navigateClient = new MockNavigateClient();
+ const mock = sinon.mock(navigateClient).expects("linkPrev").withArgs(1);
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const sut = new NavigateLinkPrevOperator(tabPresenter, navigateClient);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateOperatorFactoryChain.test.ts b/test/background/operators/impls/NavigateOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..dfb5654
--- /dev/null
+++ b/test/background/operators/impls/NavigateOperatorFactoryChain.test.ts
@@ -0,0 +1,56 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import NavigateOperatorFactoryChain from "../../../../src/background/operators/impls/NavigateOperatorFactoryChain";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockNavigateClient from "../../mock/MockNavigateClient";
+import MockBrowserSettingRepository from "../../mock/MockBrowserSettingRepository";
+import NavigateHistoryPrevOperator from "../../../../src/background/operators/impls/NavigateHistoryPrevOperator";
+import NavigateHistoryNextOperator from "../../../../src/background/operators/impls/NavigateHistoryNextOperator";
+import NavigateLinkPrevOperator from "../../../../src/background/operators/impls/NavigateLinkPrevOperator";
+import NavigateLinkNextOperator from "../../../../src/background/operators/impls/NavigateLinkNextOperator";
+import NavigateParentOperator from "../../../../src/background/operators/impls/NavigateParentOperator";
+import NavigateRootOperator from "../../../../src/background/operators/impls/NavigateRootOperator";
+import OpenHomeOperator from "../../../../src/background/operators/impls/OpenHomeOperator";
+import OpenSourceOperator from "../../../../src/background/operators/impls/OpenSourceOperator";
+import * as operations from "../../../../src/shared/operations";
+
+describe("NavigateOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const tabPresenter = new MockTabPresenter();
+ const navigateClient = new MockNavigateClient();
+ const browserSettingRepository = new MockBrowserSettingRepository([]);
+ const sut = new NavigateOperatorFactoryChain(
+ tabPresenter,
+ navigateClient,
+ browserSettingRepository
+ );
+
+ expect(
+ sut.create({ type: operations.NAVIGATE_HISTORY_PREV })
+ ).to.be.instanceOf(NavigateHistoryPrevOperator);
+ expect(
+ sut.create({ type: operations.NAVIGATE_HISTORY_NEXT })
+ ).to.be.instanceOf(NavigateHistoryNextOperator);
+ expect(
+ sut.create({ type: operations.NAVIGATE_LINK_PREV })
+ ).to.be.instanceOf(NavigateLinkPrevOperator);
+ expect(
+ sut.create({ type: operations.NAVIGATE_LINK_NEXT })
+ ).to.be.instanceOf(NavigateLinkNextOperator);
+ expect(sut.create({ type: operations.NAVIGATE_PARENT })).to.be.instanceOf(
+ NavigateParentOperator
+ );
+ expect(sut.create({ type: operations.NAVIGATE_ROOT })).to.be.instanceOf(
+ NavigateRootOperator
+ );
+ expect(sut.create({ type: operations.PAGE_SOURCE })).to.be.instanceOf(
+ OpenSourceOperator
+ );
+ expect(
+ sut.create({ type: operations.PAGE_HOME, newTab: false })
+ ).to.be.instanceOf(OpenHomeOperator);
+ expect(sut.create({ type: operations.CANCEL })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateParentOperator.test.ts b/test/background/operators/impls/NavigateParentOperator.test.ts
new file mode 100644
index 0000000..cc57f17
--- /dev/null
+++ b/test/background/operators/impls/NavigateParentOperator.test.ts
@@ -0,0 +1,53 @@
+import { expect } from "chai";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import NavigateParentOperator from "../../../../src/background/operators/impls/NavigateParentOperator";
+
+describe("NavigateParentOperator", () => {
+ describe("#run", () => {
+ it("opens a parent directory of the file in the URL", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/fruits/yellow/banana", {
+ active: true,
+ });
+ const sut = new NavigateParentOperator(tabPresenter);
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.com/fruits/yellow/");
+ });
+
+ it("opens a parent directory of the directoryin the URL", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/fruits/yellow/");
+ const sut = new NavigateParentOperator(tabPresenter);
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.com/fruits/");
+ });
+
+ it("removes a hash in the URL", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/fruits/yellow/#top");
+ const sut = new NavigateParentOperator(tabPresenter);
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.com/fruits/yellow/");
+ });
+
+ it("removes query parameters in the URL", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/search?q=apple");
+ const sut = new NavigateParentOperator(tabPresenter);
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.com/search");
+ });
+ });
+});
diff --git a/test/background/operators/impls/NavigateRootOperator.test.ts b/test/background/operators/impls/NavigateRootOperator.test.ts
new file mode 100644
index 0000000..bbe574c
--- /dev/null
+++ b/test/background/operators/impls/NavigateRootOperator.test.ts
@@ -0,0 +1,18 @@
+import { expect } from "chai";
+import NavigateRootOperator from "../../../../src/background/operators/impls/NavigateRootOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("NavigateRootOperator", () => {
+ describe("#run", () => {
+ it("opens root directory in the URL", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/search?q=apple#top");
+ const sut = new NavigateRootOperator(tabPresenter);
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.com");
+ });
+ });
+});
diff --git a/test/background/operators/impls/OpenHomeOperator.test.ts b/test/background/operators/impls/OpenHomeOperator.test.ts
new file mode 100644
index 0000000..3c9288f
--- /dev/null
+++ b/test/background/operators/impls/OpenHomeOperator.test.ts
@@ -0,0 +1,70 @@
+import { expect } from "chai";
+import OpenHomeOperator from "../../../../src/background/operators/impls/OpenHomeOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockBrowserSettingRepository from "../../mock/MockBrowserSettingRepository";
+
+describe("OpenHomeOperator", () => {
+ describe("#run", () => {
+ it("opens a home page of the browser into the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/");
+ const browserSettingRepository = new MockBrowserSettingRepository([
+ "https://example.net/",
+ ]);
+ const sut = new OpenHomeOperator(
+ tabPresenter,
+ browserSettingRepository,
+ false
+ );
+
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.be.equal("https://example.net/");
+ });
+
+ it("opens a home page of the browser into a new tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/");
+ const browserSettingRepository = new MockBrowserSettingRepository([
+ "https://example.net/",
+ ]);
+ const sut = new OpenHomeOperator(
+ tabPresenter,
+ browserSettingRepository,
+ true
+ );
+
+ await sut.run();
+
+ const urls = (await tabPresenter.getAll()).map((t) => t.url);
+ expect(urls).to.be.deep.equal([
+ "https://example.com/",
+ "https://example.net/",
+ ]);
+ });
+
+ it("opens home pages of the browser", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/");
+ const browserSettingRepository = new MockBrowserSettingRepository([
+ "https://example.net/",
+ "https://example.org/",
+ ]);
+ const sut = new OpenHomeOperator(
+ tabPresenter,
+ browserSettingRepository,
+ false
+ );
+
+ await sut.run();
+
+ const urls = (await tabPresenter.getAll()).map((t) => t.url);
+ expect(urls).to.be.deep.equal([
+ "https://example.com/",
+ "https://example.net/",
+ "https://example.org/",
+ ]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/OpenSourceOperator.test.ts b/test/background/operators/impls/OpenSourceOperator.test.ts
new file mode 100644
index 0000000..541032b
--- /dev/null
+++ b/test/background/operators/impls/OpenSourceOperator.test.ts
@@ -0,0 +1,21 @@
+import { expect } from "chai";
+import OpenSourceOperator from "../../../../src/background/operators/impls/OpenSourceOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("OpenSourceOperator", () => {
+ describe("#run", () => {
+ it("opens view-source URL of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/");
+ const sut = new OpenSourceOperator(tabPresenter);
+
+ await sut.run();
+
+ const urls = (await tabPresenter.getAll()).map((t) => t.url);
+ expect(urls).to.be.deep.equal([
+ "https://example.com/",
+ "view-source:https://example.com/",
+ ]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/PinTabOperator.test.ts b/test/background/operators/impls/PinTabOperator.test.ts
new file mode 100644
index 0000000..0c940b6
--- /dev/null
+++ b/test/background/operators/impls/PinTabOperator.test.ts
@@ -0,0 +1,25 @@
+import { expect } from "chai";
+import PinTabOperator from "../../../../src/background/operators/impls/PinTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("PinTabOperator", () => {
+ describe("#run", () => {
+ it("make pinned to the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/", {
+ active: true,
+ pinned: false,
+ });
+ await tabPresenter.create("https://example.com/", {
+ active: false,
+ pinned: false,
+ });
+ const sut = new PinTabOperator(tabPresenter);
+
+ await sut.run();
+
+ const pins = (await tabPresenter.getAll()).map((t) => t.pinned);
+ expect(pins).to.deep.equal([true, false]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/ReloadTabOperator.test.ts b/test/background/operators/impls/ReloadTabOperator.test.ts
new file mode 100644
index 0000000..e87782b
--- /dev/null
+++ b/test/background/operators/impls/ReloadTabOperator.test.ts
@@ -0,0 +1,34 @@
+import sinon from "sinon";
+import ReloadTabOperator from "../../../../src/background/operators/impls/ReloadTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("ReloadTabOperator", () => {
+ describe("#run", () => {
+ it("reloads the current tab with cache", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/", { active: true });
+ await tabPresenter.create("https://example.com/", { active: false });
+ const mock = sinon.mock(tabPresenter).expects("reload").withArgs(0, true);
+
+ const sut = new ReloadTabOperator(tabPresenter, true);
+ await sut.run();
+
+ mock.verify();
+ });
+
+ it("reloads the current tab without cache", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/", { active: true });
+ await tabPresenter.create("https://example.com/", { active: false });
+ const mock = sinon
+ .mock(tabPresenter)
+ .expects("reload")
+ .withArgs(0, false);
+
+ const sut = new ReloadTabOperator(tabPresenter, false);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ReopenTabOperator.test.ts b/test/background/operators/impls/ReopenTabOperator.test.ts
new file mode 100644
index 0000000..43b1575
--- /dev/null
+++ b/test/background/operators/impls/ReopenTabOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import ReopenTabOperator from "../../../../src/background/operators/impls/ReopenTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("ReopenTabOperator", () => {
+ describe("#run", () => {
+ it("reopens closed tabs", async () => {
+ const tabPresenter = new MockTabPresenter();
+ const mock = sinon.mock(tabPresenter).expects("reopen");
+
+ const sut = new ReopenTabOperator(tabPresenter);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/RepeatLastOperator.test.ts b/test/background/operators/impls/RepeatLastOperator.test.ts
new file mode 100644
index 0000000..57f1227
--- /dev/null
+++ b/test/background/operators/impls/RepeatLastOperator.test.ts
@@ -0,0 +1,56 @@
+import RepeatLastOperator from "../../../../src/background/operators/impls/RepeatLastOperator";
+import MockRepeatRepository from "../../mock/MockRepeatRepository";
+import OperatorFactory from "../../../../src/background/operators/OperatorFactory";
+import * as operations from "../../../../src/shared/operations";
+import Operator from "../../../../src/background/operators/Operator";
+import sinon from "sinon";
+
+class MockOperatorFactory implements OperatorFactory {
+ create(_op: operations.Operation): Operator {
+ throw new Error("not implemented");
+ }
+}
+
+class MockOperator implements Operator {
+ run(): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
+
+describe("RepeatLastOperator", () => {
+ describe("#run", () => {
+ it("repeat last operation", async () => {
+ const operator = new MockOperator();
+ const operatorMock = sinon.mock(operator).expects("run").once();
+ const repeatRepository = new MockRepeatRepository();
+ repeatRepository.setLastOperation({ type: operations.CANCEL });
+
+ const operatorFactory = new MockOperatorFactory();
+ const operatorFactoryMock = sinon
+ .mock(operatorFactory)
+ .expects("create")
+ .withArgs({ type: operations.CANCEL });
+ operatorFactoryMock.returns(operator);
+
+ const sut = new RepeatLastOperator(repeatRepository, operatorFactory);
+ await sut.run();
+
+ operatorFactoryMock.verify();
+ operatorMock.verify();
+ });
+
+ it("does nothing if no last operations", async () => {
+ const repeatRepository = new MockRepeatRepository();
+ const operatorFactory = new MockOperatorFactory();
+ const operatorFactoryMock = sinon
+ .mock(operatorFactory)
+ .expects("create")
+ .never();
+
+ const sut = new RepeatLastOperator(repeatRepository, operatorFactory);
+ await sut.run();
+
+ operatorFactoryMock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/RepeatOperatorFactoryChain.test.ts b/test/background/operators/impls/RepeatOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..e12d788
--- /dev/null
+++ b/test/background/operators/impls/RepeatOperatorFactoryChain.test.ts
@@ -0,0 +1,32 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import RepeatOperatorFactoryChain from "../../../../src/background/operators/impls/RepeatOperatorFactoryChain";
+import RepeatLastOperator from "../../../../src/background/operators/impls/RepeatLastOperator";
+import OperatorFactory from "../../../../src/background/operators/OperatorFactory";
+import MockRepeatRepository from "../../mock/MockRepeatRepository";
+import Operator from "../../../../src/content/operators/Operator";
+import * as operations from "../../../../src/shared/operations";
+
+class MockOperatorFactory implements OperatorFactory {
+ create(_op: operations.Operation): Operator {
+ throw new Error("not implemented");
+ }
+}
+
+describe("RepeatOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const repeatRepository = new MockRepeatRepository();
+ const operatorFactory = new MockOperatorFactory();
+ const sut = new RepeatOperatorFactoryChain(
+ repeatRepository,
+ operatorFactory
+ );
+
+ expect(sut.create({ type: operations.REPEAT_LAST })).to.be.instanceOf(
+ RepeatLastOperator
+ );
+ expect(sut.create({ type: operations.CANCEL })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/ResetZoomOperator.test.ts b/test/background/operators/impls/ResetZoomOperator.test.ts
new file mode 100644
index 0000000..68cda05
--- /dev/null
+++ b/test/background/operators/impls/ResetZoomOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import ResetZoomOperator from "../../../../src/background/operators/impls/ResetZoomOperator";
+import MockZoomPresenter from "../../mock/MockZoomPresenter";
+
+describe("ResetZoomOperator", () => {
+ describe("#run", () => {
+ it("resets zoom on the tab", async () => {
+ const zoomPresenter = new MockZoomPresenter();
+ const mock = sinon.mock(zoomPresenter).expects("resetZoom").once();
+
+ const sut = new ResetZoomOperator(zoomPresenter);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/SelectFirstTabOperator.test.ts b/test/background/operators/impls/SelectFirstTabOperator.test.ts
new file mode 100644
index 0000000..a3f1d7e
--- /dev/null
+++ b/test/background/operators/impls/SelectFirstTabOperator.test.ts
@@ -0,0 +1,20 @@
+import { expect } from "chai";
+import SelectFirstTabOperator from "../../../../src/background/operators/impls/SelectFirstTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("SelectFirstTabOperator", () => {
+ describe("#run", () => {
+ it("select the leftmost tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+
+ const sut = new SelectFirstTabOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/1");
+ });
+ });
+});
diff --git a/test/background/operators/impls/SelectLastTabOperator.test.ts b/test/background/operators/impls/SelectLastTabOperator.test.ts
new file mode 100644
index 0000000..b8cf5c4
--- /dev/null
+++ b/test/background/operators/impls/SelectLastTabOperator.test.ts
@@ -0,0 +1,20 @@
+import { expect } from "chai";
+import SelectLastTabOperator from "../../../../src/background/operators/impls/SelectLastTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("SelectLastTabOperator", () => {
+ describe("#run", () => {
+ it("select the rightmost tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+
+ const sut = new SelectLastTabOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/3");
+ });
+ });
+});
diff --git a/test/background/operators/impls/SelectPreviousSelectedTabOperator.test.ts b/test/background/operators/impls/SelectPreviousSelectedTabOperator.test.ts
new file mode 100644
index 0000000..5e6cc73
--- /dev/null
+++ b/test/background/operators/impls/SelectPreviousSelectedTabOperator.test.ts
@@ -0,0 +1,38 @@
+import { expect } from "chai";
+import sinon from "sinon";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import SelectPreviousSelectedTabOperator from "../../../../src/background/operators/impls/SelectPreviousSelectedTabOperator";
+
+describe("SelectPreviousSelectedTabOperator", () => {
+ describe("#run", () => {
+ it("select the last-selected tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ sinon.stub(tabPresenter, "getLastSelectedId").returns(Promise.resolve(0));
+
+ const sut = new SelectPreviousSelectedTabOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/1");
+ });
+
+ it("do nothing if no last-selected tabs", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ sinon
+ .stub(tabPresenter, "getLastSelectedId")
+ .returns(Promise.resolve(undefined));
+ const mock = sinon.mock(tabPresenter).expects("select").never();
+
+ const sut = new SelectPreviousSelectedTabOperator(tabPresenter);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/SelectTabNextOperator.test.ts b/test/background/operators/impls/SelectTabNextOperator.test.ts
new file mode 100644
index 0000000..5952d92
--- /dev/null
+++ b/test/background/operators/impls/SelectTabNextOperator.test.ts
@@ -0,0 +1,35 @@
+import { expect } from "chai";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import SelectTabNextOperator from "../../../../src/background/operators/impls/SelectTabNextOperator";
+
+describe("SelectTabNextOperator", () => {
+ describe("#run", () => {
+ it("select a right tab of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+
+ const sut = new SelectTabNextOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/3");
+ });
+ });
+
+ describe("#run", () => {
+ it("select a right tab of the current tab in rotation", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: false });
+ await tabPresenter.create("https://example.com/3", { active: true });
+
+ const sut = new SelectTabNextOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/1");
+ });
+ });
+});
diff --git a/test/background/operators/impls/SelectTabPrevOperator.test.ts b/test/background/operators/impls/SelectTabPrevOperator.test.ts
new file mode 100644
index 0000000..c9092fa
--- /dev/null
+++ b/test/background/operators/impls/SelectTabPrevOperator.test.ts
@@ -0,0 +1,35 @@
+import { expect } from "chai";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import SelectTabPrevOperator from "../../../../src/background/operators/impls/SelectTabPrevOperator";
+
+describe("SelectTabPrevOperator", () => {
+ describe("#run", () => {
+ it("select a left tab of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+
+ const sut = new SelectTabPrevOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/1");
+ });
+ });
+
+ describe("#run", () => {
+ it("select a left tab of the current tab in rotation", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: true });
+ await tabPresenter.create("https://example.com/2", { active: false });
+ await tabPresenter.create("https://example.com/3", { active: false });
+
+ const sut = new SelectTabPrevOperator(tabPresenter);
+ await sut.run();
+
+ const url = (await tabPresenter.getCurrent()).url;
+ expect(url).to.equal("https://example.com/3");
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowAddBookmarkOperator.test.ts b/test/background/operators/impls/ShowAddBookmarkOperator.test.ts
new file mode 100644
index 0000000..1e083c2
--- /dev/null
+++ b/test/background/operators/impls/ShowAddBookmarkOperator.test.ts
@@ -0,0 +1,50 @@
+import sinon from "sinon";
+import ShowAddBookmarkOperator from "../../../../src/background/operators/impls/ShowAddBookmarkOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowAddBookmarkOperator", () => {
+ describe("#run", () => {
+ it("show command with addbookmark command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "addbookmark ");
+
+ const sut = new ShowAddBookmarkOperator(
+ tabPresenter,
+ consoleClient,
+ false
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+
+ it("show command with addbookmark command and an URL of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "addbookmark welcome, world");
+
+ const sut = new ShowAddBookmarkOperator(
+ tabPresenter,
+ consoleClient,
+ true
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowBufferCommandOperator.test.ts b/test/background/operators/impls/ShowBufferCommandOperator.test.ts
new file mode 100644
index 0000000..91455b3
--- /dev/null
+++ b/test/background/operators/impls/ShowBufferCommandOperator.test.ts
@@ -0,0 +1,25 @@
+import sinon from "sinon";
+import ShowBufferCommandOperator from "../../../../src/background/operators/impls/ShowBufferCommandOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowBufferCommandOperator", () => {
+ describe("#run", () => {
+ it("show command with buffer command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "buffer ");
+
+ const sut = new ShowBufferCommandOperator(tabPresenter, consoleClient);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowCommandOperator.test.ts b/test/background/operators/impls/ShowCommandOperator.test.ts
new file mode 100644
index 0000000..83b028c
--- /dev/null
+++ b/test/background/operators/impls/ShowCommandOperator.test.ts
@@ -0,0 +1,25 @@
+import sinon from "sinon";
+import ShowCommandOperator from "../../../../src/background/operators/impls/ShowCommandOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowCommandOperator", () => {
+ describe("#run", () => {
+ it("show command with addbookmark command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "");
+
+ const sut = new ShowCommandOperator(tabPresenter, consoleClient);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowOpenCommandOperator.test.ts b/test/background/operators/impls/ShowOpenCommandOperator.test.ts
new file mode 100644
index 0000000..2c2105a
--- /dev/null
+++ b/test/background/operators/impls/ShowOpenCommandOperator.test.ts
@@ -0,0 +1,50 @@
+import sinon from "sinon";
+import ShowOpenCommandOperator from "../../../../src/background/operators/impls/ShowOpenCommandOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowOpenCommandOperator", () => {
+ describe("#run", () => {
+ it("show command with open command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "open ");
+
+ const sut = new ShowOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ false
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+
+ it("show command with open command and an URL of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "open https://example.com/2");
+
+ const sut = new ShowOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ true
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowTabOpenCommandOperator.test.ts b/test/background/operators/impls/ShowTabOpenCommandOperator.test.ts
new file mode 100644
index 0000000..e291d05
--- /dev/null
+++ b/test/background/operators/impls/ShowTabOpenCommandOperator.test.ts
@@ -0,0 +1,50 @@
+import sinon from "sinon";
+import ShowTabOpenCommandOperator from "../../../../src/background/operators/impls/ShowTabOpenCommandOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowTabOpenCommandOperator", () => {
+ describe("#run", () => {
+ it("show command with tabopen command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "tabopen ");
+
+ const sut = new ShowTabOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ false
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+
+ it("show command with tabopen command and an URL of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "tabopen https://example.com/2");
+
+ const sut = new ShowTabOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ true
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ShowWinOpenCommandOperator.test.ts b/test/background/operators/impls/ShowWinOpenCommandOperator.test.ts
new file mode 100644
index 0000000..c81a2d4
--- /dev/null
+++ b/test/background/operators/impls/ShowWinOpenCommandOperator.test.ts
@@ -0,0 +1,50 @@
+import sinon from "sinon";
+import ShowWinOpenCommandOperator from "../../../../src/background/operators/impls/ShowWinOpenCommandOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("ShowWinOpenCommandOperator", () => {
+ describe("#run", () => {
+ it("show command with winopen command", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "winopen ");
+
+ const sut = new ShowWinOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ false
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+
+ it("show command with winopen command and an URL of the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon
+ .mock(consoleClient)
+ .expects("showCommand")
+ .withArgs(1, "winopen https://example.com/2");
+
+ const sut = new ShowWinOpenCommandOperator(
+ tabPresenter,
+ consoleClient,
+ true
+ );
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/StartFindOperator.test.ts b/test/background/operators/impls/StartFindOperator.test.ts
new file mode 100644
index 0000000..7764520
--- /dev/null
+++ b/test/background/operators/impls/StartFindOperator.test.ts
@@ -0,0 +1,22 @@
+import sinon from "sinon";
+import StartFindOperator from "../../../../src/background/operators/impls/StartFindOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("StartFindOperator", () => {
+ describe("#run", () => {
+ it("show find console", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/1", { active: false });
+ await tabPresenter.create("https://example.com/2", { active: true });
+ await tabPresenter.create("https://example.com/3", { active: false });
+ const consoleClient = new MockConsoleClient();
+ const mock = sinon.mock(consoleClient).expects("showFind").withArgs(1);
+
+ const sut = new StartFindOperator(tabPresenter, consoleClient);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/TabOperatorFactoryChain.test.ts b/test/background/operators/impls/TabOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..7ab5de0
--- /dev/null
+++ b/test/background/operators/impls/TabOperatorFactoryChain.test.ts
@@ -0,0 +1,71 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import TabOperatorFactoryChain from "../../../../src/background/operators/impls/TabOperatorFactoryChain";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+import DuplicateTabOperator from "../../../../src/background/operators/impls/DuplicateTabOperator";
+import TogglePinnedTabOperator from "../../../../src/background/operators/impls/TogglePinnedTabOperator";
+import UnpinTabOperator from "../../../../src/background/operators/impls/UnpinTabOperator";
+import PinTabOperator from "../../../../src/background/operators/impls/PinTabOperator";
+import ReloadTabOperator from "../../../../src/background/operators/impls/ReloadTabOperator";
+import SelectPreviousSelectedTabOperator from "../../../../src/background/operators/impls/SelectPreviousSelectedTabOperator";
+import SelectLastTabOperator from "../../../../src/background/operators/impls/SelectLastTabOperator";
+import SelectFirstTabOperator from "../../../../src/background/operators/impls/SelectFirstTabOperator";
+import SelectTabNextOperator from "../../../../src/background/operators/impls/SelectTabNextOperator";
+import SelectTabPrevOperator from "../../../../src/background/operators/impls/SelectTabPrevOperator";
+import ReopenTabOperator from "../../../../src/background/operators/impls/ReopenTabOperator";
+import CloseTabOperator from "../../../../src/background/operators/impls/CloseTabOperator";
+import CloseTabRightOperator from "../../../../src/background/operators/impls/CloseTabRightOperator";
+import * as operations from "../../../../src/shared/operations";
+
+describe("TabOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const tabPresenter = new MockTabPresenter();
+ const sut = new TabOperatorFactoryChain(tabPresenter);
+
+ expect(sut.create({ type: operations.TAB_CLOSE })).to.be.instanceOf(
+ CloseTabOperator
+ );
+ expect(sut.create({ type: operations.TAB_CLOSE_RIGHT })).to.be.instanceOf(
+ CloseTabRightOperator
+ );
+ expect(sut.create({ type: operations.TAB_CLOSE_FORCE })).to.be.instanceOf(
+ CloseTabOperator
+ );
+ expect(sut.create({ type: operations.TAB_REOPEN })).to.be.instanceOf(
+ ReopenTabOperator
+ );
+ expect(sut.create({ type: operations.TAB_PREV })).to.be.instanceOf(
+ SelectTabPrevOperator
+ );
+ expect(sut.create({ type: operations.TAB_NEXT })).to.be.instanceOf(
+ SelectTabNextOperator
+ );
+ expect(sut.create({ type: operations.TAB_FIRST })).to.be.instanceOf(
+ SelectFirstTabOperator
+ );
+ expect(
+ sut.create({ type: operations.TAB_LAST, newTab: false })
+ ).to.be.instanceOf(SelectLastTabOperator);
+ expect(
+ sut.create({ type: operations.TAB_PREV_SEL, newTab: false })
+ ).to.be.instanceOf(SelectPreviousSelectedTabOperator);
+ expect(
+ sut.create({ type: operations.TAB_RELOAD, cache: false })
+ ).to.be.instanceOf(ReloadTabOperator);
+ expect(sut.create({ type: operations.TAB_PIN })).to.be.instanceOf(
+ PinTabOperator
+ );
+ expect(sut.create({ type: operations.TAB_UNPIN })).to.be.instanceOf(
+ UnpinTabOperator
+ );
+ expect(
+ sut.create({ type: operations.TAB_TOGGLE_PINNED })
+ ).to.be.instanceOf(TogglePinnedTabOperator);
+ expect(sut.create({ type: operations.TAB_DUPLICATE })).to.be.instanceOf(
+ DuplicateTabOperator
+ );
+ expect(sut.create({ type: operations.CANCEL })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/TogglePinnedTabOperator.test.ts b/test/background/operators/impls/TogglePinnedTabOperator.test.ts
new file mode 100644
index 0000000..f155f83
--- /dev/null
+++ b/test/background/operators/impls/TogglePinnedTabOperator.test.ts
@@ -0,0 +1,32 @@
+import { expect } from "chai";
+import TogglePinnedTabOperator from "../../../../src/background/operators/impls/TogglePinnedTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("TogglePinnedTabOperator", () => {
+ describe("#run", () => {
+ it("toggle pinned to the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/", {
+ active: true,
+ pinned: false,
+ });
+ await tabPresenter.create("https://example.com/", {
+ active: false,
+ pinned: false,
+ });
+ const sut = new TogglePinnedTabOperator(tabPresenter);
+
+ await sut.run();
+ expect((await tabPresenter.getAll()).map((t) => t.pinned)).to.deep.equal([
+ true,
+ false,
+ ]);
+
+ await sut.run();
+ expect((await tabPresenter.getAll()).map((t) => t.pinned)).to.deep.equal([
+ false,
+ false,
+ ]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/UnpinTabOperator.test.ts b/test/background/operators/impls/UnpinTabOperator.test.ts
new file mode 100644
index 0000000..745f48c
--- /dev/null
+++ b/test/background/operators/impls/UnpinTabOperator.test.ts
@@ -0,0 +1,25 @@
+import { expect } from "chai";
+import UnpinTabOperator from "../../../../src/background/operators/impls/UnpinTabOperator";
+import MockTabPresenter from "../../mock/MockTabPresenter";
+
+describe("UnpinTabOperator", () => {
+ describe("#run", () => {
+ it("make unpinned to the current tab", async () => {
+ const tabPresenter = new MockTabPresenter();
+ await tabPresenter.create("https://example.com/", {
+ active: true,
+ pinned: true,
+ });
+ await tabPresenter.create("https://example.com/", {
+ active: false,
+ pinned: true,
+ });
+ const sut = new UnpinTabOperator(tabPresenter);
+
+ await sut.run();
+
+ const pins = (await tabPresenter.getAll()).map((t) => t.pinned);
+ expect(pins).to.deep.equal([false, true]);
+ });
+ });
+});
diff --git a/test/background/operators/impls/ZoomInOperator.test.ts b/test/background/operators/impls/ZoomInOperator.test.ts
new file mode 100644
index 0000000..097e760
--- /dev/null
+++ b/test/background/operators/impls/ZoomInOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import ZoomInOperator from "../../../../src/background/operators/impls/ZoomInOperator";
+import MockZoomPresenter from "../../mock/MockZoomPresenter";
+
+describe("ZoomInOperator", () => {
+ describe("#run", () => {
+ it("zoom-out the current tab", async () => {
+ const zoomPresenter = new MockZoomPresenter();
+ const mock = sinon.mock(zoomPresenter).expects("zoomIn").once();
+
+ const sut = new ZoomInOperator(zoomPresenter);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/operators/impls/ZoomOperatorFactoryChain.test.ts b/test/background/operators/impls/ZoomOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..10c1cee
--- /dev/null
+++ b/test/background/operators/impls/ZoomOperatorFactoryChain.test.ts
@@ -0,0 +1,28 @@
+import "reflect-metadata";
+import { expect } from "chai";
+import ZoomOperatorFactoryChain from "../../../../src/background/operators/impls/ZoomOperatorFactoryChain";
+import MockZoomPresenter from "../../mock/MockZoomPresenter";
+import ZoomInOperator from "../../../../src/background/operators/impls/ZoomInOperator";
+import ZoomOutOperator from "../../../../src/background/operators/impls/ZoomOutOperator";
+import ResetZoomOperator from "../../../../src/background/operators/impls/ResetZoomOperator";
+import * as operations from "../../../../src/shared/operations";
+
+describe("ZoomOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns a operator for the operation", async () => {
+ const zoomPresenter = new MockZoomPresenter();
+ const sut = new ZoomOperatorFactoryChain(zoomPresenter);
+
+ expect(sut.create({ type: operations.ZOOM_IN })).to.be.instanceOf(
+ ZoomInOperator
+ );
+ expect(sut.create({ type: operations.ZOOM_OUT })).to.be.instanceOf(
+ ZoomOutOperator
+ );
+ expect(sut.create({ type: operations.ZOOM_NEUTRAL })).to.be.instanceOf(
+ ResetZoomOperator
+ );
+ expect(sut.create({ type: operations.CANCEL })).to.be.null;
+ });
+ });
+});
diff --git a/test/background/operators/impls/ZoomOutOperator.test.ts b/test/background/operators/impls/ZoomOutOperator.test.ts
new file mode 100644
index 0000000..e0bbcd9
--- /dev/null
+++ b/test/background/operators/impls/ZoomOutOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import ZoomOutOperator from "../../../../src/background/operators/impls/ZoomOutOperator";
+import MockZoomPresenter from "../../mock/MockZoomPresenter";
+
+describe("ZoomOutOperator", () => {
+ describe("#run", () => {
+ it("zoom-in the current tab", async () => {
+ const zoomPresenter = new MockZoomPresenter();
+ const mock = sinon.mock(zoomPresenter).expects("zoomOut").once();
+
+ const sut = new ZoomOutOperator(zoomPresenter);
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/background/usecases/NavigateUseCase.test.ts b/test/background/usecases/NavigateUseCase.test.ts
deleted file mode 100644
index 086d6cd..0000000
--- a/test/background/usecases/NavigateUseCase.test.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import "reflect-metadata";
-import TabPresenter from "../../../src/background/presenters/TabPresenter";
-import NavigateUseCase from "../../../src/background/usecases/NavigateUseCase";
-import NavigateClient from "../../../src/background/clients/NavigateClient";
-import * as sinon from "sinon";
-
-class MockTabPresenter implements TabPresenter {
- create(_url: string, _opts?: unknown): Promise<browser.tabs.Tab> {
- throw new Error("not implemented");
- }
-
- duplicate(_id: number): Promise<browser.tabs.Tab> {
- throw new Error("not implemented");
- }
-
- getAll(): Promise<browser.tabs.Tab[]> {
- throw new Error("not implemented");
- }
-
- getByKeyword(
- _keyword: string,
- _excludePinned: boolean
- ): Promise<browser.tabs.Tab[]> {
- throw new Error("not implemented");
- }
-
- getCurrent(): Promise<browser.tabs.Tab> {
- throw new Error("not implemented");
- }
-
- getLastSelectedId(): Promise<number | undefined> {
- throw new Error("not implemented");
- }
-
- getZoom(_tabId: number): Promise<number> {
- throw new Error("not implemented");
- }
-
- onSelected(
- _listener: (arg: { tabId: number; windowId: number }) => void
- ): void {
- throw new Error("not implemented");
- }
-
- open(_url: string, _tabId?: number): Promise<browser.tabs.Tab> {
- throw new Error("not implemented");
- }
-
- reload(_tabId: number, _cache: boolean): Promise<void> {
- throw new Error("not implemented");
- }
-
- remove(_ids: number[]): Promise<void> {
- throw new Error("not implemented");
- }
-
- reopen(): Promise<void> {
- throw new Error("not implemented");
- }
-
- select(_tabId: number): Promise<void> {
- throw new Error("not implemented");
- }
-
- setPinned(_tabId: number, _pinned: boolean): Promise<void> {
- throw new Error("not implemented");
- }
-
- setZoom(_tabId: number, _factor: number): Promise<void> {
- throw new Error("not implemented");
- }
-}
-
-describe("NavigateUseCase", () => {
- let sut: NavigateUseCase;
- let tabPresenter: TabPresenter;
- let navigateClient: NavigateClient;
-
- beforeEach(() => {
- tabPresenter = new MockTabPresenter();
- navigateClient = new NavigateClient();
- sut = new NavigateUseCase(tabPresenter, navigateClient);
- });
-
- const newTab = (url: string): browser.tabs.Tab => {
- return {
- index: 0,
- title: "dummy title",
- url: url,
- active: true,
- hidden: false,
- highlighted: false,
- incognito: false,
- isArticle: false,
- isInReaderMode: false,
- lastAccessed: 1585446733000,
- pinned: false,
- selected: false,
- windowId: 0,
- };
- };
-
- describe("#openParent()", async () => {
- it("opens parent directory of file", async () => {
- sinon
- .stub(tabPresenter, "getCurrent")
- .returns(
- Promise.resolve(newTab("https://google.com/fruits/yellow/banana"))
- );
-
- const mock = sinon
- .mock(tabPresenter)
- .expects("open")
- .withArgs("https://google.com/fruits/yellow/");
-
- await sut.openParent();
-
- mock.verify();
- });
-
- it("opens parent directory of directory", async () => {
- sinon
- .stub(tabPresenter, "getCurrent")
- .returns(Promise.resolve(newTab("https://google.com/fruits/yellow/")));
-
- const mock = sinon
- .mock(tabPresenter)
- .expects("open")
- .withArgs("https://google.com/fruits/");
-
- await sut.openParent();
-
- mock.verify();
- });
-
- it("removes hash", async () => {
- sinon
- .stub(tabPresenter, "getCurrent")
- .returns(Promise.resolve(newTab("https://google.com/#top")));
-
- const mock = sinon
- .mock(tabPresenter)
- .expects("open")
- .withArgs("https://google.com/");
-
- await sut.openParent();
-
- mock.verify();
- });
-
- it("removes search query", async () => {
- sinon
- .stub(tabPresenter, "getCurrent")
- .returns(Promise.resolve(newTab("https://google.com/search?q=apple")));
-
- const mock = sinon
- .mock(tabPresenter)
- .expects("open")
- .withArgs("https://google.com/search");
-
- await sut.openParent();
-
- mock.verify();
- });
- });
-
- describe("#openRoot()", () => {
- it("opens root direectory", async () => {
- sinon
- .stub(tabPresenter, "getCurrent")
- .returns(Promise.resolve(newTab("https://google.com/seach?q=apple")));
-
- const mock = sinon
- .mock(tabPresenter)
- .expects("open")
- .withArgs("https://google.com");
-
- await sut.openRoot();
-
- mock.verify();
- });
- });
-});
diff --git a/test/content/mock/MockAddonEnabledRepository.ts b/test/content/mock/MockAddonEnabledRepository.ts
new file mode 100644
index 0000000..cbe248b
--- /dev/null
+++ b/test/content/mock/MockAddonEnabledRepository.ts
@@ -0,0 +1,18 @@
+import AddonEnabledRepository from "../../../src/content/repositories/AddonEnabledRepository";
+
+export default class MockAddonEnabledRepository
+ implements AddonEnabledRepository {
+ public enabled: boolean;
+
+ constructor(initialValue = false) {
+ this.enabled = initialValue;
+ }
+
+ get(): boolean {
+ return this.enabled;
+ }
+
+ set(on: boolean): void {
+ this.enabled = on;
+ }
+}
diff --git a/test/content/mock/MockAddonIndicatorClient.ts b/test/content/mock/MockAddonIndicatorClient.ts
new file mode 100644
index 0000000..6ea9798
--- /dev/null
+++ b/test/content/mock/MockAddonIndicatorClient.ts
@@ -0,0 +1,13 @@
+import AddonIndicatorClient from "../../../src/content/client/AddonIndicatorClient";
+
+export default class MockAddonIndicatorClient implements AddonIndicatorClient {
+ public enabled: boolean;
+
+ constructor(initialValue = false) {
+ this.enabled = initialValue;
+ }
+
+ async setEnabled(enabled: boolean): Promise<void> {
+ this.enabled = enabled;
+ }
+}
diff --git a/test/content/mock/MockClipboardRepository.ts b/test/content/mock/MockClipboardRepository.ts
new file mode 100644
index 0000000..c6e53bd
--- /dev/null
+++ b/test/content/mock/MockClipboardRepository.ts
@@ -0,0 +1,16 @@
+import ClipboardRepository from "../../../src/content/repositories/ClipboardRepository";
+
+export default class MockClipboardRepository implements ClipboardRepository {
+ private value: string;
+
+ constructor(initValue = "") {
+ this.value = initValue;
+ }
+ read(): string {
+ return this.value;
+ }
+
+ write(text: string): void {
+ this.value = text;
+ }
+}
diff --git a/test/content/mock/MockFindMasterClient.ts b/test/content/mock/MockFindMasterClient.ts
new file mode 100644
index 0000000..a035cc5
--- /dev/null
+++ b/test/content/mock/MockFindMasterClient.ts
@@ -0,0 +1,11 @@
+import FindMasterClient from "../../../src/content/client/FindMasterClient";
+
+export default class MockFindMasterClient implements FindMasterClient {
+ findNext(): void {
+ throw new Error("not implemented");
+ }
+
+ findPrev(): void {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/content/mock/MockFocusPresenter.ts b/test/content/mock/MockFocusPresenter.ts
new file mode 100644
index 0000000..43454d0
--- /dev/null
+++ b/test/content/mock/MockFocusPresenter.ts
@@ -0,0 +1,7 @@
+import FocusPresenter from "../../../src/content/presenters/FocusPresenter";
+
+export default class MockFocusPresenter implements FocusPresenter {
+ focusFirstElement(): boolean {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/content/mock/MockFollowMasterClient.ts b/test/content/mock/MockFollowMasterClient.ts
new file mode 100644
index 0000000..fc660d5
--- /dev/null
+++ b/test/content/mock/MockFollowMasterClient.ts
@@ -0,0 +1,16 @@
+import FollowMasterClient from "../../../src/content/client/FollowMasterClient";
+import Key from "../../../src/shared/settings/Key";
+
+export default class MockFollowMasterClient implements FollowMasterClient {
+ responseHintCount(_count: number): void {
+ throw new Error("not implemented");
+ }
+
+ sendKey(_key: Key): void {
+ throw new Error("not implemented");
+ }
+
+ startFollow(_newTab: boolean, _background: boolean): void {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/content/mock/MockMarkKeyRepository.ts b/test/content/mock/MockMarkKeyRepository.ts
new file mode 100644
index 0000000..12bc75c
--- /dev/null
+++ b/test/content/mock/MockMarkKeyRepository.ts
@@ -0,0 +1,43 @@
+import MarkKeyRepository from "../../../src/content/repositories/MarkKeyRepository";
+
+export default class MockMarkKeyRepository implements MarkKeyRepository {
+ public jumpMode: boolean;
+ public setMode: boolean;
+
+ constructor(
+ initialValue: {
+ jumpMode: boolean;
+ setMode: boolean;
+ } = {
+ jumpMode: false,
+ setMode: false,
+ }
+ ) {
+ this.jumpMode = initialValue.jumpMode;
+ this.setMode = initialValue.setMode;
+ }
+
+ disabeJumpMode(): void {
+ this.jumpMode = false;
+ }
+
+ disabeSetMode(): void {
+ this.setMode = false;
+ }
+
+ enableJumpMode(): void {
+ this.jumpMode = true;
+ }
+
+ enableSetMode(): void {
+ this.setMode = true;
+ }
+
+ isJumpMode(): boolean {
+ return this.jumpMode;
+ }
+
+ isSetMode(): boolean {
+ return this.setMode;
+ }
+}
diff --git a/test/content/mock/MockOperationClient.ts b/test/content/mock/MockOperationClient.ts
new file mode 100644
index 0000000..2f50f77
--- /dev/null
+++ b/test/content/mock/MockOperationClient.ts
@@ -0,0 +1,16 @@
+import OperationClient from "../../../src/content/client/OperationClient";
+import * as operations from "../../../src/shared/operations";
+
+export default class MockOperationClient implements OperationClient {
+ execBackgroundOp(_repeat: number, _op: operations.Operation): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ internalOpenUrl(
+ _url: string,
+ _newTab?: boolean,
+ _background?: boolean
+ ): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
diff --git a/test/content/mock/MockScrollPresenter.ts b/test/content/mock/MockScrollPresenter.ts
index c802227..8b4cf2a 100644
--- a/test/content/mock/MockScrollPresenter.ts
+++ b/test/content/mock/MockScrollPresenter.ts
@@ -3,10 +3,10 @@ import ScrollPresenter, {
} from "../../../src/content/presenters/ScrollPresenter";
export default class MockScrollPresenter implements ScrollPresenter {
- private pos: Point;
+ private readonly pos: Point;
- constructor() {
- this.pos = { x: 0, y: 0 };
+ constructor(initX = 0, initY = 0) {
+ this.pos = { x: initX, y: initY };
}
getScroll(): Point {
diff --git a/test/content/mock/MockSettingRepository.ts b/test/content/mock/MockSettingRepository.ts
new file mode 100644
index 0000000..5242713
--- /dev/null
+++ b/test/content/mock/MockSettingRepository.ts
@@ -0,0 +1,20 @@
+import SettingRepository from "../../../src/content/repositories/SettingRepository";
+import Settings, {
+ DefaultSetting,
+} from "../../../src/shared/settings/Settings";
+
+export default class MockSettingRepository implements SettingRepository {
+ private value: Settings;
+
+ constructor(initValue: Settings = DefaultSetting) {
+ this.value = initValue;
+ }
+
+ get(): Settings {
+ return this.value;
+ }
+
+ set(setting: Settings): void {
+ this.value = setting;
+ }
+}
diff --git a/test/content/mock/MockURLRepository.ts b/test/content/mock/MockURLRepository.ts
new file mode 100644
index 0000000..731a7fb
--- /dev/null
+++ b/test/content/mock/MockURLRepository.ts
@@ -0,0 +1,9 @@
+import URLRepository from "../../../src/content/operators/impls/URLRepository";
+
+export default class MockURLRepository implements URLRepository {
+ constructor(private url: string = "https://example.com/") {}
+
+ getCurrentURL(): string {
+ return this.url;
+ }
+}
diff --git a/test/content/operators/impls/AddonOperatorFactoryChain.test.ts b/test/content/operators/impls/AddonOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..c064bb9
--- /dev/null
+++ b/test/content/operators/impls/AddonOperatorFactoryChain.test.ts
@@ -0,0 +1,29 @@
+import AddonOperatorFactoryChain from "../../../../src/content/operators/impls/AddonOperatorFactoryChain";
+import EnableAddonOperator from "../../../../src/content/operators/impls/EnableAddonOperator";
+import DisableAddonOperator from "../../../../src/content/operators/impls/DisableAddonOperator";
+import ToggleAddonOperator from "../../../../src/content/operators/impls/ToggleAddonOperator";
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import MockAddonIndicatorClient from "../../mock/MockAddonIndicatorClient";
+import MockAddonEnabledRepository from "../../mock/MockAddonEnabledRepository";
+
+describe("AddonOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new AddonOperatorFactoryChain(
+ new MockAddonIndicatorClient(),
+ new MockAddonEnabledRepository()
+ );
+ expect(sut.create({ type: operations.ADDON_ENABLE }, 0)).to.be.instanceOf(
+ EnableAddonOperator
+ );
+ expect(
+ sut.create({ type: operations.ADDON_DISABLE }, 0)
+ ).to.be.instanceOf(DisableAddonOperator);
+ expect(
+ sut.create({ type: operations.ADDON_TOGGLE_ENABLED }, 0)
+ ).to.be.instanceOf(ToggleAddonOperator);
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/BackgroundOperationOperator.test.ts b/test/content/operators/impls/BackgroundOperationOperator.test.ts
new file mode 100644
index 0000000..b8b1fbd
--- /dev/null
+++ b/test/content/operators/impls/BackgroundOperationOperator.test.ts
@@ -0,0 +1,38 @@
+import * as operations from "../../../../src/shared/operations";
+import BackgroundOperationOperator from "../../../../src/content/operators/impls/BackgroundOperationOperator";
+import OperationClient from "../../../../src/content/client/OperationClient";
+import { expect } from "chai";
+
+class MockOperationClient implements OperationClient {
+ public readonly executedOps: {
+ op: operations.Operation;
+ repeat: number;
+ }[] = [];
+ async execBackgroundOp(
+ repeat: number,
+ op: operations.Operation
+ ): Promise<void> {
+ this.executedOps.push({ repeat, op });
+ }
+
+ internalOpenUrl(): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
+
+describe("BackgroundOperationOperator", () => {
+ describe("#run", () => {
+ it("returns an operator", async () => {
+ const client = new MockOperationClient();
+ const sut = new BackgroundOperationOperator(client, 2, {
+ type: operations.TAB_CLOSE,
+ });
+
+ await sut.run();
+
+ expect(client.executedOps).to.deep.equal([
+ { op: { type: operations.TAB_CLOSE }, repeat: 2 },
+ ]);
+ });
+ });
+});
diff --git a/test/content/operators/impls/ClipboardOperatorFactoryChain.test.ts b/test/content/operators/impls/ClipboardOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..9ddc229
--- /dev/null
+++ b/test/content/operators/impls/ClipboardOperatorFactoryChain.test.ts
@@ -0,0 +1,31 @@
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import ClipboardOperatorFactoryChain from "../../../../src/content/operators/impls/ClipboardOperatorFactoryChain";
+import YankURLOperator from "../../../../src/content/operators/impls/YankURLOperator";
+import PasteOperator from "../../../../src/content/operators/impls/PasteOperator";
+import MockClipboardRepository from "../../mock/MockClipboardRepository";
+import MockOperationClient from "../../mock/MockOperationClient";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+import MockURLRepository from "../../mock/MockURLRepository";
+
+describe("ClipboardOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new ClipboardOperatorFactoryChain(
+ new MockClipboardRepository(),
+ new MockConsoleClient(),
+ new MockOperationClient(),
+ new MockSettingRepository(),
+ new MockURLRepository()
+ );
+ expect(sut.create({ type: operations.URLS_YANK }, 0)).to.be.instanceOf(
+ YankURLOperator
+ );
+ expect(
+ sut.create({ type: operations.URLS_PASTE, newTab: false }, 0)
+ ).to.be.instanceOf(PasteOperator);
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/DisableAddonOperator.test.ts b/test/content/operators/impls/DisableAddonOperator.test.ts
new file mode 100644
index 0000000..358ae44
--- /dev/null
+++ b/test/content/operators/impls/DisableAddonOperator.test.ts
@@ -0,0 +1,19 @@
+import { expect } from "chai";
+import DisableAddonOperator from "../../../../src/content/operators/impls/DisableAddonOperator";
+import MockAddonIndicatorClient from "../../mock/MockAddonIndicatorClient";
+import MockAddonEnabledRepository from "../../mock/MockAddonEnabledRepository";
+
+describe("DisableAddonOperator", () => {
+ describe("#run", () => {
+ it("disables addon", async () => {
+ const client = new MockAddonIndicatorClient(true);
+ const repository = new MockAddonEnabledRepository(true);
+ const sut = new DisableAddonOperator(client, repository);
+
+ await sut.run();
+
+ expect(client.enabled).to.be.false;
+ expect(repository.enabled).to.be.false;
+ });
+ });
+});
diff --git a/test/content/operators/impls/EnableAddonOperator.test.ts b/test/content/operators/impls/EnableAddonOperator.test.ts
new file mode 100644
index 0000000..a6ca31b
--- /dev/null
+++ b/test/content/operators/impls/EnableAddonOperator.test.ts
@@ -0,0 +1,19 @@
+import { expect } from "chai";
+import EnableAddonOperator from "../../../../src/content/operators/impls/EnableAddonOperator";
+import MockAddonIndicatorClient from "../../mock/MockAddonIndicatorClient";
+import MockAddonEnabledRepository from "../../mock/MockAddonEnabledRepository";
+
+describe("EnableAddonOperator", () => {
+ describe("#run", () => {
+ it("enabled addon", async () => {
+ const client = new MockAddonIndicatorClient(false);
+ const repository = new MockAddonEnabledRepository(false);
+ const sut = new EnableAddonOperator(client, repository);
+
+ await sut.run();
+
+ expect(client.enabled).to.be.true;
+ expect(repository.enabled).to.be.true;
+ });
+ });
+});
diff --git a/test/content/operators/impls/EnableJumpMarkOperator.test.ts b/test/content/operators/impls/EnableJumpMarkOperator.test.ts
new file mode 100644
index 0000000..66b4ecd
--- /dev/null
+++ b/test/content/operators/impls/EnableJumpMarkOperator.test.ts
@@ -0,0 +1,19 @@
+import { expect } from "chai";
+import EnableJumpMarkOperator from "../../../../src/content/operators/impls/EnableJumpMarkOperator";
+import MockMarkKeyRepository from "../../mock/MockMarkKeyRepository";
+
+describe("EnableJumpMarkOperator", () => {
+ describe("#run", () => {
+ it("starts mark jump mode", async () => {
+ const repository = new MockMarkKeyRepository({
+ jumpMode: false,
+ setMode: false,
+ });
+ const sut = new EnableJumpMarkOperator(repository);
+
+ await sut.run();
+
+ expect(repository.jumpMode).to.be.true;
+ });
+ });
+});
diff --git a/test/content/operators/impls/EnableSetMarkOperator.test.ts b/test/content/operators/impls/EnableSetMarkOperator.test.ts
new file mode 100644
index 0000000..b28874d
--- /dev/null
+++ b/test/content/operators/impls/EnableSetMarkOperator.test.ts
@@ -0,0 +1,19 @@
+import { expect } from "chai";
+import EnableSetMarkOperator from "../../../../src/content/operators/impls/EnableSetMarkOperator";
+import MockMarkKeyRepository from "../../mock/MockMarkKeyRepository";
+
+describe("EnableSetMarkOperator", () => {
+ describe("#run", () => {
+ it("starts mark set mode", async () => {
+ const repository = new MockMarkKeyRepository({
+ jumpMode: false,
+ setMode: false,
+ });
+ const sut = new EnableSetMarkOperator(repository);
+
+ await sut.run();
+
+ expect(repository.setMode).to.be.true;
+ });
+ });
+});
diff --git a/test/content/operators/impls/FindNextOperator.test.ts b/test/content/operators/impls/FindNextOperator.test.ts
new file mode 100644
index 0000000..d93d45e
--- /dev/null
+++ b/test/content/operators/impls/FindNextOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import FindNextOperator from "../../../../src/content/operators/impls/FindNextOperator";
+import MockFindMasterClient from "../../mock/MockFindMasterClient";
+
+describe("FindNextOperator", () => {
+ describe("#run", () => {
+ it("find next keyword", async () => {
+ const client = new MockFindMasterClient();
+ const mock = sinon.mock(client).expects("findNext").exactly(3);
+ const sut = new FindNextOperator(client, 3);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/content/operators/impls/FindOperatorFactoryChain.test.ts b/test/content/operators/impls/FindOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..6c599ae
--- /dev/null
+++ b/test/content/operators/impls/FindOperatorFactoryChain.test.ts
@@ -0,0 +1,21 @@
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import FindOperatorFactoryChain from "../../../../src/content/operators/impls/FindOperatorFactoryChain";
+import MockFindMasterClient from "../../mock/MockFindMasterClient";
+import FindNextOperator from "../../../../src/content/operators/impls/FindNextOperator";
+import FindPrevOperator from "../../../../src/content/operators/impls/FindPrevOperator";
+
+describe("FindOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new FindOperatorFactoryChain(new MockFindMasterClient());
+ expect(sut.create({ type: operations.FIND_NEXT }, 0)).to.be.instanceOf(
+ FindNextOperator
+ );
+ expect(sut.create({ type: operations.FIND_PREV }, 0)).to.be.instanceOf(
+ FindPrevOperator
+ );
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/FindPrevOperator.test.ts b/test/content/operators/impls/FindPrevOperator.test.ts
new file mode 100644
index 0000000..1ebde8d
--- /dev/null
+++ b/test/content/operators/impls/FindPrevOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import FindPrevOperator from "../../../../src/content/operators/impls/FindPrevOperator";
+import MockFindMasterClient from "../../mock/MockFindMasterClient";
+
+describe("FindPrevOperator", () => {
+ describe("#run", () => {
+ it("find previous keyword", async () => {
+ const client = new MockFindMasterClient();
+ const mock = sinon.mock(client).expects("findPrev").exactly(3);
+ const sut = new FindPrevOperator(client, 3);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/content/operators/impls/FocusOperator.test.ts b/test/content/operators/impls/FocusOperator.test.ts
new file mode 100644
index 0000000..a0eb53b
--- /dev/null
+++ b/test/content/operators/impls/FocusOperator.test.ts
@@ -0,0 +1,17 @@
+import sinon from "sinon";
+import FocusOperator from "../../../../src/content/operators/impls/FocusOperator";
+import MockFocusPresenter from "../../mock/MockFocusPresenter";
+
+describe("FocusOperator", () => {
+ describe("#run", () => {
+ it("focus a first input", async () => {
+ const presenter = new MockFocusPresenter();
+ const mock = sinon.mock(presenter).expects("focusFirstElement");
+ const sut = new FocusOperator(presenter);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/content/operators/impls/FocusOperatorFactoryChain.test.ts b/test/content/operators/impls/FocusOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..91f734b
--- /dev/null
+++ b/test/content/operators/impls/FocusOperatorFactoryChain.test.ts
@@ -0,0 +1,17 @@
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import FocusOperatorFactoryChain from "../../../../src/content/operators/impls/FocusOperatorFactoryChain";
+import FocusOperator from "../../../../src/content/operators/impls/FocusOperator";
+import MockFocusPresenter from "../../mock/MockFocusPresenter";
+
+describe("FocusOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new FocusOperatorFactoryChain(new MockFocusPresenter());
+ expect(sut.create({ type: operations.FOCUS_INPUT }, 0)).to.be.instanceOf(
+ FocusOperator
+ );
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/FollowOperatorFactoryChain.test.ts b/test/content/operators/impls/FollowOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..526a93c
--- /dev/null
+++ b/test/content/operators/impls/FollowOperatorFactoryChain.test.ts
@@ -0,0 +1,20 @@
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import FocusOperatorFactoryChain from "../../../../src/content/operators/impls/FocusOperatorFactoryChain";
+import FocusOperator from "../../../../src/content/operators/impls/FocusOperator";
+import MockFocusPresenter from "../../mock/MockFocusPresenter";
+
+describe("FocusOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new FocusOperatorFactoryChain(new MockFocusPresenter());
+ expect(
+ sut.create(
+ { type: operations.FOCUS_INPUT, newTab: false, background: false },
+ 0
+ )
+ ).to.be.instanceOf(FocusOperator);
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/HorizontalScrollOperator.test.ts b/test/content/operators/impls/HorizontalScrollOperator.test.ts
new file mode 100644
index 0000000..f77a34e
--- /dev/null
+++ b/test/content/operators/impls/HorizontalScrollOperator.test.ts
@@ -0,0 +1,28 @@
+import { expect } from "chai";
+import HorizontalScrollOperator from "../../../../src/content/operators/impls/HorizontalScrollOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("HorizontalScrollOperator", () => {
+ describe("#run", () => {
+ it("scroll horizontally", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new HorizontalScrollOperator(presenter, settingRepository, 1);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 1, y: 0 });
+ });
+
+ it("scroll horizontally with repeats", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new HorizontalScrollOperator(presenter, settingRepository, 5);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 5, y: 0 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/MarkOperatorFactoryChain.test.ts b/test/content/operators/impls/MarkOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..1f094dd
--- /dev/null
+++ b/test/content/operators/impls/MarkOperatorFactoryChain.test.ts
@@ -0,0 +1,21 @@
+import * as operations from "../../../../src/shared/operations";
+import { expect } from "chai";
+import MarkOperatorFactoryChain from "../../../../src/content/operators/impls/MarkOperatorFactoryChain";
+import MockMarkKeyRepository from "../../mock/MockMarkKeyRepository";
+import EnableSetMarkOperator from "../../../../src/content/operators/impls/EnableSetMarkOperator";
+import EnableJumpMarkOperator from "../../../../src/content/operators/impls/EnableJumpMarkOperator";
+
+describe("MarkOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new MarkOperatorFactoryChain(new MockMarkKeyRepository());
+ expect(
+ sut.create({ type: operations.MARK_SET_PREFIX }, 0)
+ ).to.be.instanceOf(EnableSetMarkOperator);
+ expect(
+ sut.create({ type: operations.MARK_JUMP_PREFIX }, 0)
+ ).to.be.instanceOf(EnableJumpMarkOperator);
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/PageScrollOperator.test.ts b/test/content/operators/impls/PageScrollOperator.test.ts
new file mode 100644
index 0000000..80c9185
--- /dev/null
+++ b/test/content/operators/impls/PageScrollOperator.test.ts
@@ -0,0 +1,28 @@
+import { expect } from "chai";
+import PageScrollOperator from "../../../../src/content/operators/impls/PageScrollOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("PageScrollOperator", () => {
+ describe("#run", () => {
+ it("scroll by a page", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new PageScrollOperator(presenter, settingRepository, 1);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 1, y: 0 });
+ });
+
+ it("scroll by a page with repeats", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new PageScrollOperator(presenter, settingRepository, 5);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 5, y: 0 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/PasteOperator.test.ts b/test/content/operators/impls/PasteOperator.test.ts
new file mode 100644
index 0000000..8a3a374
--- /dev/null
+++ b/test/content/operators/impls/PasteOperator.test.ts
@@ -0,0 +1,51 @@
+import sinon from "sinon";
+import PasteOperator from "../../../../src/content/operators/impls/PasteOperator";
+import MockClipboardRepository from "../../mock/MockClipboardRepository";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+import MockOperationClient from "../../mock/MockOperationClient";
+
+describe("PasteOperator", () => {
+ describe("#run", () => {
+ it("open a search url", async () => {
+ const clipboardRepository = new MockClipboardRepository("apple");
+ const settingRepository = new MockSettingRepository();
+ const operationClient = new MockOperationClient();
+ const mockOperationClient = sinon
+ .mock(operationClient)
+ .expects("internalOpenUrl")
+ .withArgs("https://google.com/search?q=apple");
+ const sut = new PasteOperator(
+ clipboardRepository,
+ settingRepository,
+ operationClient,
+ false
+ );
+
+ await sut.run();
+
+ mockOperationClient.verify();
+ });
+
+ it("open a url", async () => {
+ const clipboardRepository = new MockClipboardRepository(
+ "https://example.com/"
+ );
+ const settingRepository = new MockSettingRepository();
+ const operationClient = new MockOperationClient();
+ const mockOperationClient = sinon
+ .mock(operationClient)
+ .expects("internalOpenUrl")
+ .withArgs("https://example.com/");
+ const sut = new PasteOperator(
+ clipboardRepository,
+ settingRepository,
+ operationClient,
+ false
+ );
+
+ await sut.run();
+
+ mockOperationClient.verify();
+ });
+ });
+});
diff --git a/test/content/operators/impls/ScrollOperatorFactoryChain.test.ts b/test/content/operators/impls/ScrollOperatorFactoryChain.test.ts
new file mode 100644
index 0000000..08034cb
--- /dev/null
+++ b/test/content/operators/impls/ScrollOperatorFactoryChain.test.ts
@@ -0,0 +1,46 @@
+import { expect } from "chai";
+import ScrollOperatorFactoryChain from "../../../../src/content/operators/impls/ScrollOperatorFactoryChain";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+import HorizontalScrollOperator from "../../../../src/content/operators/impls/HorizontalScrollOperator";
+import VerticalScrollOperator from "../../../../src/content/operators/impls/VerticalScrollOperator";
+import PageScrollOperator from "../../../../src/content/operators/impls/PageScrollOperator";
+import ScrollToTopOperator from "../../../../src/content/operators/impls/ScrollToTopOperator";
+import ScrollToBottomOperator from "../../../../src/content/operators/impls/ScrollToBottomOperator";
+import ScrollToHomeOperator from "../../../../src/content/operators/impls/ScrollToHomeOperator";
+import ScrollToEndOperator from "../../../../src/content/operators/impls/ScrollToEndOperator";
+import * as operations from "../../../../src/shared/operations";
+
+describe("ScrollOperatorFactoryChain", () => {
+ describe("#create", () => {
+ it("returns an operator", () => {
+ const sut = new ScrollOperatorFactoryChain(
+ new MockScrollPresenter(),
+ new MockSettingRepository()
+ );
+ expect(
+ sut.create({ type: operations.SCROLL_HORIZONALLY, count: 10 }, 0)
+ ).to.be.instanceOf(HorizontalScrollOperator);
+ expect(
+ sut.create({ type: operations.SCROLL_VERTICALLY, count: 10 }, 0)
+ ).to.be.instanceOf(VerticalScrollOperator);
+ expect(
+ sut.create({ type: operations.SCROLL_PAGES, count: 10 }, 0)
+ ).to.be.instanceOf(PageScrollOperator);
+ expect(sut.create({ type: operations.SCROLL_TOP }, 0)).to.be.instanceOf(
+ ScrollToTopOperator
+ );
+ expect(
+ sut.create({ type: operations.SCROLL_BOTTOM }, 0)
+ ).to.be.instanceOf(ScrollToBottomOperator);
+ expect(sut.create({ type: operations.SCROLL_HOME }, 0)).to.be.instanceOf(
+ ScrollToHomeOperator
+ );
+ expect(sut.create({ type: operations.SCROLL_END }, 0)).to.be.instanceOf(
+ ScrollToEndOperator
+ );
+ expect(sut.create({ type: operations.PAGE_HOME, newTab: false }, 0)).to.be
+ .null;
+ });
+ });
+});
diff --git a/test/content/operators/impls/ScrollToBottomOperator.test.ts b/test/content/operators/impls/ScrollToBottomOperator.test.ts
new file mode 100644
index 0000000..500c8f2
--- /dev/null
+++ b/test/content/operators/impls/ScrollToBottomOperator.test.ts
@@ -0,0 +1,18 @@
+import { expect } from "chai";
+import ScrollToBottomOperator from "../../../../src/content/operators/impls/ScrollToBottomOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("ScrollToBottomOperator", () => {
+ describe("#run", () => {
+ it("scroll to bottom", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new ScrollToBottomOperator(presenter, settingRepository);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 0, y: Infinity });
+ });
+ });
+});
diff --git a/test/content/operators/impls/ScrollToEndOperator.test.ts b/test/content/operators/impls/ScrollToEndOperator.test.ts
new file mode 100644
index 0000000..0c98c8d
--- /dev/null
+++ b/test/content/operators/impls/ScrollToEndOperator.test.ts
@@ -0,0 +1,18 @@
+import { expect } from "chai";
+import ScrollToEndOperator from "../../../../src/content/operators/impls/ScrollToEndOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("ScrollToEndOperator", () => {
+ describe("#run", () => {
+ it("scroll to rightmost", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new ScrollToEndOperator(presenter, settingRepository);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: Infinity, y: 0 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/ScrollToHomeOperator.test.ts b/test/content/operators/impls/ScrollToHomeOperator.test.ts
new file mode 100644
index 0000000..f8614d2
--- /dev/null
+++ b/test/content/operators/impls/ScrollToHomeOperator.test.ts
@@ -0,0 +1,18 @@
+import { expect } from "chai";
+import ScrollToHomeOperator from "../../../../src/content/operators/impls/ScrollToHomeOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("ScrollToHomeOperator", () => {
+ describe("#run", () => {
+ it("scroll to leftmost", async () => {
+ const presenter = new MockScrollPresenter(10, 10);
+ const settingRepository = new MockSettingRepository();
+ const sut = new ScrollToHomeOperator(presenter, settingRepository);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 0, y: 10 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/ScrollToTopOperator.test.ts b/test/content/operators/impls/ScrollToTopOperator.test.ts
new file mode 100644
index 0000000..25a84ba
--- /dev/null
+++ b/test/content/operators/impls/ScrollToTopOperator.test.ts
@@ -0,0 +1,18 @@
+import { expect } from "chai";
+import ScrollToTopOperator from "../../../../src/content/operators/impls/ScrollToTopOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("ScrollToTopOperator", () => {
+ describe("#run", () => {
+ it("scroll to top", async () => {
+ const presenter = new MockScrollPresenter(10, 10);
+ const settingRepository = new MockSettingRepository();
+ const sut = new ScrollToTopOperator(presenter, settingRepository);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 10, y: 0 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/StartFollowOperator.test.ts b/test/content/operators/impls/StartFollowOperator.test.ts
new file mode 100644
index 0000000..8f9bd2d
--- /dev/null
+++ b/test/content/operators/impls/StartFollowOperator.test.ts
@@ -0,0 +1,20 @@
+import sinon from "sinon";
+import StartFollowOperator from "../../../../src/content/operators/impls/StartFollowOperator";
+import MockFollowMasterClient from "../../mock/MockFollowMasterClient";
+
+describe("StartFollowOperator", () => {
+ describe("#run", () => {
+ it("starts following links", async () => {
+ const client = new MockFollowMasterClient();
+ const mock = sinon
+ .mock(client)
+ .expects("startFollow")
+ .withArgs(true, false);
+ const sut = new StartFollowOperator(client, true, false);
+
+ await sut.run();
+
+ mock.verify();
+ });
+ });
+});
diff --git a/test/content/operators/impls/ToggleAddonOperator.test.ts b/test/content/operators/impls/ToggleAddonOperator.test.ts
new file mode 100644
index 0000000..6026eb1
--- /dev/null
+++ b/test/content/operators/impls/ToggleAddonOperator.test.ts
@@ -0,0 +1,24 @@
+import { expect } from "chai";
+import ToggleAddonOperator from "../../../../src/content/operators/impls/ToggleAddonOperator";
+import MockAddonIndicatorClient from "../../mock/MockAddonIndicatorClient";
+import MockAddonEnabledRepository from "../../mock/MockAddonEnabledRepository";
+
+describe("ToggleAddonOperator", () => {
+ describe("#run", () => {
+ it("toggles addon-enabled state", async () => {
+ const client = new MockAddonIndicatorClient(true);
+ const repository = new MockAddonEnabledRepository(true);
+ const sut = new ToggleAddonOperator(client, repository);
+
+ await sut.run();
+
+ expect(client.enabled).to.be.false;
+ expect(repository.enabled).to.be.false;
+
+ await sut.run();
+
+ expect(client.enabled).to.be.true;
+ expect(repository.enabled).to.be.true;
+ });
+ });
+});
diff --git a/test/content/operators/impls/VerticalScrollOperator.test.ts b/test/content/operators/impls/VerticalScrollOperator.test.ts
new file mode 100644
index 0000000..05b15d2
--- /dev/null
+++ b/test/content/operators/impls/VerticalScrollOperator.test.ts
@@ -0,0 +1,28 @@
+import { expect } from "chai";
+import VerticalScrollOperator from "../../../../src/content/operators/impls/VerticalScrollOperator";
+import MockScrollPresenter from "../../mock/MockScrollPresenter";
+import MockSettingRepository from "../../mock/MockSettingRepository";
+
+describe("VerticalScrollOperator", () => {
+ describe("#run", () => {
+ it("scroll vertically", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new VerticalScrollOperator(presenter, settingRepository, 1);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 0, y: 1 });
+ });
+
+ it("scroll vertically with repeats", async () => {
+ const presenter = new MockScrollPresenter();
+ const settingRepository = new MockSettingRepository();
+ const sut = new VerticalScrollOperator(presenter, settingRepository, 5);
+
+ await sut.run();
+
+ expect(presenter.getScroll()).to.deep.equal({ x: 0, y: 5 });
+ });
+ });
+});
diff --git a/test/content/operators/impls/YankURLOperator.test.ts b/test/content/operators/impls/YankURLOperator.test.ts
new file mode 100644
index 0000000..46e3d06
--- /dev/null
+++ b/test/content/operators/impls/YankURLOperator.test.ts
@@ -0,0 +1,26 @@
+import { expect } from "chai";
+import MockClipboardRepository from "../../mock/MockClipboardRepository";
+import YankURLOperator from "../../../../src/content/operators/impls/YankURLOperator";
+import MockURLRepository from "../../mock/MockURLRepository";
+import MockConsoleClient from "../../mock/MockConsoleClient";
+
+describe("YankOperation", () => {
+ describe("#run", () => {
+ it("copy current URL", async () => {
+ const clipboardRepository = new MockClipboardRepository();
+ const consoleClient = new MockConsoleClient();
+ const urlRepository = new MockURLRepository("https://example.com/");
+ const sut = new YankURLOperator(
+ clipboardRepository,
+ consoleClient,
+ urlRepository
+ );
+
+ await sut.run();
+
+ expect(clipboardRepository.read()).to.equal("https://example.com/");
+ expect(consoleClient.text).to.equal("Yanked https://example.com/");
+ expect(consoleClient.isError).to.be.false;
+ });
+ });
+});
diff --git a/test/content/usecases/ClipboardUseCase.test.ts b/test/content/usecases/ClipboardUseCase.test.ts
deleted file mode 100644
index 5de3e69..0000000
--- a/test/content/usecases/ClipboardUseCase.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import ClipboardRepository from "../../../src/content/repositories/ClipboardRepository";
-import { SettingRepositoryImpl } from "../../../src/content/repositories/SettingRepository";
-import ClipboardUseCase from "../../../src/content/usecases/ClipboardUseCase";
-import OperationClient from "../../../src/content/client/OperationClient";
-import ConsoleClient from "../../../src/content/client/ConsoleClient";
-
-import * as sinon from "sinon";
-import { expect } from "chai";
-import { Operation } from "../../../src/shared/operations";
-
-describe("ClipboardUseCase", () => {
- let clipboardRepository: ClipboardRepository;
-
- let operationClient: OperationClient;
-
- let consoleClient: ConsoleClient;
-
- let sut: ClipboardUseCase;
-
- beforeEach(() => {
- clipboardRepository = new (class implements ClipboardRepository {
- read(): string {
- return "";
- }
- write(_text: string) {}
- })();
- operationClient = new (class implements OperationClient {
- execBackgroundOp(_repeat: number, _op: Operation): Promise<void> {
- return Promise.resolve();
- }
- internalOpenUrl(
- _url: string,
- _newTab?: boolean,
- _background?: boolean
- ): Promise<void> {
- return Promise.resolve();
- }
- })();
- consoleClient = new (class implements ConsoleClient {
- error(_text: string): Promise<void> {
- return Promise.resolve();
- }
- info(_text: string): Promise<void> {
- return Promise.resolve();
- }
- })();
-
- sut = new ClipboardUseCase(
- clipboardRepository,
- new SettingRepositoryImpl(),
- consoleClient,
- operationClient
- );
- });
-
- describe("#yankCurrentURL", () => {
- it("yanks current url", async () => {
- const href = window.location.href;
- const mockRepository = sinon.mock(clipboardRepository);
- mockRepository.expects("write").withArgs(href);
- const mockConsoleClient = sinon.mock(consoleClient);
- mockConsoleClient.expects("info").withArgs("Yanked " + href);
-
- const yanked = await sut.yankCurrentURL();
-
- expect(yanked).to.equal(href);
- mockRepository.verify();
- mockConsoleClient.verify();
- });
- });
-
- describe("#openOrSearch", () => {
- it("opens url from the clipboard", async () => {
- const url = "https://github.com/ueokande/vim-vixen";
- sinon.stub(clipboardRepository, "read").returns(url);
- const mockOperationClient = sinon.mock(operationClient);
- mockOperationClient.expects("internalOpenUrl").withArgs(url, true);
-
- await sut.openOrSearch(true);
-
- mockOperationClient.verify();
- });
-
- it("opens search results from the clipboard", async () => {
- const url = "https://google.com/search?q=banana";
- sinon.stub(clipboardRepository, "read").returns("banana");
- const mockOperationClient = sinon.mock(operationClient);
- mockOperationClient.expects("internalOpenUrl").withArgs(url, true);
-
- await sut.openOrSearch(true);
-
- mockOperationClient.verify();
- });
- });
-});
diff --git a/test/content/usecases/HintKeyProducer.test.ts b/test/content/usecases/HintKeyProducer.test.ts
index f7e02ea..9d320b4 100644
--- a/test/content/usecases/HintKeyProducer.test.ts
+++ b/test/content/usecases/HintKeyProducer.test.ts
@@ -1,13 +1,7 @@
-import HintKeyProducer from "../../../src/content/usecases/HintKeyProducer";
+import { HintKeyRepositoryImpl } from "../../../src/content/repositories/HintKeyRepository";
import { expect } from "chai";
-describe("HintKeyProducer class", () => {
- describe("#constructor", () => {
- it("throws an exception on empty charset", () => {
- expect(() => new HintKeyProducer("")).to.throw(TypeError);
- });
- });
-
+describe("HintKeyProducerImpl class", () => {
describe("#produce", () => {
it("produce incremented keys", () => {
const charset = "abc";
@@ -30,10 +24,29 @@ describe("HintKeyProducer class", () => {
"aba",
];
- const producer = new HintKeyProducer(charset);
+ const sut = new HintKeyRepositoryImpl();
+ sut.reset(charset);
for (let i = 0; i < sequences.length; ++i) {
- expect(producer.produce()).to.equal(sequences[i]);
+ expect(sut.produce()).to.equal(sequences[i]);
}
});
});
+
+ describe("#reset", () => {
+ it("resets charset", () => {
+ const sut = new HintKeyRepositoryImpl();
+
+ sut.reset("ab");
+ expect(sut.produce()).to.equal("a");
+ expect(sut.produce()).to.equal("b");
+
+ sut.reset("xy");
+ expect(sut.produce()).to.equal("x");
+ expect(sut.produce()).to.equal("y");
+ });
+ it("throws an exception on empty charset", () => {
+ const sut = new HintKeyRepositoryImpl();
+ expect(() => sut.reset("")).to.throw(TypeError);
+ });
+ });
});