diff options
47 files changed, 1199 insertions, 553 deletions
diff --git a/e2e/find.test.ts b/e2e/find.test.ts index accf37b..dd5069c 100644 --- a/e2e/find.test.ts +++ b/e2e/find.test.ts @@ -41,23 +41,31 @@ describe("find test", () => { await console.execCommand("hello"); await page.switchToTop(); - let selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 2, to: 7 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 2, to: 7 }); + }); // search next keyword await page.sendKeys("n"); - selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 9, to: 14 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 9, to: 14 }); + }); // search previous keyword await page.sendKeys(Key.SHIFT, "N"); - selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 2, to: 7 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 2, to: 7 }); + }); // search previous keyword by wrap-search await page.sendKeys(Key.SHIFT, "N"); - selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 16, to: 21 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 16, to: 21 }); + }); }); it("shows error if pattern not found", async () => { @@ -66,8 +74,10 @@ describe("find test", () => { await console.execCommand("world"); await page.switchToTop(); - const selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 0, to: 0 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 0, to: 0 }); + }); await eventually(async () => { console = await page.getConsole(); @@ -83,16 +93,20 @@ describe("find test", () => { await page.switchToTop(); await page.clearSelection(); - let selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 0, to: 0 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 0, to: 0 }); + }); await page.sendKeys("/"); console = await page.getConsole(); await console.execCommand(""); await page.switchToTop(); - selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 2, to: 7 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 2, to: 7 }); + }); }); it("search with last keyword on new page", async () => { @@ -102,12 +116,16 @@ describe("find test", () => { await page.switchToTop(); await page.sendKeys("n"); - let selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 9, to: 14 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 9, to: 14 }); + }); page = await Page.navigateTo(webdriver, server.url()); await page.sendKeys("n"); - selection = await page.getSelection(); - assert.deepStrictEqual(selection, { from: 2, to: 7 }); + await eventually(async () => { + const selection = await page.getSelection(); + assert.deepStrictEqual(selection, { from: 2, to: 7 }); + }); }); }); diff --git a/manifest.json b/manifest.json index ab432b3..5f73744 100644 --- a/manifest.json +++ b/manifest.json @@ -34,7 +34,8 @@ "clipboardRead", "notifications", "bookmarks", - "browserSettings" + "browserSettings", + "webNavigation" ], "web_accessible_resources": [ "build/console.html", diff --git a/src/background/Application.ts b/src/background/Application.ts index 69fe4a4..2006965 100644 --- a/src/background/Application.ts +++ b/src/background/Application.ts @@ -3,6 +3,7 @@ import ContentMessageListener from "./infrastructures/ContentMessageListener"; import SettingController from "./controllers/SettingController"; import VersionController from "./controllers/VersionController"; import SettingRepository from "./repositories/SettingRepository"; +import FindRepositoryImpl from "./repositories/FindRepository"; @injectable() export default class Application { @@ -11,12 +12,19 @@ export default class Application { private settingController: SettingController, private versionController: VersionController, @inject("SyncSettingRepository") - private syncSettingRepository: SettingRepository + private syncSettingRepository: SettingRepository, + @inject("FindRepository") + private readonly findRepository: FindRepositoryImpl ) {} run() { this.settingController.reload(); + browser.tabs.onUpdated.addListener((tabId: number, info) => { + if (info.status == "loading") { + this.findRepository.deleteLocalState(tabId); + } + }); browser.runtime.onInstalled.addListener((details) => { if (details.reason !== "install" && details.reason !== "update") { return; diff --git a/src/background/clients/FindClient.ts b/src/background/clients/FindClient.ts new file mode 100644 index 0000000..b46b964 --- /dev/null +++ b/src/background/clients/FindClient.ts @@ -0,0 +1,45 @@ +import * as messages from "../../shared/messages"; + +export default interface FindClient { + findNext(tabId: number, frameId: number, keyword: string): Promise<boolean>; + + findPrev(tabId: number, frameId: number, keyword: string): Promise<boolean>; + + clearSelection(tabId: number, frameId: number): Promise<void>; +} + +export class FindClientImpl implements FindClient { + async findNext( + tabId: number, + frameId: number, + keyword: string + ): Promise<boolean> { + const found = (await browser.tabs.sendMessage( + tabId, + { type: messages.FIND_NEXT, keyword }, + { frameId } + )) as boolean; + return found; + } + + async findPrev( + tabId: number, + frameId: number, + keyword: string + ): Promise<boolean> { + const found = (await browser.tabs.sendMessage( + tabId, + { type: messages.FIND_PREV, keyword }, + { frameId } + )) as boolean; + return found; + } + + clearSelection(tabId: number, frameId: number): Promise<void> { + return browser.tabs.sendMessage( + tabId, + { type: messages.FIND_CLEAR_SELECTION }, + { frameId } + ); + } +} diff --git a/src/background/controllers/FindController.ts b/src/background/controllers/FindController.ts index f3ea93f..0772866 100644 --- a/src/background/controllers/FindController.ts +++ b/src/background/controllers/FindController.ts @@ -1,15 +1,11 @@ import { injectable } from "tsyringe"; -import FindUseCase from "../usecases/FindUseCase"; +import StartFindUseCase from "../usecases/StartFindUseCase"; @injectable() export default class FindController { - constructor(private findUseCase: FindUseCase) {} + constructor(private startFindUseCase: StartFindUseCase) {} - getKeyword(): Promise<string> { - return this.findUseCase.getKeyword(); - } - - setKeyword(keyword: string): Promise<void> { - return this.findUseCase.setKeyword(keyword); + startFind(tabId: number, keyword?: string): Promise<void> { + return this.startFindUseCase.startFind(tabId, keyword); } } diff --git a/src/background/di.ts b/src/background/di.ts index a571c7d..e97c4a8 100644 --- a/src/background/di.ts +++ b/src/background/di.ts @@ -18,7 +18,10 @@ import { BrowserSettingRepositoryImpl } from "./repositories/BrowserSettingRepos import { RepeatRepositoryImpl } from "./repositories/RepeatRepository"; import { ZoomPresenterImpl } from "./presenters/ZoomPresenter"; import { WindowPresenterImpl } from "./presenters/WindowPresenter"; +import { FramePresenterImpl } from "./presenters/FramePresenter"; +import { FindClientImpl } from "./clients/FindClient"; import { ConsoleFrameClientImpl } from "./clients/ConsoleFrameClient"; +import { FindRepositoryImpl } from "./repositories/FindRepository"; container.register("LocalSettingRepository", { useClass: LocalSettingRepository, @@ -40,6 +43,9 @@ container.register("TabRepository", { useClass: TabRepositoryImpl }); container.register("ZoomPresenter", { useClass: ZoomPresenterImpl }); container.register("TabPresenter", { useClass: TabPresenterImpl }); container.register("WindowPresenter", { useClass: WindowPresenterImpl }); +container.register("FramePresenter", { useClass: FramePresenterImpl }); +container.register("FindRepository", { useClass: FindRepositoryImpl }); +container.register("FindClient", { useClass: FindClientImpl }); container.register("NavigateClient", { useClass: NavigateClientImpl }); container.register("ConsoleClient", { useClass: ConsoleClientImpl }); container.register("ConsoleFrameClient", { useClass: ConsoleFrameClientImpl }); diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 6023047..ce7ff09 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -3,13 +3,13 @@ import * as messages from "../../shared/messages"; import * as operations from "../../shared/operations"; import CommandController from "../controllers/CommandController"; import SettingController from "../controllers/SettingController"; -import FindController from "../controllers/FindController"; import AddonEnabledController from "../controllers/AddonEnabledController"; import LinkController from "../controllers/LinkController"; import OperationController from "../controllers/OperationController"; import MarkController from "../controllers/MarkController"; import CompletionController from "../controllers/CompletionController"; import ConsoleController from "../controllers/ConsoleController"; +import FindController from "../controllers/FindController"; @injectable() export default class ContentMessageListener { @@ -19,12 +19,12 @@ export default class ContentMessageListener { private readonly settingController: SettingController, private readonly commandController: CommandController, private readonly completionController: CompletionController, - private readonly findController: FindController, private readonly addonEnabledController: AddonEnabledController, private readonly linkController: LinkController, private readonly operationController: OperationController, private readonly markController: MarkController, - private readonly consoleController: ConsoleController + private readonly consoleController: ConsoleController, + private readonly findController: FindController ) {} run(): void { @@ -36,6 +36,7 @@ export default class ContentMessageListener { return {}; } return ret.catch((e) => { + console.error(e); if (!sender.tab || !sender.tab.id) { return; } @@ -45,6 +46,7 @@ export default class ContentMessageListener { }); }); } catch (e) { + console.error(e); if (!sender.tab || !sender.tab.id) { return; } @@ -80,6 +82,8 @@ export default class ContentMessageListener { return this.completionController.getProperties(); case messages.CONSOLE_ENTER_COMMAND: return this.onConsoleEnterCommand(message.text); + case messages.CONSOLE_ENTER_FIND: + return this.findController.startFind(senderTab.id!, message.keyword); case messages.CONSOLE_RESIZE: return this.onConsoleResize( senderTab.id!, @@ -88,10 +92,6 @@ export default class ContentMessageListener { ); case messages.SETTINGS_QUERY: return this.onSettingsQuery(); - case messages.FIND_GET_KEYWORD: - return this.onFindGetKeyword(); - case messages.FIND_SET_KEYWORD: - return this.onFindSetKeyword(message.keyword); case messages.ADDON_ENABLED_RESPONSE: return this.onAddonEnabledResponse(message.enabled); case messages.OPEN_URL: @@ -132,14 +132,6 @@ export default class ContentMessageListener { return (await this.settingController.getSetting()).toJSON(); } - onFindGetKeyword(): Promise<string> { - return this.findController.getKeyword(); - } - - onFindSetKeyword(keyword: string): Promise<void> { - return this.findController.setKeyword(keyword); - } - onAddonEnabledResponse(enabled: boolean): Promise<void> { return this.addonEnabledController.indicate(enabled); } diff --git a/src/background/operators/impls/FindNextOperator.ts b/src/background/operators/impls/FindNextOperator.ts new file mode 100644 index 0000000..241f71d --- /dev/null +++ b/src/background/operators/impls/FindNextOperator.ts @@ -0,0 +1,94 @@ +import Operator from "../Operator"; +import TabPresenter from "../../presenters/TabPresenter"; +import FindRepository from "../../repositories/FindRepository"; +import FindClient from "../../clients/FindClient"; +import ConsoleClient from "../../infrastructures/ConsoleClient"; +import FramePresenter from "../../presenters/FramePresenter"; + +export default class FindNextOperator implements Operator { + constructor( + private readonly tabPresenter: TabPresenter, + private readonly findRepository: FindRepository, + private readonly findClient: FindClient, + private readonly consoleClient: ConsoleClient, + private readonly framePresenter: FramePresenter + ) {} + + async run(): Promise<void> { + const tab = await this.tabPresenter.getCurrent(); + const tabId = tab?.id; + if (tabId == null) { + return; + } + + const state = await this.findRepository.getLocalState(tabId); + if (state) { + // Start to find the keyword from the current frame which last found on, + // and concat it to end of frame ids to perform a wrap-search + // + // ,- keyword should be in this frame + // | + // [100, 101, 0, 100] + // | + // `- continue from frame id 100 + // + const targetFrameIds = state.frameIds + .slice(state.framePos) + .concat( + state.frameIds.slice(0, state.framePos), + state.frameIds[state.framePos] + ); + + for (let i = 0; i < targetFrameIds.length; ++i) { + const found = await this.findClient.findNext( + tabId, + targetFrameIds[i], + state.keyword + ); + if (found) { + this.findRepository.setLocalState(tabId, { + ...state, + framePos: (i + state.framePos) % state.frameIds.length, // save current frame position or first + }); + return; + } + this.findClient.clearSelection(tabId, targetFrameIds[i]); + } + + // The keyword is gone. + this.consoleClient.showError( + tabId, + "Pattern not found: " + state.keyword + ); + return; + } + + const keyword = await this.findRepository.getGlobalKeyword(); + if (keyword) { + const frameIds = await this.framePresenter.getAllFrameIds(tabId); + for (const frameId of frameIds) { + await this.findClient.clearSelection(tabId, frameId); + } + + for (let framePos = 0; framePos < frameIds.length; ++framePos) { + const found = await this.findClient.findNext( + tabId, + frameIds[framePos], + keyword + ); + if (found) { + await this.findRepository.setLocalState(tabId, { + frameIds, + framePos, + keyword, + }); + await this.consoleClient.showInfo(tabId, "Pattern found: " + keyword); + return; + } + } + this.consoleClient.showError(tabId, "Pattern not found: " + keyword); + return; + } + await this.consoleClient.showError(tabId, "No previous search keywords"); + } +} diff --git a/src/background/operators/impls/FindOperatorFactoryChain.ts b/src/background/operators/impls/FindOperatorFactoryChain.ts new file mode 100644 index 0000000..b71f032 --- /dev/null +++ b/src/background/operators/impls/FindOperatorFactoryChain.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from "tsyringe"; +import Operator from "../Operator"; +import OperatorFactoryChain from "../OperatorFactoryChain"; +import TabPresenter from "../../presenters/TabPresenter"; +import * as operations from "../../../shared/operations"; +import FindNextOperator from "./FindNextOperator"; +import FindPrevOperator from "./FindPrevOperator"; +import FindRepository from "../../repositories/FindRepository"; +import FindClient from "../../clients/FindClient"; +import FramePresenter from "../../presenters/FramePresenter"; +import ConsoleClient from "../../infrastructures/ConsoleClient"; + +@injectable() +export default class FindOperatorFactoryChain implements OperatorFactoryChain { + constructor( + @inject("TabPresenter") + private readonly tabPresenter: TabPresenter, + @inject("FindRepository") + private readonly findRepository: FindRepository, + @inject("FindClient") + private readonly findClient: FindClient, + @inject("ConsoleClient") + private readonly consoleClient: ConsoleClient, + @inject("FramePresenter") + private readonly framePresenter: FramePresenter + ) {} + + create(op: operations.Operation): Operator | null { + switch (op.type) { + case operations.FIND_NEXT: + return new FindNextOperator( + this.tabPresenter, + this.findRepository, + this.findClient, + this.consoleClient, + this.framePresenter + ); + case operations.FIND_PREV: + return new FindPrevOperator( + this.tabPresenter, + this.findRepository, + this.findClient, + this.consoleClient, + this.framePresenter + ); + } + return null; + } +} diff --git a/src/background/operators/impls/FindPrevOperator.ts b/src/background/operators/impls/FindPrevOperator.ts new file mode 100644 index 0000000..822c386 --- /dev/null +++ b/src/background/operators/impls/FindPrevOperator.ts @@ -0,0 +1,94 @@ +import Operator from "../Operator"; +import TabPresenter from "../../presenters/TabPresenter"; +import FindRepository from "../../repositories/FindRepository"; +import FindClient from "../../clients/FindClient"; +import ConsoleClient from "../../infrastructures/ConsoleClient"; +import FramePresenter from "../../presenters/FramePresenter"; + +export default class FindPrevOperator implements Operator { + constructor( + private readonly tabPresenter: TabPresenter, + private readonly findRepository: FindRepository, + private readonly findClient: FindClient, + private readonly consoleClient: ConsoleClient, + private readonly framePresenter: FramePresenter + ) {} + + async run(): Promise<void> { + const tab = await this.tabPresenter.getCurrent(); + const tabId = tab?.id; + if (tabId == null) { + return; + } + + const state = await this.findRepository.getLocalState(tabId); + if (state) { + // Start to find the keyword from the current frame which last found on, + // and concat it to end of frame ids to perform a wrap-search + // + // ,- keyword should be in this frame + // | + // [100, 101, 0, 100] + // | + // `- continue from frame id 100 + // + const targetFrameIds = state.frameIds + .slice(state.framePos) + .concat( + state.frameIds.slice(0, state.framePos), + state.frameIds[state.framePos] + ); + + for (let i = targetFrameIds.length - 1; i >= 0; --i) { + const found = await this.findClient.findPrev( + tabId, + targetFrameIds[i], + state.keyword + ); + if (found) { + this.findRepository.setLocalState(tabId, { + ...state, + framePos: (i + state.framePos) % state.frameIds.length, // save current frame position or first + }); + return; + } + this.findClient.clearSelection(tabId, targetFrameIds[i]); + } + + // The keyword is gone. + this.consoleClient.showError( + tabId, + "Pattern not found: " + state.keyword + ); + return; + } + + const keyword = await this.findRepository.getGlobalKeyword(); + if (keyword) { + const frameIds = await this.framePresenter.getAllFrameIds(tabId); + for (const frameId of frameIds) { + await this.findClient.clearSelection(tabId, frameId); + } + + for (let framePos = frameIds.length - 1; framePos >= 0; --framePos) { + const found = await this.findClient.findPrev( + tabId, + frameIds[framePos], + keyword + ); + if (found) { + await this.findRepository.setLocalState(tabId, { + frameIds, + framePos, + keyword, + }); + await this.consoleClient.showInfo(tabId, "Pattern found: " + keyword); + return; + } + } + this.consoleClient.showError(tabId, "Pattern not found: " + keyword); + return; + } + await this.consoleClient.showError(tabId, "No previous search keywords"); + } +} diff --git a/src/background/operators/impls/OperatorFactoryImpl.ts b/src/background/operators/impls/OperatorFactoryImpl.ts index 34e7bb5..ce87491 100644 --- a/src/background/operators/impls/OperatorFactoryImpl.ts +++ b/src/background/operators/impls/OperatorFactoryImpl.ts @@ -8,6 +8,7 @@ import NavigateOperatorFactoryChain from "./NavigateOperatorFactoryChain"; import RepeatOperatorFactoryChain from "./RepeatOperatorFactoryChain"; import TabOperatorFactoryChain from "./TabOperatorFactoryChain"; import ZoomOperatorFactoryChain from "./ZoomOperatorFactoryChain"; +import FindOperatorFactoryChain from "./FindOperatorFactoryChain"; import * as operations from "../../../shared/operations"; @injectable() @@ -20,6 +21,7 @@ export class OperatorFactoryImpl implements OperatorFactory { navigateOperatorFactoryChain: NavigateOperatorFactoryChain, tabOperatorFactoryChain: TabOperatorFactoryChain, zoomOperatorFactoryChain: ZoomOperatorFactoryChain, + findOperatorFactoryChain: FindOperatorFactoryChain, @inject(delay(() => RepeatOperatorFactoryChain)) repeatOperatorFactoryChain: RepeatOperatorFactoryChain ) { @@ -30,6 +32,7 @@ export class OperatorFactoryImpl implements OperatorFactory { repeatOperatorFactoryChain, tabOperatorFactoryChain, zoomOperatorFactoryChain, + findOperatorFactoryChain, ]; } diff --git a/src/background/presenters/FramePresenter.ts b/src/background/presenters/FramePresenter.ts new file mode 100644 index 0000000..c94f8dd --- /dev/null +++ b/src/background/presenters/FramePresenter.ts @@ -0,0 +1,12 @@ +export default interface FramePresenter { + getAllFrameIds(tabId: number): Promise<Array<number>>; +} + +export class FramePresenterImpl implements FramePresenter { + async getAllFrameIds(tabId: number): Promise<Array<number>> { + const frames = await browser.webNavigation.getAllFrames({ tabId: tabId }); + return frames + .filter((f) => !f.url.startsWith("moz-extension://")) + .map((f) => f.frameId); + } +} diff --git a/src/background/repositories/FindRepository.ts b/src/background/repositories/FindRepository.ts index 813e065..46ee390 100644 --- a/src/background/repositories/FindRepository.ts +++ b/src/background/repositories/FindRepository.ts @@ -1,22 +1,69 @@ import { injectable } from "tsyringe"; import MemoryStorage from "../infrastructures/MemoryStorage"; -const FIND_KEYWORD_KEY = "find-keyword"; +const FIND_GLOBAL_KEYWORD_KEY = "find-global-keyword"; +const FIND_LOCAL_KEYWORD_KEY = "find-local-keyword"; + +export type FindState = { + keyword: string; + framePos: number; + frameIds: number[]; +}; + +export default interface FindRepository { + getGlobalKeyword(): Promise<string | undefined>; + + setGlobalKeyword(keyword: string): Promise<void>; + + getLocalState(tabId: number): Promise<undefined | FindState>; + + setLocalState(tabId: number, state: FindState): Promise<void>; + + deleteLocalState(tabId: number): Promise<void>; +} @injectable() -export default class FindRepository { +export class FindRepositoryImpl implements FindRepository { private cache: MemoryStorage; constructor() { this.cache = new MemoryStorage(); } - getKeyword(): Promise<string> { - return Promise.resolve(this.cache.get(FIND_KEYWORD_KEY)); + getGlobalKeyword(): Promise<string | undefined> { + return Promise.resolve(this.cache.get(FIND_GLOBAL_KEYWORD_KEY)); + } + + setGlobalKeyword(keyword: string): Promise<void> { + this.cache.set(FIND_GLOBAL_KEYWORD_KEY, keyword); + return Promise.resolve(); + } + + getLocalState(tabId: number): Promise<FindState | undefined> { + let states = this.cache.get(FIND_LOCAL_KEYWORD_KEY); + if (typeof states === "undefined") { + states = {}; + } + return Promise.resolve(states[tabId]); + } + + setLocalState(tabId: number, state: FindState): Promise<void> { + let states = this.cache.get(FIND_LOCAL_KEYWORD_KEY); + if (typeof states === "undefined") { + states = {}; + } + states[tabId] = state; + this.cache.set(FIND_LOCAL_KEYWORD_KEY, states); + return Promise.resolve(); } - setKeyword(keyword: string): Promise<void> { - this.cache.set(FIND_KEYWORD_KEY, keyword); + deleteLocalState(tabId: number): Promise<void> { + const states = this.cache.get(FIND_LOCAL_KEYWORD_KEY); + if (typeof states === "undefined") { + return Promise.resolve(); + } + delete states[tabId]; + this.cache.set(FIND_LOCAL_KEYWORD_KEY, states); return Promise.resolve(); } } diff --git a/src/background/usecases/FindUseCase.ts b/src/background/usecases/FindUseCase.ts deleted file mode 100644 index cc111f2..0000000 --- a/src/background/usecases/FindUseCase.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { injectable } from "tsyringe"; -import FindRepository from "../repositories/FindRepository"; - -@injectable() -export default class FindUseCase { - constructor(private readonly findRepository: FindRepository) {} - - getKeyword(): Promise<string> { - return this.findRepository.getKeyword(); - } - - setKeyword(keyword: string): Promise<void> { - return this.findRepository.setKeyword(keyword); - } -} diff --git a/src/background/usecases/StartFindUseCase.ts b/src/background/usecases/StartFindUseCase.ts new file mode 100644 index 0000000..066d930 --- /dev/null +++ b/src/background/usecases/StartFindUseCase.ts @@ -0,0 +1,57 @@ +import { inject, injectable } from "tsyringe"; +import ConsoleClient from "../infrastructures/ConsoleClient"; +import FindRepositoryImpl from "../repositories/FindRepository"; +import FindClient from "../clients/FindClient"; +import FramePresenter from "../presenters/FramePresenter"; + +@injectable() +export default class StartFindUseCase { + constructor( + @inject("FindClient") + private readonly findClient: FindClient, + @inject("FindRepository") + private readonly findRepository: FindRepositoryImpl, + @inject("ConsoleClient") + private readonly consoleClient: ConsoleClient, + @inject("FramePresenter") + private readonly framePresenter: FramePresenter + ) {} + + async startFind(tabId: number, keyword?: string): Promise<void> { + if (typeof keyword === "undefined") { + keyword = (await this.findRepository.getLocalState(tabId))?.keyword; + } + if (typeof keyword === "undefined") { + keyword = await this.findRepository.getGlobalKeyword(); + } + if (typeof keyword === "undefined") { + await this.consoleClient.showError(tabId, "No previous search keywords"); + return; + } + + this.findRepository.setGlobalKeyword(keyword); + + const frameIds = await this.framePresenter.getAllFrameIds(tabId); + for (const frameId of frameIds) { + await this.findClient.clearSelection(tabId, frameId); + } + + for (let framePos = 0; framePos < frameIds.length; ++framePos) { + const found = await this.findClient.findNext( + tabId, + frameIds[framePos], + keyword + ); + if (found) { + await this.findRepository.setLocalState(tabId, { + frameIds, + framePos, + keyword, + }); + await this.consoleClient.showInfo(tabId, "Pattern found: " + keyword); + return; + } + } + this.consoleClient.showError(tabId, "Pattern not found: " + keyword); + } +} diff --git a/src/console/app/hooks.ts b/src/console/app/hooks.ts index eefdea3..00ac05c 100644 --- a/src/console/app/hooks.ts +++ b/src/console/app/hooks.ts @@ -103,13 +103,10 @@ export const useExecCommand = () => { export const useExecFind = () => { const execFind = React.useCallback((text?: string) => { - window.top.postMessage( - JSON.stringify({ - type: messages.CONSOLE_ENTER_FIND, - text, - }), - "*" - ); + browser.runtime.sendMessage({ + type: messages.CONSOLE_ENTER_FIND, + keyword: text, + }); }, []); return execFind; }; diff --git a/src/content/Application.ts b/src/content/Application.ts index b09edfa..a12c3c6 100644 --- a/src/content/Application.ts +++ b/src/content/Application.ts @@ -1,6 +1,5 @@ import { injectable } from "tsyringe"; import MessageListener from "./MessageListener"; -import FindController from "./controllers/FindController"; import MarkController from "./controllers/MarkController"; import FollowMasterController from "./controllers/FollowMasterController"; import FollowSlaveController from "./controllers/FollowSlaveController"; @@ -14,6 +13,7 @@ import SettingController from "./controllers/SettingController"; import ConsoleFrameController from "./controllers/ConsoleFrameController"; import NavigateController from "./controllers/NavigateController"; import * as messages from "../shared/messages"; +import FindController from "./controllers/FindController"; type Message = messages.Message; @@ -22,7 +22,6 @@ export default class Application { // eslint-disable-next-line max-params constructor( private messageListener: MessageListener, - private findController: FindController, private markController: MarkController, private followMasterController: FollowMasterController, private followSlaveController: FollowSlaveController, @@ -33,7 +32,8 @@ export default class Application { private addonEnabledController: AddonEnabledController, private settingController: SettingController, private consoleFrameController: ConsoleFrameController, - private navigateController: NavigateController + private navigateController: NavigateController, + private findController: FindController ) {} init(): Promise<void> { @@ -47,12 +47,6 @@ export default class Application { private routeMasterComponents() { this.messageListener.onWebMessage((msg: Message, sender: Window) => { switch (msg.type) { - case messages.CONSOLE_ENTER_FIND: - return this.findController.start(msg); - case messages.FIND_NEXT: - return this.findController.next(msg); - case messages.FIND_PREV: - return this.findController.prev(msg); case messages.CONSOLE_UNFOCUS: return this.consoleFrameController.unfocus(msg); case messages.FOLLOW_START: @@ -64,16 +58,6 @@ export default class Application { } return undefined; }); - - this.messageListener.onBackgroundMessage((msg: Message) => { - switch (msg.type) { - case messages.ADDON_ENABLED_QUERY: - return this.addonEnabledController.getAddonEnabled(msg); - case messages.TAB_SCROLL_TO: - return this.markController.scrollTo(msg); - } - return undefined; - }); } private routeCommonComponents(): Promise<void> { @@ -109,6 +93,21 @@ export default class Application { return this.navigateController.openLinkPrev(msg); case messages.CONSOLE_RESIZE: return this.consoleFrameController.resize(msg); + case messages.FIND_NEXT: + return this.findController.findNext(msg.keyword); + case messages.FIND_PREV: + return this.findController.findPrev(msg.keyword); + case messages.FIND_CLEAR_SELECTION: + return this.findController.clearSelection(); + } + + if (window.self === window.top) { + switch (msg.type) { + case messages.ADDON_ENABLED_QUERY: + return this.addonEnabledController.getAddonEnabled(msg); + case messages.TAB_SCROLL_TO: + return this.markController.scrollTo(msg); + } } }); diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts index 3fe1dcd..31cc1ae 100644 --- a/src/content/MessageListener.ts +++ b/src/content/MessageListener.ts @@ -27,7 +27,12 @@ export default class MessageListener { ) { browser.runtime.onMessage.addListener( (msg: any, sender: WebExtMessageSender) => { - return listener(valueOf(msg), sender); + try { + return Promise.resolve(listener(valueOf(msg), sender)); + } catch (e) { + console.warn(e); + return; + } } ); } diff --git a/src/content/client/FindClient.ts b/src/content/client/FindClient.ts deleted file mode 100644 index 7da5069..0000000 --- a/src/content/client/FindClient.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as messages from "../../shared/messages"; - -export default interface FindClient { - getGlobalLastKeyword(): Promise<string | null>; - - setGlobalLastKeyword(keyword: string): Promise<void>; -} - -export class FindClientImpl implements FindClient { - async getGlobalLastKeyword(): Promise<string | null> { - const keyword = await browser.runtime.sendMessage({ - type: messages.FIND_GET_KEYWORD, - }); - return keyword as string; - } - - async setGlobalLastKeyword(keyword: string): Promise<void> { - await browser.runtime.sendMessage({ - type: messages.FIND_SET_KEYWORD, - keyword: keyword, - }); - } -} diff --git a/src/content/client/FindMasterClient.ts b/src/content/client/FindMasterClient.ts deleted file mode 100644 index 9c3f812..0000000 --- a/src/content/client/FindMasterClient.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as messages from "../../shared/messages"; - -export default interface FindMasterClient { - findNext(): void; - - findPrev(): void; -} - -export class FindMasterClientImpl implements FindMasterClient { - findNext(): void { - window.top.postMessage( - JSON.stringify({ - type: messages.FIND_NEXT, - }), - "*" - ); - } - - findPrev(): void { - window.top.postMessage( - JSON.stringify({ - type: messages.FIND_PREV, - }), - "*" - ); - } -} diff --git a/src/content/controllers/FindController.ts b/src/content/controllers/FindController.ts index 3087d5d..adcdb0d 100644 --- a/src/content/controllers/FindController.ts +++ b/src/content/controllers/FindController.ts @@ -1,20 +1,19 @@ import { injectable } from "tsyringe"; -import * as messages from "../../shared/messages"; import FindUseCase from "../usecases/FindUseCase"; @injectable() export default class FindController { constructor(private findUseCase: FindUseCase) {} - async start(m: messages.ConsoleEnterFindMessage): Promise<void> { - await this.findUseCase.startFind(m.text); + findNext(keyword: string): boolean { + return this.findUseCase.findNext(keyword); } - async next(_: messages.FindNextMessage): Promise<void> { - await this.findUseCase.findNext(); + findPrev(keyword: string): boolean { + return this.findUseCase.findPrev(keyword); } - async prev(_: messages.FindPrevMessage): Promise<void> { - await this.findUseCase.findPrev(); + clearSelection() { + return this.findUseCase.clearSelection(); } } diff --git a/src/content/di.ts b/src/content/di.ts index e74d7ac..4c85e76 100644 --- a/src/content/di.ts +++ b/src/content/di.ts @@ -6,10 +6,6 @@ import { AddressRepositoryImpl } from "./repositories/AddressRepository"; import { ClipboardRepositoryImpl } from "./repositories/ClipboardRepository"; import { ConsoleClientImpl } from "./client/ConsoleClient"; import { ConsoleFramePresenterImpl } from "./presenters/ConsoleFramePresenter"; -import { FindClientImpl } from "./client/FindClient"; -import { FindMasterClientImpl } from "./client/FindMasterClient"; -import { FindPresenterImpl } from "./presenters/FindPresenter"; -import { FindRepositoryImpl } from "./repositories/FindRepository"; import { FocusPresenterImpl } from "./presenters/FocusPresenter"; import { FollowKeyRepositoryImpl } from "./repositories/FollowKeyRepository"; import { FollowMasterClientImpl } from "./client/FollowMasterClient"; @@ -31,6 +27,7 @@ import { TabsClientImpl } from "./client/TabsClient"; import { container } from "tsyringe"; import OperatorFactoryImpl from "./operators/impls/OperatorFactoryImpl"; import { URLRepositoryImpl } from "./operators/impls/URLRepository"; +import { FindPresenterImpl } from "./presenters/FindPresenter"; container.register("FollowMasterClient", { useValue: new FollowMasterClientImpl(window.top), @@ -49,10 +46,6 @@ container.register("ConsoleClient", { useClass: ConsoleClientImpl }); container.register("ConsoleFramePresenter", { useClass: ConsoleFramePresenterImpl, }); -container.register("FindClient", { useClass: FindClientImpl }); -container.register("FindMasterClient", { useClass: FindMasterClientImpl }); -container.register("FindPresenter", { useClass: FindPresenterImpl }); -container.register("FindRepository", { useClass: FindRepositoryImpl }); container.register("FocusPresenter", { useClass: FocusPresenterImpl }); container.register("FollowKeyRepository", { useClass: FollowKeyRepositoryImpl, @@ -79,6 +72,7 @@ container.register("NavigationPresenter", { }); container.register("OperationClient", { useClass: OperationClientImpl }); container.register("ScrollPresenter", { useClass: ScrollPresenterImpl }); +container.register("FindPresenter", { useClass: FindPresenterImpl }); container.register("SettingClient", { useClass: SettingClientImpl }); container.register("SettingRepository", { useClass: SettingRepositoryImpl }); container.register("URLRepository", { useClass: URLRepositoryImpl }); diff --git a/src/content/operators/impls/FindNextOperator.ts b/src/content/operators/impls/FindNextOperator.ts deleted file mode 100644 index c67f6d9..0000000 --- a/src/content/operators/impls/FindNextOperator.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index b3524c1..0000000 --- a/src/content/operators/impls/FindOperatorFactoryChain.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index f73e605..0000000 --- a/src/content/operators/impls/FindPrevOperator.ts +++ /dev/null @@ -1,15 +0,0 @@ -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/OperatorFactoryImpl.ts b/src/content/operators/impls/OperatorFactoryImpl.ts index 22b35c8..bc9bbee 100644 --- a/src/content/operators/impls/OperatorFactoryImpl.ts +++ b/src/content/operators/impls/OperatorFactoryImpl.ts @@ -7,7 +7,6 @@ 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"; @@ -20,7 +19,6 @@ export default class OperatorFactoryImpl implements OperatorFactory { constructor( addonOperatorFactoryChain: AddonOperatorFactoryChain, clipboardOperatorFactoryChain: ClipboardOperatorFactoryChain, - findOperatorFactoryChain: FindOperatorFactoryChain, focusOperatorFactoryChain: FocusOperatorFactoryChain, followOperatorFactoryChain: FollowOperatorFactoryChain, markOperatorFactoryChain: MarkOperatorFactoryChain, @@ -31,7 +29,6 @@ export default class OperatorFactoryImpl implements OperatorFactory { this.factoryChains = [ addonOperatorFactoryChain, clipboardOperatorFactoryChain, - findOperatorFactoryChain, focusOperatorFactoryChain, followOperatorFactoryChain, markOperatorFactoryChain, diff --git a/src/content/presenters/FindPresenter.ts b/src/content/presenters/FindPresenter.ts index b25190c..569f161 100644 --- a/src/content/presenters/FindPresenter.ts +++ b/src/content/presenters/FindPresenter.ts @@ -7,16 +7,10 @@ export default interface FindPresenter { export class FindPresenterImpl implements FindPresenter { find(keyword: string, backwards: boolean): boolean { const caseSensitive = false; - const wrapScan = true; + const wrapScan = false; // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work // because of same origin policy - const found = window.find(keyword, caseSensitive, backwards, wrapScan); - if (found) { - return found; - } - this.clearSelection(); - return window.find(keyword, caseSensitive, backwards, wrapScan); } diff --git a/src/content/repositories/FindRepository.ts b/src/content/repositories/FindRepository.ts deleted file mode 100644 index aeb200f..0000000 --- a/src/content/repositories/FindRepository.ts +++ /dev/null @@ -1,17 +0,0 @@ -export default interface FindRepository { - getLastKeyword(): string | null; - - setLastKeyword(keyword: string): void; -} - -let current: string | null = null; - -export class FindRepositoryImpl implements FindRepository { - getLastKeyword(): string | null { - return current; - } - - setLastKeyword(keyword: string): void { - current = keyword; - } -} diff --git a/src/content/usecases/FindUseCase.ts b/src/content/usecases/FindUseCase.ts index bff0eee..9e77124 100644 --- a/src/content/usecases/FindUseCase.ts +++ b/src/content/usecases/FindUseCase.ts @@ -1,67 +1,22 @@ -import { injectable, inject } from "tsyringe"; +import { inject, injectable } from "tsyringe"; import FindPresenter from "../presenters/FindPresenter"; -import FindRepository from "../repositories/FindRepository"; -import FindClient from "../client/FindClient"; -import ConsoleClient from "../client/ConsoleClient"; @injectable() export default class FindUseCase { constructor( - @inject("FindPresenter") private presenter: FindPresenter, - @inject("FindRepository") private repository: FindRepository, - @inject("FindClient") private client: FindClient, - @inject("ConsoleClient") private consoleClient: ConsoleClient + @inject("FindPresenter") + private readonly findPresenter: FindPresenter ) {} - async startFind(keyword?: string): Promise<void> { - this.presenter.clearSelection(); - if (keyword) { - this.saveKeyword(keyword); - } else { - const lastKeyword = await this.getKeyword(); - if (!lastKeyword) { - return this.showNoLastKeywordError(); - } - this.saveKeyword(lastKeyword); - } - return this.findNext(); + findNext(keyword: string): boolean { + return this.findPresenter.find(keyword, false); } - findNext(): Promise<void> { - return this.findNextPrev(false); + findPrev(keyword: string): boolean { + return this.findPresenter.find(keyword, true); } - findPrev(): Promise<void> { - return this.findNextPrev(true); - } - - private async findNextPrev(backwards: boolean): Promise<void> { - const keyword = await this.getKeyword(); - if (!keyword) { - return this.showNoLastKeywordError(); - } - const found = this.presenter.find(keyword, backwards); - if (found) { - this.consoleClient.info("Pattern found: " + keyword); - } else { - this.consoleClient.error("Pattern not found: " + keyword); - } - } - - private async getKeyword(): Promise<string | null> { - let keyword = this.repository.getLastKeyword(); - if (!keyword) { - keyword = await this.client.getGlobalLastKeyword(); - } - return keyword; - } - - private async saveKeyword(keyword: string): Promise<void> { - this.repository.setLastKeyword(keyword); - await this.client.setGlobalLastKeyword(keyword); - } - - private async showNoLastKeywordError(): Promise<void> { - await this.consoleClient.error("No previous search keywords"); + clearSelection() { + this.findPresenter.clearSelection(); } } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 59329e7..8b9e598 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -37,8 +37,7 @@ export const TAB_SCROLL_TO = "tab.scroll.to"; export const FIND_NEXT = "find.next"; export const FIND_PREV = "find.prev"; -export const FIND_GET_KEYWORD = "find.get.keyword"; -export const FIND_SET_KEYWORD = "find.set.keyword"; +export const FIND_CLEAR_SELECTION = "find.clear.selection"; export const ADDON_ENABLED_QUERY = "addon.enabled.query"; export const ADDON_ENABLED_RESPONSE = "addon.enabled.response"; @@ -73,7 +72,7 @@ export interface ConsoleEnterCommandMessage { export interface ConsoleEnterFindMessage { type: typeof CONSOLE_ENTER_FIND; - text?: string; + keyword?: string; } export interface ConsoleShowCommandMessage { @@ -229,20 +228,16 @@ export interface TabScrollToMessage { export interface FindNextMessage { type: typeof FIND_NEXT; + keyword: string; } export interface FindPrevMessage { type: typeof FIND_PREV; + keyword: string; } -export interface FindGetKeywordMessage { - type: typeof FIND_GET_KEYWORD; -} - -export interface FindSetKeywordMessage { - type: typeof FIND_SET_KEYWORD; - keyword: string; - found: boolean; +export interface FindClearSelection { + type: typeof FIND_CLEAR_SELECTION; } export interface AddonEnabledQueryMessage { @@ -324,8 +319,7 @@ export type Message = | TabScrollToMessage | FindNextMessage | FindPrevMessage - | FindGetKeywordMessage - | FindSetKeywordMessage + | FindClearSelection | AddonEnabledQueryMessage | AddonEnabledResponseMessage | AddonToggleEnabledMessage @@ -362,8 +356,7 @@ export const valueOf = (o: any): Message => { case TAB_SCROLL_TO: case FIND_NEXT: case FIND_PREV: - case FIND_GET_KEYWORD: - case FIND_SET_KEYWORD: + case FIND_CLEAR_SELECTION: case ADDON_ENABLED_QUERY: case ADDON_ENABLED_RESPONSE: case ADDON_TOGGLE_ENABLED: diff --git a/test/background/completion/TabCompletionUseCase.test.ts b/test/background/completion/TabCompletionUseCase.test.ts index e1a88a2..319f217 100644 --- a/test/background/completion/TabCompletionUseCase.test.ts +++ b/test/background/completion/TabCompletionUseCase.test.ts @@ -82,6 +82,10 @@ class MockTabPresenter implements TabPresenter { setZoom(_tabId: number, _factor: number): Promise<void> { throw new Error("not implemented"); } + + toggleReaderMode(_tabId: number): Promise<void> { + throw new Error("not implemented"); + } } describe("TabCompletionUseCase", () => { diff --git a/test/background/mock/MockFindClient.ts b/test/background/mock/MockFindClient.ts new file mode 100644 index 0000000..dd6d8f3 --- /dev/null +++ b/test/background/mock/MockFindClient.ts @@ -0,0 +1,23 @@ +import FindClient from "../../../src/background/clients/FindClient"; + +export default class MockFindClient implements FindClient { + findNext( + _tabId: number, + _frameId: number, + _keyword: string + ): Promise<boolean> { + throw new Error("not implemented"); + } + + findPrev( + _tabId: number, + _frameId: number, + _keyword: string + ): Promise<boolean> { + throw new Error("not implemented"); + } + + clearSelection(_tabId: number, _frameId: number): Promise<void> { + throw new Error("not implemented"); + } +} diff --git a/test/background/mock/MockFindRepository.ts b/test/background/mock/MockFindRepository.ts new file mode 100644 index 0000000..d5151f8 --- /dev/null +++ b/test/background/mock/MockFindRepository.ts @@ -0,0 +1,31 @@ +import FindRepository, { + FindState, +} from "../../../src/background/repositories/FindRepository"; + +export default class MockFindRepository implements FindRepository { + private globalKeyword: string | undefined; + private localStates: { [tabId: number]: FindState } = {}; + + getGlobalKeyword(): Promise<string | undefined> { + return Promise.resolve(this.globalKeyword); + } + + setGlobalKeyword(keyword: string): Promise<void> { + this.globalKeyword = keyword; + return Promise.resolve(); + } + + getLocalState(tabId: number): Promise<FindState | undefined> { + return Promise.resolve(this.localStates[tabId]); + } + + setLocalState(tabId: number, state: FindState): Promise<void> { + this.localStates[tabId] = state; + return Promise.resolve(); + } + + deleteLocalState(tabId: number): Promise<void> { + delete this.localStates[tabId]; + return Promise.resolve(); + } +} diff --git a/test/background/mock/MockFramePresenter.ts b/test/background/mock/MockFramePresenter.ts new file mode 100644 index 0000000..d688780 --- /dev/null +++ b/test/background/mock/MockFramePresenter.ts @@ -0,0 +1,7 @@ +import FramePresenter from "../../../src/background/presenters/FramePresenter"; + +export default class MockFramePresenter implements FramePresenter { + getAllFrameIds(): Promise<number[]> { + throw new Error("not implemented"); + } +} diff --git a/test/background/mock/MockTabPresenter.ts b/test/background/mock/MockTabPresenter.ts index 22fb947..0968e44 100644 --- a/test/background/mock/MockTabPresenter.ts +++ b/test/background/mock/MockTabPresenter.ts @@ -176,4 +176,8 @@ export default class MockTabPresenter implements TabPresenter { this.zooms[index] = factor; return Promise.resolve(); } + + toggleReaderMode(_tabId: number): Promise<void> { + throw new Error("not implemented"); + } } diff --git a/test/background/operators/impls/FindNextOperator.test.ts b/test/background/operators/impls/FindNextOperator.test.ts new file mode 100644 index 0000000..0bee3f5 --- /dev/null +++ b/test/background/operators/impls/FindNextOperator.test.ts @@ -0,0 +1,179 @@ +import * as sinon from "sinon"; +import MockTabPresenter from "../../mock/MockTabPresenter"; +import FindNextOperator from "../../../../src/background/operators/impls/FindNextOperator"; +import MockFindRepository from "../../mock/MockFindRepository"; +import MockFindClient from "../../mock/MockFindClient"; +import MockConsoleClient from "../../mock/MockConsoleClient"; +import MockFramePresenter from "../../mock/MockFramePresenter"; + +describe("FindNextOperator", () => { + const keyword = "hello"; + const frameIds = [0, 100, 101]; + + const tabPresenter = new MockTabPresenter(); + const findRepository = new MockFindRepository(); + const findClient = new MockFindClient(); + const consoleClient = new MockConsoleClient(); + const framePresenter = new MockFramePresenter(); + const sut = new FindNextOperator( + tabPresenter, + findRepository, + findClient, + consoleClient, + framePresenter + ); + + let currentTabId: number; + + beforeEach(async () => { + sinon.restore(); + + const currentTab = await tabPresenter.create("https://example.com/", { + active: true, + }); + currentTabId = currentTab.id!; + }); + + describe("#run", () => { + it("shows errors if no previous keywords", async () => { + sinon + .stub(findRepository, "getLocalState") + .returns(Promise.resolve(undefined)); + + const mock = sinon.mock(consoleClient); + mock + .expects("showError") + .withArgs(currentTabId, "No previous search keywords"); + + await sut.run(); + + mock.verify(); + }); + + it("continues a search on the same frame", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 1, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 1 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("continues a search on next frame", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 1, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("clearSelection") + .withArgs(currentTabId, 100) + .returns(Promise.resolve()); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 101, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 2 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("exercise a wrap-search", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 2, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 101, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("clearSelection") + .withArgs(currentTabId, 101) + .returns(Promise.resolve()); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 0 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("starts a search with last keywords", async () => { + sinon + .stub(findRepository, "getLocalState") + .returns(Promise.resolve(undefined)); + sinon + .stub(findRepository, "getGlobalKeyword") + .returns(Promise.resolve(keyword)); + sinon + .stub(framePresenter, "getAllFrameIds") + .returns(Promise.resolve(frameIds)); + sinon.stub(consoleClient, "showInfo").returns(Promise.resolve()); + + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 0 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + }); +}); diff --git a/test/background/operators/impls/FindOperatorFactoryChain.ts b/test/background/operators/impls/FindOperatorFactoryChain.ts new file mode 100644 index 0000000..0fd234f --- /dev/null +++ b/test/background/operators/impls/FindOperatorFactoryChain.ts @@ -0,0 +1,23 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import TabOperatorFactoryChain from "../../../../src/background/operators/impls/TabOperatorFactoryChain"; +import MockTabPresenter from "../../mock/MockTabPresenter"; +import * as operations from "../../../../src/shared/operations"; +import FindNextOperator from "../../../../src/background/operators/impls/FindNextOperator"; +import FindPrevOperator from "../../../../src/background/operators/impls/FindPrevOperator"; + +describe("FindOperatorFactoryChain", () => { + describe("#create", () => { + it("returns a operator for the operation", async () => { + const tabPresenter = new MockTabPresenter(); + const sut = new TabOperatorFactoryChain(tabPresenter); + + expect(sut.create({ type: operations.FIND_NEXT })).to.be.instanceOf( + FindNextOperator + ); + expect(sut.create({ type: operations.FIND_PREV })).to.be.instanceOf( + FindPrevOperator + ); + }); + }); +}); diff --git a/test/background/operators/impls/FindPrevOperator.test.ts b/test/background/operators/impls/FindPrevOperator.test.ts new file mode 100644 index 0000000..ebac0dc --- /dev/null +++ b/test/background/operators/impls/FindPrevOperator.test.ts @@ -0,0 +1,179 @@ +import * as sinon from "sinon"; +import MockTabPresenter from "../../mock/MockTabPresenter"; +import FindPrevOperator from "../../../../src/background/operators/impls/FindPrevOperator"; +import MockFindRepository from "../../mock/MockFindRepository"; +import MockFindClient from "../../mock/MockFindClient"; +import MockConsoleClient from "../../mock/MockConsoleClient"; +import MockFramePresenter from "../../mock/MockFramePresenter"; + +describe("FindPrevOperator", () => { + const keyword = "hello"; + const frameIds = [0, 100, 101]; + + const tabPresenter = new MockTabPresenter(); + const findRepository = new MockFindRepository(); + const findClient = new MockFindClient(); + const consoleClient = new MockConsoleClient(); + const framePresenter = new MockFramePresenter(); + const sut = new FindPrevOperator( + tabPresenter, + findRepository, + findClient, + consoleClient, + framePresenter + ); + + let currentTabId: number; + + beforeEach(async () => { + sinon.restore(); + + const currentTab = await tabPresenter.create("https://example.com/", { + active: true, + }); + currentTabId = currentTab.id!; + }); + + describe("#run", () => { + it("shows errors if no previous keywords", async () => { + sinon + .stub(findRepository, "getLocalState") + .returns(Promise.resolve(undefined)); + + const mock = sinon.mock(consoleClient); + mock + .expects("showError") + .withArgs(currentTabId, "No previous search keywords"); + + await sut.run(); + + mock.verify(); + }); + + it("continues a search on the same frame", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 1, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 1 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("continues a search on next frame", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 1, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("clearSelection") + .withArgs(currentTabId, 100) + .returns(Promise.resolve()); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 0 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("exercise a wrap-search", async () => { + sinon.stub(findRepository, "getLocalState").returns( + Promise.resolve({ + keyword, + frameIds, + framePos: 0, + }) + ); + + const mockFindClient = sinon.mock(findClient); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("clearSelection") + .withArgs(currentTabId, 0) + .returns(Promise.resolve()); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 101, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 2 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + + it("starts a search with last keywords", async () => { + sinon + .stub(findRepository, "getLocalState") + .returns(Promise.resolve(undefined)); + sinon + .stub(findRepository, "getGlobalKeyword") + .returns(Promise.resolve(keyword)); + sinon + .stub(framePresenter, "getAllFrameIds") + .returns(Promise.resolve(frameIds)); + sinon.stub(consoleClient, "showInfo").returns(Promise.resolve()); + + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findPrev") + .withArgs(currentTabId, 101, keyword) + .returns(Promise.resolve(true)); + + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { keyword, frameIds, framePos: 2 }); + + await sut.run(); + + mockFindRepository.verify(); + mockFindClient.verify(); + }); + }); +}); diff --git a/test/background/operators/impls/TabOperatorFactoryChain.test.ts b/test/background/operators/impls/TabOperatorFactoryChain.test.ts index 7ab5de0..a777973 100644 --- a/test/background/operators/impls/TabOperatorFactoryChain.test.ts +++ b/test/background/operators/impls/TabOperatorFactoryChain.test.ts @@ -44,12 +44,12 @@ describe("TabOperatorFactoryChain", () => { 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_LAST })).to.be.instanceOf( + SelectLastTabOperator + ); + expect(sut.create({ type: operations.TAB_PREV_SEL })).to.be.instanceOf( + SelectPreviousSelectedTabOperator + ); expect( sut.create({ type: operations.TAB_RELOAD, cache: false }) ).to.be.instanceOf(ReloadTabOperator); diff --git a/test/background/repositories/FindRepository.test.ts b/test/background/repositories/FindRepository.test.ts new file mode 100644 index 0000000..a08dc6d --- /dev/null +++ b/test/background/repositories/FindRepository.test.ts @@ -0,0 +1,38 @@ +import { expect } from "chai"; +import { FindRepositoryImpl } from "../../../src/background/repositories/FindRepository"; + +describe("background/repositories/FindRepositoryImpl", () => { + let sut: FindRepositoryImpl; + + beforeEach(() => { + sut = new FindRepositoryImpl(); + }); + + describe("global keyword", () => { + it("get and set a keyword", async () => { + expect(await sut.getGlobalKeyword()).to.be.undefined; + + await sut.setGlobalKeyword("Hello, world"); + + const keyword = await sut.getGlobalKeyword(); + expect(keyword).to.equal("Hello, world"); + }); + }); + + describe("local state", () => { + it("get and set a keyword", async () => { + expect(await sut.getLocalState(10)).to.be.undefined; + + await sut.setLocalState(10, { + keyword: "Hello, world", + frameIds: [20, 21], + framePos: 0, + }); + + const state = await sut.getLocalState(10); + expect(state?.keyword).to.equal("Hello, world"); + + expect(await sut.getLocalState(20)).to.be.undefined; + }); + }); +}); diff --git a/test/background/usecases/StartFindUseCase.test.ts b/test/background/usecases/StartFindUseCase.test.ts new file mode 100644 index 0000000..22ff9a5 --- /dev/null +++ b/test/background/usecases/StartFindUseCase.test.ts @@ -0,0 +1,180 @@ +import * as sinon from "sinon"; +import MockFindClient from "../mock/MockFindClient"; +import MockFindRepository from "../mock/MockFindRepository"; +import MockConsoleClient from "../mock/MockConsoleClient"; +import MockFramePresenter from "../mock/MockFramePresenter"; +import StartFindUseCase from "../../../src/background/usecases/StartFindUseCase"; + +describe("StartFindUseCase", () => { + const currentTabId = 100; + const frameIds = [0, 100, 101]; + const keyword = "hello"; + + const findClient = new MockFindClient(); + const findRepository = new MockFindRepository(); + const consoleClient = new MockConsoleClient(); + const framePresenter = new MockFramePresenter(); + const sut = new StartFindUseCase( + findClient, + findRepository, + consoleClient, + framePresenter + ); + + beforeEach(async () => { + sinon.restore(); + + sinon + .stub(framePresenter, "getAllFrameIds") + .returns(Promise.resolve(frameIds)); + }); + + describe("startFind", () => { + it("starts a find with a keyword", async () => { + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(true)); + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { frameIds, framePos: 1, keyword }); + const mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient + .expects("showInfo") + .withArgs(currentTabId, "Pattern found: " + keyword); + + await sut.startFind(currentTabId, keyword); + + mockFindClient.verify(); + mockFindRepository.verify(); + mockConsoleClient.verify(); + }); + + it("starts a find with last local state", async () => { + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(true)); + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("getLocalState") + .withArgs(currentTabId) + .returns(Promise.resolve({ keyword, frameIds, framePos: 0 })); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { frameIds, framePos: 1, keyword }); + const mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient + .expects("showInfo") + .withArgs(currentTabId, "Pattern found: " + keyword); + + await sut.startFind(currentTabId, undefined); + + mockFindClient.verify(); + mockFindRepository.verify(); + mockConsoleClient.verify(); + }); + + it("starts a find with last global state", async () => { + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(true)); + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository + .expects("getLocalState") + .withArgs(currentTabId) + .returns(Promise.resolve(undefined)); + mockFindRepository + .expects("getGlobalKeyword") + .returns(Promise.resolve(keyword)); + mockFindRepository + .expects("setLocalState") + .withArgs(currentTabId, { frameIds, framePos: 1, keyword }); + const mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient + .expects("showInfo") + .withArgs(currentTabId, "Pattern found: " + keyword); + + await sut.startFind(currentTabId, undefined); + + mockFindClient.verify(); + mockFindRepository.verify(); + mockConsoleClient.verify(); + }); + + it("shows an error when pattern not found", async () => { + const mockFindClient = sinon.mock(findClient); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 0); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 100); + mockFindClient.expects("clearSelection").withArgs(currentTabId, 101); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 0, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 100, keyword) + .returns(Promise.resolve(false)); + mockFindClient + .expects("findNext") + .withArgs(currentTabId, 101, keyword) + .returns(Promise.resolve(false)); + const mockFindRepository = sinon.mock(findRepository); + mockFindRepository.expects("setLocalState").never(); + const mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient + .expects("showError") + .withArgs(currentTabId, "Pattern not found: " + keyword); + + await sut.startFind(currentTabId, keyword); + + mockFindClient.verify(); + mockFindRepository.verify(); + mockConsoleClient.verify(); + }); + + it("shows an error when no last keywords", async () => { + sinon + .stub(findRepository, "getLocalState") + .returns(Promise.resolve(undefined)); + sinon + .stub(findRepository, "getGlobalKeyword") + .returns(Promise.resolve(undefined)); + + const mockConsoleClient = sinon.mock(consoleClient); + mockConsoleClient + .expects("showError") + .withArgs(currentTabId, "No previous search keywords"); + + await sut.startFind(currentTabId, undefined); + + mockConsoleClient.verify(); + }); + }); +}); diff --git a/test/content/mock/MockFindMasterClient.ts b/test/content/mock/MockFindMasterClient.ts deleted file mode 100644 index a035cc5..0000000 --- a/test/content/mock/MockFindMasterClient.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/operators/impls/FindNextOperator.test.ts b/test/content/operators/impls/FindNextOperator.test.ts deleted file mode 100644 index d93d45e..0000000 --- a/test/content/operators/impls/FindNextOperator.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 6c599ae..0000000 --- a/test/content/operators/impls/FindOperatorFactoryChain.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 1ebde8d..0000000 --- a/test/content/operators/impls/FindPrevOperator.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/repositories/FindRepository.test.ts b/test/content/repositories/FindRepository.test.ts deleted file mode 100644 index e0abb9d..0000000 --- a/test/content/repositories/FindRepository.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FindRepositoryImpl } from "../../../src/content/repositories/FindRepository"; -import { expect } from "chai"; - -describe("FindRepositoryImpl", () => { - it("updates and gets last keyword", () => { - const sut = new FindRepositoryImpl(); - - expect(sut.getLastKeyword()).to.be.null; - - sut.setLastKeyword("monkey"); - - expect(sut.getLastKeyword()).to.equal("monkey"); - }); -}); diff --git a/test/content/usecases/FindUseCase.test.ts b/test/content/usecases/FindUseCase.test.ts deleted file mode 100644 index b53ef74..0000000 --- a/test/content/usecases/FindUseCase.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import FindRepository from "../../../src/content/repositories/FindRepository"; -import FindPresenter from "../../../src/content/presenters/FindPresenter"; -import FindClient from "../../../src/content/client/FindClient"; -import FindUseCase from "../../../src/content/usecases/FindUseCase"; -import MockConsoleClient from "../mock/MockConsoleClient"; -import { expect } from "chai"; - -class MockFindRepository implements FindRepository { - public keyword: string | null; - - constructor() { - this.keyword = null; - } - - getLastKeyword(): string | null { - return this.keyword; - } - - setLastKeyword(keyword: string): void { - this.keyword = keyword; - } -} - -class MockFindPresenter implements FindPresenter { - public document: string; - - public highlighted: boolean; - - constructor() { - this.document = ""; - this.highlighted = false; - } - - find(keyword: string, _backward: boolean): boolean { - const found = this.document.includes(keyword); - this.highlighted = found; - return found; - } - - clearSelection(): void { - this.highlighted = false; - } -} - -class MockFindClient implements FindClient { - public keyword: string | null; - - constructor() { - this.keyword = null; - } - - getGlobalLastKeyword(): Promise<string | null> { - return Promise.resolve(this.keyword); - } - - setGlobalLastKeyword(keyword: string): Promise<void> { - this.keyword = keyword; - return Promise.resolve(); - } -} - -describe("FindUseCase", () => { - let repository: MockFindRepository; - let presenter: MockFindPresenter; - let client: MockFindClient; - let consoleClient: MockConsoleClient; - let sut: FindUseCase; - - beforeEach(() => { - repository = new MockFindRepository(); - presenter = new MockFindPresenter(); - client = new MockFindClient(); - consoleClient = new MockConsoleClient(); - sut = new FindUseCase(presenter, repository, client, consoleClient); - }); - - describe("#startFind", () => { - it("find next by ketword", async () => { - presenter.document = "monkey punch"; - - await sut.startFind("monkey"); - - expect(await presenter.highlighted).to.be.true; - expect(await consoleClient.text).to.equal("Pattern found: monkey"); - expect(await repository.getLastKeyword()).to.equal("monkey"); - expect(await client.getGlobalLastKeyword()).to.equal("monkey"); - }); - - it("find next by last keyword", async () => { - presenter.document = "gorilla kick"; - repository.keyword = "gorilla"; - - await sut.startFind(undefined); - - expect(await presenter.highlighted).to.be.true; - expect(await consoleClient.text).to.equal("Pattern found: gorilla"); - expect(await repository.getLastKeyword()).to.equal("gorilla"); - expect(await client.getGlobalLastKeyword()).to.equal("gorilla"); - }); - - it("find next by global last keyword", async () => { - presenter.document = "chimpanzee typing"; - - repository.keyword = null; - client.keyword = "chimpanzee"; - - await sut.startFind(undefined); - - expect(await presenter.highlighted).to.be.true; - expect(await consoleClient.text).to.equal("Pattern found: chimpanzee"); - expect(await repository.getLastKeyword()).to.equal("chimpanzee"); - expect(await client.getGlobalLastKeyword()).to.equal("chimpanzee"); - }); - - it("find not found error", async () => { - presenter.document = "zoo"; - - await sut.startFind("giraffe"); - - expect(await presenter.highlighted).to.be.false; - expect(await consoleClient.text).to.equal("Pattern not found: giraffe"); - expect(await repository.getLastKeyword()).to.equal("giraffe"); - expect(await client.getGlobalLastKeyword()).to.equal("giraffe"); - }); - - it("show errors when no last keywords", async () => { - repository.keyword = null; - client.keyword = null; - - await sut.startFind(undefined); - - expect(await consoleClient.text).to.equal("No previous search keywords"); - expect(await consoleClient.isError).to.be.true; - }); - }); - - describe("#findNext", () => { - it("finds by last keyword", async () => { - presenter.document = "monkey punch"; - repository.keyword = "monkey"; - - await sut.findNext(); - - expect(await presenter.highlighted).to.be.true; - expect(await consoleClient.text).to.equal("Pattern found: monkey"); - }); - - it("show errors when no last keywords", async () => { - repository.keyword = null; - client.keyword = null; - - await sut.findNext(); - - expect(await consoleClient.text).to.equal("No previous search keywords"); - expect(await consoleClient.isError).to.be.true; - }); - }); - - describe("#findPrev", () => {}); -}); |