diff --git a/src/background/command/SetCommand.ts b/src/background/command/SetCommand.ts index e04d44cd..cbe07bb2 100644 --- a/src/background/command/SetCommand.ts +++ b/src/background/command/SetCommand.ts @@ -2,6 +2,7 @@ import type Command from "./Command"; import type { CommandContext, Completions } from "./Command"; import type PropertySettings from "../settings/PropertySettings"; import type PropertyRegistry from "../property/PropertyRegistry"; +import type ConsoleClient from "../clients/ConsoleClient"; const mustNumber = (v: any): number => { const num = Number(v); @@ -14,7 +15,8 @@ const mustNumber = (v: any): number => { class SetCommand implements Command { constructor( private readonly propretySettings: PropertySettings, - private readonly propertyRegsitry: PropertyRegistry + private readonly propertyRegsitry: PropertyRegistry, + private readonly consoleClient: ConsoleClient ) {} names(): string[] { @@ -65,43 +67,97 @@ class SetCommand implements Command { } async exec( - _ctx: CommandContext, + ctx: CommandContext, _force: boolean, args: string ): Promise { if (args.length === 0) { - return; + // set + return this.showProperties(ctx); + } else if (args.includes("=")) { + // set key=value + const [key, value]: string[] = args.split("="); + return this.setProperty(key, value); + } else if (args.endsWith("?")) { + // set key? + const key = args.slice(0, -1); + return this.showProperty(ctx, key); + } else { + // set key + // set nokey + return this.showPropertyOrSetBoolean(ctx, args); } - const [name, value] = this.parseSetOption(args); - await this.propretySettings.setProperty(name, value); } - private parseSetOption(args: string): [string, string | number | boolean] { - let [key, value]: any[] = args.split("="); - if (value === undefined) { - value = !key.startsWith("no"); - key = value ? key : key.slice(2); + private async showProperties(ctx: CommandContext): Promise { + const props = this.propertyRegsitry.getProperties(); + const kvs = []; + for (const p of props) { + const value = await this.propretySettings.getProperty(p.name()); + if (p.type() === "boolean") { + if (value) { + kvs.push(`${p.name()}`); + } else { + kvs.push(`no${p.name()}`); + } + } else { + kvs.push(`${p.name()}=${value}`); + } } + await this.consoleClient.showInfo(ctx.sender.tabId, kvs.join("\n")); + } + + private async showProperty(ctx: CommandContext, key: string): Promise { const def = this.propertyRegsitry.getProperty(key); - if (!def) { + if (typeof def === "undefined") { throw new Error("Unknown property: " + key); } - if ( - (def.type() === "boolean" && typeof value !== "boolean") || - (def.type() !== "boolean" && typeof value === "boolean") - ) { - throw new Error("Invalid argument: " + args); + const value = await this.propretySettings.getProperty(key); + + if (def.type() === "boolean") { + if (value) { + await this.consoleClient.showInfo(ctx.sender.tabId, key); + } else { + await this.consoleClient.showInfo(ctx.sender.tabId, `no${key}`); + } + } else { + const message = `${key}=${value}`; + await this.consoleClient.showInfo(ctx.sender.tabId, message); } + } + private async setProperty(key: string, value: string): Promise { + const def = this.propertyRegsitry.getProperty(key); + if (!def) { + throw new Error("Unknown property: " + key); + } switch (def.type()) { case "string": - return [key, value]; + return this.propretySettings.setProperty(key, value); case "number": - return [key, mustNumber(value)]; + return this.propretySettings.setProperty(key, mustNumber(value)); case "boolean": - return [key, value]; - default: - throw new Error("Unknown property type: " + def.type); + throw new Error("Invalid argument: " + value); + } + } + + private async showPropertyOrSetBoolean( + ctx: CommandContext, + args: string + ): Promise { + const def = this.propertyRegsitry.getProperty(args); + if (def?.type() === "boolean") { + return this.propretySettings.setProperty(def.name(), true); + } + if (typeof def !== "undefined") { + return this.showProperty(ctx, def.name()); + } + if (args.startsWith("no")) { + const def2 = this.propertyRegsitry.getProperty(args.slice(2)); + if (def2?.type() !== "boolean") { + throw new Error("Invalid argument: " + args); + } + return this.propretySettings.setProperty(def2.name(), false); } } } diff --git a/src/background/command/index.ts b/src/background/command/index.ts index 2ff78151..661d6329 100644 --- a/src/background/command/index.ts +++ b/src/background/command/index.ts @@ -67,7 +67,11 @@ export class CommandRegistryFactory { registory.register(new QuitAllCommand()); registory.register(new QuitCommand()); registory.register( - new SetCommand(this.propertySettings, this.propertyRegistry) + new SetCommand( + this.propertySettings, + this.propertyRegistry, + this.consoleClient + ) ); return registory; diff --git a/src/background/settings/PropertySettings.ts b/src/background/settings/PropertySettings.ts index 5c41fa39..b65fa4f2 100644 --- a/src/background/settings/PropertySettings.ts +++ b/src/background/settings/PropertySettings.ts @@ -1,6 +1,6 @@ import { injectable, inject } from "inversify"; -import PropertyRegistry from "../property/PropertyRegistry"; -import SettingsRepository from "./SettingsRepository"; +import type PropertyRegistry from "../property/PropertyRegistry"; +import type SettingsRepository from "./SettingsRepository"; export default interface PropertySettings { setProperty(name: string, value: string | number | boolean): Promise; diff --git a/src/console/components/InfoMessage.tsx b/src/console/components/InfoMessage.tsx index e72b99a3..32699557 100644 --- a/src/console/components/InfoMessage.tsx +++ b/src/console/components/InfoMessage.tsx @@ -7,6 +7,7 @@ const Wrapper = styled.p` background-color: ${({ theme }) => theme.info?.background}; color: ${({ theme }) => theme.info?.foreground}; font-weight: normal; + white-space: pre-wrap; `; const InfoMessage: React.FC = ({ children }) => { diff --git a/test/background/command/SetCommand.test.ts b/test/background/command/SetCommand.test.ts index eed052e1..f75f6fc7 100644 --- a/test/background/command/SetCommand.test.ts +++ b/test/background/command/SetCommand.test.ts @@ -1,59 +1,181 @@ import SetCommand from "../../../src/background/command/SetCommand"; -import { PropertyRegistryFactry } from "../../../src/background/property"; +import type { CommandContext } from "../../../src/background/command/Command"; +import { PropertyRegistryImpl } from "../../../src/background/property/PropertyRegistry"; import MockPropertySettings from "../mock/MockPropertySettings"; +import MockConsoleClient from "../mock/MockConsoleClient"; + +const strprop1 = { + name: () => "strprop1", + description: () => "", + type: () => "string" as const, + defaultValue: () => "foo", + validate: () => {}, +}; + +const strprop2 = { + name: () => "strprop2", + description: () => "", + type: () => "string" as const, + defaultValue: () => "bar", + validate: () => {}, +}; + +const booleanprop = { + name: () => "booleanprop", + description: () => "", + type: () => "boolean" as const, + defaultValue: () => false, + validate: () => {}, +}; describe("SetCommand", () => { const propertySettings = new MockPropertySettings(); - const propertyRegistry = new PropertyRegistryFactry().create(); + const propertyRegistry = new PropertyRegistryImpl(); + const consoleClient = new MockConsoleClient(); + const ctx = { + sender: { + tabId: 10, + }, + } as CommandContext; + + propertyRegistry.register(strprop1); + propertyRegistry.register(strprop2); + propertyRegistry.register(booleanprop); describe("exec", () => { const mockSetProperty = jest.spyOn(propertySettings, "setProperty"); + const mockGetProperty = jest.spyOn(propertySettings, "getProperty"); + const mockShowInfo = jest.spyOn(consoleClient, "showInfo"); + const cmd = new SetCommand( + propertySettings, + propertyRegistry, + consoleClient + ); beforeEach(() => { mockSetProperty.mockClear(); - mockSetProperty.mockResolvedValue(); + mockGetProperty.mockClear(); + mockShowInfo.mockClear(); + mockShowInfo.mockResolvedValue(); }); it("saves string property", async () => { - const cmd = new SetCommand(propertySettings, propertyRegistry); - await cmd.exec({} as any, false, "hintchars=abcdef"); + await cmd.exec(ctx, false, "strprop1=newvalue"); + expect(mockSetProperty).toHaveBeenCalledWith("strprop1", "newvalue"); + }); + + it("shows string value with non value", async () => { + mockGetProperty.mockResolvedValue("saved-value"); + await cmd.exec(ctx, false, "strprop1"); + expect(mockShowInfo).toHaveBeenCalledWith( + ctx.sender.tabId, + "strprop1=saved-value" + ); + }); + + it("shows string value with ?-suffix", async () => { + mockGetProperty.mockResolvedValue("saved-value"); + await cmd.exec(ctx, false, "strprop1?"); + expect(mockShowInfo).toHaveBeenCalledWith( + ctx.sender.tabId, + "strprop1=saved-value" + ); + }); - expect(mockSetProperty).toHaveBeenCalledWith("hintchars", "abcdef"); + it("shows truthly boolean value with ?-suffix", async () => { + mockGetProperty.mockResolvedValue(true); + await cmd.exec(ctx, false, "booleanprop?"); + expect(mockShowInfo).toHaveBeenCalledWith( + ctx.sender.tabId, + "booleanprop" + ); + }); + + it("shows falsy boolean value with ?-suffix", async () => { + mockGetProperty.mockResolvedValue(false); + await cmd.exec(ctx, false, "booleanprop?"); + expect(mockShowInfo).toHaveBeenCalledWith( + ctx.sender.tabId, + "nobooleanprop" + ); }); it("saves boolean property", async () => { - const cmd = new SetCommand(propertySettings, propertyRegistry); + await cmd.exec(ctx, false, "booleanprop"); + expect(mockSetProperty).toHaveBeenCalledWith("booleanprop", true); - await cmd.exec({} as any, false, "smoothscroll"); - expect(mockSetProperty).toHaveBeenCalledWith("smoothscroll", true); + await cmd.exec(ctx, false, "nobooleanprop"); + expect(mockSetProperty).toHaveBeenCalledWith("booleanprop", false); + }); + + it("shows all properties", async () => { + mockGetProperty.mockImplementation((name: string) => { + switch (name) { + case "strprop1": + return Promise.resolve("foo"); + case "strprop2": + return Promise.resolve("bar"); + case "booleanprop": + return Promise.resolve(false); + } + throw new Error("an error"); + }); + await cmd.exec(ctx, false, ""); + expect(mockShowInfo).toHaveBeenCalledWith( + ctx.sender.tabId, + "strprop1=foo\nstrprop2=bar\nnobooleanprop" + ); + }); - await cmd.exec({} as any, false, "nosmoothscroll"); - expect(mockSetProperty).toHaveBeenCalledWith("smoothscroll", false); + it("throws error when invalid boolean statement", async () => { + await expect(cmd.exec(ctx, false, "booleanprop=1")).rejects.toThrowError( + "Invalid" + ); }); }); describe("getCompletions", () => { it("returns all properties", async () => { - const cmd = new SetCommand(propertySettings, propertyRegistry); + const cmd = new SetCommand( + propertySettings, + propertyRegistry, + consoleClient + ); const completions = await cmd.getCompletions(false, ""); expect(completions).toHaveLength(1); expect(completions[0].items).toMatchObject([ - { primary: "hintchars", value: "hintchars" }, - { primary: "smoothscroll", value: "smoothscroll" }, - { primary: "nosmoothscroll", value: "nosmoothscroll" }, - { primary: "complete", value: "complete" }, - { primary: "colorscheme", value: "colorscheme" }, + { primary: "strprop1", value: "strprop1" }, + { primary: "strprop2", value: "strprop2" }, + { primary: "booleanprop", value: "booleanprop" }, + { primary: "nobooleanprop", value: "nobooleanprop" }, ]); }); it("returns properties matched with a prefix", async () => { - const cmd = new SetCommand(propertySettings, propertyRegistry); - const completions = await cmd.getCompletions(false, "c"); + const cmd = new SetCommand( + propertySettings, + propertyRegistry, + consoleClient + ); + const completions = await cmd.getCompletions(false, "str"); + expect(completions).toHaveLength(1); + expect(completions[0].items).toMatchObject([ + { primary: "strprop1", value: "strprop1" }, + { primary: "strprop2", value: "strprop2" }, + ]); + }); + + it("returns properties matched with 'no' prefix", async () => { + const cmd = new SetCommand( + propertySettings, + propertyRegistry, + consoleClient + ); + const completions = await cmd.getCompletions(false, "no"); expect(completions).toHaveLength(1); expect(completions[0].items).toMatchObject([ - { primary: "complete", value: "complete" }, - { primary: "colorscheme", value: "colorscheme" }, + { primary: "nobooleanprop", value: "nobooleanprop" }, ]); }); }); diff --git a/test/background/mock/MockConsoleClient.ts b/test/background/mock/MockConsoleClient.ts index bd9161fc..49322625 100644 --- a/test/background/mock/MockConsoleClient.ts +++ b/test/background/mock/MockConsoleClient.ts @@ -17,7 +17,7 @@ export default class MockConsoleClient implements ConsoleClient { throw new Error("not implemented"); } - showInfo(_tabId: number, _message: string): Promise { + showInfo(_tabId: number, _message: string): Promise { throw new Error("not implemented"); } }