Merge pull request #592 from ueokande/repeat-last-operation
Add "repeat last operation" command
This commit is contained in:
commit
cd584c8e24
28 changed files with 606 additions and 142 deletions
92
e2e/repeat.test.js
Normal file
92
e2e/repeat.test.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
const express = require('express');
|
||||
const lanthan = require('lanthan');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
const eventually = require('./eventually');
|
||||
|
||||
const Key = lanthan.Key;
|
||||
|
||||
const newApp = () => {
|
||||
let app = express();
|
||||
app.get('/', (req, res) => {
|
||||
res.send('ok');
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
describe("tab test", () => {
|
||||
|
||||
const port = 12321;
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
|
||||
let http;
|
||||
let firefox;
|
||||
let session;
|
||||
let browser;
|
||||
let tabs;
|
||||
|
||||
before(async() => {
|
||||
firefox = await lanthan.firefox();
|
||||
await firefox.session.installAddonFromPath(path.join(__dirname, '..'));
|
||||
session = firefox.session;
|
||||
browser = firefox.browser;
|
||||
http = newApp().listen(port);
|
||||
|
||||
await session.navigateTo(`${url}`);
|
||||
});
|
||||
|
||||
after(async() => {
|
||||
http.close();
|
||||
if (firefox) {
|
||||
await firefox.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('repeats last operation', async () => {
|
||||
let before = await browser.tabs.query({});
|
||||
|
||||
let body = await session.findElementByCSS('body');
|
||||
await body.sendKeys(':');
|
||||
|
||||
await session.switchToFrame(0);
|
||||
let input = await session.findElementByCSS('input');
|
||||
input.sendKeys(`tabopen ${url}newtab`, Key.Enter);
|
||||
|
||||
await eventually(async() => {
|
||||
let current = await browser.tabs.query({ url: `*://*/newtab` });
|
||||
assert.equal(current.length, 1);
|
||||
});
|
||||
|
||||
body = await session.findElementByCSS('body');
|
||||
await body.sendKeys('.');
|
||||
|
||||
await eventually(async() => {
|
||||
let current = await browser.tabs.query({ url: `*://*/newtab` });
|
||||
assert.equal(current.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('repeats last operation', async () => {
|
||||
for (let i = 1; i < 5; ++i) {
|
||||
await browser.tabs.create({ url: `${url}#${i}` });
|
||||
}
|
||||
let before = await browser.tabs.query({});
|
||||
|
||||
let body = await session.findElementByCSS('body');
|
||||
await body.sendKeys('d');
|
||||
|
||||
await eventually(async() => {
|
||||
let current = await browser.tabs.query({});
|
||||
assert.equal(current.length, before.length - 1);
|
||||
});
|
||||
|
||||
await browser.tabs.update(before[2].id, { active: true });
|
||||
body = await session.findElementByCSS('body');
|
||||
await body.sendKeys('.');
|
||||
|
||||
await eventually(async() => {
|
||||
let current = await browser.tabs.query({});
|
||||
assert.equal(current.length, before.length - 2);
|
||||
});
|
||||
});
|
||||
});
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -105,14 +105,6 @@
|
|||
"@sinonjs/commons": "^1.0.2",
|
||||
"array-from": "^2.1.1",
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinonjs/text-encoding": {
|
||||
|
@ -202,6 +194,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@types/sinon": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.11.tgz",
|
||||
"integrity": "sha512-6ee09Ugx6GyEr0opUIakmxIWFNmqYPjkqa3/BuxCBokA0klsOLPgMD5K4q40lH7/yZVuJVzOfQpd7pipwjngkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.9.0.tgz",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-redux": "^7.0.8",
|
||||
"@types/redux-promise": "^0.5.28",
|
||||
"@types/sinon": "^7.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^1.9.0",
|
||||
"chai": "^4.2.0",
|
||||
"css-loader": "^2.1.1",
|
||||
|
@ -54,6 +55,7 @@
|
|||
"redux-promise": "^0.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sinon": "^7.3.2",
|
||||
"sinon-chrome": "^3.0.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^6.0.1",
|
||||
|
|
29
src/background/clients/NavigateClient.ts
Normal file
29
src/background/clients/NavigateClient.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
@injectable()
|
||||
export default class NavigateClient {
|
||||
async historyNext(tabId: number): Promise<void> {
|
||||
await browser.tabs.sendMessage(tabId, {
|
||||
type: messages.NAVIGATE_HISTORY_NEXT,
|
||||
});
|
||||
}
|
||||
|
||||
async historyPrev(tabId: number): Promise<void> {
|
||||
await browser.tabs.sendMessage(tabId, {
|
||||
type: messages.NAVIGATE_HISTORY_PREV,
|
||||
});
|
||||
}
|
||||
|
||||
async linkNext(tabId: number): Promise<void> {
|
||||
await browser.tabs.sendMessage(tabId, {
|
||||
type: messages.NAVIGATE_LINK_NEXT,
|
||||
});
|
||||
}
|
||||
|
||||
async linkPrev(tabId: number): Promise<void> {
|
||||
await browser.tabs.sendMessage(tabId, {
|
||||
type: messages.NAVIGATE_LINK_PREV,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ 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 RepeatUseCase from '../usecases/RepeatUseCase';
|
||||
|
||||
@injectable()
|
||||
export default class OperationController {
|
||||
|
@ -14,11 +16,20 @@ export default class OperationController {
|
|||
private tabUseCase: TabUseCase,
|
||||
private tabSelectUseCase: TabSelectUseCase,
|
||||
private zoomUseCase: ZoomUseCase,
|
||||
private navigateUseCase: NavigateUseCase,
|
||||
private repeatUseCase: RepeatUseCase,
|
||||
) {
|
||||
}
|
||||
|
||||
async exec(op: operations.Operation): Promise<any> {
|
||||
await this.doOperation(op);
|
||||
if (this.repeatUseCase.isRepeatable(op)) {
|
||||
this.repeatUseCase.storeLastOperation(op);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
exec(operation: operations.Operation): Promise<any> {
|
||||
doOperation(operation: operations.Operation): Promise<any> {
|
||||
switch (operation.type) {
|
||||
case operations.TAB_CLOSE:
|
||||
return this.tabUseCase.close(false);
|
||||
|
@ -74,6 +85,29 @@ export default class OperationController {
|
|||
return this.findUseCase.findStart();
|
||||
case operations.CANCEL:
|
||||
return this.consoleUseCase.hideConsole();
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
return this.navigateUseCase.openHistoryPrev();
|
||||
case operations.NAVIGATE_HISTORY_NEXT:
|
||||
return this.navigateUseCase.openHistoryNext();
|
||||
case operations.NAVIGATE_LINK_PREV:
|
||||
return this.navigateUseCase.openLinkPrev();
|
||||
case operations.NAVIGATE_LINK_NEXT:
|
||||
return this.navigateUseCase.openLinkNext();
|
||||
case operations.NAVIGATE_PARENT:
|
||||
return this.navigateUseCase.openParent();
|
||||
case operations.NAVIGATE_ROOT:
|
||||
return this.navigateUseCase.openRoot();
|
||||
case operations.REPEAT_LAST:
|
||||
{
|
||||
let last = this.repeatUseCase.getLastOperation();
|
||||
if (typeof last !== 'undefined') {
|
||||
return this.doOperation(last);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
case operations.INTERNAL_OPEN_URL:
|
||||
return this.tabUseCase.openURL(
|
||||
operation.url, operation.newTab, operation.newWindow);
|
||||
}
|
||||
throw new Error('unknown operation: ' + operation.type);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ export default class TabPresenter {
|
|||
return tabId;
|
||||
}
|
||||
|
||||
async getByKeyword(keyword: string, excludePinned = false): Promise<Tab[]> {
|
||||
async getByKeyword(
|
||||
keyword: string, excludePinned: boolean = false,
|
||||
): Promise<Tab[]> {
|
||||
let tabs = await browser.tabs.query({ currentWindow: true });
|
||||
return tabs.filter((t) => {
|
||||
return t.url && t.url.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
|
|
22
src/background/repositories/RepeatRepository.ts
Normal file
22
src/background/repositories/RepeatRepository.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import { Operation } from '../../shared/operations';
|
||||
import MemoryStorage from '../infrastructures/MemoryStorage';
|
||||
|
||||
const REPEAT_KEY = 'repeat';
|
||||
|
||||
@injectable()
|
||||
export default class RepeatRepository {
|
||||
private cache: MemoryStorage;
|
||||
|
||||
constructor() {
|
||||
this.cache = new MemoryStorage();
|
||||
}
|
||||
|
||||
getLastOperation(): Operation | undefined {
|
||||
return this.cache.get(REPEAT_KEY);
|
||||
}
|
||||
|
||||
setLastOperation(op: Operation): void {
|
||||
this.cache.set(REPEAT_KEY, op);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import * as operations from '../../shared/operations';
|
||||
import * as parsers from './parsers';
|
||||
import * as urls from '../../shared/urls';
|
||||
import TabPresenter from '../presenters/TabPresenter';
|
||||
|
@ -7,6 +8,7 @@ import SettingRepository from '../repositories/SettingRepository';
|
|||
import BookmarkRepository from '../repositories/BookmarkRepository';
|
||||
import ConsoleClient from '../infrastructures/ConsoleClient';
|
||||
import ContentMessageClient from '../infrastructures/ContentMessageClient';
|
||||
import RepeatUseCase from '../usecases/RepeatUseCase';
|
||||
|
||||
@injectable()
|
||||
export default class CommandIndicator {
|
||||
|
@ -17,21 +19,36 @@ export default class CommandIndicator {
|
|||
private bookmarkRepository: BookmarkRepository,
|
||||
private consoleClient: ConsoleClient,
|
||||
private contentMessageClient: ContentMessageClient,
|
||||
private repeatUseCase: RepeatUseCase,
|
||||
) {
|
||||
}
|
||||
|
||||
async open(keywords: string): Promise<browser.tabs.Tab> {
|
||||
let url = await this.urlOrSearch(keywords);
|
||||
this.repeatUseCase.storeLastOperation({
|
||||
type: operations.INTERNAL_OPEN_URL,
|
||||
url,
|
||||
});
|
||||
return this.tabPresenter.open(url);
|
||||
}
|
||||
|
||||
async tabopen(keywords: string): Promise<browser.tabs.Tab> {
|
||||
let url = await this.urlOrSearch(keywords);
|
||||
this.repeatUseCase.storeLastOperation({
|
||||
type: operations.INTERNAL_OPEN_URL,
|
||||
url,
|
||||
newTab: true,
|
||||
});
|
||||
return this.tabPresenter.create(url);
|
||||
}
|
||||
|
||||
async winopen(keywords: string): Promise<browser.windows.Window> {
|
||||
let url = await this.urlOrSearch(keywords);
|
||||
this.repeatUseCase.storeLastOperation({
|
||||
type: operations.INTERNAL_OPEN_URL,
|
||||
url,
|
||||
newWindow: true,
|
||||
});
|
||||
return this.windowPresenter.create(url);
|
||||
}
|
||||
|
||||
|
|
57
src/background/usecases/NavigateUseCase.ts
Normal file
57
src/background/usecases/NavigateUseCase.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import NavigateClient from '../clients/NavigateClient';
|
||||
import TabPresenter from '../presenters/TabPresenter';
|
||||
|
||||
@injectable()
|
||||
export default class NavigateUseCase {
|
||||
constructor(
|
||||
private tabPresenter: TabPresenter,
|
||||
private navigateClient: NavigateClient,
|
||||
) {
|
||||
}
|
||||
|
||||
async openHistoryNext(): Promise<void> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
await this.navigateClient.historyNext(tab.id!!);
|
||||
}
|
||||
|
||||
async openHistoryPrev(): Promise<void> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
await this.navigateClient.historyPrev(tab.id!!);
|
||||
}
|
||||
|
||||
async openLinkNext(): Promise<void> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
await this.navigateClient.linkNext(tab.id!!);
|
||||
}
|
||||
|
||||
async openLinkPrev(): Promise<void> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
await this.navigateClient.linkPrev(tab.id!!);
|
||||
}
|
||||
|
||||
async openParent(): Promise<void> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
let 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> {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
let url = new URL(tab.url!!);
|
||||
await this.tabPresenter.open(url.origin);
|
||||
}
|
||||
}
|
50
src/background/usecases/RepeatUseCase.ts
Normal file
50
src/background/usecases/RepeatUseCase.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import * as operations from '../../shared/operations';
|
||||
import RepeatRepository from '../repositories/RepeatRepository';
|
||||
|
||||
type Operation = operations.Operation;
|
||||
|
||||
@injectable()
|
||||
export default class RepeatUseCase {
|
||||
constructor(
|
||||
private repeatRepository: RepeatRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
storeLastOperation(op: Operation): void {
|
||||
this.repeatRepository.setLastOperation(op);
|
||||
}
|
||||
|
||||
getLastOperation(): operations.Operation | undefined {
|
||||
return this.repeatRepository.getLastOperation();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
isRepeatable(op: Operation): boolean {
|
||||
switch (op.type) {
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
case operations.NAVIGATE_HISTORY_NEXT:
|
||||
case operations.NAVIGATE_LINK_PREV:
|
||||
case operations.NAVIGATE_LINK_NEXT:
|
||||
case operations.NAVIGATE_PARENT:
|
||||
case operations.NAVIGATE_ROOT:
|
||||
case operations.PAGE_SOURCE:
|
||||
case operations.PAGE_HOME:
|
||||
case operations.TAB_CLOSE:
|
||||
case operations.TAB_CLOSE_FORCE:
|
||||
case operations.TAB_CLOSE_RIGHT:
|
||||
case operations.TAB_REOPEN:
|
||||
case operations.TAB_RELOAD:
|
||||
case operations.TAB_PIN:
|
||||
case operations.TAB_UNPIN:
|
||||
case operations.TAB_TOGGLE_PINNED:
|
||||
case operations.TAB_DUPLICATE:
|
||||
case operations.ZOOM_IN:
|
||||
case operations.ZOOM_OUT:
|
||||
case operations.ZOOM_NEUTRAL:
|
||||
case operations.INTERNAL_OPEN_URL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import TabPresenter from '../presenters/TabPresenter';
|
||||
import WindowPresenter from '../presenters/WindowPresenter';
|
||||
import BrowserSettingRepository from '../repositories/BrowserSettingRepository';
|
||||
|
||||
@injectable()
|
||||
export default class TabUseCase {
|
||||
constructor(
|
||||
private tabPresenter: TabPresenter,
|
||||
private windowPresenter: WindowPresenter,
|
||||
private browserSettingRepository: BrowserSettingRepository,
|
||||
) {
|
||||
}
|
||||
|
@ -77,4 +79,17 @@ export default class TabUseCase {
|
|||
this.tabPresenter.create(url);
|
||||
}
|
||||
}
|
||||
|
||||
async openURL(
|
||||
url: string, newTab?: boolean, newWindow?: boolean,
|
||||
): Promise<void> {
|
||||
if (newWindow) {
|
||||
await this.windowPresenter.create(url);
|
||||
} else if (newTab) {
|
||||
await this.tabPresenter.create(url);
|
||||
} else {
|
||||
let tab = await this.tabPresenter.getCurrent();
|
||||
await this.tabPresenter.open(url, tab.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import MarkKeyController from './controllers/MarkKeyController';
|
|||
import AddonEnabledController from './controllers/AddonEnabledController';
|
||||
import SettingController from './controllers/SettingController';
|
||||
import ConsoleFrameController from './controllers/ConsoleFrameController';
|
||||
import NavigateController from './controllers/NavigateController';
|
||||
import * as messages from '../shared/messages';
|
||||
|
||||
type Message = messages.Message;
|
||||
|
@ -33,6 +34,7 @@ export default class Application {
|
|||
private addonEnabledController: AddonEnabledController,
|
||||
private settingController: SettingController,
|
||||
private consoleFrameController: ConsoleFrameController,
|
||||
private navigateController: NavigateController,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -98,6 +100,14 @@ export default class Application {
|
|||
return this.settingController.reloadSettings(msg);
|
||||
case messages.ADDON_TOGGLE_ENABLED:
|
||||
return this.addonEnabledUseCase.toggle();
|
||||
case messages.NAVIGATE_HISTORY_NEXT:
|
||||
return this.navigateController.openHistoryNext(msg);
|
||||
case messages.NAVIGATE_HISTORY_PREV:
|
||||
return this.navigateController.openHistoryPrev(msg);
|
||||
case messages.NAVIGATE_LINK_NEXT:
|
||||
return this.navigateController.openLinkNext(msg);
|
||||
case messages.NAVIGATE_LINK_PREV:
|
||||
return this.navigateController.openLinkPrev(msg);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import * as operations from '../../shared/operations';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
@injectable()
|
||||
export default class BackgroundClient {
|
||||
execBackgroundOp(op: operations.Operation): Promise<void> {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
operation: op,
|
||||
});
|
||||
}
|
||||
}
|
33
src/content/client/OperationClient.ts
Normal file
33
src/content/client/OperationClient.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import * as operations from '../../shared/operations';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
export default interface OperationClient {
|
||||
execBackgroundOp(op: operations.Operation): Promise<void>;
|
||||
|
||||
internalOpenUrl(
|
||||
url: string, newTab?: boolean, background?: boolean,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export class OperationClientImpl implements OperationClient {
|
||||
execBackgroundOp(op: operations.Operation): Promise<void> {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
operation: op,
|
||||
});
|
||||
}
|
||||
|
||||
internalOpenUrl(
|
||||
url: string, newTab?: boolean, background?: boolean,
|
||||
): Promise<void> {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
operation: {
|
||||
type: operations.INTERNAL_OPEN_URL,
|
||||
url,
|
||||
newTab,
|
||||
background,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,10 +4,9 @@ import KeymapUseCase from '../usecases/KeymapUseCase';
|
|||
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase';
|
||||
import FindSlaveUseCase from '../usecases/FindSlaveUseCase';
|
||||
import ScrollUseCase from '../usecases/ScrollUseCase';
|
||||
import NavigateUseCase from '../usecases/NavigateUseCase';
|
||||
import FocusUseCase from '../usecases/FocusUseCase';
|
||||
import ClipboardUseCase from '../usecases/ClipboardUseCase';
|
||||
import BackgroundClient from '../client/BackgroundClient';
|
||||
import OperationClient from '../client/OperationClient';
|
||||
import MarkKeyyUseCase from '../usecases/MarkKeyUseCase';
|
||||
import FollowMasterClient from '../client/FollowMasterClient';
|
||||
import Key from '../domains/Key';
|
||||
|
@ -19,12 +18,13 @@ export default class KeymapController {
|
|||
private addonEnabledUseCase: AddonEnabledUseCase,
|
||||
private findSlaveUseCase: FindSlaveUseCase,
|
||||
private scrollUseCase: ScrollUseCase,
|
||||
private navigateUseCase: NavigateUseCase,
|
||||
private focusUseCase: FocusUseCase,
|
||||
private clipbaordUseCase: ClipboardUseCase,
|
||||
private backgroundClient: BackgroundClient,
|
||||
private markKeyUseCase: MarkKeyyUseCase,
|
||||
|
||||
@inject('OperationClient')
|
||||
private backgroundClient: OperationClient,
|
||||
|
||||
@inject('FollowMasterClient')
|
||||
private followMasterClient: FollowMasterClient,
|
||||
) {
|
||||
|
@ -84,24 +84,6 @@ export default class KeymapController {
|
|||
case operations.MARK_JUMP_PREFIX:
|
||||
this.markKeyUseCase.enableJumpMode();
|
||||
break;
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
this.navigateUseCase.openHistoryPrev();
|
||||
break;
|
||||
case operations.NAVIGATE_HISTORY_NEXT:
|
||||
this.navigateUseCase.openHistoryNext();
|
||||
break;
|
||||
case operations.NAVIGATE_LINK_PREV:
|
||||
this.navigateUseCase.openLinkPrev();
|
||||
break;
|
||||
case operations.NAVIGATE_LINK_NEXT:
|
||||
this.navigateUseCase.openLinkNext();
|
||||
break;
|
||||
case operations.NAVIGATE_PARENT:
|
||||
this.navigateUseCase.openParent();
|
||||
break;
|
||||
case operations.NAVIGATE_ROOT:
|
||||
this.navigateUseCase.openRoot();
|
||||
break;
|
||||
case operations.FOCUS_INPUT:
|
||||
this.focusUseCase.focusFirstInput();
|
||||
break;
|
||||
|
|
31
src/content/controllers/NavigateController.ts
Normal file
31
src/content/controllers/NavigateController.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import { Message } from '../../shared/messages';
|
||||
import NavigateUseCase from '../usecases/NavigateUseCase';
|
||||
|
||||
@injectable()
|
||||
export default class NavigateController {
|
||||
constructor(
|
||||
private navigateUseCase: NavigateUseCase,
|
||||
) {
|
||||
}
|
||||
|
||||
openHistoryNext(_m: Message): Promise<void> {
|
||||
this.navigateUseCase.openHistoryNext();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
openHistoryPrev(_m: Message): Promise<void> {
|
||||
this.navigateUseCase.openHistoryPrev();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
openLinkNext(_m: Message): Promise<void> {
|
||||
this.navigateUseCase.openLinkNext();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
openLinkPrev(_m: Message): Promise<void> {
|
||||
this.navigateUseCase.openLinkPrev();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import { MarkClientImpl } from './client/MarkClient';
|
|||
import { MarkKeyRepositoryImpl } from './repositories/MarkKeyRepository';
|
||||
import { MarkRepositoryImpl } from './repositories/MarkRepository';
|
||||
import { NavigationPresenterImpl } from './presenters/NavigationPresenter';
|
||||
import { OperationClientImpl } from './client/OperationClient';
|
||||
import { ScrollPresenterImpl } from './presenters/ScrollPresenter';
|
||||
import { SettingClientImpl } from './client/SettingClient';
|
||||
import { SettingRepositoryImpl } from './repositories/SettingRepository';
|
||||
|
@ -48,6 +49,7 @@ container.register('MarkClient', { useClass: MarkClientImpl });
|
|||
container.register('MarkKeyRepository', { useClass: MarkKeyRepositoryImpl });
|
||||
container.register('MarkRepository', { useClass: MarkRepositoryImpl });
|
||||
container.register('NavigationPresenter', { useClass: NavigationPresenterImpl });
|
||||
container.register('OperationClient', { useClass: OperationClientImpl });
|
||||
container.register('ScrollPresenter', { useClass: ScrollPresenterImpl });
|
||||
container.register('SettingClient', { useClass: SettingClientImpl });
|
||||
container.register('SettingRepository', { useClass: SettingRepositoryImpl });
|
||||
|
|
|
@ -6,10 +6,6 @@ export default interface NavigationPresenter {
|
|||
openLinkPrev(): void;
|
||||
|
||||
openLinkNext(): void;
|
||||
|
||||
openParent(): void;
|
||||
|
||||
openRoot(): void;
|
||||
}
|
||||
|
||||
const REL_PATTERN: {[key: string]: RegExp} = {
|
||||
|
@ -51,29 +47,6 @@ export class NavigationPresenterImpl implements NavigationPresenter {
|
|||
this.linkRel('next');
|
||||
}
|
||||
|
||||
openParent(): void {
|
||||
const loc = window.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
return;
|
||||
} else if (loc.search !== '') {
|
||||
loc.search = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const basenamePattern = /\/[^/]+$/;
|
||||
const lastDirPattern = /\/[^/]+\/$/;
|
||||
if (basenamePattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(basenamePattern, '/');
|
||||
} else if (lastDirPattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(lastDirPattern, '/');
|
||||
}
|
||||
}
|
||||
|
||||
openRoot(): void {
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
private linkRel(rel: 'prev' | 'next'): void {
|
||||
let link = selectLast<HTMLLinkElement>(`link[rel~=${rel}][href]`);
|
||||
|
|
|
@ -2,16 +2,16 @@ import { injectable, inject } from 'tsyringe';
|
|||
import * as urls from '../../shared/urls';
|
||||
import ClipboardRepository from '../repositories/ClipboardRepository';
|
||||
import SettingRepository from '../repositories/SettingRepository';
|
||||
import TabsClient from '../client/TabsClient';
|
||||
import ConsoleClient from '../client/ConsoleClient';
|
||||
import OperationClient from '../client/OperationClient';
|
||||
|
||||
@injectable()
|
||||
export default class ClipboardUseCase {
|
||||
constructor(
|
||||
@inject('ClipboardRepository') private repository: ClipboardRepository,
|
||||
@inject('SettingRepository') private settingRepository: SettingRepository,
|
||||
@inject('TabsClient') private client: TabsClient,
|
||||
@inject('ConsoleClient') private consoleClient: ConsoleClient,
|
||||
@inject('OperationClient') private operationClinet: OperationClient,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,10 @@ export default class ClipboardUseCase {
|
|||
let search = this.settingRepository.get().search;
|
||||
let text = this.repository.read();
|
||||
let url = urls.searchUrl(text, search);
|
||||
await this.client.openUrl(url, newTab);
|
||||
|
||||
// TODO: Repeat pasting from clipboard instead of opening a certain url.
|
||||
// 'Repeat last' command is implemented in the background script and cannot
|
||||
// access to clipboard until Firefox 63.
|
||||
await this.operationClinet.internalOpenUrl(url, newTab);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,12 +24,4 @@ export default class NavigateUseCase {
|
|||
openLinkNext(): void {
|
||||
this.navigationPresenter.openLinkNext();
|
||||
}
|
||||
|
||||
openParent(): void {
|
||||
this.navigationPresenter.openParent();
|
||||
}
|
||||
|
||||
openRoot(): void {
|
||||
this.navigationPresenter.openRoot();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ const fields = [
|
|||
['zoom.out', 'Zoom-out'],
|
||||
['zoom.neutral', 'Reset zoom level'],
|
||||
['page.source', 'Open a page source'],
|
||||
['repeat.last', 'Repeat last change'],
|
||||
]
|
||||
];
|
||||
|
||||
|
|
|
@ -390,6 +390,7 @@ export const DefaultSettingData: SettingData = SettingData.valueOf({
|
|||
"/": { "type": "find.start" },
|
||||
"n": { "type": "find.next" },
|
||||
"N": { "type": "find.prev" },
|
||||
".": { "type": "repeat.last" },
|
||||
"<S-Esc>": { "type": "addon.toggle.enabled" }
|
||||
},
|
||||
"search": {
|
||||
|
|
|
@ -177,6 +177,7 @@ export const DefaultSetting: Settings = {
|
|||
'/': { 'type': 'find.start' },
|
||||
'n': { 'type': 'find.next' },
|
||||
'N': { 'type': 'find.prev' },
|
||||
'.': { 'type': 'repeat.last' },
|
||||
'<S-Esc>': { 'type': 'addon.toggle.enabled' }
|
||||
},
|
||||
search: {
|
||||
|
|
|
@ -42,6 +42,11 @@ export const SETTINGS_QUERY = 'settings.query';
|
|||
|
||||
export const CONSOLE_FRAME_MESSAGE = 'console.frame.message';
|
||||
|
||||
export const NAVIGATE_HISTORY_NEXT = 'navigate.history.next';
|
||||
export const NAVIGATE_HISTORY_PREV = 'navigate.history.prev';
|
||||
export const NAVIGATE_LINK_NEXT = 'navigate.link.next';
|
||||
export const NAVIGATE_LINK_PREV = 'navigate.link.prev';
|
||||
|
||||
export interface BackgroundOperationMessage {
|
||||
type: typeof BACKGROUND_OPERATION;
|
||||
operation: operations.Operation;
|
||||
|
@ -204,6 +209,22 @@ export interface ConsoleFrameMessageMessage {
|
|||
message: any;
|
||||
}
|
||||
|
||||
export interface NavigateHistoryNextMessage {
|
||||
type: typeof NAVIGATE_HISTORY_NEXT;
|
||||
}
|
||||
|
||||
export interface NavigateHistoryPrevMessage {
|
||||
type: typeof NAVIGATE_HISTORY_PREV;
|
||||
}
|
||||
|
||||
export interface NavigateLinkNext {
|
||||
type: typeof NAVIGATE_LINK_NEXT;
|
||||
}
|
||||
|
||||
export interface NavigateLinkPrev {
|
||||
type: typeof NAVIGATE_LINK_PREV;
|
||||
}
|
||||
|
||||
export type Message =
|
||||
BackgroundOperationMessage |
|
||||
ConsoleUnfocusMessage |
|
||||
|
@ -236,7 +257,11 @@ export type Message =
|
|||
OpenUrlMessage |
|
||||
SettingsChangedMessage |
|
||||
SettingsQueryMessage |
|
||||
ConsoleFrameMessageMessage;
|
||||
ConsoleFrameMessageMessage |
|
||||
NavigateHistoryNextMessage |
|
||||
NavigateHistoryPrevMessage |
|
||||
NavigateLinkNext |
|
||||
NavigateLinkPrev;
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export const valueOf = (o: any): Message => {
|
||||
|
@ -272,6 +297,10 @@ export const valueOf = (o: any): Message => {
|
|||
case SETTINGS_CHANGED:
|
||||
case SETTINGS_QUERY:
|
||||
case CONSOLE_FRAME_MESSAGE:
|
||||
case NAVIGATE_HISTORY_NEXT:
|
||||
case NAVIGATE_HISTORY_PREV:
|
||||
case NAVIGATE_LINK_NEXT:
|
||||
case NAVIGATE_LINK_PREV:
|
||||
return o;
|
||||
}
|
||||
throw new Error('unknown operation type: ' + o.type);
|
||||
|
|
|
@ -75,6 +75,12 @@ export const FIND_PREV = 'find.prev';
|
|||
export const MARK_SET_PREFIX = 'mark.set.prefix';
|
||||
export const MARK_JUMP_PREFIX = 'mark.jump.prefix';
|
||||
|
||||
// Repeat
|
||||
export const REPEAT_LAST = 'repeat.last';
|
||||
|
||||
// Internal
|
||||
export const INTERNAL_OPEN_URL = 'internal.open.url';
|
||||
|
||||
export interface CancelOperation {
|
||||
type: typeof CANCEL;
|
||||
}
|
||||
|
@ -291,6 +297,18 @@ export interface MarkJumpPrefixOperation {
|
|||
type: typeof MARK_JUMP_PREFIX;
|
||||
}
|
||||
|
||||
export interface RepeatLastOperation {
|
||||
type: typeof REPEAT_LAST;
|
||||
}
|
||||
|
||||
export interface InternalOpenUrl {
|
||||
type: typeof INTERNAL_OPEN_URL;
|
||||
url: string;
|
||||
newTab?: boolean;
|
||||
newWindow?: boolean;
|
||||
background?: boolean;
|
||||
}
|
||||
|
||||
export type Operation =
|
||||
CancelOperation |
|
||||
AddonEnableOperation |
|
||||
|
@ -342,7 +360,9 @@ export type Operation =
|
|||
FindNextOperation |
|
||||
FindPrevOperation |
|
||||
MarkSetPrefixOperation |
|
||||
MarkJumpPrefixOperation;
|
||||
MarkJumpPrefixOperation |
|
||||
RepeatLastOperation |
|
||||
InternalOpenUrl;
|
||||
|
||||
const assertOptionalBoolean = (obj: any, name: string) => {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, name) &&
|
||||
|
@ -358,6 +378,13 @@ const assertRequiredNumber = (obj: any, name: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const assertRequiredString = (obj: any, name: string) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, name) ||
|
||||
typeof obj[name] !== 'string') {
|
||||
throw new TypeError(`Missing string parameter '${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
export const valueOf = (o: any): Operation => {
|
||||
if (!Object.prototype.hasOwnProperty.call(o, 'type')) {
|
||||
|
@ -401,6 +428,18 @@ export const valueOf = (o: any): Operation => {
|
|||
type: URLS_PASTE,
|
||||
newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
|
||||
};
|
||||
case INTERNAL_OPEN_URL:
|
||||
assertOptionalBoolean(o, 'newTab');
|
||||
assertOptionalBoolean(o, 'newWindow');
|
||||
assertOptionalBoolean(o, 'background');
|
||||
assertRequiredString(o, 'url');
|
||||
return {
|
||||
type: INTERNAL_OPEN_URL,
|
||||
url: o.url,
|
||||
newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
|
||||
newWindow: Boolean(typeof o.newWindow === undefined ? false : o.newWindow), // eslint-disable-line max-len
|
||||
background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len
|
||||
};
|
||||
case CANCEL:
|
||||
case ADDON_ENABLE:
|
||||
case ADDON_DISABLE:
|
||||
|
@ -441,6 +480,7 @@ export const valueOf = (o: any): Operation => {
|
|||
case FIND_PREV:
|
||||
case MARK_SET_PREFIX:
|
||||
case MARK_JUMP_PREFIX:
|
||||
case REPEAT_LAST:
|
||||
return { type: o.type };
|
||||
}
|
||||
throw new TypeError('unknown operation type: ' + o.type);
|
||||
|
|
82
test/background/usecases/NavigateUseCase.test.ts
Normal file
82
test/background/usecases/NavigateUseCase.test.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import TabPresenter from '../../../src/background/presenters/TabPresenter';
|
||||
import NavigateUseCase from '../../../src/background/usecases/NavigateUseCase';
|
||||
import NavigateClient from '../../../src/background/clients/NavigateClient';
|
||||
// import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
describe('NavigateUseCase', () => {
|
||||
let sut: NavigateUseCase;
|
||||
let tabPresenter: TabPresenter;
|
||||
let navigateClient: NavigateClient;
|
||||
beforeEach(() => {
|
||||
tabPresenter = new TabPresenter();
|
||||
navigateClient = new NavigateClient();
|
||||
sut = new NavigateUseCase(tabPresenter, navigateClient);
|
||||
});
|
||||
|
||||
describe('#openParent()', async () => {
|
||||
it('opens parent directory of file', async() => {
|
||||
var stub = sinon.stub(tabPresenter, 'getCurrent');
|
||||
stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/banana' }))
|
||||
|
||||
var mock = sinon.mock(tabPresenter);
|
||||
mock.expects('open').withArgs('https://google.com/fruits/yellow/');
|
||||
|
||||
await sut.openParent();
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it('opens parent directory of directory', async() => {
|
||||
var stub = sinon.stub(tabPresenter, 'getCurrent');
|
||||
stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/' }))
|
||||
|
||||
var mock = sinon.mock(tabPresenter);
|
||||
mock.expects('open').withArgs('https://google.com/fruits/');
|
||||
|
||||
await sut.openParent();
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it('removes hash', async() => {
|
||||
var stub = sinon.stub(tabPresenter, 'getCurrent');
|
||||
stub.returns(Promise.resolve({ url: 'https://google.com/#top' }))
|
||||
|
||||
var mock = sinon.mock(tabPresenter);
|
||||
mock.expects('open').withArgs('https://google.com/');
|
||||
|
||||
await sut.openParent();
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it('removes search query', async() => {
|
||||
var stub = sinon.stub(tabPresenter, 'getCurrent');
|
||||
stub.returns(Promise.resolve({ url: 'https://google.com/search?q=apple' }))
|
||||
|
||||
var mock = sinon.mock(tabPresenter);
|
||||
mock.expects('open').withArgs('https://google.com/search');
|
||||
|
||||
await sut.openParent();
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#openRoot()', () => {
|
||||
it('opens root direectory', async() => {
|
||||
var stub = sinon.stub(tabPresenter, 'getCurrent');
|
||||
stub.returns(Promise.resolve({
|
||||
url: 'https://google.com/seach?q=apple',
|
||||
}))
|
||||
|
||||
var mock = sinon.mock(tabPresenter);
|
||||
mock.expects('open').withArgs('https://google.com');
|
||||
|
||||
await sut.openRoot();
|
||||
|
||||
mock.verify();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -132,13 +132,4 @@ describe('NavigationPresenter', () => {
|
|||
'<a href="#dummy">next page</a><a rel="next" href="#next">click me</a>'
|
||||
));
|
||||
});
|
||||
|
||||
describe('#parent', () => {
|
||||
// NOTE: not able to test location
|
||||
it('removes hash', () => {
|
||||
window.location.hash = '#section-1';
|
||||
sut.openParent();
|
||||
expect(document.location.hash).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,82 +1,69 @@
|
|||
import ClipboardRepository from '../../../src/content/repositories/ClipboardRepository';
|
||||
import { SettingRepositoryImpl } from '../../../src/content/repositories/SettingRepository';
|
||||
import TabsClient from '../../../src/content/client/TabsClient';
|
||||
import MockConsoleClient from '../mock/MockConsoleClient';
|
||||
import ClipboardUseCase from '../../../src/content/usecases/ClipboardUseCase';
|
||||
import OperationClient from '../../../src/content/client/OperationClient';
|
||||
import ConsoleClient from '../../../src/content/client/ConsoleClient';
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
|
||||
class MockClipboardRepository implements ClipboardRepository {
|
||||
public clipboard: string;
|
||||
|
||||
constructor() {
|
||||
this.clipboard = '';
|
||||
}
|
||||
|
||||
read(): string {
|
||||
return this.clipboard;
|
||||
}
|
||||
|
||||
write(text: string): void {
|
||||
this.clipboard = text;
|
||||
}
|
||||
}
|
||||
|
||||
class MockTabsClient implements TabsClient {
|
||||
public last: string;
|
||||
|
||||
constructor() {
|
||||
this.last = '';
|
||||
}
|
||||
|
||||
openUrl(url: string, _newTab: boolean): Promise<void> {
|
||||
this.last = url;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ClipboardUseCase', () => {
|
||||
let repository: MockClipboardRepository;
|
||||
let client: MockTabsClient;
|
||||
let consoleClient: MockConsoleClient;
|
||||
let clipboardRepository: ClipboardRepository;
|
||||
let operationClient: OperationClient;
|
||||
let consoleClient: ConsoleClient;
|
||||
let sut: ClipboardUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
repository = new MockClipboardRepository();
|
||||
client = new MockTabsClient();
|
||||
consoleClient = new MockConsoleClient();
|
||||
var modal = <ConsoleClient>{};
|
||||
|
||||
clipboardRepository = <ClipboardRepository>{ read() {}, write(_) {} };
|
||||
operationClient = <OperationClient>{ internalOpenUrl(_) {} };
|
||||
consoleClient = <ConsoleClient>{ info(_) {}};
|
||||
sut = new ClipboardUseCase(
|
||||
repository,
|
||||
clipboardRepository,
|
||||
new SettingRepositoryImpl(),
|
||||
client,
|
||||
consoleClient
|
||||
consoleClient,
|
||||
operationClient,
|
||||
);
|
||||
});
|
||||
|
||||
describe('#yankCurrentURL', () => {
|
||||
it('yanks current url', async () => {
|
||||
let href = window.location.href;
|
||||
var mockRepository = sinon.mock(clipboardRepository);
|
||||
mockRepository.expects('write').withArgs(href);
|
||||
var mockConsoleClient = sinon.mock(consoleClient);
|
||||
mockConsoleClient.expects('info').withArgs('Yanked ' + href);
|
||||
|
||||
let yanked = await sut.yankCurrentURL();
|
||||
|
||||
expect(yanked).to.equal(window.location.href);
|
||||
expect(repository.clipboard).to.equal(yanked);
|
||||
expect(consoleClient.text).to.equal('Yanked ' + yanked);
|
||||
expect(yanked).to.equal(href);
|
||||
mockRepository.verify();
|
||||
mockConsoleClient.verify();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#openOrSearch', () => {
|
||||
it('opens url from the clipboard', async () => {
|
||||
let url = 'https://github.com/ueokande/vim-vixen'
|
||||
repository.clipboard = url;
|
||||
sinon.stub(clipboardRepository, 'read').returns(url);
|
||||
let mockOperationClient = sinon.mock(operationClient);
|
||||
mockOperationClient.expects('internalOpenUrl').withArgs(url, true);
|
||||
|
||||
await sut.openOrSearch(true);
|
||||
|
||||
expect(client.last).to.equal(url);
|
||||
mockOperationClient.verify();
|
||||
});
|
||||
|
||||
it('opens search results from the clipboard', async () => {
|
||||
repository.clipboard = 'banana';
|
||||
let url = 'https://google.com/search?q=banana';
|
||||
sinon.stub(clipboardRepository, 'read').returns('banana');
|
||||
let mockOperationClient = sinon.mock(operationClient);
|
||||
mockOperationClient.expects('internalOpenUrl').withArgs(url, true);
|
||||
|
||||
await sut.openOrSearch(true);
|
||||
|
||||
expect(client.last).to.equal('https://google.com/search?q=banana');
|
||||
mockOperationClient.verify();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Reference in a new issue