aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--e2e/completion.test.ts2
-rw-r--r--src/background/completion/BookmarkRepository.ts8
-rw-r--r--src/background/completion/HistoryRepository.ts8
-rw-r--r--src/background/completion/OpenCompletionUseCase.ts59
-rw-r--r--src/background/completion/PropertyCompletionUseCase.ts16
-rw-r--r--src/background/completion/TabCompletionUseCase.ts55
-rw-r--r--src/background/completion/TabItem.ts11
-rw-r--r--src/background/completion/TabRepository.ts14
-rw-r--r--src/background/completion/impl/BookmarkRepositoryImpl.ts28
-rw-r--r--src/background/completion/impl/HistoryRepositoryImpl.ts28
-rw-r--r--src/background/completion/impl/TabRepositoryImpl.ts41
-rw-r--r--src/background/completion/impl/filters.ts (renamed from src/background/usecases/filters.ts)0
-rw-r--r--src/background/controllers/CommandController.ts38
-rw-r--r--src/background/controllers/CompletionController.ts47
-rw-r--r--src/background/di.ts8
-rw-r--r--src/background/domains/CommandDocs.ts12
-rw-r--r--src/background/domains/CompletionGroup.ts6
-rw-r--r--src/background/domains/CompletionItem.ts6
-rw-r--r--src/background/infrastructures/ContentMessageListener.ts22
-rw-r--r--src/background/presenters/TabPresenter.ts65
-rw-r--r--src/background/repositories/CompletionsRepository.ts40
-rw-r--r--src/background/usecases/AddonEnabledUseCase.ts4
-rw-r--r--src/background/usecases/CommandUseCase.ts4
-rw-r--r--src/background/usecases/CompletionsUseCase.ts220
-rw-r--r--src/background/usecases/ConsoleUseCase.ts4
-rw-r--r--src/background/usecases/FindUseCase.ts4
-rw-r--r--src/background/usecases/LinkUseCase.ts4
-rw-r--r--src/background/usecases/MarkUseCase.ts4
-rw-r--r--src/background/usecases/NavigateUseCase.ts4
-rw-r--r--src/background/usecases/TabSelectUseCase.ts4
-rw-r--r--src/background/usecases/TabUseCase.ts4
-rw-r--r--src/background/usecases/VersionUseCase.ts2
-rw-r--r--src/background/usecases/ZoomUseCase.ts4
-rw-r--r--src/console/Completions.ts11
-rw-r--r--src/console/actions/console.ts193
-rw-r--r--src/console/actions/index.ts27
-rw-r--r--src/console/clients/CompletionClient.ts84
-rw-r--r--src/console/commandline/CommandLineParser.ts38
-rw-r--r--src/console/commandline/CommandParser.ts52
-rw-r--r--src/console/components/Console.tsx42
-rw-r--r--src/console/index.tsx4
-rw-r--r--src/console/reducers/index.ts7
-rw-r--r--src/shared/Command.ts44
-rw-r--r--src/shared/CompletionType.ts7
-rw-r--r--src/shared/TabFlag.ts7
-rw-r--r--src/shared/messages.ts80
-rw-r--r--test/background/completion/OpenCompletionUseCase.test.ts131
-rw-r--r--test/background/completion/PropertyCompletionUseCase.test.ts15
-rw-r--r--test/background/completion/TabCompletionUseCase.test.ts144
-rw-r--r--test/background/completion/impl/filters.test.ts114
-rw-r--r--test/background/usecases/NavigateUseCase.test.ts127
-rw-r--r--test/background/usecases/filters.test.ts113
-rw-r--r--test/console/actions/console.test.ts9
-rw-r--r--test/console/commandline/CommandLineParser.test.ts29
-rw-r--r--test/console/commandline/CommandParser.test.ts15
55 files changed, 1512 insertions, 557 deletions
diff --git a/e2e/completion.test.ts b/e2e/completion.test.ts
index dd4477f..1ce430e 100644
--- a/e2e/completion.test.ts
+++ b/e2e/completion.test.ts
@@ -33,7 +33,7 @@ describe("general completion test", () => {
const console = await page.showConsole();
const items = await console.getCompletions();
- assert.strictEqual(items.length, 11);
+ assert.strictEqual(items.length, 12);
assert.deepStrictEqual(items[0], { type: 'title', text: 'Console Command' });
assert.ok(items[1].text.startsWith('set'));
assert.ok(items[2].text.startsWith('open'));
diff --git a/src/background/completion/BookmarkRepository.ts b/src/background/completion/BookmarkRepository.ts
new file mode 100644
index 0000000..14105c8
--- /dev/null
+++ b/src/background/completion/BookmarkRepository.ts
@@ -0,0 +1,8 @@
+export type BookmarkItem = {
+ title: string
+ url: string
+}
+
+export default interface BookmarkRepository {
+ queryBookmarks(query: string): Promise<BookmarkItem[]>;
+}
diff --git a/src/background/completion/HistoryRepository.ts b/src/background/completion/HistoryRepository.ts
new file mode 100644
index 0000000..5eb3a2b
--- /dev/null
+++ b/src/background/completion/HistoryRepository.ts
@@ -0,0 +1,8 @@
+export type HistoryItem = {
+ title: string
+ url: string
+}
+
+export default interface HistoryRepository {
+ queryHistories(keywords: string): Promise<HistoryItem[]>;
+}
diff --git a/src/background/completion/OpenCompletionUseCase.ts b/src/background/completion/OpenCompletionUseCase.ts
new file mode 100644
index 0000000..1b63e7c
--- /dev/null
+++ b/src/background/completion/OpenCompletionUseCase.ts
@@ -0,0 +1,59 @@
+import { inject, injectable } from "tsyringe";
+import CachedSettingRepository from "../repositories/CachedSettingRepository";
+import CompletionType from "../../shared/CompletionType";
+import BookmarkRepository from "./BookmarkRepository";
+import HistoryRepository from "./HistoryRepository";
+
+export type BookmarkItem = {
+ title: string
+ url: string
+}
+
+export type HistoryItem = {
+ title: string
+ url: string
+}
+
+@injectable()
+export default class OpenCompletionUseCase {
+ constructor(
+ @inject('BookmarkRepository') private bookmarkRepository: BookmarkRepository,
+ @inject('HistoryRepository') private historyRepository: HistoryRepository,
+ @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository,
+ ) {
+ }
+
+ async getCompletionTypes(): Promise<CompletionType[]> {
+ const settings = await this.cachedSettingRepository.get();
+ const types: CompletionType[] = [];
+ for (const c of settings.properties.complete) {
+ switch (c) {
+ case 's':
+ types.push(CompletionType.SearchEngines);
+ break;
+ case 'h':
+ types.push(CompletionType.History);
+ break;
+ case 'b':
+ types.push(CompletionType.Bookmarks);
+ break;
+ }
+ // ignore invalid characters in the complete property
+ }
+ return types;
+ }
+
+ async requestSearchEngines(query: string): Promise<string[]> {
+ const settings = await this.cachedSettingRepository.get();
+ return Object.keys(settings.search.engines)
+ .filter(key => key.startsWith(query))
+ }
+
+ requestBookmarks(query: string): Promise<BookmarkItem[]> {
+ return this.bookmarkRepository.queryBookmarks(query);
+ }
+
+ requestHistory(query: string): Promise<HistoryItem[]> {
+ return this.historyRepository.queryHistories(query);
+ }
+} \ No newline at end of file
diff --git a/src/background/completion/PropertyCompletionUseCase.ts b/src/background/completion/PropertyCompletionUseCase.ts
new file mode 100644
index 0000000..049cfb8
--- /dev/null
+++ b/src/background/completion/PropertyCompletionUseCase.ts
@@ -0,0 +1,16 @@
+import { injectable } from "tsyringe";
+import Properties from "../../shared/settings/Properties";
+
+type Property = {
+ name: string;
+ type: 'string' | 'boolean' | 'number';
+}
+@injectable()
+export default class PropertyCompletionUseCase {
+ async getProperties(): Promise<Property[]> {
+ return Properties.defs().map(def => ({
+ name: def.name,
+ type: def.type,
+ }));
+ }
+} \ No newline at end of file
diff --git a/src/background/completion/TabCompletionUseCase.ts b/src/background/completion/TabCompletionUseCase.ts
new file mode 100644
index 0000000..dec86e9
--- /dev/null
+++ b/src/background/completion/TabCompletionUseCase.ts
@@ -0,0 +1,55 @@
+import { inject, injectable } from "tsyringe";
+import TabItem from "./TabItem";
+import TabRepository, { Tab } from "./TabRepository";
+import TabPresenter from "../presenters/TabPresenter";
+import TabFlag from "../../shared/TabFlag";
+
+@injectable()
+export default class TabCompletionUseCase {
+ constructor(
+ @inject('TabRepository') private tabRepository: TabRepository,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
+ ) {
+ }
+
+ async queryTabs(query: string, excludePinned: boolean): Promise<TabItem[]> {
+ const lastTabId = await this.tabPresenter.getLastSelectedId();
+ const allTabs = await this.tabRepository.getAllTabs(excludePinned);
+ const num = parseInt(query, 10);
+ let tabs: Tab[] = [];
+ if (!isNaN(num)) {
+ const tab = allTabs.find(t => t.index === num - 1);
+ if (tab) {
+ tabs = [tab];
+ }
+ } else if (query == '%') {
+ const tab = allTabs.find(t => t.active);
+ if (tab) {
+ tabs = [tab];
+ }
+ } else if (query == '#') {
+ const tab = allTabs.find(t => t.id === lastTabId);
+ if (tab) {
+ tabs = [tab];
+ }
+ } else {
+ tabs = await this.tabRepository.queryTabs(query, excludePinned);
+ }
+
+ return tabs.map(tab => {
+ let flag = TabFlag.None;
+ if (tab.active) {
+ flag = TabFlag.CurrentTab
+ } else if (tab.id == lastTabId) {
+ flag = TabFlag.LastTab
+ }
+ return {
+ index: tab.index + 1,
+ flag: flag,
+ title: tab.title,
+ url: tab.url,
+ faviconUrl : tab.faviconUrl
+ }
+ });
+ }
+}
diff --git a/src/background/completion/TabItem.ts b/src/background/completion/TabItem.ts
new file mode 100644
index 0000000..630855a
--- /dev/null
+++ b/src/background/completion/TabItem.ts
@@ -0,0 +1,11 @@
+import TabFlag from "../../shared/TabFlag";
+
+type TabItem = {
+ index: number
+ flag: TabFlag
+ title: string
+ url: string
+ faviconUrl?: string
+}
+
+export default TabItem; \ No newline at end of file
diff --git a/src/background/completion/TabRepository.ts b/src/background/completion/TabRepository.ts
new file mode 100644
index 0000000..fe1b601
--- /dev/null
+++ b/src/background/completion/TabRepository.ts
@@ -0,0 +1,14 @@
+export type Tab = {
+ id: number
+ index: number
+ active: boolean
+ title: string
+ url: string
+ faviconUrl?: string
+}
+
+export default interface TabRepository {
+ queryTabs(query: string, excludePinned: boolean): Promise<Tab[]>;
+
+ getAllTabs(excludePinned: boolean): Promise<Tab[]>
+}
diff --git a/src/background/completion/impl/BookmarkRepositoryImpl.ts b/src/background/completion/impl/BookmarkRepositoryImpl.ts
new file mode 100644
index 0000000..58df129
--- /dev/null
+++ b/src/background/completion/impl/BookmarkRepositoryImpl.ts
@@ -0,0 +1,28 @@
+import { injectable } from "tsyringe";
+import BookmarkRepository, {BookmarkItem} from "../BookmarkRepository";
+
+const COMPLETION_ITEM_LIMIT = 10;
+
+@injectable()
+export default class BookmarkRepositoryImpl implements BookmarkRepository {
+ async queryBookmarks(query: string): Promise<BookmarkItem[]> {
+ const items = await browser.bookmarks.search({query});
+ return items
+ .filter(item => item.title && item.title.length > 0)
+ .filter(item => item.type === 'bookmark' && item.url)
+ .filter((item) => {
+ let url = undefined;
+ try {
+ url = new URL(item.url!!);
+ } catch (e) {
+ return false;
+ }
+ return url.protocol !== 'place:';
+ })
+ .slice(0, COMPLETION_ITEM_LIMIT)
+ .map(item => ({
+ title: item.title!!,
+ url: item.url!!,
+ }));
+ }
+}
diff --git a/src/background/completion/impl/HistoryRepositoryImpl.ts b/src/background/completion/impl/HistoryRepositoryImpl.ts
new file mode 100644
index 0000000..42691aa
--- /dev/null
+++ b/src/background/completion/impl/HistoryRepositoryImpl.ts
@@ -0,0 +1,28 @@
+import { injectable } from "tsyringe";
+import * as filters from "./filters";
+import HistoryRepository, {HistoryItem} from "../HistoryRepository";
+
+const COMPLETION_ITEM_LIMIT = 10;
+
+@injectable()
+export default class HistoryRepositoryImpl implements HistoryRepository {
+ async queryHistories(keywords: string): Promise<HistoryItem[]> {
+ const items = await browser.history.search({
+ text: keywords,
+ startTime: 0,
+ });
+
+ return [items]
+ .map(filters.filterBlankTitle)
+ .map(filters.filterHttp)
+ .map(filters.filterByTailingSlash)
+ .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT))
+ .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0]
+ .sort((x, y) => Number(y.visitCount) - Number(x.visitCount))
+ .slice(0, COMPLETION_ITEM_LIMIT)
+ .map(item => ({
+ title: item.title!!,
+ url: item.url!!,
+ }))
+ }
+}
diff --git a/src/background/completion/impl/TabRepositoryImpl.ts b/src/background/completion/impl/TabRepositoryImpl.ts
new file mode 100644
index 0000000..adcaba7
--- /dev/null
+++ b/src/background/completion/impl/TabRepositoryImpl.ts
@@ -0,0 +1,41 @@
+import TabRepository, { Tab } from "../TabRepository";
+
+const COMPLETION_ITEM_LIMIT = 10;
+
+export default class TabRepositoryImpl implements TabRepository {
+ async queryTabs(query: string, excludePinned: boolean): Promise<Tab[]> {
+ const tabs = await browser.tabs.query({ currentWindow: true });
+ return tabs
+ .filter((t) => {
+ return t.url && t.url.toLowerCase().includes(query.toLowerCase()) ||
+ t.title && t.title.toLowerCase().includes(query.toLowerCase());
+ })
+ .filter((t) => {
+ return !(excludePinned && t.pinned);
+ })
+ .filter(item => item.id && item.title && item.url)
+ .slice(0, COMPLETION_ITEM_LIMIT)
+ .map(TabRepositoryImpl.toEntity);
+ }
+
+ async getAllTabs(excludePinned: boolean): Promise<Tab[]> {
+ if (excludePinned) {
+ return (await browser.tabs.query({ currentWindow: true, pinned: true }))
+ .map(TabRepositoryImpl.toEntity)
+
+ }
+ return (await browser.tabs.query({ currentWindow: true }))
+ .map(TabRepositoryImpl.toEntity)
+ }
+
+ private static toEntity(tab: browser.tabs.Tab,): Tab {
+ return {
+ id: tab.id!!,
+ url: tab.url!!,
+ active: tab.active,
+ title: tab.title!!,
+ faviconUrl: tab.favIconUrl,
+ index: tab.index,
+ }
+ }
+}
diff --git a/src/background/usecases/filters.ts b/src/background/completion/impl/filters.ts
index 98957a7..98957a7 100644
--- a/src/background/usecases/filters.ts
+++ b/src/background/completion/impl/filters.ts
diff --git a/src/background/controllers/CommandController.ts b/src/background/controllers/CommandController.ts
index 7297ef8..16aa6e8 100644
--- a/src/background/controllers/CommandController.ts
+++ b/src/background/controllers/CommandController.ts
@@ -1,7 +1,5 @@
import { injectable } from 'tsyringe';
-import CompletionsUseCase from '../usecases/CompletionsUseCase';
import CommandUseCase from '../usecases/CommandUseCase';
-import CompletionGroup from '../domains/CompletionGroup';
const trimStart = (str: string): string => {
// NOTE String.trimStart is available on Firefox 61
@@ -11,46 +9,10 @@ const trimStart = (str: string): string => {
@injectable()
export default class CommandController {
constructor(
- private completionsUseCase: CompletionsUseCase,
private commandIndicator: CommandUseCase,
) {
}
- getCompletions(line: string): Promise<CompletionGroup[]> {
- const trimmed = trimStart(line);
- const words = trimmed.split(/ +/);
- const name = words[0];
- if (words.length === 1) {
- return this.completionsUseCase.queryConsoleCommand(name);
- }
- const keywords = trimStart(trimmed.slice(name.length));
- switch (words[0]) {
- case 'o':
- case 'open':
- case 't':
- case 'tabopen':
- case 'w':
- case 'winopen':
- return this.completionsUseCase.queryOpen(name, keywords);
- case 'b':
- case 'buffer':
- return this.completionsUseCase.queryBuffer(name, keywords);
- case 'bd':
- case 'bdel':
- case 'bdelete':
- case 'bdeletes':
- return this.completionsUseCase.queryBdelete(name, keywords);
- case 'bd!':
- case 'bdel!':
- case 'bdelete!':
- case 'bdeletes!':
- return this.completionsUseCase.queryBdeleteForce(name, keywords);
- case 'set':
- return this.completionsUseCase.querySet(name, keywords);
- }
- return Promise.resolve([]);
- }
-
// eslint-disable-next-line complexity
exec(line: string): Promise<any> {
const trimmed = trimStart(line);
diff --git a/src/background/controllers/CompletionController.ts b/src/background/controllers/CompletionController.ts
new file mode 100644
index 0000000..fb6137c
--- /dev/null
+++ b/src/background/controllers/CompletionController.ts
@@ -0,0 +1,47 @@
+import {
+ ConsoleGetCompletionTypesResponse,
+ ConsoleGetPropertiesResponse,
+ ConsoleRequestBookmarksResponse,
+ ConsoleRequestHistoryResponse,
+ ConsoleRequestSearchEnginesResponse,
+ ConsoleRequesttabsResponse
+} from "../../shared/messages";
+import { injectable } from "tsyringe";
+import OpenCompletionUseCase from "../completion/OpenCompletionUseCase";
+import TabCompletionUseCase from "../completion/TabCompletionUseCase";
+import PropertyCompletionUseCase from "../completion/PropertyCompletionUseCase";
+
+@injectable()
+export default class CompletionController {
+ constructor(
+ private completionUseCase: OpenCompletionUseCase,
+ private tabCompletionUseCase: TabCompletionUseCase,
+ private propertyCompletionUseCase: PropertyCompletionUseCase,
+ ) {
+ }
+
+ async getCompletionTypes(): Promise<ConsoleGetCompletionTypesResponse> {
+ return this.completionUseCase.getCompletionTypes();
+ }
+
+ async requestSearchEngines(query: string): Promise<ConsoleRequestSearchEnginesResponse> {
+ const items = await this.completionUseCase.requestSearchEngines(query);
+ return items.map(name => ({ title: name }));
+ }
+
+ async requestBookmarks(query: string): Promise<ConsoleRequestBookmarksResponse> {
+ return this.completionUseCase.requestBookmarks(query);
+ }
+
+ async requestHistory(query: string): Promise<ConsoleRequestHistoryResponse> {
+ return this.completionUseCase.requestHistory(query);
+ }
+
+ async queryTabs(query: string, excludePinned: boolean): Promise<ConsoleRequesttabsResponse> {
+ return this.tabCompletionUseCase.queryTabs(query, excludePinned);
+ }
+
+ async getProperties(): Promise<ConsoleGetPropertiesResponse> {
+ return this.propertyCompletionUseCase.getProperties();
+ }
+} \ No newline at end of file
diff --git a/src/background/di.ts b/src/background/di.ts
index 9fc230c..c186262 100644
--- a/src/background/di.ts
+++ b/src/background/di.ts
@@ -4,8 +4,16 @@ import { LocalSettingRepository, SyncSettingRepository } from "./repositories/Se
import { NotifierImpl } from "./presenters/Notifier";
import { CachedSettingRepositoryImpl } from "./repositories/CachedSettingRepository";
import { container } from 'tsyringe';
+import HistoryRepositoryImpl from "./completion/impl/HistoryRepositoryImpl";
+import BookmarkRepositoryImpl from "./completion/impl/BookmarkRepositoryImpl";
+import TabRepositoryImpl from "./completion/impl/TabRepositoryImpl";
+import {TabPresenterImpl} from "./presenters/TabPresenter";
container.register('LocalSettingRepository', { useValue: LocalSettingRepository });
container.register('SyncSettingRepository', { useClass: SyncSettingRepository });
container.register('CachedSettingRepository', { useClass: CachedSettingRepositoryImpl });
container.register('Notifier', { useClass: NotifierImpl });
+container.register('HistoryRepository', { useClass: HistoryRepositoryImpl });
+container.register('BookmarkRepository', { useClass: BookmarkRepositoryImpl });
+container.register('TabRepository', { useClass: TabRepositoryImpl });
+container.register('TabPresenter', { useClass: TabPresenterImpl });
diff --git a/src/background/domains/CommandDocs.ts b/src/background/domains/CommandDocs.ts
deleted file mode 100644
index e926851..0000000
--- a/src/background/domains/CommandDocs.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export default {
- set: 'Set a value of the property',
- open: 'Open a URL or search by keywords in current tab',
- tabopen: 'Open a URL or search by keywords in new tab',
- winopen: 'Open a URL or search by keywords in new window',
- buffer: 'Select tabs by matched keywords',
- bdelete: 'Close a certain tab matched by keywords',
- bdeletes: 'Close all tabs matched by keywords',
- quit: 'Close the current tab',
- quitall: 'Close all tabs',
- help: 'Open Vim Vixen help in new tab',
-} as {[key: string]: string};
diff --git a/src/background/domains/CompletionGroup.ts b/src/background/domains/CompletionGroup.ts
deleted file mode 100644
index 8e89bf0..0000000
--- a/src/background/domains/CompletionGroup.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import CompletionItem from './CompletionItem';
-
-export default interface CompletionGroup {
- name: string;
- items: CompletionItem[];
-}
diff --git a/src/background/domains/CompletionItem.ts b/src/background/domains/CompletionItem.ts
deleted file mode 100644
index 96f6ee6..0000000
--- a/src/background/domains/CompletionItem.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export default interface CompletionItem {
- readonly caption?: string;
- readonly content?: string;
- readonly url?: string;
- readonly icon?: string;
-}
diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts
index d063810..2fbb9cf 100644
--- a/src/background/infrastructures/ContentMessageListener.ts
+++ b/src/background/infrastructures/ContentMessageListener.ts
@@ -1,7 +1,6 @@
import { injectable } from 'tsyringe';
import * as messages from '../../shared/messages';
import * as operations from '../../shared/operations';
-import CompletionGroup from '../domains/CompletionGroup';
import CommandController from '../controllers/CommandController';
import SettingController from '../controllers/SettingController';
import FindController from '../controllers/FindController';
@@ -9,6 +8,7 @@ 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";
@injectable()
export default class ContentMessageListener {
@@ -17,6 +17,7 @@ export default class ContentMessageListener {
constructor(
private settingController: SettingController,
private commandController: CommandController,
+ private completionController: CompletionController,
private findController: FindController,
private addonEnabledController: AddonEnabledController,
private linkController: LinkController,
@@ -61,8 +62,18 @@ export default class ContentMessageListener {
message: messages.Message, senderTab: browser.tabs.Tab,
): Promise<any> | any {
switch (message.type) {
- case messages.CONSOLE_QUERY_COMPLETIONS:
- return this.onConsoleQueryCompletions(message.text);
+ case messages.CONSOLE_GET_COMPLETION_TYPES:
+ return this.completionController.getCompletionTypes();
+ case messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE:
+ return this.completionController.requestSearchEngines(message.query);
+ case messages.CONSOLE_REQUEST_BOOKMARKS:
+ return this.completionController.requestBookmarks(message.query);
+ case messages.CONSOLE_REQUEST_HISTORY:
+ return this.completionController.requestHistory(message.query);
+ case messages.CONSOLE_REQUEST_TABS:
+ return this.completionController.queryTabs(message.query, message.excludePinned);
+ case messages.CONSOLE_GET_PROPERTIES:
+ return this.completionController.getProperties();
case messages.CONSOLE_ENTER_COMMAND:
return this.onConsoleEnterCommand(message.text);
case messages.SETTINGS_QUERY:
@@ -93,11 +104,6 @@ export default class ContentMessageListener {
throw new Error('unsupported message: ' + message.type);
}
- async onConsoleQueryCompletions(line: string): Promise<CompletionGroup[]> {
- const completions = await this.commandController.getCompletions(line);
- return Promise.resolve(completions);
- }
-
onConsoleEnterCommand(text: string): Promise<any> {
return this.commandController.exec(text);
}
diff --git a/src/background/presenters/TabPresenter.ts b/src/background/presenters/TabPresenter.ts
index 33d8bea..bded5a2 100644
--- a/src/background/presenters/TabPresenter.ts
+++ b/src/background/presenters/TabPresenter.ts
@@ -1,4 +1,3 @@
-import { injectable } from 'tsyringe';
import MemoryStorage from '../infrastructures/MemoryStorage';
const CURRENT_SELECTED_KEY = 'tabs.current.selected';
@@ -6,8 +5,41 @@ const LAST_SELECTED_KEY = 'tabs.last.selected';
type Tab = browser.tabs.Tab;
-@injectable()
-export default class TabPresenter {
+export default interface TabPresenter {
+ open(url: string, tabId?: number): Promise<Tab>;
+
+ create(url: string, opts?: object): Promise<Tab>;
+
+ getCurrent(): Promise<Tab>;
+
+ getAll(): Promise<Tab[]>;
+
+ getLastSelectedId(): Promise<number | undefined>;
+
+ getByKeyword(keyword: string, excludePinned: boolean): Promise<Tab[]>;
+
+ select(tabId: number): Promise<void>;
+
+ remove(ids: number[]): Promise<void>;
+
+ reopen(): Promise<void>;
+
+ reload(tabId: number, cache: boolean): Promise<void>;
+
+ setPinned(tabId: number, pinned: boolean): Promise<void>;
+
+ duplicate(id: number): Promise<Tab>;
+
+ getZoom(tabId: number): Promise<number>;
+
+ setZoom(tabId: number, factor: number): Promise<void>;
+
+ onSelected(
+ listener: (arg: { tabId: number, windowId: number}) => void,
+ ): void;
+}
+
+export class TabPresenterImpl implements TabPresenter {
open(url: string, tabId?: number): Promise<Tab> {
return browser.tabs.update(tabId, { url });
}
@@ -48,15 +80,15 @@ export default class TabPresenter {
});
}
- select(tabId: number): Promise<Tab> {
- return browser.tabs.update(tabId, { active: true });
+ async select(tabId: number): Promise<void> {
+ await browser.tabs.update(tabId, { active: true });
}
- remove(ids: number[]): Promise<void> {
- return browser.tabs.remove(ids);
+ async remove(ids: number[]): Promise<void> {
+ await browser.tabs.remove(ids);
}
- async reopen(): Promise<any> {
+ async reopen(): Promise<void> {
const window = await browser.windows.getCurrent();
const sessions = await browser.sessions.getRecentlyClosed();
const session = sessions.find((s) => {
@@ -66,19 +98,18 @@ export default class TabPresenter {
return;
}
if (session.tab && session.tab.sessionId) {
- return browser.sessions.restore(session.tab.sessionId);
- }
- if (session.window && session.window.sessionId) {
- return browser.sessions.restore(session.window.sessionId);
+ await browser.sessions.restore(session.tab.sessionId);
+ } else if (session.window && session.window.sessionId) {
+ await browser.sessions.restore(session.window.sessionId);
}
}
- reload(tabId: number, cache: boolean): Promise<void> {
- return browser.tabs.reload(tabId, { bypassCache: cache });
+ async reload(tabId: number, cache: boolean): Promise<void> {
+ await browser.tabs.reload(tabId, { bypassCache: cache });
}
- setPinned(tabId: number, pinned: boolean): Promise<Tab> {
- return browser.tabs.update(tabId, { pinned });
+ async setPinned(tabId: number, pinned: boolean): Promise<void> {
+ await browser.tabs.update(tabId, { pinned });
}
duplicate(id: number): Promise<Tab> {
@@ -100,7 +131,7 @@ export default class TabPresenter {
}
}
-const tabPresenter = new TabPresenter();
+const tabPresenter = new TabPresenterImpl();
tabPresenter.onSelected((tab: any) => {
const cache = new MemoryStorage();
diff --git a/src/background/repositories/CompletionsRepository.ts b/src/background/repositories/CompletionsRepository.ts
deleted file mode 100644
index dfecff0..0000000
--- a/src/background/repositories/CompletionsRepository.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { injectable } from 'tsyringe';
-
-type Tab = browser.tabs.Tab;
-type BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
-
-@injectable()
-export default class CompletionsRepository {
- async queryBookmarks(keywords: string): Promise<BookmarkTreeNode[]> {
- const items = await browser.bookmarks.search({ query: keywords });
- return items.filter((item) => {
- if (!item.url) {
- return false;
- }
- let url = undefined;
- try {
- url = new URL(item.url);
- } catch (e) {
- return false;
- }
- return item.type === 'bookmark' && url.protocol !== 'place:';
- });
- }
-
- queryHistories(keywords: string): Promise<browser.history.HistoryItem[]> {
- return browser.history.search({
- text: keywords,
- startTime: 0,
- });
- }
-
- async queryTabs(keywords: string, excludePinned: boolean): Promise<Tab[]> {
- const tabs = await browser.tabs.query({ currentWindow: true });
- return tabs.filter((t) => {
- return t.url && t.url.toLowerCase().includes(keywords.toLowerCase()) ||
- t.title && t.title.toLowerCase().includes(keywords.toLowerCase());
- }).filter((t) => {
- return !(excludePinned && t.pinned);
- });
- }
-}
diff --git a/src/background/usecases/AddonEnabledUseCase.ts b/src/background/usecases/AddonEnabledUseCase.ts
index 9abd3dc..51f02e1 100644
--- a/src/background/usecases/AddonEnabledUseCase.ts
+++ b/src/background/usecases/AddonEnabledUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import IndicatorPresenter from '../presenters/IndicatorPresenter';
import TabPresenter from '../presenters/TabPresenter';
import ContentMessageClient from '../infrastructures/ContentMessageClient';
@@ -7,7 +7,7 @@ import ContentMessageClient from '../infrastructures/ContentMessageClient';
export default class AddonEnabledUseCase {
constructor(
private indicatorPresentor: IndicatorPresenter,
- private tabPresenter: TabPresenter,
+ @inject("TabPresenter") private tabPresenter: TabPresenter,
private contentMessageClient: ContentMessageClient,
) {
this.indicatorPresentor.onClick((tab) => {
diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts
index 7dba664..d2d707e 100644
--- a/src/background/usecases/CommandUseCase.ts
+++ b/src/background/usecases/CommandUseCase.ts
@@ -14,7 +14,7 @@ import RepeatUseCase from '../usecases/RepeatUseCase';
@injectable()
export default class CommandIndicator {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private windowPresenter: WindowPresenter,
private helpPresenter: HelpPresenter,
@inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository,
@@ -80,7 +80,7 @@ export default class CommandIndicator {
}
const current = await this.tabPresenter.getCurrent();
- const tabs = await this.tabPresenter.getByKeyword(keywords);
+ const tabs = await this.tabPresenter.getByKeyword(keywords, false);
if (tabs.length === 0) {
throw new RangeError('No matching buffer for ' + keywords);
}
diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts
deleted file mode 100644
index 9874644..0000000
--- a/src/background/usecases/CompletionsUseCase.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-import { injectable, inject } from 'tsyringe';
-import CompletionGroup from '../domains/CompletionGroup';
-import CommandDocs from '../domains/CommandDocs';
-import CompletionsRepository from '../repositories/CompletionsRepository';
-import * as filters from './filters';
-import CachedSettingRepository from '../repositories/CachedSettingRepository';
-import TabPresenter from '../presenters/TabPresenter';
-import Properties from '../../shared/settings/Properties';
-
-const COMPLETION_ITEM_LIMIT = 10;
-
-type Tab = browser.tabs.Tab;
-type HistoryItem = browser.history.HistoryItem;
-
-@injectable()
-export default class CompletionsUseCase {
- constructor(
- private tabPresenter: TabPresenter,
- private completionsRepository: CompletionsRepository,
- @inject("CachedSettingRepository") private cachedSettingRepository: CachedSettingRepository,
- ) {
- }
-
- queryConsoleCommand(prefix: string): Promise<CompletionGroup[]> {
- const keys = Object.keys(CommandDocs);
- const items = keys
- .filter(name => name.startsWith(prefix))
- .map(name => ({
- caption: name,
- content: name,
- url: CommandDocs[name],
- }));
-
- if (items.length === 0) {
- return Promise.resolve([]);
- }
- return Promise.resolve([{ name: 'Console Command', items }]);
- }
-
- async queryOpen(name: string, keywords: string): Promise<CompletionGroup[]> {
- // TODO This logic contains view entities. They should be defined on
- // content script
-
- const settings = await this.cachedSettingRepository.get();
- const groups: CompletionGroup[] = [];
-
- const complete = settings.properties.complete;
- for (const c of complete) {
- if (c === 's') {
- // eslint-disable-next-line no-await-in-loop
- const engines = await this.querySearchEngineItems(name, keywords);
- if (engines.length > 0) {
- groups.push({ name: 'Search Engines', items: engines });
- }
- // browser.history not supported on Android
- } else if (c === 'h' && typeof browser.history === 'object') {
- // eslint-disable-next-line no-await-in-loop
- const histories = await this.queryHistoryItems(name, keywords);
- if (histories.length > 0) {
- groups.push({ name: 'History', items: histories });
- }
- // browser.bookmarks not supported on Android
- } else if (c === 'b' && typeof browser.bookmarks === 'object') {
- // eslint-disable-next-line no-await-in-loop
- const bookmarks = await this.queryBookmarkItems(name, keywords);
- if (bookmarks.length > 0) {
- groups.push({ name: 'Bookmarks', items: bookmarks });
- }
- }
- }
- return groups;
- }
-
- // eslint-disable-next-line max-statements
- async queryBuffer(
- name: string,
- keywords: string,
- ): Promise<CompletionGroup[]> {
- const lastId = await this.tabPresenter.getLastSelectedId();
- const trimmed = keywords.trim();
- let tabs: Tab[] = [];
- if (trimmed.length > 0 && !isNaN(Number(trimmed))) {
- const all = await this.tabPresenter.getAll();
- const index = parseInt(trimmed, 10) - 1;
- if (index >= 0 && index < all.length) {
- tabs = [all[index]];
- }
- } else if (trimmed === '%') {
- const all = await this.tabPresenter.getAll();
- const tab = all.find(t => t.active) as Tab;
- tabs = [tab];
- } else if (trimmed === '#') {
- if (typeof lastId !== 'undefined' && lastId !== null) {
- const all = await this.tabPresenter.getAll();
- const tab = all.find(t => t.id === lastId) as Tab;
- tabs = [tab];
- }
- } else {
- tabs = await this.completionsRepository.queryTabs(keywords, false);
- }
- const flag = (tab: Tab) => {
- if (tab.active) {
- return '%';
- } else if (tab.id === lastId) {
- return '#';
- }
- return ' ';
- };
- const items = tabs.map(tab => ({
- caption: tab.index + 1 + ': ' + flag(tab) + ' ' + tab.title,
- content: name + ' ' + tab.title,
- url: tab.url,
- icon: tab.favIconUrl,
- }));
- if (items.length === 0) {
- return Promise.resolve([]);
- }
- return [{ name: 'Buffers', items }];
- }
-
- queryBdelete(name: string, keywords: string): Promise<CompletionGroup[]> {
- return this.queryTabs(name, true, keywords);
- }
-
- queryBdeleteForce(
- name: string, keywords: string,
- ): Promise<CompletionGroup[]> {
- return this.queryTabs(name, false, keywords);
- }
-
- querySet(name: string, keywords: string): Promise<CompletionGroup[]> {
- const items = Properties.defs().map((def) => {
- if (def.type === 'boolean') {
- return [
- {
- caption: def.name,
- content: name + ' ' + def.name,
- url: 'Enable ' + def.description,
- }, {
- caption: 'no' + def.name,
- content: name + ' no' + def.name,
- url: 'Disable ' + def.description
- }
- ];
- }
- return [
- {
- caption: def.name,
- content: name + ' ' + def.name,
- url: 'Set ' + def.description,
- }
- ];
- });
- let flatten = items.reduce((acc, val) => acc.concat(val), []);
- flatten = flatten.filter((item) => {
- return item.caption.startsWith(keywords);
- });
- if (flatten.length === 0) {
- return Promise.resolve([]);
- }
- return Promise.resolve(
- [{ name: 'Properties', items: flatten }],
- );
- }
-
- async queryTabs(
- name: string, excludePinned: boolean, args: string,
- ): Promise<CompletionGroup[]> {
- const tabs = await this.completionsRepository.queryTabs(args, excludePinned);
- const items = tabs.map(tab => ({
- caption: tab.title,
- content: name + ' ' + tab.title,
- url: tab.url,
- icon: tab.favIconUrl
- }));
- if (items.length === 0) {
- return Promise.resolve([]);
- }
- return [{ name: 'Buffers', items }];
- }
-
- async querySearchEngineItems(name: string, keywords: string) {
- const settings = await this.cachedSettingRepository.get();
- const engines = Object.keys(settings.search.engines)
- .filter(key => key.startsWith(keywords));
- return engines.map(key => ({
- caption: key,
- content: name + ' ' + key,
- }));
- }
-
- async queryHistoryItems(name: string, keywords: string) {
- let histories = await this.completionsRepository.queryHistories(keywords);
- histories = [histories]
- .map(filters.filterBlankTitle)
- .map(filters.filterHttp)
- .map(filters.filterByTailingSlash)
- .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT))
- .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0]
- .sort((x: HistoryItem, y: HistoryItem): number => {
- return Number(y.visitCount) - Number(x.visitCount);
- })
- .slice(0, COMPLETION_ITEM_LIMIT);
- return histories.map(page => ({
- caption: page.title,
- content: name + ' ' + page.url,
- url: page.url
- }));
- }
-
- async queryBookmarkItems(name: string, keywords: string) {
- const bookmarks = await this.completionsRepository.queryBookmarks(keywords);
- return bookmarks.slice(0, COMPLETION_ITEM_LIMIT)
- .map(page => ({
- caption: page.title,
- content: name + ' ' + page.url,
- url: page.url
- }));
- }
-}
diff --git a/src/background/usecases/ConsoleUseCase.ts b/src/background/usecases/ConsoleUseCase.ts
index 775a1e0..2de5bc1 100644
--- a/src/background/usecases/ConsoleUseCase.ts
+++ b/src/background/usecases/ConsoleUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
import ConsoleClient from '../infrastructures/ConsoleClient';
@@ -6,7 +6,7 @@ import ConsoleClient from '../infrastructures/ConsoleClient';
export default class ConsoleUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private consoleClient: ConsoleClient,
) {
}
diff --git a/src/background/usecases/FindUseCase.ts b/src/background/usecases/FindUseCase.ts
index b8593c6..cb41cd5 100644
--- a/src/background/usecases/FindUseCase.ts
+++ b/src/background/usecases/FindUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import FindRepository from '../repositories/FindRepository';
import TabPresenter from '../presenters/TabPresenter';
import ConsoleClient from '../infrastructures/ConsoleClient';
@@ -6,7 +6,7 @@ import ConsoleClient from '../infrastructures/ConsoleClient';
@injectable()
export default class FindUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private findRepository: FindRepository,
private consoleClient: ConsoleClient,
) {
diff --git a/src/background/usecases/LinkUseCase.ts b/src/background/usecases/LinkUseCase.ts
index 9c0eab5..be076c7 100644
--- a/src/background/usecases/LinkUseCase.ts
+++ b/src/background/usecases/LinkUseCase.ts
@@ -1,10 +1,10 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
@injectable()
export default class LinkUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
) {
}
diff --git a/src/background/usecases/MarkUseCase.ts b/src/background/usecases/MarkUseCase.ts
index eeac40f..2c0bc13 100644
--- a/src/background/usecases/MarkUseCase.ts
+++ b/src/background/usecases/MarkUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
import MarkRepository from '../repositories/MarkRepository';
import ConsoleClient from '../infrastructures/ConsoleClient';
@@ -7,7 +7,7 @@ import ContentMessageClient from '../infrastructures/ContentMessageClient';
@injectable()
export default class MarkUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private markRepository: MarkRepository,
private consoleClient: ConsoleClient,
private contentMessageClient: ContentMessageClient,
diff --git a/src/background/usecases/NavigateUseCase.ts b/src/background/usecases/NavigateUseCase.ts
index 25e7f20..3aa1ed6 100644
--- a/src/background/usecases/NavigateUseCase.ts
+++ b/src/background/usecases/NavigateUseCase.ts
@@ -1,11 +1,11 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import NavigateClient from '../clients/NavigateClient';
import TabPresenter from '../presenters/TabPresenter';
@injectable()
export default class NavigateUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private navigateClient: NavigateClient,
) {
}
diff --git a/src/background/usecases/TabSelectUseCase.ts b/src/background/usecases/TabSelectUseCase.ts
index 62098de..271bb6c 100644
--- a/src/background/usecases/TabSelectUseCase.ts
+++ b/src/background/usecases/TabSelectUseCase.ts
@@ -1,10 +1,10 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
@injectable()
export default class TabSelectUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
) {
}
diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts
index 66f8573..418dde5 100644
--- a/src/background/usecases/TabUseCase.ts
+++ b/src/background/usecases/TabUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import {inject, injectable} from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
import WindowPresenter from '../presenters/WindowPresenter';
import BrowserSettingRepository from '../repositories/BrowserSettingRepository';
@@ -6,7 +6,7 @@ import BrowserSettingRepository from '../repositories/BrowserSettingRepository';
@injectable()
export default class TabUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
private windowPresenter: WindowPresenter,
private browserSettingRepository: BrowserSettingRepository,
) {
diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts
index 9ea8af9..21a5e2c 100644
--- a/src/background/usecases/VersionUseCase.ts
+++ b/src/background/usecases/VersionUseCase.ts
@@ -5,7 +5,7 @@ import Notifier from '../presenters/Notifier';
@injectable()
export default class VersionUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
@inject("Notifier") private notifier: Notifier,
) {
}
diff --git a/src/background/usecases/ZoomUseCase.ts b/src/background/usecases/ZoomUseCase.ts
index f598871..ca1368d 100644
--- a/src/background/usecases/ZoomUseCase.ts
+++ b/src/background/usecases/ZoomUseCase.ts
@@ -1,4 +1,4 @@
-import { injectable } from 'tsyringe';
+import { inject, injectable } from 'tsyringe';
import TabPresenter from '../presenters/TabPresenter';
const ZOOM_SETTINGS: number[] = [
@@ -9,7 +9,7 @@ const ZOOM_SETTINGS: number[] = [
@injectable()
export default class ZoomUseCase {
constructor(
- private tabPresenter: TabPresenter,
+ @inject('TabPresenter') private tabPresenter: TabPresenter,
) {
}
diff --git a/src/console/Completions.ts b/src/console/Completions.ts
new file mode 100644
index 0000000..ec9135f
--- /dev/null
+++ b/src/console/Completions.ts
@@ -0,0 +1,11 @@
+type Completions = {
+ readonly name: string;
+ readonly items: {
+ readonly caption?: string;
+ readonly content?: string;
+ readonly url?: string;
+ readonly icon?: string;
+ }[];
+}[]
+
+export default Completions; \ No newline at end of file
diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts
index f7fa7a2..e44c974 100644
--- a/src/console/actions/console.ts
+++ b/src/console/actions/console.ts
@@ -1,5 +1,32 @@
import * as messages from '../../shared/messages';
import * as actions from './index';
+import {Command} from "../../shared/Command";
+import CompletionClient from "../clients/CompletionClient";
+import CompletionType from "../../shared/CompletionType";
+import Completions from "../Completions";
+import TabFlag from "../../shared/TabFlag";
+
+const completionClient = new CompletionClient();
+
+const commandDocs = {
+ [Command.Set]: 'Set a value of the property',
+ [Command.Open]: 'Open a URL or search by keywords in current tab',
+ [Command.TabOpen]: 'Open a URL or search by keywords in new tab',
+ [Command.WindowOpen]: 'Open a URL or search by keywords in new window',
+ [Command.Buffer]: 'Select tabs by matched keywords',
+ [Command.BufferDelete]: 'Close a certain tab matched by keywords',
+ [Command.BuffersDelete]: 'Close all tabs matched by keywords',
+ [Command.Quit]: 'Close the current tab',
+ [Command.QuitAll]: 'Close all tabs',
+ [Command.AddBookmark]: 'Add current page to bookmarks',
+ [Command.Help]: 'Open Vim Vixen help in new tab',
+};
+
+const propertyDocs: {[key: string]: string} = {
+ 'hintchars': 'hint characters on follow mode',
+ 'smoothscroll': 'smooth scroll',
+ 'complete': 'which are completed at the open page',
+};
const hide = (): actions.ConsoleAction => {
return {
@@ -7,34 +34,36 @@ const hide = (): actions.ConsoleAction => {
};
};
-const showCommand = (text: string): actions.ConsoleAction => {
+const showCommand = async (text: string): Promise<actions.ShowCommand> => {
+ const completionTypes = await completionClient.getCompletionTypes();
return {
type: actions.CONSOLE_SHOW_COMMAND,
- text: text
+ completionTypes,
+ text,
};
};
-const showFind = (): actions.ConsoleAction => {
+const showFind = (): actions.ShowFindAction => {
return {
type: actions.CONSOLE_SHOW_FIND,
};
};
-const showError = (text: string): actions.ConsoleAction => {
+const showError = (text: string): actions.ShowErrorAction => {
return {
type: actions.CONSOLE_SHOW_ERROR,
text: text
};
};
-const showInfo = (text: string): actions.ConsoleAction => {
+const showInfo = (text: string): actions.ShowInfoAction => {
return {
type: actions.CONSOLE_SHOW_INFO,
text: text
};
};
-const hideCommand = (): actions.ConsoleAction => {
+const hideCommand = (): actions.HideCommandAction => {
window.top.postMessage(JSON.stringify({
type: messages.CONSOLE_UNFOCUS,
}), '*');
@@ -43,9 +72,7 @@ const hideCommand = (): actions.ConsoleAction => {
};
};
-const enterCommand = async(
- text: string,
-): Promise<actions.ConsoleAction> => {
+const enterCommand = async(text: string): Promise<actions.HideCommandAction> => {
await browser.runtime.sendMessage({
type: messages.CONSOLE_ENTER_COMMAND,
text,
@@ -53,7 +80,7 @@ const enterCommand = async(
return hideCommand();
};
-const enterFind = (text?: string): actions.ConsoleAction => {
+const enterFind = (text?: string): actions.HideCommandAction => {
window.top.postMessage(JSON.stringify({
type: messages.CONSOLE_ENTER_FIND,
text,
@@ -61,38 +88,164 @@ const enterFind = (text?: string): actions.ConsoleAction => {
return hideCommand();
};
-const setConsoleText = (consoleText: string): actions.ConsoleAction => {
+const setConsoleText = (consoleText: string): actions.SetConsoleTextAction => {
return {
type: actions.CONSOLE_SET_CONSOLE_TEXT,
consoleText,
};
};
-const getCompletions = async(text: string): Promise<actions.ConsoleAction> => {
- const completions = await browser.runtime.sendMessage({
- type: messages.CONSOLE_QUERY_COMPLETIONS,
- text,
- });
+const getCommandCompletions = (text: string): actions.SetCompletionsAction => {
+ const items = Object.entries(commandDocs)
+ .filter(([name]) => name.startsWith(text.trimLeft()))
+ .map(([name, doc]) => ({
+ caption: name,
+ content: name,
+ url: doc,
+ }));
+ const completions = [{
+ name: "Console Command",
+ items,
+ }];
return {
type: actions.CONSOLE_SET_COMPLETIONS,
completions,
completionSource: text,
+ }
+};
+
+const getOpenCompletions = async(
+ types: CompletionType[], original: string, command: Command, query: string,
+): Promise<actions.SetCompletionsAction> => {
+ const completions: Completions = [];
+ for (const type of types) {
+ switch (type) {
+ case CompletionType.SearchEngines: {
+ const items = await completionClient.requestSearchEngines(query);
+ if (items.length === 0) {
+ break;
+ }
+ completions.push({
+ name: 'Search Engines',
+ items: items.map(key => ({
+ caption: key.title,
+ content: command + ' ' + key.title,
+ }))
+ });
+ break;
+ }
+ case CompletionType.History: {
+ const items = await completionClient.requestHistory(query);
+ if (items.length === 0) {
+ break;
+ }
+ completions.push({
+ name: 'History',
+ items: items.map(item => ({
+ caption: item.title,
+ content: command + ' ' + item.url,
+ url: item.url
+ })),
+ });
+ break;
+ }
+ case CompletionType.Bookmarks: {
+ const items = await completionClient.requestBookmarks(query);
+ if (items.length === 0) {
+ break;
+ }
+ completions.push({
+ name: 'Bookmarks',
+ items: items.map(item => ({
+ caption: item.title,
+ content: command + ' ' + item.url,
+ url: item.url
+ }))
+ });
+ break;
+ }
+ }
+ }
+
+ return {
+ type: actions.CONSOLE_SET_COMPLETIONS,
+ completions,
+ completionSource: original,
};
};
-const completionNext = (): actions.ConsoleAction => {
+const getTabCompletions = async (
+ original: string, command: Command, query: string, excludePinned: boolean,
+): Promise<actions.SetCompletionsAction> => {
+ const items = await completionClient.requestTabs(query, excludePinned);
+ let completions: Completions = [];
+ if (items.length > 0) {
+ completions = [{
+ name: 'Buffers',
+ items: items.map(item => ({
+ content: command + ' ' + item.url,
+ caption: `${item.index}: ${item.flag != TabFlag.None ? item.flag : ' ' } ${item.title}`,
+ url: item.url,
+ icon: item.faviconUrl,
+ })),
+ }];
+ }
+ return {
+ type: actions.CONSOLE_SET_COMPLETIONS,
+ completions,
+ completionSource: original,
+ }
+};
+
+const getPropertyCompletions = async(
+ original: string, command: Command, query: string,
+): Promise<actions.SetCompletionsAction> => {
+ const properties = await completionClient.getProperties();
+ const items = properties
+ .map(item => {
+ const desc = propertyDocs[item.name] || '';
+ if (item.type === 'boolean') {
+ return [{
+ caption: item.name,
+ content: command + ' ' + item.name,
+ url: 'Enable ' + desc,
+ }, {
+ caption: 'no' + item.name,
+ content: command + ' no' + item.name,
+ url: 'Disable ' + desc,
+ }];
+ } else {
+ return [{
+ caption: item.name,
+ content: name + ' ' + item.name,
+ url: 'Set ' + desc,
+ }];
+ }
+ })
+ .reduce((acc, val) => acc.concat(val), [])
+ .filter(item => item.caption.startsWith(query));
+ const completions: Completions = [{ name: 'Properties', items }];
+ return {
+ type: actions.CONSOLE_SET_COMPLETIONS,
+ completions,
+ completionSource: original,
+ }
+};
+
+const completionNext = (): actions.CompletionNextAction => {
return {
type: actions.CONSOLE_COMPLETION_NEXT,
};
};
-const completionPrev = (): actions.ConsoleAction => {
+const completionPrev = (): actions.CompletionPrevAction => {
return {
type: actions.CONSOLE_COMPLETION_PREV,
};
};
export {
- hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
- enterCommand, enterFind, getCompletions, completionNext, completionPrev,
+ hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, enterCommand, enterFind,
+ getCommandCompletions, getOpenCompletions, getTabCompletions, getPropertyCompletions,
+ completionNext, completionPrev,
};
diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts
index 3770496..e292608 100644
--- a/src/console/actions/index.ts
+++ b/src/console/actions/index.ts
@@ -1,4 +1,6 @@
-// console commands
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
+
export const CONSOLE_HIDE = 'console.hide';
export const CONSOLE_SHOW_COMMAND = 'console.show.command';
export const CONSOLE_SHOW_ERROR = 'console.show.error';
@@ -10,49 +12,50 @@ export const CONSOLE_COMPLETION_NEXT = 'console.completion.next';
export const CONSOLE_COMPLETION_PREV = 'console.completion.prev';
export const CONSOLE_SHOW_FIND = 'console.show.find';
-interface HideAction {
+export interface HideAction {
type: typeof CONSOLE_HIDE;
}
-interface ShowCommand {
+export interface ShowCommand {
type: typeof CONSOLE_SHOW_COMMAND;
text: string;
+ completionTypes: CompletionType[];
}
-interface ShowFindAction {
+export interface ShowFindAction {
type: typeof CONSOLE_SHOW_FIND;
}
-interface ShowErrorAction {
+export interface ShowErrorAction {
type: typeof CONSOLE_SHOW_ERROR;
text: string;
}
-interface ShowInfoAction {
+export interface ShowInfoAction {
type: typeof CONSOLE_SHOW_INFO;
text: string;
}
-interface HideCommandAction {
+export interface HideCommandAction {
type: typeof CONSOLE_HIDE_COMMAND;
}
-interface SetConsoleTextAction {
+export interface SetConsoleTextAction {
type: typeof CONSOLE_SET_CONSOLE_TEXT;
consoleText: string;
}
-interface SetCompletionsAction {
+export interface SetCompletionsAction {
type: typeof CONSOLE_SET_COMPLETIONS;
- completions: any[];
+ completions: Completions;
completionSource: string;
}
-interface CompletionNextAction {
+export interface CompletionNextAction {
type: typeof CONSOLE_COMPLETION_NEXT;
}
-interface CompletionPrevAction {
+export interface CompletionPrevAction {
type: typeof CONSOLE_COMPLETION_PREV;
}
diff --git a/src/console/clients/CompletionClient.ts b/src/console/clients/CompletionClient.ts
new file mode 100644
index 0000000..56dc665
--- /dev/null
+++ b/src/console/clients/CompletionClient.ts
@@ -0,0 +1,84 @@
+import * as messages from "../../shared/messages";
+import {
+ ConsoleGetCompletionTypesResponse, ConsoleGetPropertiesResponse,
+ ConsoleRequestBookmarksResponse,
+ ConsoleRequestHistoryResponse, ConsoleRequestSearchEnginesResponse, ConsoleRequesttabsResponse
+} from "../../shared/messages";
+import CompletionType from "../../shared/CompletionType";
+import TabFlag from "../../shared/TabFlag";
+
+export type SearchEngines = {
+ title: string
+}
+
+export type BookmarkItem = {
+ title: string
+ url: string
+}
+
+export type HistoryItem = {
+ title: string
+ url: string
+}
+
+export type TabItem = {
+ index: number
+ flag: TabFlag
+ title: string
+ url: string
+ faviconUrl?: string
+}
+
+export type Property = {
+ name: string
+ type: 'string' | 'boolean' | 'number';
+}
+
+export default class CompletionClient {
+ async getCompletionTypes(): Promise<CompletionType[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_GET_COMPLETION_TYPES,
+ }) as ConsoleGetCompletionTypesResponse;
+ return resp;
+ }
+
+ async requestSearchEngines(query: string): Promise<SearchEngines[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE,
+ query,
+ }) as ConsoleRequestSearchEnginesResponse;
+ return resp;
+ }
+
+ async requestBookmarks(query: string): Promise<BookmarkItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_BOOKMARKS,
+ query,
+ }) as ConsoleRequestBookmarksResponse;
+ return resp;
+ }
+
+ async requestHistory(query: string): Promise<HistoryItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_HISTORY,
+ query,
+ }) as ConsoleRequestHistoryResponse;
+ return resp;
+ }
+
+ async requestTabs(query: string, excludePinned: boolean): Promise<TabItem[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_REQUEST_TABS,
+ query,
+ excludePinned,
+ }) as ConsoleRequesttabsResponse;
+ return resp;
+ }
+
+ async getProperties(): Promise<Property[]> {
+ const resp = await browser.runtime.sendMessage({
+ type: messages.CONSOLE_GET_PROPERTIES,
+ }) as ConsoleGetPropertiesResponse;
+ return resp;
+ }
+}
diff --git a/src/console/commandline/CommandLineParser.ts b/src/console/commandline/CommandLineParser.ts
new file mode 100644
index 0000000..a166f49
--- /dev/null
+++ b/src/console/commandline/CommandLineParser.ts
@@ -0,0 +1,38 @@
+import CommandParser from "./CommandParser";
+import { Command } from "../../shared/Command";
+
+export type CommandLine = {
+ readonly command: Command,
+ readonly args: string
+}
+
+export enum InputPhase {
+ OnCommand,
+ OnArgs,
+}
+
+export default class CommandLineParser {
+ private commandParser: CommandParser = new CommandParser();
+
+ inputPhase(line: string): InputPhase {
+ line = line.trimLeft();
+ if (line.length == 0) {
+ return InputPhase.OnCommand
+ }
+ const command = line.split(/\s+/, 1)[0];
+ if (line.length == command.length) {
+ return InputPhase.OnCommand
+ }
+ return InputPhase.OnArgs;
+ }
+
+ parse(line: string): CommandLine {
+ const trimLeft = line.trimLeft();
+ const command = trimLeft.split(/\s+/, 1)[0];
+ const args = trimLeft.slice(command.length).trimLeft();
+ return {
+ command: this.commandParser.parse(command),
+ args: args,
+ }
+ }
+}
diff --git a/src/console/commandline/CommandParser.ts b/src/console/commandline/CommandParser.ts
new file mode 100644
index 0000000..5228c77
--- /dev/null
+++ b/src/console/commandline/CommandParser.ts
@@ -0,0 +1,52 @@
+import { Command } from "../../shared/Command";
+
+export class UnknownCommandError extends Error {
+ constructor(value: string) {
+ super(`unknown command '${value}'`);
+ }
+}
+
+export default class CommandParser {
+ parse(value: string): Command {
+ switch (value) {
+ case 'o':
+ case 'open':
+ return Command.Open;
+ case 't':
+ case 'tabopen':
+ return Command.TabOpen;
+ case 'w':
+ case 'winopen':
+ return Command.WindowOpen;
+ case 'b':
+ case 'buffer':
+ return Command.Buffer;
+ case 'bd':
+ case 'bdel':
+ case 'bdelete':
+ return Command.BufferDelete;
+ case 'bd!':
+ case 'bdel!':
+ case 'bdelete!':
+ return Command.BufferDeleteForce;
+ case 'bdeletes':
+ return Command.BuffersDelete;
+ case 'bdeletes!':
+ return Command.BuffersDeleteForce;
+ case 'addbookmark':
+ return Command.AddBookmark;
+ case 'q':
+ case 'quit':
+ return Command.Quit;
+ case 'qa':
+ case 'quitall':
+ return Command.QuitAll;
+ case 'set':
+ return Command.Set;
+ case 'h':
+ case 'help':
+ return Command.Help;
+ }
+ throw new UnknownCommandError(value);
+ }
+}
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index eafe2a7..3fe5cee 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -6,6 +6,8 @@ import Completion from './console/Completion';
import Message from './console/Message';
import * as consoleActions from '../../console/actions/console';
import { State as AppState } from '../reducers';
+import CommandLineParser, { InputPhase } from "../commandline/CommandLineParser";
+import { Command } from "../../shared/Command";
const COMPLETION_MAX_ITEMS = 33;
@@ -18,6 +20,8 @@ type Props = StateProps & DispatchProps;
class Console extends React.Component<Props> {
private input: React.RefObject<Input>;
+ private commandLineParser: CommandLineParser = new CommandLineParser();
+
constructor(props: Props) {
super(props);
@@ -103,16 +107,16 @@ class Console extends React.Component<Props> {
onChange(e: React.ChangeEvent<HTMLInputElement>) {
const text = e.target.value;
this.props.dispatch(consoleActions.setConsoleText(text));
- if (this.props.mode === 'command') {
- this.props.dispatch(consoleActions.getCompletions(text));
+ if (this.props.mode !== 'command') {
+ return
}
+ this.updateCompletions(text)
}
componentDidUpdate(prevProps: Props) {
if (prevProps.mode !== 'command' && this.props.mode === 'command') {
- this.props.dispatch(
- consoleActions.getCompletions(this.props.consoleText));
+ this.updateCompletions(this.props.consoleText);
this.focus();
} else if (prevProps.mode !== 'find' && this.props.mode === 'find') {
this.focus();
@@ -154,6 +158,36 @@ class Console extends React.Component<Props> {
this.input.current.focus();
}
}
+
+ private updateCompletions(text: string) {
+ const phase = this.commandLineParser.inputPhase(text);
+ if (phase === InputPhase.OnCommand) {
+ return this.props.dispatch(consoleActions.getCommandCompletions(text));
+ } else {
+ const cmd = this.commandLineParser.parse(text);
+ switch (cmd.command) {
+ case Command.Open:
+ case Command.TabOpen:
+ case Command.WindowOpen:
+ this.props.dispatch(consoleActions.getOpenCompletions(this.props.completionTypes, text, cmd.command, cmd.args));
+ break;
+ case Command.Buffer:
+ this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false));
+ break;
+ case Command.BufferDelete:
+ case Command.BuffersDelete:
+ this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, true));
+ break;
+ case Command.BufferDeleteForce:
+ case Command.BuffersDeleteForce:
+ this.props.dispatch(consoleActions.getTabCompletions(text, cmd.command, cmd.args, false));
+ break;
+ case Command.Set:
+ this.props.dispatch(consoleActions.getPropertyCompletions(text, cmd.command, cmd.args));
+ break;
+ }
+ }
+ }
}
const mapStateToProps = (state: AppState) => ({ ...state });
diff --git a/src/console/index.tsx b/src/console/index.tsx
index 1209ec2..7bee746 100644
--- a/src/console/index.tsx
+++ b/src/console/index.tsx
@@ -22,11 +22,11 @@ window.addEventListener('load', () => {
wrapper);
});
-const onMessage = (message: any): any => {
+const onMessage = async (message: any): Promise<any> => {
const msg = messages.valueOf(message);
switch (msg.type) {
case messages.CONSOLE_SHOW_COMMAND:
- return store.dispatch(consoleActions.showCommand(msg.command));
+ return store.dispatch(await consoleActions.showCommand(msg.command));
case messages.CONSOLE_SHOW_FIND:
return store.dispatch(consoleActions.showFind());
case messages.CONSOLE_SHOW_ERROR:
diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts
index 048a24f..f1508bb 100644
--- a/src/console/reducers/index.ts
+++ b/src/console/reducers/index.ts
@@ -1,11 +1,14 @@
import * as actions from '../actions';
+import Completions from "../Completions";
+import CompletionType from "../../shared/CompletionType";
export interface State {
mode: string;
messageText: string;
consoleText: string;
+ completionTypes: CompletionType[];
completionSource: string;
- completions: any[],
+ completions: Completions;
select: number;
viewIndex: number;
}
@@ -14,6 +17,7 @@ const defaultState = {
mode: '',
messageText: '',
consoleText: '',
+ completionTypes: [],
completionSource: '',
completions: [],
select: -1,
@@ -68,6 +72,7 @@ export default function reducer(
return { ...state,
mode: 'command',
consoleText: action.text,
+ completionTypes: action.completionTypes,
completions: []};
case actions.CONSOLE_SHOW_FIND:
return { ...state,
diff --git a/src/shared/Command.ts b/src/shared/Command.ts
new file mode 100644
index 0000000..b8c21ce
--- /dev/null
+++ b/src/shared/Command.ts
@@ -0,0 +1,44 @@
+export enum Command {
+ Open = "open",
+ TabOpen = "tabopen",
+ WindowOpen = "winopen",
+ Buffer = "buffer",
+ BufferDelete = "bdelete",
+ BufferDeleteForce = "bdelete!",
+ BuffersDelete = "bdeletes",
+ BuffersDeleteForce = "bdeletes!",
+ AddBookmark = "addbookmark",
+ Quit = "quit",
+ QuitAll = "quitall",
+ Set = "set",
+ Help = "help",
+}
+
+export namespace Command {
+ export function members(): Command[] {
+ return [
+ Command.Open ,
+ Command.TabOpen ,
+ Command.WindowOpen ,
+ Command.Buffer ,
+ Command.BufferDelete ,
+ Command.BufferDeleteForce ,
+ Command.BuffersDelete ,
+ Command.BuffersDeleteForce ,
+ Command.AddBookmark ,
+ Command.Quit ,
+ Command.QuitAll ,
+ Command.Set ,
+ Command.Help ,
+ ]
+ }
+
+ export function valueOf(value: string): Command {
+ const map = new Map(members().map(cmd => [cmd.toString(), cmd]));
+ const cmd = map.get(value);
+ if (!cmd) {
+ throw new Error(`unknown command '${value}`);
+ }
+ return cmd;
+ }
+}
diff --git a/src/shared/CompletionType.ts b/src/shared/CompletionType.ts
new file mode 100644
index 0000000..e104455
--- /dev/null
+++ b/src/shared/CompletionType.ts
@@ -0,0 +1,7 @@
+enum CompletionType {
+ SearchEngines,
+ History,
+ Bookmarks,
+}
+
+export default CompletionType; \ No newline at end of file
diff --git a/src/shared/TabFlag.ts b/src/shared/TabFlag.ts
new file mode 100644
index 0000000..b10d5c6
--- /dev/null
+++ b/src/shared/TabFlag.ts
@@ -0,0 +1,7 @@
+enum TabFlag {
+ CurrentTab = '%',
+ LastTab = '#',
+ None = '',
+}
+
+export default TabFlag \ No newline at end of file
diff --git a/src/shared/messages.ts b/src/shared/messages.ts
index 7f8bd5b..edb7935 100644
--- a/src/shared/messages.ts
+++ b/src/shared/messages.ts
@@ -1,16 +1,23 @@
import * as operations from './operations';
+import CompletionType from "./CompletionType";
+import TabFlag from "./TabFlag";
export const BACKGROUND_OPERATION = 'background.operation';
export const CONSOLE_UNFOCUS = 'console.unfocus';
export const CONSOLE_ENTER_COMMAND = 'console.enter.command';
export const CONSOLE_ENTER_FIND = 'console.enter.find';
-export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions';
export const CONSOLE_SHOW_COMMAND = 'console.show.command';
export const CONSOLE_SHOW_ERROR = 'console.show.error';
export const CONSOLE_SHOW_INFO = 'console.show.info';
export const CONSOLE_SHOW_FIND = 'console.show.find';
export const CONSOLE_HIDE = 'console.hide';
+export const CONSOLE_GET_COMPLETION_TYPES = 'console.get.completion.types';
+export const CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE = 'console.qresut.searchEngines';
+export const CONSOLE_REQUEST_BOOKMARKS = 'console.request.bookmarks';
+export const CONSOLE_REQUEST_HISTORY = 'console.request.history';
+export const CONSOLE_REQUEST_TABS = 'console.request.tabs';
+export const CONSOLE_GET_PROPERTIES = 'console.get.properties';
export const FOLLOW_START = 'follow.start';
export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets';
@@ -67,11 +74,6 @@ export interface ConsoleEnterFindMessage {
text?: string;
}
-export interface ConsoleQueryCompletionsMessage {
- type: typeof CONSOLE_QUERY_COMPLETIONS;
- text: string;
-}
-
export interface ConsoleShowCommandMessage {
type: typeof CONSOLE_SHOW_COMMAND;
command: string;
@@ -95,6 +97,64 @@ export interface ConsoleHideMessage {
type: typeof CONSOLE_HIDE;
}
+export interface ConsoleGetCompletionTypesMessage {
+ type: typeof CONSOLE_GET_COMPLETION_TYPES;
+}
+
+export interface ConsoleRequestSearchEnginesMessage {
+ type: typeof CONSOLE_REQUEST_SEARCH_ENGINES_MESSAGE;
+ query: string
+}
+
+export interface ConsoleRequestBookmarksMessage {
+ type: typeof CONSOLE_REQUEST_BOOKMARKS;
+ query: string;
+}
+
+export interface ConsoleRequestHistoryMessage {
+ type: typeof CONSOLE_REQUEST_HISTORY;
+ query: string;
+}
+
+export interface ConsoleRequestTabsMessage {
+ type: typeof CONSOLE_REQUEST_TABS;
+ query: string;
+ excludePinned: boolean;
+}
+
+export interface ConsoleGetPropertiesMessage {
+ type: typeof CONSOLE_GET_PROPERTIES;
+}
+
+export type ConsoleRequesttabsResponse = {
+ index: number
+ flag: TabFlag
+ title: string
+ url: string
+ faviconUrl?: string
+}[]
+
+export type ConsoleGetCompletionTypesResponse = CompletionType[];
+
+export type ConsoleRequestSearchEnginesResponse = {
+ title: string;
+}[]
+
+export type ConsoleRequestBookmarksResponse = {
+ title: string;
+ url: string;
+}[]
+
+export type ConsoleRequestHistoryResponse = {
+ title: string;
+ url: string;
+}[]
+
+export type ConsoleGetPropertiesResponse = {
+ name: string
+ type: 'string' | 'boolean' | 'number'
+}[]
+
export interface FollowStartMessage {
type: typeof FOLLOW_START;
newTab: boolean;
@@ -231,12 +291,17 @@ export type Message =
ConsoleUnfocusMessage |
ConsoleEnterCommandMessage |
ConsoleEnterFindMessage |
- ConsoleQueryCompletionsMessage |
ConsoleShowCommandMessage |
ConsoleShowErrorMessage |
ConsoleShowInfoMessage |
ConsoleShowFindMessage |
ConsoleHideMessage |
+ ConsoleRequestBookmarksMessage |
+ ConsoleRequestHistoryMessage |
+ ConsoleRequestTabsMessage |
+ ConsoleGetPropertiesMessage |
+ ConsoleGetCompletionTypesMessage |
+ ConsoleRequestSearchEnginesMessage |
FollowStartMessage |
FollowRequestCountTargetsMessage |
FollowResponseCountTargetsMessage |
@@ -270,7 +335,6 @@ export const valueOf = (o: any): Message => {
case CONSOLE_UNFOCUS:
case CONSOLE_ENTER_COMMAND:
case CONSOLE_ENTER_FIND:
- case CONSOLE_QUERY_COMPLETIONS:
case CONSOLE_SHOW_COMMAND:
case CONSOLE_SHOW_ERROR:
case CONSOLE_SHOW_INFO:
diff --git a/test/background/completion/OpenCompletionUseCase.test.ts b/test/background/completion/OpenCompletionUseCase.test.ts
new file mode 100644
index 0000000..421ce69
--- /dev/null
+++ b/test/background/completion/OpenCompletionUseCase.test.ts
@@ -0,0 +1,131 @@
+import "reflect-metadata";
+import CompletionType from "../../../src/shared/CompletionType";
+import BookmarkRepository, {BookmarkItem} from "../../../src/background/completion/BookmarkRepository";
+import HistoryRepository, {HistoryItem} from "../../../src/background/completion/HistoryRepository";
+import OpenCompletionUseCase from "../../../src/background/completion/OpenCompletionUseCase";
+import CachedSettingRepository from "../../../src/background/repositories/CachedSettingRepository";
+import Settings, {DefaultSetting} from "../../../src/shared/settings/Settings";
+import { expect } from 'chai';
+import sinon from 'sinon';
+import Properties from "../../../src/shared/settings/Properties";
+import Search from "../../../src/shared/settings/Search";
+
+class MockBookmarkRepository implements BookmarkRepository {
+ queryBookmarks(_query: string): Promise<BookmarkItem[]> {
+ throw new Error("not implemented")
+ }
+}
+
+class MockHistoryRepository implements HistoryRepository {
+ queryHistories(_keywords: string): Promise<HistoryItem[]> {
+ throw new Error("not implemented")
+ }
+}
+
+class MockSettingRepository implements CachedSettingRepository {
+ get(): Promise<Settings> {
+ throw new Error("not implemented")
+ }
+
+ setProperty(_name: string, _value: string | number | boolean): Promise<void> {
+ throw new Error("not implemented")
+ }
+
+ update(_value: Settings): Promise<void> {
+ throw new Error("not implemented")
+ }
+}
+
+describe('OpenCompletionUseCase', () => {
+ let bookmarkRepository: MockBookmarkRepository;
+ let historyRepository: MockHistoryRepository;
+ let settingRepository: MockSettingRepository;
+ let sut: OpenCompletionUseCase;
+
+ beforeEach(() => {
+ bookmarkRepository = new MockBookmarkRepository();
+ historyRepository = new MockHistoryRepository();
+ settingRepository = new MockSettingRepository();
+ sut = new OpenCompletionUseCase(bookmarkRepository, historyRepository, settingRepository)
+ });
+
+ describe('#getCompletionTypes', () => {
+ it("returns completion types from the property", async () => {
+ sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({
+ keymaps: DefaultSetting.keymaps,
+ search: DefaultSetting.search,
+ properties: new Properties({ complete: "shb" }),
+ blacklist: DefaultSetting.blacklist,
+ })));
+
+ const items = await sut.getCompletionTypes();
+ expect(items).to.deep.equal([
+ CompletionType.SearchEngines,
+ CompletionType.History,
+ CompletionType.Bookmarks
+ ]);
+ });
+ });
+
+ describe('#requestSearchEngines', () => {
+ it("returns search engines matches by the query", async () => {
+ sinon.stub(settingRepository, 'get').returns(Promise.resolve(new Settings({
+ keymaps: DefaultSetting.keymaps,
+ search: new Search("google", {
+ "google": "https://google.com/search?q={}",
+ "yahoo": "https://search.yahoo.com/search?q={}",
+ "bing": "https://bing.com/search?q={}",
+ "google_ja": "https://google.co.jp/search?q={}",
+ }),
+ properties: DefaultSetting.properties,
+ blacklist: DefaultSetting.blacklist,
+ })));
+
+ expect(await sut.requestSearchEngines("")).to.deep.equal([
+ "google",
+ "yahoo",
+ "bing",
+ "google_ja",
+ ]);
+ expect(await sut.requestSearchEngines("go")).to.deep.equal([
+ "google",
+ "google_ja",
+ ]);
+ expect(await sut.requestSearchEngines("x")).to.be.empty;
+ })
+ });
+
+ describe('#requestBookmarks', () => {
+ it("returns bookmarks from the repository", async() => {
+ sinon.stub(bookmarkRepository, 'queryBookmarks')
+ .withArgs("site").returns(Promise.resolve([
+ { title: "site1", url: "https://site1.example.com" },
+ { title: "site2", url: "https://site2.example.com/" },
+ ]))
+ .withArgs("xyz").returns(Promise.resolve([]));
+
+ expect(await sut.requestBookmarks("site")).to.deep.equal([
+ { title: "site1", url: "https://site1.example.com" },
+ { title: "site2", url: "https://site2.example.com/" },
+ ]);
+ expect(await sut.requestBookmarks("xyz")).to.be.empty;
+ });
+ });
+
+ describe('#requestHistory', () => {
+ it("returns histories from the repository", async() => {
+ sinon.stub(historyRepository, 'queryHistories')
+ .withArgs("site").returns(Promise.resolve([
+ { title: "site1", url: "https://site1.example.com" },
+ { title: "site2", url: "https://site2.example.com/" },
+ ]))
+ .withArgs("xyz").returns(Promise.resolve([]));
+
+ expect(await sut.requestHistory("site")).to.deep.equal([
+ { title: "site1", url: "https://site1.example.com" },
+ { title: "site2", url: "https://site2.example.com/" },
+ ]);
+ expect(await sut.requestHistory("xyz")).to.be.empty;
+ });
+ });
+}); \ No newline at end of file
diff --git a/test/background/completion/PropertyCompletionUseCase.test.ts b/test/background/completion/PropertyCompletionUseCase.test.ts
new file mode 100644
index 0000000..57f5bff
--- /dev/null
+++ b/test/background/completion/PropertyCompletionUseCase.test.ts
@@ -0,0 +1,15 @@
+import 'reflect-metadata';
+import PropertyCompletionUseCase from "../../../src/background/completion/PropertyCompletionUseCase";
+import { expect } from 'chai';
+
+describe('PropertyCompletionUseCase', () => {
+ describe('getProperties', () => {
+ it('returns property types', async () => {
+ const sut = new PropertyCompletionUseCase();
+
+ const properties = await sut.getProperties();
+ expect(properties).to.deep.contain({ name: 'smoothscroll', type: 'boolean' });
+ expect(properties).to.deep.contain({ name: 'complete', type: 'string' });
+ })
+ });
+}); \ No newline at end of file
diff --git a/test/background/completion/TabCompletionUseCase.test.ts b/test/background/completion/TabCompletionUseCase.test.ts
new file mode 100644
index 0000000..b9dc60b
--- /dev/null
+++ b/test/background/completion/TabCompletionUseCase.test.ts
@@ -0,0 +1,144 @@
+import "reflect-metadata"
+import TabRepositoryImpl from "../../../src/background/completion/impl/TabRepositoryImpl";
+import {Tab} from "../../../src/background/completion/TabRepository";
+import TabPresenter from "../../../src/background/presenters/TabPresenter";
+import TabCompletionUseCase from "../../../src/background/completion/TabCompletionUseCase";
+import sinon from 'sinon';
+import { expect } from 'chai';
+import TabFlag from "../../../src/shared/TabFlag";
+
+class MockTabRepository implements TabRepositoryImpl {
+ async queryTabs(_query: string, _excludePinned: boolean): Promise<Tab[]> {
+ throw new Error("not implemented")
+ }
+
+ async getAllTabs(_excludePinned: boolean): Promise<Tab[]> {
+ throw new Error("not implemented")
+ }
+}
+
+class MockTabPresenter implements TabPresenter {
+ create(_url: string, _opts?: object): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented")
+ }
+
+ duplicate(_id: number): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented")
+ }
+
+ getAll(): Promise<browser.tabs.Tab[]> {
+ throw new Error("not implemented")
+ }
+
+ getByKeyword(_keyword: string, _excludePinned: boolean): Promise<browser.tabs.Tab[]> {
+ throw new Error("not implemented")
+ }
+
+ getCurrent(): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented")
+ }
+
+ getLastSelectedId(): Promise<number | undefined> {
+ throw new Error("not implemented")
+ }
+
+ getZoom(_tabId: number): Promise<number> {
+ throw new Error("not implemented")
+ }
+
+ onSelected(_listener: (arg: { tabId: number; windowId: number }) => void): void {
+ throw new Error("not implemented")
+ }
+
+ open(_url: string, _tabId?: number): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented")
+ }
+
+ reload(_tabId: number, _cache: boolean): Promise<void> {
+ throw new Error("not implemented")
+ }
+
+ remove(_ids: number[]): Promise<void> {
+ throw new Error("not implemented")
+ }
+
+ reopen(): Promise<any> {
+ throw new Error("not implemented")
+ }
+
+ select(_tabId: number): Promise<void> {
+ throw new Error("not implemented")
+ }
+
+ setPinned(_tabId: number, _pinned: boolean): Promise<void> {
+ throw new Error("not implemented")
+ }
+
+ setZoom(_tabId: number, _factor: number): Promise<void> {
+ throw new Error("not implemented")
+ }
+}
+
+describe('TabCompletionUseCase', () => {
+ let tabRepository: MockTabRepository;
+ let tabPresenter: TabPresenter;
+ let sut: TabCompletionUseCase;
+
+ beforeEach(() => {
+ tabRepository = new MockTabRepository();
+ tabPresenter = new MockTabPresenter();
+ sut = new TabCompletionUseCase(tabRepository, tabPresenter);
+
+ sinon.stub(tabPresenter, 'getLastSelectedId').returns(Promise.resolve(12));
+ sinon.stub(tabRepository, 'getAllTabs').returns(Promise.resolve([
+ { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false },
+ { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true },
+ { id: 12, index: 2, title: 'Bing', url: 'https://bing.com/', active: false },
+ ]));
+ });
+
+ describe('#queryTabs', () => {
+ it("returns tab items", async () => {
+ sinon.stub(tabRepository, 'queryTabs').withArgs('', false).returns(Promise.resolve([
+ { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false },
+ { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true },
+ { id: 12, index: 2, title: 'Bing', url: 'https://bing.com/', active: false },
+ ])).withArgs('oo', false).returns(Promise.resolve([
+ { id: 10, index: 0, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', active: false },
+ { id: 11, index: 1, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', active: true },
+ ]));
+
+ expect(await sut.queryTabs('', false)).to.deep.equal([
+ { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.None },
+ { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.CurrentTab },
+ { index: 3, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.LastTab },
+ ]);
+
+ expect(await sut.queryTabs('oo', false)).to.deep.equal([
+ { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.None },
+ { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.CurrentTab },
+ ]);
+ });
+
+ it("returns a tab by the index", async () => {
+ expect(await sut.queryTabs('1', false)).to.deep.equal([
+ { index: 1, title: 'Google', url: 'https://google.com/', faviconUrl: 'https://google.com/favicon.ico', flag: TabFlag.None },
+ ]);
+
+ expect(await sut.queryTabs('10', false)).to.be.empty;
+ expect(await sut.queryTabs('-1', false)).to.be.empty;
+ });
+
+ it("returns the current tab by % flag", async () => {
+ expect(await sut.queryTabs('%', false)).to.deep.equal([
+ { index: 2, title: 'Yahoo', url: 'https://yahoo.com/', faviconUrl: 'https://yahoo.com/favicon.ico', flag: TabFlag.CurrentTab },
+ ]);
+ });
+
+ it("returns the current tab by # flag", async () => {
+ expect(await sut.queryTabs('#', false)).to.deep.equal([
+ { index: 3, title: 'Bing', url: 'https://bing.com/', faviconUrl: undefined, flag: TabFlag.LastTab },
+ ]);
+ })
+ });
+}); \ No newline at end of file
diff --git a/test/background/completion/impl/filters.test.ts b/test/background/completion/impl/filters.test.ts
new file mode 100644
index 0000000..2b15a9b
--- /dev/null
+++ b/test/background/completion/impl/filters.test.ts
@@ -0,0 +1,114 @@
+import * as filters from '../../../../src/background/completion/impl/filters'
+import { expect } from 'chai';
+
+describe('background/usecases/filters', () => {
+ describe('filterHttp', () => {
+ it('filters http URLs duplicates to https hosts', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/foo' },
+ { id: '1', url: 'https://i-beam.org/bar' },
+ { id: '2', url: 'http://i-beam.net/hoge' },
+ { id: '3', url: 'http://i-beam.net/fuga' },
+ ];
+ const filtered = filters.filterHttp(pages);
+
+ const urls = filtered.map(x => x.url);
+ expect(urls).to.deep.equal([
+ 'https://i-beam.org/bar', 'http://i-beam.net/hoge', 'http://i-beam.net/fuga'
+ ]);
+ })
+ });
+
+ describe('filterBlankTitle', () => {
+ it('filters blank titles', () => {
+ const pages = [
+ { id: '0', title: 'hello' },
+ { id: '1', title: '' },
+ { id: '2' },
+ ];
+ const filtered = filters.filterBlankTitle(pages);
+
+ expect(filtered).to.deep.equal([{ id: '0', title: 'hello' }]);
+ });
+ });
+
+ describe('filterByTailingSlash', () => {
+ it('filters duplicated pathname on tailing slash', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/content' },
+ { id: '1', url: 'http://i-beam.org/content/' },
+ { id: '2', url: 'http://i-beam.org/search' },
+ { id: '3', url: 'http://i-beam.org/search?q=apple_banana_cherry' },
+ ];
+ const filtered = filters.filterByTailingSlash(pages);
+
+ const urls = filtered.map(x => x.url);
+ expect(urls).to.deep.equal([
+ 'http://i-beam.org/content',
+ 'http://i-beam.org/search',
+ 'http://i-beam.org/search?q=apple_banana_cherry',
+ ]);
+ });
+ })
+
+ describe('filterByPathname', () => {
+ it('remains items less than minimam length', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ { id: '1', url: 'http://i-beam.org/search?q=apple_banana' },
+ { id: '2', url: 'http://i-beam.org/search?q=apple_banana_cherry' },
+ { id: '3', url: 'http://i-beam.org/request?q=apple' },
+ { id: '4', url: 'http://i-beam.org/request?q=apple_banana' },
+ { id: '5', url: 'http://i-beam.org/request?q=apple_banana_cherry' },
+ ];
+ const filtered = filters.filterByPathname(pages, 10);
+ expect(filtered).to.have.lengthOf(6);
+ });
+
+ it('filters by length of pathname', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ { id: '1', url: 'http://i-beam.org/search?q=apple_banana' },
+ { id: '2', url: 'http://i-beam.org/search?q=apple_banana_cherry' },
+ { id: '3', url: 'http://i-beam.net/search?q=apple' },
+ { id: '4', url: 'http://i-beam.net/search?q=apple_banana' },
+ { id: '5', url: 'http://i-beam.net/search?q=apple_banana_cherry' },
+ ];
+ const filtered = filters.filterByPathname(pages, 0);
+ expect(filtered).to.deep.equal([
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ { id: '3', url: 'http://i-beam.net/search?q=apple' },
+ ]);
+ });
+ });
+
+ describe('filterByOrigin', () => {
+ it('remains items less than minimam length', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ { id: '1', url: 'http://i-beam.org/search?q=apple_banana' },
+ { id: '2', url: 'http://i-beam.org/search?q=apple_banana_cherry' },
+ { id: '3', url: 'http://i-beam.org/request?q=apple' },
+ { id: '4', url: 'http://i-beam.org/request?q=apple_banana' },
+ { id: '5', url: 'http://i-beam.org/request?q=apple_banana_cherry' },
+ ];
+ const filtered = filters.filterByOrigin(pages, 10);
+ expect(filtered).to.have.lengthOf(6);
+ });
+
+ it('filters by length of pathname', () => {
+ const pages = [
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ { id: '1', url: 'http://i-beam.org/search?q=apple_banana' },
+ { id: '2', url: 'http://i-beam.org/search?q=apple_banana_cherry' },
+ { id: '3', url: 'http://i-beam.org/request?q=apple' },
+ { id: '4', url: 'http://i-beam.org/request?q=apple_banana' },
+ { id: '5', url: 'http://i-beam.org/request?q=apple_banana_cherry' },
+ ];
+ const filtered = filters.filterByOrigin(pages, 0);
+ expect(filtered).to.deep.equal([
+ { id: '0', url: 'http://i-beam.org/search?q=apple' },
+ ]);
+ });
+ });
+});
diff --git a/test/background/usecases/NavigateUseCase.test.ts b/test/background/usecases/NavigateUseCase.test.ts
index 48a1c5b..7ad0e4f 100644
--- a/test/background/usecases/NavigateUseCase.test.ts
+++ b/test/background/usecases/NavigateUseCase.test.ts
@@ -1,26 +1,107 @@
+import "reflect-metadata";
import TabPresenter from '../../../src/background/presenters/TabPresenter';
import NavigateUseCase from '../../../src/background/usecases/NavigateUseCase';
import NavigateClient from '../../../src/background/clients/NavigateClient';
-// import { expect } from 'chai';
import * as sinon from 'sinon';
+class MockTabPresenter implements TabPresenter {
+ create(_url: string, _opts?: object): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented");
+ }
+
+ duplicate(_id: number): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented");
+ }
+
+ getAll(): Promise<browser.tabs.Tab[]> {
+ throw new Error("not implemented");
+ }
+
+ getByKeyword(_keyword: string, _excludePinned: boolean): Promise<browser.tabs.Tab[]> {
+ throw new Error("not implemented");
+ }
+
+ getCurrent(): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented");
+ }
+
+ getLastSelectedId(): Promise<number | undefined> {
+ throw new Error("not implemented");
+ }
+
+ getZoom(_tabId: number): Promise<number> {
+ throw new Error("not implemented");
+ }
+
+ onSelected(_listener: (arg: { tabId: number; windowId: number }) => void): void {
+ throw new Error("not implemented");
+ }
+
+ open(_url: string, _tabId?: number): Promise<browser.tabs.Tab> {
+ throw new Error("not implemented");
+ }
+
+ reload(_tabId: number, _cache: boolean): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ remove(_ids: number[]): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ reopen(): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ select(_tabId: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ setPinned(_tabId: number, _pinned: boolean): Promise<void> {
+ throw new Error("not implemented");
+ }
+
+ setZoom(_tabId: number, _factor: number): Promise<void> {
+ throw new Error("not implemented");
+ }
+}
+
describe('NavigateUseCase', () => {
let sut: NavigateUseCase;
let tabPresenter: TabPresenter;
let navigateClient: NavigateClient;
+
beforeEach(() => {
- tabPresenter = new TabPresenter();
+ tabPresenter = new MockTabPresenter();
navigateClient = new NavigateClient();
sut = new NavigateUseCase(tabPresenter, navigateClient);
});
+ const newTab = (url: string): browser.tabs.Tab => {
+ return {
+ index: 0,
+ title: 'dummy title',
+ url: url,
+ active: true,
+ hidden: false,
+ highlighted: false,
+ incognito: false,
+ isArticle: false,
+ isInReaderMode: false,
+ lastAccessed: 1585446733000,
+ pinned: false,
+ selected: false,
+ windowId: 0
+ };
+ };
+
describe('#openParent()', async () => {
it('opens parent directory of file', async() => {
- const stub = sinon.stub(tabPresenter, 'getCurrent');
- stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/banana' }))
+ sinon.stub(tabPresenter, 'getCurrent')
+ .returns(Promise.resolve(newTab('https://google.com/fruits/yellow/banana')));
- const mock = sinon.mock(tabPresenter);
- mock.expects('open').withArgs('https://google.com/fruits/yellow/');
+ const mock = sinon.mock(tabPresenter)
+ .expects('open').withArgs('https://google.com/fruits/yellow/');
await sut.openParent();
@@ -28,11 +109,11 @@ describe('NavigateUseCase', () => {
});
it('opens parent directory of directory', async() => {
- const stub = sinon.stub(tabPresenter, 'getCurrent');
- stub.returns(Promise.resolve({ url: 'https://google.com/fruits/yellow/' }))
+ sinon.stub(tabPresenter, 'getCurrent')
+ .returns(Promise.resolve(newTab('https://google.com/fruits/yellow/')));
- const mock = sinon.mock(tabPresenter);
- mock.expects('open').withArgs('https://google.com/fruits/');
+ const mock = sinon.mock(tabPresenter)
+ .expects('open').withArgs('https://google.com/fruits/');
await sut.openParent();
@@ -40,11 +121,11 @@ describe('NavigateUseCase', () => {
});
it('removes hash', async() => {
- const stub = sinon.stub(tabPresenter, 'getCurrent');
- stub.returns(Promise.resolve({ url: 'https://google.com/#top' }))
+ sinon.stub(tabPresenter, 'getCurrent')
+ .returns(Promise.resolve(newTab('https://google.com/#top')));
- const mock = sinon.mock(tabPresenter);
- mock.expects('open').withArgs('https://google.com/');
+ const mock = sinon.mock(tabPresenter)
+ .expects('open').withArgs('https://google.com/');
await sut.openParent();
@@ -52,11 +133,11 @@ describe('NavigateUseCase', () => {
});
it('removes search query', async() => {
- const stub = sinon.stub(tabPresenter, 'getCurrent');
- stub.returns(Promise.resolve({ url: 'https://google.com/search?q=apple' }))
+ sinon.stub(tabPresenter, 'getCurrent')
+ .returns(Promise.resolve(newTab('https://google.com/search?q=apple')));
- const mock = sinon.mock(tabPresenter);
- mock.expects('open').withArgs('https://google.com/search');
+ const mock = sinon.mock(tabPresenter)
+ .expects('open').withArgs('https://google.com/search');
await sut.openParent();
@@ -66,13 +147,11 @@ describe('NavigateUseCase', () => {
describe('#openRoot()', () => {
it('opens root direectory', async() => {
- const stub = sinon.stub(tabPresenter, 'getCurrent');
- stub.returns(Promise.resolve({
- url: 'https://google.com/seach?q=apple',
- }))
+ sinon.stub(tabPresenter, 'getCurrent')
+ .returns(Promise.resolve(newTab('https://google.com/seach?q=apple')));
- const mock = sinon.mock(tabPresenter);
- mock.expects('open').withArgs('https://google.com');
+ const mock = sinon.mock(tabPresenter)
+ .expects('open').withArgs('https://google.com');
await sut.openRoot();
diff --git a/test/background/usecases/filters.test.ts b/test/background/usecases/filters.test.ts
deleted file mode 100644
index 90541ff..0000000
--- a/test/background/usecases/filters.test.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import * as filters from 'background/usecases/filters';
-
-describe("background/usecases/filters", () => {
- describe('filterHttp', () => {
- it('filters http URLs duplicates to https hosts', () => {
- const pages = [
- { url: 'http://i-beam.org/foo' },
- { url: 'https://i-beam.org/bar' },
- { url: 'http://i-beam.net/hoge' },
- { url: 'http://i-beam.net/fuga' },
- ];
- const filtered = filters.filterHttp(pages);
-
- const urls = filtered.map(x => x.url);
- expect(urls).to.deep.equal([
- 'https://i-beam.org/bar', 'http://i-beam.net/hoge', 'http://i-beam.net/fuga'
- ]);
- })
- });
-
- describe('filterBlankTitle', () => {
- it('filters blank titles', () => {
- const pages = [
- { title: 'hello' },
- { title: '' },
- {},
- ];
- const filtered = filters.filterBlankTitle(pages);
-
- expect(filtered).to.deep.equal([{ title: 'hello' }]);
- });
- })
-
- describe('filterByTailingSlash', () => {
- it('filters duplicated pathname on tailing slash', () => {
- const pages = [
- { url: 'http://i-beam.org/content' },
- { url: 'http://i-beam.org/content/' },
- { url: 'http://i-beam.org/search' },
- { url: 'http://i-beam.org/search?q=apple_banana_cherry' },
- ];
- const filtered = filters.filterByTailingSlash(pages);
-
- const urls = filtered.map(x => x.url);
- expect(urls).to.deep.equal([
- 'http://i-beam.org/content',
- 'http://i-beam.org/search',
- 'http://i-beam.org/search?q=apple_banana_cherry',
- ]);
- });
- })
-
- describe('filterByPathname', () => {
- it('remains items less than minimam length', () => {
- const pages = [
- { url: 'http://i-beam.org/search?q=apple' },
- { url: 'http://i-beam.org/search?q=apple_banana' },
- { url: 'http://i-beam.org/search?q=apple_banana_cherry' },
- { url: 'http://i-beam.org/request?q=apple' },
- { url: 'http://i-beam.org/request?q=apple_banana' },
- { url: 'http://i-beam.org/request?q=apple_banana_cherry' },
- ];
- const filtered = filters.filterByPathname(pages, 10);
- expect(filtered).to.have.lengthOf(6);
- });
-
- it('filters by length of pathname', () => {
- const pages = [
- { url: 'http://i-beam.org/search?q=apple' },
- { url: 'http://i-beam.org/search?q=apple_banana' },
- { url: 'http://i-beam.org/search?q=apple_banana_cherry' },
- { url: 'http://i-beam.net/search?q=apple' },
- { url: 'http://i-beam.net/search?q=apple_banana' },
- { url: 'http://i-beam.net/search?q=apple_banana_cherry' },
- ];
- const filtered = filters.filterByPathname(pages, 0);
- expect(filtered).to.deep.equal([
- { url: 'http://i-beam.org/search?q=apple' },
- { url: 'http://i-beam.net/search?q=apple' },
- ]);
- });
- })
-
- describe('filterByOrigin', () => {
- it('remains items less than minimam length', () => {
- const pages = [
- { url: 'http://i-beam.org/search?q=apple' },
- { url: 'http://i-beam.org/search?q=apple_banana' },
- { url: 'http://i-beam.org/search?q=apple_banana_cherry' },
- { url: 'http://i-beam.org/request?q=apple' },
- { url: 'http://i-beam.org/request?q=apple_banana' },
- { url: 'http://i-beam.org/request?q=apple_banana_cherry' },
- ];
- const filtered = filters.filterByOrigin(pages, 10);
- expect(filtered).to.have.lengthOf(6);
- });
-
- it('filters by length of pathname', () => {
- const pages = [
- { url: 'http://i-beam.org/search?q=apple' },
- { url: 'http://i-beam.org/search?q=apple_banana' },
- { url: 'http://i-beam.org/search?q=apple_banana_cherry' },
- { url: 'http://i-beam.org/request?q=apple' },
- { url: 'http://i-beam.org/request?q=apple_banana' },
- { url: 'http://i-beam.org/request?q=apple_banana_cherry' },
- ];
- const filtered = filters.filterByOrigin(pages, 0);
- expect(filtered).to.deep.equal([
- { url: 'http://i-beam.org/search?q=apple' },
- ]);
- });
- })
-});
diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts
index 583c878..e6567b2 100644
--- a/test/console/actions/console.test.ts
+++ b/test/console/actions/console.test.ts
@@ -1,5 +1,6 @@
-import * as actions from 'console/actions';
-import * as consoleActions from 'console/actions/console';
+import * as actions from '../../../src/console/actions';
+import * as consoleActions from '../../../src/console/actions/console';
+import { expect } from 'chai';
describe("console actions", () => {
describe('hide', () => {
@@ -9,8 +10,8 @@ describe("console actions", () => {
});
});
describe("showCommand", () => {
- it('create CONSOLE_SHOW_COMMAND action', () => {
- const action = consoleActions.showCommand('hello');
+ it('create CONSOLE_SHOW_COMMAND action', async () => {
+ const action = await consoleActions.showCommand('hello');
expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND);
expect(action.text).to.equal('hello');
});
diff --git a/test/console/commandline/CommandLineParser.test.ts b/test/console/commandline/CommandLineParser.test.ts
new file mode 100644
index 0000000..6aec682
--- /dev/null
+++ b/test/console/commandline/CommandLineParser.test.ts
@@ -0,0 +1,29 @@
+import CommandLineParser, {InputPhase} from "../../../src/console/commandline/CommandLineParser";
+import { Command } from "../../../src/shared/Command";
+import { expect } from "chai";
+
+describe("CommandLineParser", () => {
+ describe("#inputPhase", () => {
+ it("returns parsed command-line", () => {
+ const sut = new CommandLineParser();
+ expect(sut.inputPhase("")).to.equal(InputPhase.OnCommand);
+ expect(sut.inputPhase("op")).to.equal(InputPhase.OnCommand);
+ expect(sut.inputPhase("open ")).to.equal(InputPhase.OnArgs);
+ expect(sut.inputPhase("open apple")).to.equal(InputPhase.OnArgs)
+ });
+ });
+ describe("#parse", () => {
+ it("returns parsed command-line", () => {
+ const sut = new CommandLineParser();
+ expect(sut.parse("open google apple")).to.deep.equal({
+ command: Command.Open,
+ args: "google apple",
+ });
+
+ expect(sut.parse("qa")).to.deep.equal({
+ command: Command.QuitAll,
+ args: "",
+ });
+ })
+ })
+});
diff --git a/test/console/commandline/CommandParser.test.ts b/test/console/commandline/CommandParser.test.ts
new file mode 100644
index 0000000..4ad78fd
--- /dev/null
+++ b/test/console/commandline/CommandParser.test.ts
@@ -0,0 +1,15 @@
+import CommandParser, { UnknownCommandError } from "../../../src/console/commandline/CommandParser";
+import { Command } from "../../../src/shared/Command";
+import { expect } from "chai"
+
+describe("CommandParser", () => {
+ describe("#parse", () => {
+ it("returns matched command with the string", () => {
+ const sut = new CommandParser();
+ expect(sut.parse("open")).to.equal(Command.Open);
+ expect(sut.parse("w")).to.equal(Command.WindowOpen);
+ expect(sut.parse("bdelete!")).to.equal(Command.BufferDeleteForce);
+ expect(() => sut.parse("harakiri")).to.throw(UnknownCommandError);
+ })
+ })
+});