From 3a7e55fd292196f600c11fad36425014677a1351 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 4 Apr 2021 21:34:13 +0900 Subject: Separate Command and Completion reducer --- test/console/reducers/completion.test.ts | 102 +++++++++++++++++++++++++ test/console/reducers/console.test.ts | 127 +++++-------------------------- 2 files changed, 123 insertions(+), 106 deletions(-) create mode 100644 test/console/reducers/completion.test.ts (limited to 'test/console/reducers') diff --git a/test/console/reducers/completion.test.ts b/test/console/reducers/completion.test.ts new file mode 100644 index 0000000..6c76369 --- /dev/null +++ b/test/console/reducers/completion.test.ts @@ -0,0 +1,102 @@ +import reducer, { State } from "../../../src/console/reducers/completion"; +import { expect } from "chai"; +import { + COMPLETION_COMPLETION_NEXT, + COMPLETION_COMPLETION_PREV, + COMPLETION_SET_COMPLETIONS, + CompletionAction, +} from "../../../src/console/actions/completion"; + +describe("completion reducer", () => { + it("return next state for CONSOLE_SET_COMPLETIONS", () => { + const initialState = reducer(undefined, {} as any); + let state: State = { + ...initialState, + select: 0, + completions: [], + }; + const action: CompletionAction = { + type: COMPLETION_SET_COMPLETIONS, + completions: [ + { + name: "Apple", + items: [{}, {}, {}], + }, + { + name: "Banana", + items: [{}, {}, {}], + }, + ], + completionSource: "", + }; + state = reducer(state, action); + expect(state).to.have.property("completions", action.completions); + expect(state).to.have.property("select", -1); + }); + + it("return next state for CONSOLE_COMPLETION_NEXT", () => { + const initialState = reducer(undefined, {} as any); + const action: CompletionAction = { + type: COMPLETION_COMPLETION_NEXT, + }; + let state = { + ...initialState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, action); + expect(state).to.have.property("select", 0); + + state = reducer(state, action); + expect(state).to.have.property("select", 1); + + state = reducer(state, action); + expect(state).to.have.property("select", 2); + + state = reducer(state, action); + expect(state).to.have.property("select", -1); + }); + + it("return next state for CONSOLE_COMPLETION_PREV", () => { + const initialState = reducer(undefined, {} as any); + const action: CompletionAction = { + type: COMPLETION_COMPLETION_PREV, + }; + let state = { + ...initialState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, action); + expect(state).to.have.property("select", 2); + + state = reducer(state, action); + expect(state).to.have.property("select", 1); + + state = reducer(state, action); + expect(state).to.have.property("select", 0); + + state = reducer(state, action); + expect(state).to.have.property("select", -1); + }); +}); diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts index 64e8eb3..4d4859d 100644 --- a/test/console/reducers/console.test.ts +++ b/test/console/reducers/console.test.ts @@ -1,8 +1,14 @@ -import * as actions from "../../../src/console/actions"; -import reducer, { State } from "../../../src/console/reducers"; +import reducer from "../../../src/console/reducers/console"; import { expect } from "chai"; -import CompletionType from "../../../src/shared/CompletionType"; -import { ConsoleAction } from "../../../src/console/actions"; +import { + CONSOLE_HIDE, + CONSOLE_HIDE_COMMAND, + CONSOLE_SET_CONSOLE_TEXT, + CONSOLE_SHOW_COMMAND, + CONSOLE_SHOW_ERROR, + CONSOLE_SHOW_INFO, + ConsoleAction, +} from "../../../src/console/actions/console"; describe("console reducer", () => { it("return the initial state", () => { @@ -10,21 +16,18 @@ describe("console reducer", () => { expect(state).to.have.property("mode", ""); expect(state).to.have.property("messageText", ""); expect(state).to.have.property("consoleText", ""); - expect(state).to.have.deep.property("completions", []); - expect(state).to.have.property("select", -1); }); it("return next state for CONSOLE_HIDE", () => { const initialState = reducer(undefined, {} as any); - const action: actions.ConsoleAction = { type: actions.CONSOLE_HIDE }; + const action: ConsoleAction = { type: CONSOLE_HIDE }; const state = reducer({ ...initialState, mode: "error" }, action); expect(state).to.have.property("mode", ""); }); it("return next state for CONSOLE_SHOW_COMMAND", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_COMMAND, - completionTypes: [CompletionType.SearchEngines, CompletionType.History], + const action: ConsoleAction = { + type: CONSOLE_SHOW_COMMAND, text: "open ", }; const state = reducer(undefined, action); @@ -33,8 +36,8 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SHOW_INFO", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_INFO, + const action: ConsoleAction = { + type: CONSOLE_SHOW_INFO, text: "an info", }; const state = reducer(undefined, action); @@ -43,8 +46,8 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SHOW_ERROR", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SHOW_ERROR, + const action: ConsoleAction = { + type: CONSOLE_SHOW_ERROR, text: "an error", }; const state = reducer(undefined, action); @@ -54,8 +57,8 @@ describe("console reducer", () => { it("return next state for CONSOLE_HIDE_COMMAND", () => { const initialState = reducer(undefined, {} as any); - const action: actions.ConsoleAction = { - type: actions.CONSOLE_HIDE_COMMAND, + const action: ConsoleAction = { + type: CONSOLE_HIDE_COMMAND, }; let state = reducer({ ...initialState, mode: "command" }, action); expect(state).to.have.property("mode", ""); @@ -65,100 +68,12 @@ describe("console reducer", () => { }); it("return next state for CONSOLE_SET_CONSOLE_TEXT", () => { - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SET_CONSOLE_TEXT, + const action: ConsoleAction = { + type: CONSOLE_SET_CONSOLE_TEXT, consoleText: "hello world", }; const state = reducer(undefined, action); expect(state).to.have.property("consoleText", "hello world"); }); - - it("return next state for CONSOLE_SET_COMPLETIONS", () => { - const initialState = reducer(undefined, {} as any); - let state: State = { - ...initialState, - select: 0, - completions: [], - }; - const action: actions.ConsoleAction = { - type: actions.CONSOLE_SET_COMPLETIONS, - completions: [ - { - name: "Apple", - items: [{}, {}, {}], - }, - { - name: "Banana", - items: [{}, {}, {}], - }, - ], - completionSource: "", - }; - state = reducer(state, action); - expect(state).to.have.property("completions", action.completions); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_NEXT", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: actions.CONSOLE_COMPLETION_NEXT }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_PREV", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: actions.CONSOLE_COMPLETION_PREV }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); }); -- cgit v1.2.3 From 618fb497c443662531eb3befe7696a04efe9651d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 11 Apr 2021 18:00:51 +0900 Subject: Replace completion state with Custom Hooks --- src/console/actions/completion.ts | 243 --------------------------- src/console/actions/console.ts | 17 +- src/console/completion/actions.ts | 76 +++++++++ src/console/completion/context.ts | 9 + src/console/completion/hooks.ts | 277 +++++++++++++++++++++++++++++++ src/console/completion/provider.tsx | 25 +++ src/console/completion/reducer.ts | 96 +++++++++++ src/console/components/CommandPrompt.tsx | 219 +++++++----------------- src/console/components/Console.tsx | 2 +- src/console/reducers/completion.ts | 99 ----------- src/console/reducers/console.ts | 3 - test/console/actions/completion.test.ts | 28 ---- test/console/actions/console.test.ts | 9 - test/console/completion/reducer.test.ts | 168 +++++++++++++++++++ test/console/reducers/completion.test.ts | 102 ------------ test/console/reducers/console.test.ts | 11 -- 16 files changed, 717 insertions(+), 667 deletions(-) delete mode 100644 src/console/actions/completion.ts create mode 100644 src/console/completion/actions.ts create mode 100644 src/console/completion/context.ts create mode 100644 src/console/completion/hooks.ts create mode 100644 src/console/completion/provider.tsx create mode 100644 src/console/completion/reducer.ts delete mode 100644 src/console/reducers/completion.ts delete mode 100644 test/console/actions/completion.test.ts create mode 100644 test/console/completion/reducer.test.ts delete mode 100644 test/console/reducers/completion.test.ts (limited to 'test/console/reducers') diff --git a/src/console/actions/completion.ts b/src/console/actions/completion.ts deleted file mode 100644 index 2f6f82f..0000000 --- a/src/console/actions/completion.ts +++ /dev/null @@ -1,243 +0,0 @@ -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", - colorscheme: "color scheme of the console", -}; - -export const COMPLETION_START_COMPLETION = "console.start.completion"; -export const COMPLETION_SET_COMPLETIONS = "console.set.completions"; -export const COMPLETION_COMPLETION_NEXT = "completion.completion.next"; -export const COMPLETION_COMPLETION_PREV = "completion.completion.prev"; - -export interface CompletionStartCompletionAction { - type: typeof COMPLETION_START_COMPLETION; - completionTypes: CompletionType[]; -} - -export interface SetCompletionsAction { - type: typeof COMPLETION_SET_COMPLETIONS; - completions: Completions; - completionSource: string; -} - -export interface CompletionNextAction { - type: typeof COMPLETION_COMPLETION_NEXT; -} - -export interface CompletionPrevAction { - type: typeof COMPLETION_COMPLETION_PREV; -} - -export type CompletionAction = - | CompletionStartCompletionAction - | SetCompletionsAction - | CompletionNextAction - | CompletionPrevAction; -const startCompletion = async (): Promise => { - const completionTypes = await completionClient.getCompletionTypes(); - return { - type: COMPLETION_START_COMPLETION, - completionTypes, - }; -}; - -const getCommandCompletions = (text: string): 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: COMPLETION_SET_COMPLETIONS, - completions, - completionSource: text, - }; -}; - -const getOpenCompletions = async ( - types: CompletionType[], - original: string, - command: Command, - query: string -): Promise => { - 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: COMPLETION_SET_COMPLETIONS, - completions, - completionSource: original, - }; -}; - -const getTabCompletions = async ( - original: string, - command: Command, - query: string, - excludePinned: boolean -): Promise => { - 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: COMPLETION_SET_COMPLETIONS, - completions, - completionSource: original, - }; -}; - -const getPropertyCompletions = async ( - original: string, - command: Command, - query: string -): Promise => { - 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: command + " " + 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: COMPLETION_SET_COMPLETIONS, - completions, - completionSource: original, - }; -}; - -const completionNext = (): CompletionNextAction => { - return { - type: COMPLETION_COMPLETION_NEXT, - }; -}; - -const completionPrev = (): CompletionPrevAction => { - return { - type: COMPLETION_COMPLETION_PREV, - }; -}; - -export { - startCompletion, - getCommandCompletions, - getOpenCompletions, - getTabCompletions, - getPropertyCompletions, - completionNext, - completionPrev, -}; diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index bce2c67..2338067 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -4,7 +4,6 @@ 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_HIDE_COMMAND = "console.hide.command"; -export const CONSOLE_SET_CONSOLE_TEXT = "console.set.command"; export const CONSOLE_SHOW_FIND = "console.show.find"; export const CONSOLE_HIDE = "console.hide"; @@ -35,19 +34,13 @@ export interface HideCommandAction { type: typeof CONSOLE_HIDE_COMMAND; } -export interface SetConsoleTextAction { - type: typeof CONSOLE_SET_CONSOLE_TEXT; - consoleText: string; -} - export type ConsoleAction = | HideAction | ShowCommand | ShowFindAction | ShowErrorAction | ShowInfoAction - | HideCommandAction - | SetConsoleTextAction; + | HideCommandAction; const hide = (): ConsoleAction => { return { @@ -113,13 +106,6 @@ const enterFind = (text?: string): HideCommandAction => { return hideCommand(); }; -const setConsoleText = (consoleText: string): SetConsoleTextAction => { - return { - type: CONSOLE_SET_CONSOLE_TEXT, - consoleText, - }; -}; - export { hide, showCommand, @@ -127,7 +113,6 @@ export { showError, showInfo, hideCommand, - setConsoleText, enterCommand, enterFind, }; diff --git a/src/console/completion/actions.ts b/src/console/completion/actions.ts new file mode 100644 index 0000000..59d1a04 --- /dev/null +++ b/src/console/completion/actions.ts @@ -0,0 +1,76 @@ +import CompletionType from "../../shared/CompletionType"; +import Completions from "../Completions"; + +export const INIT_COMPLETIONS = "reset.completions"; +export const SET_COMPLETION_SOURCE = "set.completion.source"; +export const SET_COMPLETIONS = "set.completions"; +export const COMPLETION_NEXT = "completion.next"; +export const COMPLETION_PREV = "completion.prev"; + +export interface InitCompletionAction { + type: typeof INIT_COMPLETIONS; + completionTypes: CompletionType[]; +} + +export interface SetCompletionSourceAction { + type: typeof SET_COMPLETION_SOURCE; + completionSource: string; +} + +export interface SetCompletionsAction { + type: typeof SET_COMPLETIONS; + completions: Completions; +} + +export interface CompletionNextAction { + type: typeof COMPLETION_NEXT; +} + +export interface CompletionPrevAction { + type: typeof COMPLETION_PREV; +} + +export type CompletionAction = + | InitCompletionAction + | SetCompletionSourceAction + | SetCompletionsAction + | CompletionNextAction + | CompletionPrevAction; + +export const initCompletion = ( + completionTypes: CompletionType[] +): InitCompletionAction => { + return { + type: INIT_COMPLETIONS, + completionTypes, + }; +}; +export const setCompletionSource = ( + query: string +): SetCompletionSourceAction => { + return { + type: SET_COMPLETION_SOURCE, + completionSource: query, + }; +}; + +export const setCompletions = ( + completions: Completions +): SetCompletionsAction => { + return { + type: SET_COMPLETIONS, + completions, + }; +}; + +export const selectNext = (): CompletionNextAction => { + return { + type: COMPLETION_NEXT, + }; +}; + +export const selectPrev = (): CompletionPrevAction => { + return { + type: COMPLETION_PREV, + }; +}; diff --git a/src/console/completion/context.ts b/src/console/completion/context.ts new file mode 100644 index 0000000..2fafcf8 --- /dev/null +++ b/src/console/completion/context.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { State, defaultState } from "./reducer"; +import { CompletionAction } from "./actions"; + +export const CompletionStateContext = React.createContext(defaultState); + +export const CompletionDispatchContext = React.createContext< + (action: CompletionAction) => void +>(() => {}); diff --git a/src/console/completion/hooks.ts b/src/console/completion/hooks.ts new file mode 100644 index 0000000..aac431b --- /dev/null +++ b/src/console/completion/hooks.ts @@ -0,0 +1,277 @@ +import React from "react"; +import * as actions from "./actions"; +import { Command } from "../../shared/Command"; +import TabFlag from "../../shared/TabFlag"; +import { CompletionStateContext, CompletionDispatchContext } from "./context"; +import CompletionClient from "../clients/CompletionClient"; +import CommandLineParser, { + CommandLine, + InputPhase, +} from "../commandline/CommandLineParser"; +import { UnknownCommandError } from "../commandline/CommandParser"; +import Completions from "../Completions"; +import CompletionType from "../../shared/CompletionType"; + +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", + colorscheme: "color scheme of the console", +}; + +const completionClient = new CompletionClient(); + +const getCommandCompletions = async (query: string): Promise => { + const items = Object.entries(commandDocs) + .filter(([name]) => name.startsWith(query)) + .map(([name, doc]) => ({ + caption: name, + content: name, + url: doc, + })); + return [ + { + name: "Console Command", + items, + }, + ]; +}; + +const getOpenCompletions = async ( + command: string, + query: string, + completionTypes: CompletionType[] +): Promise => { + const completions: Completions = []; + for (const type of completionTypes) { + 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 completions; +}; + +export const getTabCompletions = async ( + command: string, + query: string, + excludePinned: boolean +): Promise => { + const items = await completionClient.requestTabs(query, excludePinned); + if (items.length === 0) { + return []; + } + + return [ + { + 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, + })), + }, + ]; +}; + +export const getPropertyCompletions = async ( + command: string, + query: string +): Promise => { + 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: command + " " + item.name, + url: "Set " + desc, + }, + ]; + } + }) + .reduce((acc, val) => acc.concat(val), []) + .filter((item) => item.caption.startsWith(query)); + return [{ name: "Properties", items }]; +}; + +export const useCompletions = () => { + const state = React.useContext(CompletionStateContext); + const dispatch = React.useContext(CompletionDispatchContext); + const commandLineParser = React.useMemo(() => new CommandLineParser(), []); + + const updateCompletions = React.useCallback((source: string) => { + dispatch(actions.setCompletionSource(source)); + }, []); + + const initCompletion = React.useCallback((source: string) => { + completionClient.getCompletionTypes().then((completionTypes) => { + dispatch(actions.initCompletion(completionTypes)); + dispatch(actions.setCompletionSource(source)); + }); + }, []); + + React.useEffect(() => { + const text = state.completionSource; + const phase = commandLineParser.inputPhase(text); + if (phase === InputPhase.OnCommand) { + getCommandCompletions(text).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + } else { + let cmd: CommandLine | null = null; + try { + cmd = commandLineParser.parse(text); + } catch (e) { + if (e instanceof UnknownCommandError) { + return; + } + } + switch (cmd?.command) { + case Command.Open: + case Command.TabOpen: + case Command.WindowOpen: + if (!state.completionTypes) { + initCompletion(text); + return; + } + + getOpenCompletions( + cmd.command, + cmd.args, + state.completionTypes + ).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + break; + case Command.Buffer: + getTabCompletions(cmd.command, cmd.args, false).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + break; + case Command.BufferDelete: + case Command.BuffersDelete: + getTabCompletions(cmd.command, cmd.args, true).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + break; + case Command.BufferDeleteForce: + case Command.BuffersDeleteForce: + getTabCompletions(cmd.command, cmd.args, false).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + break; + case Command.Set: + getPropertyCompletions(cmd.command, cmd.args).then((completions) => + dispatch(actions.setCompletions(completions)) + ); + break; + } + } + }, [state.completionSource, state.completionTypes]); + + return { + completions: state.completions, + updateCompletions, + initCompletion, + }; +}; + +export const useSelectCompletion = () => { + const state = React.useContext(CompletionStateContext); + const dispatch = React.useContext(CompletionDispatchContext); + const next = React.useCallback(() => dispatch(actions.selectNext()), [ + dispatch, + ]); + const prev = React.useCallback(() => dispatch(actions.selectPrev()), [ + dispatch, + ]); + const currentValue = React.useMemo(() => { + if (state.select < 0) { + return state.completionSource; + } + const items = state.completions.map((g) => g.items).flat(); + return items[state.select]?.content || ""; + }, [state.completionSource, state.select]); + + return { + select: state.select, + currentValue, + selectNext: next, + selectPrev: prev, + }; +}; diff --git a/src/console/completion/provider.tsx b/src/console/completion/provider.tsx new file mode 100644 index 0000000..c0de250 --- /dev/null +++ b/src/console/completion/provider.tsx @@ -0,0 +1,25 @@ +import reducer, { defaultState } from "./reducer"; +import React from "react"; +import { CompletionDispatchContext, CompletionStateContext } from "./context"; + +interface Props { + initialInputValue: string; +} + +export const CompletionProvider: React.FC = ({ + initialInputValue, + children, +}) => { + const initialState = { + ...defaultState, + completionSource: initialInputValue, + }; + const [state, dispatch] = React.useReducer(reducer, initialState); + return ( + + + {children} + + + ); +}; diff --git a/src/console/completion/reducer.ts b/src/console/completion/reducer.ts new file mode 100644 index 0000000..905451f --- /dev/null +++ b/src/console/completion/reducer.ts @@ -0,0 +1,96 @@ +import Completions from "../Completions"; +import CompletionType from "../../shared/CompletionType"; +import { + INIT_COMPLETIONS, + SET_COMPLETION_SOURCE, + SET_COMPLETIONS, + COMPLETION_NEXT, + COMPLETION_PREV, + CompletionAction, +} from "./actions"; + +export interface State { + completionTypes?: CompletionType[]; + completionSource: string; + completions: Completions; + select: number; +} + +export const defaultState = { + completionTypes: undefined, + completionSource: "", + completions: [], + select: -1, +}; + +const nextSelection = (state: State): number => { + const length = state.completions + .map((g) => g.items.length) + .reduce((x, y) => x + y, 0); + if (length === 0) { + return -1; + } + if (state.select < 0) { + return 0; + } + if (state.select + 1 < length) { + return state.select + 1; + } + return -1; +}; + +const prevSelection = (state: State): number => { + if (state.completions.length === 0) { + return -1; + } + const length = state.completions + .map((g) => g.items.length) + .reduce((x, y) => x + y); + if (state.select < 0) { + return length - 1; + } + return state.select - 1; +}; + +// eslint-disable-next-line max-lines-per-function +export default function reducer( + state: State = defaultState, + action: CompletionAction +): State { + switch (action.type) { + case INIT_COMPLETIONS: + return { + ...state, + completionTypes: action.completionTypes, + completions: [], + select: -1, + }; + case SET_COMPLETION_SOURCE: + return { + ...state, + completionSource: action.completionSource, + select: -1, + }; + case SET_COMPLETIONS: + return { + ...state, + completions: action.completions, + }; + case COMPLETION_NEXT: { + const select = nextSelection(state); + return { + ...state, + select: select, + }; + } + case COMPLETION_PREV: { + const select = prevSelection(state); + return { + ...state, + select: select, + }; + } + default: + return state; + } +} diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx index f6f4d8f..24f46ae 100644 --- a/src/console/components/CommandPrompt.tsx +++ b/src/console/components/CommandPrompt.tsx @@ -1,17 +1,12 @@ import React from "react"; import * as consoleActions from "../actions/console"; -import * as completionActions from "../actions/completion"; import AppContext from "./AppContext"; -import CommandLineParser, { - InputPhase, -} from "../commandline/CommandLineParser"; import Completion from "./console/Completion"; import Input from "./console//Input"; -import { Command } from "../../shared/Command"; import styled from "styled-components"; -import reducer, { defaultState, completedText } from "../reducers/completion"; -import CompletionType from "../../shared/CompletionType"; +import { useCompletions, useSelectCompletion } from "../completion/hooks"; import useAutoResize from "../hooks/useAutoResize"; +import { CompletionProvider } from "../completion/provider"; const COMPLETION_MAX_ITEMS = 33; @@ -19,13 +14,20 @@ const ConsoleWrapper = styled.div` border-top: 1px solid gray; `; -const CommandPrompt: React.FC = () => { - const { state, dispatch } = React.useContext(AppContext); - const [completionState, completionDispatch] = React.useReducer( - reducer, - defaultState - ); - const commandLineParser = new CommandLineParser(); +interface Props { + initialInputValue: string; +} + +const CommandPromptInner: React.FC = ({ initialInputValue }) => { + const { dispatch } = React.useContext(AppContext); + const [inputValue, setInputValue] = React.useState(initialInputValue); + const { completions, updateCompletions } = useCompletions(); + const { + select, + currentValue, + selectNext, + selectPrev, + } = useSelectCompletion(); useAutoResize(); @@ -33,174 +35,81 @@ const CommandPrompt: React.FC = () => { dispatch(consoleActions.hideCommand()); }; - const doEnter = (e: React.KeyboardEvent) => { - e.stopPropagation(); - e.preventDefault(); + const isCancelKey = React.useCallback( + (e: React.KeyboardEvent) => + e.key === "Escape" || + (e.ctrlKey && e.key === "[") || + (e.ctrlKey && e.key === "c"), + [] + ); - const value = (e.target as HTMLInputElement).value; - dispatch(consoleActions.enterCommand(value)); - }; + const isNextKey = React.useCallback( + (e: React.KeyboardEvent) => + (!e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "n"), + [] + ); - const selectNext = (e: React.KeyboardEvent) => { - completionDispatch(completionActions.completionNext()); - e.stopPropagation(); - e.preventDefault(); - }; + const isPrevKey = React.useCallback( + (e: React.KeyboardEvent) => + (e.shiftKey && e.key === "Tab") || (e.ctrlKey && e.key === "p"), + [] + ); - const selectPrev = (e: React.KeyboardEvent) => { - completionDispatch(completionActions.completionPrev()); - e.stopPropagation(); - e.preventDefault(); - }; + const isEnterKey = React.useCallback( + (e: React.KeyboardEvent) => + e.key === "Enter" || (e.ctrlKey && e.key === "m"), + [] + ); const onKeyDown = (e: React.KeyboardEvent) => { - switch (e.key) { - case "Escape": - dispatch(consoleActions.hideCommand()); - break; - case "Enter": - doEnter(e); - break; - case "Tab": - if (e.shiftKey) { - completionDispatch(completionActions.completionPrev()); - } else { - completionDispatch(completionActions.completionNext()); - } - e.stopPropagation(); - e.preventDefault(); - break; - case "[": - if (e.ctrlKey) { - e.preventDefault(); - dispatch(consoleActions.hideCommand()); - } - break; - case "c": - if (e.ctrlKey) { - e.preventDefault(); - dispatch(consoleActions.hideCommand()); - } - break; - case "m": - if (e.ctrlKey) { - doEnter(e); - } - break; - case "n": - if (e.ctrlKey) { - selectNext(e); - } - break; - case "p": - if (e.ctrlKey) { - selectPrev(e); - } - break; + if (isCancelKey(e)) { + dispatch(consoleActions.hideCommand()); + } else if (isEnterKey(e)) { + const value = (e.target as HTMLInputElement).value; + dispatch(consoleActions.enterCommand(value)); + } else if (isNextKey(e)) { + selectNext(); + } else if (isPrevKey(e)) { + selectPrev(); + } else { + return; } + + e.stopPropagation(); + e.preventDefault(); }; const onChange = (e: React.ChangeEvent) => { const text = e.target.value; - dispatch(consoleActions.setConsoleText(text)); - const action = getCompletionAction(text); - Promise.resolve(action).then((a) => { - if (a) { - completionDispatch(a); - } - }); + setInputValue(text); }; React.useEffect(() => { - completionActions.startCompletion().then((action) => { - completionDispatch(action); - - const completionAction = getCompletionAction( - state.consoleText, - action.completionTypes - ); - Promise.resolve(completionAction).then((a) => { - if (a) { - completionDispatch(a); - } - }); - }); - }, []); - - const getCompletionAction = ( - text: string, - completionTypes: CompletionType[] | undefined = undefined - ) => { - const types = completionTypes || completionState.completionTypes; - const phase = commandLineParser.inputPhase(text); - if (phase === InputPhase.OnCommand) { - return completionActions.getCommandCompletions(text); - } else { - const cmd = commandLineParser.parse(text); - switch (cmd.command) { - case Command.Open: - case Command.TabOpen: - case Command.WindowOpen: - return completionActions.getOpenCompletions( - types, - text, - cmd.command, - cmd.args - ); - case Command.Buffer: - return completionActions.getTabCompletions( - text, - cmd.command, - cmd.args, - false - ); - case Command.BufferDelete: - case Command.BuffersDelete: - return completionActions.getTabCompletions( - text, - cmd.command, - cmd.args, - true - ); - case Command.BufferDeleteForce: - case Command.BuffersDeleteForce: - return completionActions.getTabCompletions( - text, - cmd.command, - cmd.args, - false - ); - case Command.Set: - return completionActions.getPropertyCompletions( - text, - cmd.command, - cmd.args - ); - } - } - return undefined; - }; + updateCompletions(inputValue); + }, [inputValue]); return ( ); }; +const CommandPrompt: React.FC = ({ initialInputValue }) => ( + + + +); + export default CommandPrompt; diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index f6f4234..508c6eb 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -16,7 +16,7 @@ const Console: React.FC = () => { switch (state.mode) { case "command": - return ; + return ; case "find": return ; case "info": diff --git a/src/console/reducers/completion.ts b/src/console/reducers/completion.ts deleted file mode 100644 index 2c7ee55..0000000 --- a/src/console/reducers/completion.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Completions from "../Completions"; -import CompletionType from "../../shared/CompletionType"; -import { - COMPLETION_COMPLETION_NEXT, - COMPLETION_COMPLETION_PREV, - COMPLETION_SET_COMPLETIONS, - COMPLETION_START_COMPLETION, - CompletionAction, -} from "../actions/completion"; - -export interface State { - completionTypes: CompletionType[]; - completionSource: string; - completions: Completions; - select: number; -} - -export const defaultState = { - completionTypes: [], - completionSource: "", - completions: [], - select: -1, -}; - -const nextSelection = (state: State): number => { - if (state.completions.length === 0) { - return -1; - } - if (state.select < 0) { - return 0; - } - - const length = state.completions - .map((g) => g.items.length) - .reduce((x, y) => x + y); - if (state.select + 1 < length) { - return state.select + 1; - } - return -1; -}; - -const prevSelection = (state: State): number => { - const length = state.completions - .map((g) => g.items.length) - .reduce((x, y) => x + y); - if (state.select < 0) { - return length - 1; - } - return state.select - 1; -}; - -export const completedText = (state: State): string => { - if (state.select < 0) { - return state.completionSource; - } - const items = state.completions - .map((g) => g.items) - .reduce((g1, g2) => g1.concat(g2)); - return items[state.select].content || ""; -}; - -// eslint-disable-next-line max-lines-per-function -export default function reducer( - state: State = defaultState, - action: CompletionAction -): State { - switch (action.type) { - case COMPLETION_START_COMPLETION: - return { - ...state, - completionTypes: action.completionTypes, - completions: [], - select: -1, - }; - case COMPLETION_SET_COMPLETIONS: - return { - ...state, - completions: action.completions, - completionSource: action.completionSource, - select: -1, - }; - case COMPLETION_COMPLETION_NEXT: { - const select = nextSelection(state); - return { - ...state, - select: select, - }; - } - case COMPLETION_COMPLETION_PREV: { - const select = prevSelection(state); - return { - ...state, - select: select, - }; - } - default: - return state; - } -} diff --git a/src/console/reducers/console.ts b/src/console/reducers/console.ts index 37f1efc..babad69 100644 --- a/src/console/reducers/console.ts +++ b/src/console/reducers/console.ts @@ -1,7 +1,6 @@ import { CONSOLE_HIDE, CONSOLE_HIDE_COMMAND, - CONSOLE_SET_CONSOLE_TEXT, CONSOLE_SHOW_COMMAND, CONSOLE_SHOW_ERROR, CONSOLE_SHOW_FIND, @@ -47,8 +46,6 @@ export default function reducer( mode: state.mode === "command" || state.mode === "find" ? "" : state.mode, }; - case CONSOLE_SET_CONSOLE_TEXT: - return { ...state, consoleText: action.consoleText }; default: return state; } diff --git a/test/console/actions/completion.test.ts b/test/console/actions/completion.test.ts deleted file mode 100644 index cd6899a..0000000 --- a/test/console/actions/completion.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as completionActions from "../../../src/console/actions/completion"; -import { - COMPLETION_COMPLETION_NEXT, - COMPLETION_COMPLETION_PREV, -} from "../../../src/console/actions/completion"; -import { expect } from "chai"; - -import browserFake from "webextensions-api-fake"; - -describe("completion actions", () => { - beforeEach(() => { - (global as any).browser = browserFake(); - }); - - describe("completionPrev", () => { - it("create COMPLETION_COMPLETION_PREV action", () => { - const action = completionActions.completionPrev(); - expect(action.type).to.equal(COMPLETION_COMPLETION_PREV); - }); - }); - - describe("completionNext", () => { - it("create COMPLETION_COMPLETION_NEXT action", () => { - const action = completionActions.completionNext(); - expect(action.type).to.equal(COMPLETION_COMPLETION_NEXT); - }); - }); -}); diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts index f5f102b..736dd54 100644 --- a/test/console/actions/console.test.ts +++ b/test/console/actions/console.test.ts @@ -2,7 +2,6 @@ import * as consoleActions from "../../../src/console/actions/console"; import { CONSOLE_HIDE, CONSOLE_HIDE_COMMAND, - CONSOLE_SET_CONSOLE_TEXT, CONSOLE_SHOW_COMMAND, CONSOLE_SHOW_ERROR, CONSOLE_SHOW_FIND, @@ -60,12 +59,4 @@ describe("console actions", () => { expect(action.type).to.equal(CONSOLE_HIDE_COMMAND); }); }); - - describe("setConsoleText", () => { - it("create CONSOLE_SET_CONSOLE_TEXT action", () => { - const action = consoleActions.setConsoleText("hello world"); - expect(action.type).to.equal(CONSOLE_SET_CONSOLE_TEXT); - expect(action.consoleText).to.equal("hello world"); - }); - }); }); diff --git a/test/console/completion/reducer.test.ts b/test/console/completion/reducer.test.ts new file mode 100644 index 0000000..b742872 --- /dev/null +++ b/test/console/completion/reducer.test.ts @@ -0,0 +1,168 @@ +import reducer, { + defaultState, + State, +} from "../../../src/console/completion/reducer"; +import { expect } from "chai"; +import { + initCompletion, + selectNext, + selectPrev, + setCompletions, + setCompletionSource, +} from "../../../src/console/completion/actions"; +import CompletionType from "../../../src/shared/CompletionType"; + +describe("completion reducer", () => { + describe("initCompletion", () => { + it("initializes completions", () => { + const nextState = reducer( + defaultState, + initCompletion([CompletionType.Bookmarks, CompletionType.History]) + ); + + expect(nextState.completionTypes).deep.equals([ + CompletionType.Bookmarks, + CompletionType.History, + ]); + }); + }); + + describe("setCompletionSource", () => { + it("sets a completion source", () => { + const nextState = reducer(defaultState, setCompletionSource("open ")); + + expect(nextState.completionSource).equals("open "); + }); + }); + + describe("setCompletions", () => { + it("sets completions", () => { + const nextState = reducer( + defaultState, + setCompletions([ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ]) + ); + + expect(nextState.completions).deep.equals([ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ]); + }); + }); + + describe("selectNext", () => { + context("when no completion groups", () => { + it("does nothing", () => { + const nextState = reducer(defaultState, selectNext()); + expect(nextState.select).equals(-1); + }); + }); + + context("when no completion items", () => { + it("does nothing", () => { + const state = { + ...defaultState, + completions: [{ name: "apple", items: [] }], + }; + const nextState = reducer(state, selectNext()); + expect(nextState.select).equals(-1); + }); + }); + + context("when completions exist", () => { + it("selects next selection", () => { + let state: State = { + ...defaultState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, selectNext()); + expect(state.select).equals(0); + + state = reducer(state, selectNext()); + expect(state.select).equals(1); + + state = reducer(state, selectNext()); + expect(state.select).equals(2); + + state = reducer(state, selectNext()); + expect(state.select).equals(-1); + }); + }); + }); + + describe("selectPrev", () => { + context("when no completion groups", () => { + it("does nothing", () => { + const nextState = reducer(defaultState, selectPrev()); + expect(nextState.select).equals(-1); + }); + + context("when no completion items", () => { + it("does nothing", () => { + const state = { + ...defaultState, + completions: [{ name: "apple", items: [] }], + }; + const nextState = reducer(state, selectPrev()); + expect(nextState.select).equals(-1); + }); + }); + }); + + context("when completions exist", () => { + it("selects a previous completion", () => { + let state: State = { + ...defaultState, + select: -1, + completions: [ + { + name: "Apple", + items: [{}, {}], + }, + { + name: "Banana", + items: [{}], + }, + ], + }; + + state = reducer(state, selectPrev()); + expect(state).to.have.property("select", 2); + + state = reducer(state, selectPrev()); + expect(state).to.have.property("select", 1); + + state = reducer(state, selectPrev()); + expect(state).to.have.property("select", 0); + + state = reducer(state, selectPrev()); + expect(state).to.have.property("select", -1); + }); + }); + }); +}); diff --git a/test/console/reducers/completion.test.ts b/test/console/reducers/completion.test.ts deleted file mode 100644 index 6c76369..0000000 --- a/test/console/reducers/completion.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import reducer, { State } from "../../../src/console/reducers/completion"; -import { expect } from "chai"; -import { - COMPLETION_COMPLETION_NEXT, - COMPLETION_COMPLETION_PREV, - COMPLETION_SET_COMPLETIONS, - CompletionAction, -} from "../../../src/console/actions/completion"; - -describe("completion reducer", () => { - it("return next state for CONSOLE_SET_COMPLETIONS", () => { - const initialState = reducer(undefined, {} as any); - let state: State = { - ...initialState, - select: 0, - completions: [], - }; - const action: CompletionAction = { - type: COMPLETION_SET_COMPLETIONS, - completions: [ - { - name: "Apple", - items: [{}, {}, {}], - }, - { - name: "Banana", - items: [{}, {}, {}], - }, - ], - completionSource: "", - }; - state = reducer(state, action); - expect(state).to.have.property("completions", action.completions); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_NEXT", () => { - const initialState = reducer(undefined, {} as any); - const action: CompletionAction = { - type: COMPLETION_COMPLETION_NEXT, - }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); - - it("return next state for CONSOLE_COMPLETION_PREV", () => { - const initialState = reducer(undefined, {} as any); - const action: CompletionAction = { - type: COMPLETION_COMPLETION_PREV, - }; - let state = { - ...initialState, - select: -1, - completions: [ - { - name: "Apple", - items: [{}, {}], - }, - { - name: "Banana", - items: [{}], - }, - ], - }; - - state = reducer(state, action); - expect(state).to.have.property("select", 2); - - state = reducer(state, action); - expect(state).to.have.property("select", 1); - - state = reducer(state, action); - expect(state).to.have.property("select", 0); - - state = reducer(state, action); - expect(state).to.have.property("select", -1); - }); -}); diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts index 4d4859d..390dc66 100644 --- a/test/console/reducers/console.test.ts +++ b/test/console/reducers/console.test.ts @@ -3,7 +3,6 @@ import { expect } from "chai"; import { CONSOLE_HIDE, CONSOLE_HIDE_COMMAND, - CONSOLE_SET_CONSOLE_TEXT, CONSOLE_SHOW_COMMAND, CONSOLE_SHOW_ERROR, CONSOLE_SHOW_INFO, @@ -66,14 +65,4 @@ describe("console reducer", () => { state = reducer({ ...initialState, mode: "error" }, action); expect(state).to.have.property("mode", "error"); }); - - it("return next state for CONSOLE_SET_CONSOLE_TEXT", () => { - const action: ConsoleAction = { - type: CONSOLE_SET_CONSOLE_TEXT, - consoleText: "hello world", - }; - const state = reducer(undefined, action); - - expect(state).to.have.property("consoleText", "hello world"); - }); }); -- cgit v1.2.3 From 8a5bba1da639355a25da8c279a9f1cf0a7300a9f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 11 Apr 2021 22:30:41 +0900 Subject: Replace app state with Custom Hooks --- src/console/actions/console.ts | 118 ------------------------------- src/console/app/actions.ts | 82 +++++++++++++++++++++ src/console/app/contexts.ts | 9 +++ src/console/app/hooks.ts | 115 ++++++++++++++++++++++++++++++ src/console/app/provider.tsx | 14 ++++ src/console/app/recuer.ts | 52 ++++++++++++++ src/console/components/AppContext.ts | 13 ---- src/console/components/CommandPrompt.tsx | 13 ++-- src/console/components/Console.tsx | 36 ++++++---- src/console/components/FindPrompt.tsx | 12 ++-- src/console/index.tsx | 62 ++++++++-------- src/console/reducers/console.ts | 52 -------------- test/console/actions/console.test.ts | 62 ---------------- test/console/app/actions.test.ts | 62 ++++++++++++++++ test/console/app/reducer.test.ts | 85 ++++++++++++++++++++++ test/console/reducers/console.test.ts | 68 ------------------ 16 files changed, 486 insertions(+), 369 deletions(-) delete mode 100644 src/console/actions/console.ts create mode 100644 src/console/app/actions.ts create mode 100644 src/console/app/contexts.ts create mode 100644 src/console/app/hooks.ts create mode 100644 src/console/app/provider.tsx create mode 100644 src/console/app/recuer.ts delete mode 100644 src/console/components/AppContext.ts delete mode 100644 src/console/reducers/console.ts delete mode 100644 test/console/actions/console.test.ts create mode 100644 test/console/app/actions.test.ts create mode 100644 test/console/app/reducer.test.ts delete mode 100644 test/console/reducers/console.test.ts (limited to 'test/console/reducers') diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts deleted file mode 100644 index 2338067..0000000 --- a/src/console/actions/console.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as messages from "../../shared/messages"; - -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_HIDE_COMMAND = "console.hide.command"; -export const CONSOLE_SHOW_FIND = "console.show.find"; -export const CONSOLE_HIDE = "console.hide"; - -export interface HideAction { - type: typeof CONSOLE_HIDE; -} - -export interface ShowCommand { - type: typeof CONSOLE_SHOW_COMMAND; - text: string; -} - -export interface ShowFindAction { - type: typeof CONSOLE_SHOW_FIND; -} - -export interface ShowErrorAction { - type: typeof CONSOLE_SHOW_ERROR; - text: string; -} - -export interface ShowInfoAction { - type: typeof CONSOLE_SHOW_INFO; - text: string; -} - -export interface HideCommandAction { - type: typeof CONSOLE_HIDE_COMMAND; -} - -export type ConsoleAction = - | HideAction - | ShowCommand - | ShowFindAction - | ShowErrorAction - | ShowInfoAction - | HideCommandAction; - -const hide = (): ConsoleAction => { - return { - type: CONSOLE_HIDE, - }; -}; - -const showCommand = (text: string): ShowCommand => { - return { - type: CONSOLE_SHOW_COMMAND, - text, - }; -}; - -const showFind = (): ShowFindAction => { - return { - type: CONSOLE_SHOW_FIND, - }; -}; - -const showError = (text: string): ShowErrorAction => { - return { - type: CONSOLE_SHOW_ERROR, - text: text, - }; -}; - -const showInfo = (text: string): ShowInfoAction => { - return { - type: CONSOLE_SHOW_INFO, - text: text, - }; -}; - -const hideCommand = (): HideCommandAction => { - window.top.postMessage( - JSON.stringify({ - type: messages.CONSOLE_UNFOCUS, - }), - "*" - ); - return { - type: CONSOLE_HIDE_COMMAND, - }; -}; - -const enterCommand = async (text: string): Promise => { - await browser.runtime.sendMessage({ - type: messages.CONSOLE_ENTER_COMMAND, - text, - }); - return hideCommand(); -}; - -const enterFind = (text?: string): HideCommandAction => { - window.top.postMessage( - JSON.stringify({ - type: messages.CONSOLE_ENTER_FIND, - text, - }), - "*" - ); - return hideCommand(); -}; - -export { - hide, - showCommand, - showFind, - showError, - showInfo, - hideCommand, - enterCommand, - enterFind, -}; diff --git a/src/console/app/actions.ts b/src/console/app/actions.ts new file mode 100644 index 0000000..5538ae5 --- /dev/null +++ b/src/console/app/actions.ts @@ -0,0 +1,82 @@ +export const SHOW_COMMAND = "show.command"; +export const SHOW_ERROR = "show.error"; +export const SHOW_INFO = "show.info"; +export const HIDE_COMMAND = "hide.command"; +export const SHOW_FIND = "show.find"; +export const HIDE = "hide"; + +export interface HideAction { + type: typeof HIDE; +} + +export interface ShowCommand { + type: typeof SHOW_COMMAND; + text: string; +} + +export interface ShowFindAction { + type: typeof SHOW_FIND; +} + +export interface ShowErrorAction { + type: typeof SHOW_ERROR; + text: string; +} + +export interface ShowInfoAction { + type: typeof SHOW_INFO; + text: string; +} + +export interface HideCommandAction { + type: typeof HIDE_COMMAND; +} + +export type AppAction = + | HideAction + | ShowCommand + | ShowFindAction + | ShowErrorAction + | ShowInfoAction + | HideCommandAction; + +const hide = (): HideAction => { + return { + type: HIDE, + }; +}; + +const showCommand = (text: string): ShowCommand => { + return { + type: SHOW_COMMAND, + text, + }; +}; + +const showFind = (): ShowFindAction => { + return { + type: SHOW_FIND, + }; +}; + +const showError = (text: string): ShowErrorAction => { + return { + type: SHOW_ERROR, + text: text, + }; +}; + +const showInfo = (text: string): ShowInfoAction => { + return { + type: SHOW_INFO, + text: text, + }; +}; + +const hideCommand = (): HideCommandAction => { + return { + type: HIDE_COMMAND, + }; +}; + +export { hide, showCommand, showFind, showError, showInfo, hideCommand }; diff --git a/src/console/app/contexts.ts b/src/console/app/contexts.ts new file mode 100644 index 0000000..7e4f323 --- /dev/null +++ b/src/console/app/contexts.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { State, defaultState } from "./recuer"; +import { AppAction } from "./actions"; + +export const AppStateContext = React.createContext(defaultState); + +export const AppDispatchContext = React.createContext< + (action: AppAction) => void +>(() => {}); diff --git a/src/console/app/hooks.ts b/src/console/app/hooks.ts new file mode 100644 index 0000000..eefdea3 --- /dev/null +++ b/src/console/app/hooks.ts @@ -0,0 +1,115 @@ +import React from "react"; +import * as actions from "./actions"; +import { AppDispatchContext, AppStateContext } from "./contexts"; +import * as messages from "../../shared/messages"; + +export const useHide = () => { + const dispatch = React.useContext(AppDispatchContext); + const hide = React.useCallback(() => { + window.top.postMessage( + JSON.stringify({ + type: messages.CONSOLE_UNFOCUS, + }), + "*" + ); + dispatch(actions.hide()); + }, [dispatch]); + + return hide; +}; + +export const useCommandMode = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (initialInputValue: string) => { + dispatch(actions.showCommand(initialInputValue)); + }, + [dispatch] + ); + + return { + visible: state.mode === "command", + initialInputValue: state.consoleText, + show, + }; +}; + +export const useFindMode = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback(() => { + dispatch(actions.showFind()); + }, [dispatch]); + + return { + visible: state.mode === "find", + show, + }; +}; + +export const useInfoMessage = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (message: string) => { + dispatch(actions.showInfo(message)); + }, + [dispatch] + ); + + return { + visible: state.mode === "info", + message: state.mode === "info" ? state.messageText : "", + show, + }; +}; + +export const useErrorMessage = () => { + const state = React.useContext(AppStateContext); + const dispatch = React.useContext(AppDispatchContext); + + const show = React.useCallback( + (message: string) => { + dispatch(actions.showError(message)); + }, + [dispatch] + ); + + return { + visible: state.mode === "error", + message: state.mode === "error" ? state.messageText : "", + show, + }; +}; + +export const getInitialInputValue = () => { + const state = React.useContext(AppStateContext); + return state.consoleText; +}; + +export const useExecCommand = () => { + const execCommand = React.useCallback((text: string) => { + browser.runtime.sendMessage({ + type: messages.CONSOLE_ENTER_COMMAND, + text, + }); + }, []); + return execCommand; +}; + +export const useExecFind = () => { + const execFind = React.useCallback((text?: string) => { + window.top.postMessage( + JSON.stringify({ + type: messages.CONSOLE_ENTER_FIND, + text, + }), + "*" + ); + }, []); + return execFind; +}; diff --git a/src/console/app/provider.tsx b/src/console/app/provider.tsx new file mode 100644 index 0000000..397f165 --- /dev/null +++ b/src/console/app/provider.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import reducer, { defaultState } from "./recuer"; +import { AppDispatchContext, AppStateContext } from "./contexts"; + +export const AppProvider: React.FC = ({ children }) => { + const [state, dispatch] = React.useReducer(reducer, defaultState); + return ( + + + {children} + + + ); +}; diff --git a/src/console/app/recuer.ts b/src/console/app/recuer.ts new file mode 100644 index 0000000..e3043ee --- /dev/null +++ b/src/console/app/recuer.ts @@ -0,0 +1,52 @@ +import { + HIDE, + HIDE_COMMAND, + SHOW_COMMAND, + SHOW_ERROR, + SHOW_FIND, + SHOW_INFO, + AppAction, +} from "./actions"; + +export interface State { + mode: string; + messageText: string; + consoleText: string; +} + +export const defaultState = { + mode: "", + messageText: "", + consoleText: "", +}; + +// eslint-disable-next-line max-lines-per-function +export default function reducer( + state: State = defaultState, + action: AppAction +): State { + switch (action.type) { + case HIDE: + return { ...state, mode: "" }; + case SHOW_COMMAND: + return { + ...state, + mode: "command", + consoleText: action.text, + }; + case SHOW_FIND: + return { ...state, mode: "find", consoleText: "" }; + case SHOW_ERROR: + return { ...state, mode: "error", messageText: action.text }; + case SHOW_INFO: + return { ...state, mode: "info", messageText: action.text }; + case HIDE_COMMAND: + return { + ...state, + mode: + state.mode === "command" || state.mode === "find" ? "" : state.mode, + }; + default: + return state; + } +} diff --git a/src/console/components/AppContext.ts b/src/console/components/AppContext.ts deleted file mode 100644 index a930e14..0000000 --- a/src/console/components/AppContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { State, defaultState } from "../reducers/console"; -import { ConsoleAction } from "../actions/console"; - -const AppContext = React.createContext<{ - state: State; - dispatch: React.Dispatch | ConsoleAction>; -}>({ - state: defaultState, - dispatch: () => null, -}); - -export default AppContext; diff --git a/src/console/components/CommandPrompt.tsx b/src/console/components/CommandPrompt.tsx index 24f46ae..1b6281b 100644 --- a/src/console/components/CommandPrompt.tsx +++ b/src/console/components/CommandPrompt.tsx @@ -1,12 +1,11 @@ import React from "react"; -import * as consoleActions from "../actions/console"; -import AppContext from "./AppContext"; import Completion from "./console/Completion"; import Input from "./console//Input"; import styled from "styled-components"; import { useCompletions, useSelectCompletion } from "../completion/hooks"; import useAutoResize from "../hooks/useAutoResize"; import { CompletionProvider } from "../completion/provider"; +import { useExecCommand, useHide } from "../app/hooks"; const COMPLETION_MAX_ITEMS = 33; @@ -19,7 +18,7 @@ interface Props { } const CommandPromptInner: React.FC = ({ initialInputValue }) => { - const { dispatch } = React.useContext(AppContext); + const hide = useHide(); const [inputValue, setInputValue] = React.useState(initialInputValue); const { completions, updateCompletions } = useCompletions(); const { @@ -28,11 +27,12 @@ const CommandPromptInner: React.FC = ({ initialInputValue }) => { selectNext, selectPrev, } = useSelectCompletion(); + const execCommand = useExecCommand(); useAutoResize(); const onBlur = () => { - dispatch(consoleActions.hideCommand()); + hide(); }; const isCancelKey = React.useCallback( @@ -63,10 +63,11 @@ const CommandPromptInner: React.FC = ({ initialInputValue }) => { const onKeyDown = (e: React.KeyboardEvent) => { if (isCancelKey(e)) { - dispatch(consoleActions.hideCommand()); + hide(); } else if (isEnterKey(e)) { const value = (e.target as HTMLInputElement).value; - dispatch(consoleActions.enterCommand(value)); + execCommand(value); + hide(); } else if (isNextKey(e)) { selectNext(); } else if (isPrevKey(e)) { diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index c8642c8..db18fa0 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -3,31 +3,37 @@ import FindPrompt from "./FindPrompt"; import CommandPrompt from "./CommandPrompt"; import InfoMessage from "./InfoMessage"; import ErrorMessage from "./ErrorMessage"; -import AppContext from "./AppContext"; import { useColorSchemeRefresh } from "../colorscheme/hooks"; +import { + useCommandMode, + useErrorMessage, + useFindMode, + useInfoMessage, +} from "../app/hooks"; const Console: React.FC = () => { - const { state } = React.useContext(AppContext); const refreshColorScheme = useColorSchemeRefresh(); + const { visible: visibleCommand, initialInputValue } = useCommandMode(); + const { visible: visibleFind } = useFindMode(); + const { visible: visibleInfo, message: infoMessage } = useInfoMessage(); + const { visible: visibleError, message: errorMessage } = useErrorMessage(); React.useEffect(() => { - if (state.mode !== "") { + if (visibleCommand || visibleFind || visibleInfo || visibleError) { refreshColorScheme(); } - }, [state.mode]); + }, [visibleCommand, visibleFind, visibleInfo, visibleError]); - switch (state.mode) { - case "command": - return ; - case "find": - return ; - case "info": - return {state.messageText}; - case "error": - return {state.messageText}; - default: - return null; + if (visibleCommand) { + return ; + } else if (visibleFind) { + return ; + } else if (visibleInfo) { + return {infoMessage}; + } else if (visibleError) { + return {errorMessage}; } + return null; }; export default Console; diff --git a/src/console/components/FindPrompt.tsx b/src/console/components/FindPrompt.tsx index 10fa6c3..c437d16 100644 --- a/src/console/components/FindPrompt.tsx +++ b/src/console/components/FindPrompt.tsx @@ -1,20 +1,20 @@ import React from "react"; -import * as consoleActions from "../../console/actions/console"; -import AppContext from "./AppContext"; import Input from "./console/Input"; import styled from "styled-components"; import useAutoResize from "../hooks/useAutoResize"; +import { useExecFind, useHide } from "../app/hooks"; const ConsoleWrapper = styled.div` border-top: 1px solid gray; `; const FindPrompt: React.FC = () => { - const { dispatch } = React.useContext(AppContext); const [inputValue, setInputValue] = React.useState(""); + const hide = useHide(); + const execFind = useExecFind(); const onBlur = () => { - dispatch(consoleActions.hideCommand()); + hide(); }; useAutoResize(); @@ -24,13 +24,13 @@ const FindPrompt: React.FC = () => { e.preventDefault(); const value = (e.target as HTMLInputElement).value; - dispatch(consoleActions.enterFind(value === "" ? undefined : value)); + execFind(value === "" ? undefined : value); }; const onKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case "Escape": - dispatch(consoleActions.hideCommand()); + hide(); break; case "Enter": doEnter(e); diff --git a/src/console/index.tsx b/src/console/index.tsx index 71f2a27..29fa11f 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,42 +1,44 @@ import * as messages from "../shared/messages"; -import reducers, { defaultState } from "./reducers/console"; -import * as consoleActions from "./actions/console"; import Console from "./components/Console"; -import AppContext from "./components/AppContext"; import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; import ColorSchemeProvider from "./colorscheme/providers"; - -const wrapAsync = ( - dispatch: React.Dispatch -): React.Dispatch> => { - return (action: T | Promise) => { - if (action instanceof Promise) { - action.then((a) => dispatch(a)).catch(console.error); - } else { - dispatch(action); - } - }; -}; +import { AppProvider } from "./app/provider"; +import { + useCommandMode, + useFindMode, + useInfoMessage, + useErrorMessage, + useHide, +} from "./app/hooks"; const RootComponent: React.FC = () => { - const [state, dispatch] = React.useReducer(reducers, defaultState); + const hide = useHide(); + const { show: showCommand } = useCommandMode(); + const { show: showFind } = useFindMode(); + const { show: showError } = useErrorMessage(); + const { show: showInfo } = useInfoMessage(); React.useEffect(() => { const onMessage = async (message: any): Promise => { const msg = messages.valueOf(message); switch (msg.type) { case messages.CONSOLE_SHOW_COMMAND: - return dispatch(await consoleActions.showCommand(msg.command)); + showCommand(msg.command); + break; case messages.CONSOLE_SHOW_FIND: - return dispatch(consoleActions.showFind()); + showFind(); + break; case messages.CONSOLE_SHOW_ERROR: - return dispatch(consoleActions.showError(msg.text)); + showError(msg.text); + break; case messages.CONSOLE_SHOW_INFO: - return dispatch(consoleActions.showInfo(msg.text)); + showInfo(msg.text); + break; case messages.CONSOLE_HIDE: - return dispatch(consoleActions.hide()); + hide(); + break; } }; @@ -47,16 +49,18 @@ const RootComponent: React.FC = () => { port.onMessage.addListener(onMessage); }, []); - return ( - - - - - - ); + return ; }; +const App: React.FC = () => ( + + + + + +); + window.addEventListener("DOMContentLoaded", () => { const wrapper = document.getElementById("vimvixen-console"); - ReactDOM.render(, wrapper); + ReactDOM.render(, wrapper); }); diff --git a/src/console/reducers/console.ts b/src/console/reducers/console.ts deleted file mode 100644 index babad69..0000000 --- a/src/console/reducers/console.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_FIND, - CONSOLE_SHOW_INFO, - ConsoleAction, -} from "../actions/console"; - -export interface State { - mode: string; - messageText: string; - consoleText: string; -} - -export const defaultState = { - mode: "", - messageText: "", - consoleText: "", -}; - -// eslint-disable-next-line max-lines-per-function -export default function reducer( - state: State = defaultState, - action: ConsoleAction -): State { - switch (action.type) { - case CONSOLE_HIDE: - return { ...state, mode: "" }; - case CONSOLE_SHOW_COMMAND: - return { - ...state, - mode: "command", - consoleText: action.text, - }; - case CONSOLE_SHOW_FIND: - return { ...state, mode: "find", consoleText: "" }; - case CONSOLE_SHOW_ERROR: - return { ...state, mode: "error", messageText: action.text }; - case CONSOLE_SHOW_INFO: - return { ...state, mode: "info", messageText: action.text }; - case CONSOLE_HIDE_COMMAND: - return { - ...state, - mode: - state.mode === "command" || state.mode === "find" ? "" : state.mode, - }; - default: - return state; - } -} diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts deleted file mode 100644 index 736dd54..0000000 --- a/test/console/actions/console.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as consoleActions from "../../../src/console/actions/console"; -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_FIND, - CONSOLE_SHOW_INFO, -} from "../../../src/console/actions/console"; -import { expect } from "chai"; - -import browserFake from "webextensions-api-fake"; - -describe("console actions", () => { - beforeEach(() => { - (global as any).browser = browserFake(); - }); - - describe("hide", () => { - it("create CONSOLE_HIDE action", () => { - const action = consoleActions.hide(); - expect(action.type).to.equal(CONSOLE_HIDE); - }); - }); - describe("showCommand", () => { - it("create CONSOLE_SHOW_COMMAND action", async () => { - const action = await consoleActions.showCommand("hello"); - expect(action.type).to.equal(CONSOLE_SHOW_COMMAND); - expect(action.text).to.equal("hello"); - }); - }); - - describe("showFind", () => { - it("create CONSOLE_SHOW_FIND action", () => { - const action = consoleActions.showFind(); - expect(action.type).to.equal(CONSOLE_SHOW_FIND); - }); - }); - - describe("showError", () => { - it("create CONSOLE_SHOW_ERROR action", () => { - const action = consoleActions.showError("an error"); - expect(action.type).to.equal(CONSOLE_SHOW_ERROR); - expect(action.text).to.equal("an error"); - }); - }); - - describe("showInfo", () => { - it("create CONSOLE_SHOW_INFO action", () => { - const action = consoleActions.showInfo("an info"); - expect(action.type).to.equal(CONSOLE_SHOW_INFO); - expect(action.text).to.equal("an info"); - }); - }); - - describe("hideCommand", () => { - it("create CONSOLE_HIDE_COMMAND action", () => { - const action = consoleActions.hideCommand(); - expect(action.type).to.equal(CONSOLE_HIDE_COMMAND); - }); - }); -}); diff --git a/test/console/app/actions.test.ts b/test/console/app/actions.test.ts new file mode 100644 index 0000000..2f9dc71 --- /dev/null +++ b/test/console/app/actions.test.ts @@ -0,0 +1,62 @@ +import * as consoleActions from "../../../src/console/app/actions"; +import { + HIDE, + HIDE_COMMAND, + SHOW_COMMAND, + SHOW_ERROR, + SHOW_FIND, + SHOW_INFO, +} from "../../../src/console/app/actions"; +import { expect } from "chai"; + +import browserFake from "webextensions-api-fake"; + +describe("console actions", () => { + beforeEach(() => { + (global as any).browser = browserFake(); + }); + + describe("hide", () => { + it("create CONSOLE_HIDE action", () => { + const action = consoleActions.hide(); + expect(action.type).to.equal(HIDE); + }); + }); + describe("showCommand", () => { + it("create CONSOLE_SHOW_COMMAND action", async () => { + const action = await consoleActions.showCommand("hello"); + expect(action.type).to.equal(SHOW_COMMAND); + expect(action.text).to.equal("hello"); + }); + }); + + describe("showFind", () => { + it("create CONSOLE_SHOW_FIND action", () => { + const action = consoleActions.showFind(); + expect(action.type).to.equal(SHOW_FIND); + }); + }); + + describe("showError", () => { + it("create CONSOLE_SHOW_ERROR action", () => { + const action = consoleActions.showError("an error"); + expect(action.type).to.equal(SHOW_ERROR); + expect(action.text).to.equal("an error"); + }); + }); + + describe("showInfo", () => { + it("create CONSOLE_SHOW_INFO action", () => { + const action = consoleActions.showInfo("an info"); + expect(action.type).to.equal(SHOW_INFO); + expect(action.text).to.equal("an info"); + }); + }); + + describe("hideCommand", () => { + it("create CONSOLE_HIDE_COMMAND action", () => { + const action = consoleActions.hideCommand(); + expect(action.type).to.equal(HIDE_COMMAND); + }); + }); +}); diff --git a/test/console/app/reducer.test.ts b/test/console/app/reducer.test.ts new file mode 100644 index 0000000..4406adc --- /dev/null +++ b/test/console/app/reducer.test.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import reducer, { defaultState, State } from "../../../src/console/app/recuer"; +import { + hide, + hideCommand, + showCommand, + showError, + showFind, + showInfo, +} from "../../../src/console/app/actions"; + +describe("app reducer", () => { + describe("hide", () => { + it("switches to none mode", () => { + const initialState: State = { + ...defaultState, + mode: "info", + }; + const nextState = reducer(initialState, hide()); + + expect(nextState.mode).to.be.empty; + }); + }); + + describe("showCommand", () => { + it("switches to command mode with a message", () => { + const nextState = reducer(defaultState, showCommand("open ")); + + expect(nextState.mode).equals("command"); + expect(nextState.consoleText).equals("open "); + }); + }); + + describe("showFind", () => { + it("switches to find mode with a message", () => { + const nextState = reducer(defaultState, showFind()); + + expect(nextState.mode).equals("find"); + }); + }); + + describe("showError", () => { + it("switches to error message mode with a message", () => { + const nextState = reducer(defaultState, showError("error occurs")); + + expect(nextState.mode).equals("error"); + expect(nextState.messageText).equals("error occurs"); + }); + }); + + describe("showInfo", () => { + it("switches to info message mode with a message", () => { + const nextState = reducer(defaultState, showInfo("what's up")); + + expect(nextState.mode).equals("info"); + expect(nextState.messageText).equals("what's up"); + }); + }); + + describe("hideCommand", () => { + describe("when command mode", () => { + it("switches to none mode", () => { + const initialState: State = { + ...defaultState, + mode: "command", + }; + const nextState = reducer(initialState, hideCommand()); + + expect(nextState.mode).to.be.empty; + }); + }); + + describe("when info message mode", () => { + it("does nothing", () => { + const initialState: State = { + ...defaultState, + mode: "info", + }; + const nextState = reducer(initialState, hideCommand()); + + expect(nextState.mode).equals("info"); + }); + }); + }); +}); diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts deleted file mode 100644 index 390dc66..0000000 --- a/test/console/reducers/console.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import reducer from "../../../src/console/reducers/console"; -import { expect } from "chai"; -import { - CONSOLE_HIDE, - CONSOLE_HIDE_COMMAND, - CONSOLE_SHOW_COMMAND, - CONSOLE_SHOW_ERROR, - CONSOLE_SHOW_INFO, - ConsoleAction, -} from "../../../src/console/actions/console"; - -describe("console reducer", () => { - it("return the initial state", () => { - const state = reducer(undefined, {} as any); - expect(state).to.have.property("mode", ""); - expect(state).to.have.property("messageText", ""); - expect(state).to.have.property("consoleText", ""); - }); - - it("return next state for CONSOLE_HIDE", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { type: CONSOLE_HIDE }; - const state = reducer({ ...initialState, mode: "error" }, action); - expect(state).to.have.property("mode", ""); - }); - - it("return next state for CONSOLE_SHOW_COMMAND", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_COMMAND, - text: "open ", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "command"); - expect(state).to.have.property("consoleText", "open "); - }); - - it("return next state for CONSOLE_SHOW_INFO", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_INFO, - text: "an info", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "info"); - expect(state).to.have.property("messageText", "an info"); - }); - - it("return next state for CONSOLE_SHOW_ERROR", () => { - const action: ConsoleAction = { - type: CONSOLE_SHOW_ERROR, - text: "an error", - }; - const state = reducer(undefined, action); - expect(state).to.have.property("mode", "error"); - expect(state).to.have.property("messageText", "an error"); - }); - - it("return next state for CONSOLE_HIDE_COMMAND", () => { - const initialState = reducer(undefined, {} as any); - const action: ConsoleAction = { - type: CONSOLE_HIDE_COMMAND, - }; - let state = reducer({ ...initialState, mode: "command" }, action); - expect(state).to.have.property("mode", ""); - - state = reducer({ ...initialState, mode: "error" }, action); - expect(state).to.have.property("mode", "error"); - }); -}); -- cgit v1.2.3