diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js new file mode 100644 index 0000000..09600e3 --- /dev/null +++ b/src/__mocks__/service.mocks.js @@ -0,0 +1,15 @@ +import { vi } from "vitest"; + +export const mockUiService = { + showLogo: vi.fn(), + showInfoMessage: vi.fn(), + showErrorMessage: vi.fn(), + askMultipleChoice: vi.fn(), + askPrompt: vi.fn() +}; + +export const mockConfigService = { + get: vi.fn(), + saveConfig: vi.fn(), + loadConfig: vi.fn(), +}; diff --git a/src/__mocks__/ui.mocks.js b/src/__mocks__/ui.mocks.js new file mode 100644 index 0000000..ad74c7d --- /dev/null +++ b/src/__mocks__/ui.mocks.js @@ -0,0 +1,9 @@ +import { vi } from "vitest"; + +export const mockInstallMenu = { + show: vi.fn() +}; + +export const mockConfigMenu = { + show: vi.fn() +}; diff --git a/src/__mocks__/utils.mocks.js b/src/__mocks__/utils.mocks.js new file mode 100644 index 0000000..2274803 --- /dev/null +++ b/src/__mocks__/utils.mocks.js @@ -0,0 +1,16 @@ +import { vi } from "vitest"; + +export const mockPathSelector = { + show: vi.fn(), +}; + +export const mockNumberSelector = { + show: vi.fn(), +}; + +export const mockMenuLoop = { + initialize: vi.fn(), + showOnce: vi.fn(), + showLoop: vi.fn(), + stopLoop: vi.fn(), +}; diff --git a/src/ui/configmenu.test.js b/src/ui/configmenu.test.js new file mode 100644 index 0000000..8887c81 --- /dev/null +++ b/src/ui/configmenu.test.js @@ -0,0 +1,144 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { ConfigMenu } from "./configmenu.js"; +import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockConfigService } from "../__mocks__/service.mocks.js"; +import { mockPathSelector, mockNumberSelector } from "../__mocks__/ui.mocks.js"; + +describe("ConfigMenu", () => { + let configMenu; + + beforeEach(() => { + vi.resetAllMocks(); + mockConfigService.get.mockReturnValue({ + dataDir: "/data", + logsDir: "/logs", + storageQuota: 1024 * 1024 * 1024, + ports: { + discPort: 8090, + listenPort: 8070, + apiPort: 8080, + } + }); + + configMenu = new ConfigMenu( + mockUiService, + mockConfigService, + mockPathSelector, + mockNumberSelector + ); + }); + + // it("displays the configuration menu", async () => { + // configMenu.running = false; // Prevent infinite loop + // await configMenu.show(); + + // expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + // "Codex Configuration", + // ); + // expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith([ + // { + // label: `Data path = "${mockConfigService.get().dataDir}"`, + // action: configMenu.editDataDir, + // }, + // { + // label: `Logs path = "${mockConfigService.get().logsDir}"`, + // action: configMenu.editLogsDir, + // }, + // { + // label: `Storage quota = 1Gb`, + // action: configMenu.editStorageQuota, + // }, + // { + // label: `Discovery port = ${mockConfigService.get().ports.discPort}`, + // action: configMenu.editDiscPort, + // }, + // { + // label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`, + // action: configMenu.editListenPort, + // }, + // { + // label: `API port = ${mockConfigService.get().ports.apiPort}`, + // action: configMenu.editApiPort, + // }, + // { + // label: "Save changes and exit", + // action: configMenu.saveChangesAndExit, + // }, + // { + // label: "Discard changes and exit", + // action: configMenu.discardChangesAndExit, + // } + // ]); + // }); + +// it("edits the logs directory", async () => { +// mockPathSelector.show.mockResolvedValue("/new-logs"); +// await configMenu.editLogsDir(); + +// expect(mockPathSelector.show).toHaveBeenCalledWith("/logs", true); +// expect(configMenu.config.logsDir).toEqual("/new-logs"); +// }); + +// it("edits the storage quota", async () => { +// mockNumberSelector.show.mockResolvedValue(200 * 1024 * 1024); +// await configMenu.editStorageQuota(); + +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "You can use: 'GB' or 'gb', etc.", +// ); +// expect(mockNumberSelector.show).toHaveBeenCalledWith( +// 1024 * 1024 * 1024, +// "Storage quota", +// true, +// ); +// expect(configMenu.config.storageQuota).toEqual(200 * 1024 * 1024); +// }); + +// it("shows an error if storage quota is too small", async () => { +// mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024); +// await configMenu.editStorageQuota(); + +// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( +// "Storage quote should be >= 100mb.", +// ); +// expect(configMenu.config.storageQuota).toEqual(1024 * 1024 * 1024); // Unchanged +// }); + +// it("edits the discovery port", async () => { +// mockNumberSelector.show.mockResolvedValue(9000); +// await configMenu.editDiscPort(); + +// expect(mockNumberSelector.show).toHaveBeenCalledWith(8090, "Discovery port", false); +// expect(configMenu.config.ports.discPort).toEqual(9000); +// }); + +// it("shows an error if port is out of range", async () => { +// mockNumberSelector.show.mockResolvedValue(1000); +// await configMenu.editDiscPort(); + +// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( +// "Port should be between 1024 and 65535.", +// ); +// expect(configMenu.config.ports.discPort).toEqual(8090); // Unchanged +// }); + +// it("saves changes and exits", async () => { +// await configMenu.saveChangesAndExit(); + +// expect(mockConfigService.saveConfig).toHaveBeenCalled(); +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "Configuration changes saved.", +// ); +// expect(configMenu.running).toEqual(false); +// }); + +// it("discards changes and exits", async () => { +// await configMenu.discardChangesAndExit(); + +// expect(mockConfigService.loadConfig).toHaveBeenCalled(); +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "Changes discarded.", +// ); +// expect(configMenu.running).toEqual(false); +// }); +}); diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 817ac54..7c5c928 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -1,18 +1,17 @@ export class MainMenu { - constructor(uiService, installMenu, configMenu) { + constructor(uiService, menuLoop, installMenu, configMenu) { this.ui = uiService; + this.loop = menuLoop; this.installMenu = installMenu; this.configMenu = configMenu; - this.running = true; + + this.loop.initialize(this.promptMainMenu); } show = async () => { this.ui.showLogo(); - this.ui.showInfoMessage("hello"); - while (this.running) { - await this.promptMainMenu(); - } + await this.loop.showLoop(); this.ui.showInfoMessage("K-THX-BYE"); }; @@ -29,12 +28,8 @@ export class MainMenu { }, { label: "Exit", - action: this.closeMainMenu, + action: this.loop.stopLoop, }, ]); }; - - closeMainMenu = async () => { - this.running = false; - }; } diff --git a/src/ui/mainmenu.test.js b/src/ui/mainmenu.test.js index d0de034..de28c54 100644 --- a/src/ui/mainmenu.test.js +++ b/src/ui/mainmenu.test.js @@ -1,56 +1,49 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MainMenu } from "./mainmenu.js"; +import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; +import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; describe("mainmenu", () => { let mainmenu; - const mockUiService = { - showLogo: vi.fn(), - showInfoMessage: vi.fn(), - askMultipleChoice: vi.fn(), - }; - const mockInstallMenu = { - show: vi.fn(), - }; - - const mockConfigMenu = { - show: vi.fn(), - } beforeEach(() => { vi.resetAllMocks(); - mainmenu = new MainMenu(mockUiService, mockInstallMenu, mockConfigMenu); - - // Presents test getting stuck in main loop. - const originalPrompt = mainmenu.promptMainMenu; - mainmenu.promptMainMenu = async () => { - mainmenu.running = false; - await originalPrompt(); - }; + mainmenu = new MainMenu(mockUiService, mockMenuLoop, mockInstallMenu, mockConfigMenu); }); - it("shows the main menu", async () => { + it("initializes the menu loop with the promptMainMenu function", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith(mainmenu.promptMainMenu); + }); + + it("shows the logo", async () => { await mainmenu.show(); expect(mockUiService.showLogo).toHaveBeenCalled(); - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("hello"); // example, delete this later. - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); // example, delete this later. + }); + it("starts the menu loop", async () => { + await mainmenu.show(); + + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); + + it("shows the exit message after the menu loop", async () => { + await mainmenu.show(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); + }); + + it("prompts the main menu with multiple choices", async () => { + await mainmenu.promptMainMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Select an option", [ { label: "Install Codex", action: mockInstallMenu.show }, { label: "Configure Codex", action: mockConfigMenu.show }, - { label: "Exit", action: mainmenu.closeMainMenu }, + { label: "Exit", action: mockMenuLoop.stopLoop }, ], ); }); - - it("sets running to false when closeMainMenu is called", async () => { - mainmenu.running = true; - - await mainmenu.closeMainMenu(); - - expect(mainmenu.running).toEqual(false); - }); }); diff --git a/src/utils/menuLoop.js b/src/utils/menuLoop.js new file mode 100644 index 0000000..a914ac7 --- /dev/null +++ b/src/utils/menuLoop.js @@ -0,0 +1,20 @@ +export class MenuLoop { + initialize = (menuPrompt) => { + this.menuPrompt = menuPrompt; + } + + showOnce = async () => { + await this.menuPrompt(); + } + + showLoop = async () => { + this.running = true; + while (this.running) { + await this.menuPrompt(); + } + } + + stopLoop = () => { + this.running = false; + } +} diff --git a/src/utils/menuLoop.test.js b/src/utils/menuLoop.test.js new file mode 100644 index 0000000..756f2b1 --- /dev/null +++ b/src/utils/menuLoop.test.js @@ -0,0 +1,42 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { MenuLoop } from "./menuLoop.js"; + +describe("MenuLoop", () => { + let menuLoop; + const mockPrompt = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + menuLoop = new MenuLoop(); + menuLoop.initialize(mockPrompt); + }); + + it("can show menu once", async () => { + await menuLoop.showOnce(); + expect(mockPrompt).toHaveBeenCalledTimes(1); + }); + + it("can stop the menu loop", async () => { + mockPrompt.mockImplementation(() => { + menuLoop.stopLoop(); + }); + await menuLoop.showLoop(); + + expect(mockPrompt).toHaveBeenCalledTimes(1); + expect(menuLoop.running).toBe(false); + }); + + it("can run menu in a loop", async () => { + let calls = 0; + mockPrompt.mockImplementation(() => { + calls++; + if (calls >= 3) { + menuLoop.stopLoop(); + } + }); + + await menuLoop.showLoop(); + + expect(mockPrompt).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/utils/numberSelector.test.js b/src/utils/numberSelector.test.js index 4ee3269..20d67c3 100644 --- a/src/utils/numberSelector.test.js +++ b/src/utils/numberSelector.test.js @@ -16,7 +16,7 @@ describe("number selector", () => { }); it("shows the prompt", async () => { - await numberSelector.showNumberSelector(0, prompt, false); + await numberSelector.show(0, prompt, false); expect(mockUiService.askPrompt).toHaveBeenCalledWith(prompt); }); @@ -24,7 +24,7 @@ describe("number selector", () => { it("returns a number given valid input", async () => { mockUiService.askPrompt.mockResolvedValue("123"); - const number = await numberSelector.showNumberSelector(0, prompt, false); + const number = await numberSelector.show(0, prompt, false); expect(number).toEqual(123); }); @@ -34,7 +34,7 @@ describe("number selector", () => { mockUiService.askPrompt.mockResolvedValue("what?!"); - const number = await numberSelector.showNumberSelector( + const number = await numberSelector.show( currentValue, prompt, false, @@ -45,7 +45,7 @@ describe("number selector", () => { async function run(input) { mockUiService.askPrompt.mockResolvedValue(input); - return await numberSelector.showNumberSelector(0, prompt, true); + return await numberSelector.show(0, prompt, true); } it("allows for metric postfixes (k)", async () => {