mainMenu.test.js
1 import { describe, beforeEach, it, expect, vi } from "vitest"; 2 import { MainMenu } from "./mainMenu.js"; 3 import { mockUiService, mockCodexApp } from "../__mocks__/service.mocks.js"; 4 import { 5 mockInstallMenu, 6 mockConfigMenu, 7 mockDataMenu, 8 mockNodeStatusMenu, 9 } from "../__mocks__/ui.mocks.js"; 10 import { 11 mockInstaller, 12 mockProcessControl, 13 } from "../__mocks__/handler.mocks.js"; 14 import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; 15 16 describe("mainmenu", () => { 17 let mainmenu; 18 19 beforeEach(() => { 20 vi.resetAllMocks(); 21 22 mainmenu = new MainMenu( 23 mockUiService, 24 mockMenuLoop, 25 mockInstallMenu, 26 mockConfigMenu, 27 mockInstaller, 28 mockProcessControl, 29 mockCodexApp, 30 mockDataMenu, 31 mockNodeStatusMenu, 32 ); 33 }); 34 35 describe("constructor", () => { 36 it("initializes the menu loop with the promptMainMenu function", () => { 37 expect(mockMenuLoop.initialize).toHaveBeenCalledWith( 38 mainmenu.promptMainMenu, 39 ); 40 }); 41 }); 42 43 describe("show", () => { 44 it("shows the logo", async () => { 45 await mainmenu.show(); 46 47 expect(mockUiService.showLogo).toHaveBeenCalled(); 48 }); 49 50 it("starts the menu loop", async () => { 51 await mainmenu.show(); 52 53 expect(mockMenuLoop.showLoop).toHaveBeenCalled(); 54 }); 55 }); 56 57 describe("promptMainMenu", () => { 58 beforeEach(() => { 59 mainmenu.showRunningMenu = vi.fn(); 60 mainmenu.showNotRunningMenu = vi.fn(); 61 mainmenu.showNotInstalledMenu = vi.fn(); 62 }); 63 64 it("shows running menu when number of codex processes is greater than zero", async () => { 65 mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(1); 66 67 await mainmenu.promptMainMenu(); 68 69 expect(mainmenu.showRunningMenu).toHaveBeenCalled(); 70 expect(mainmenu.showNotRunningMenu).not.toHaveBeenCalled(); 71 expect(mainmenu.showNotInstalledMenu).not.toHaveBeenCalled(); 72 }); 73 74 it("shows not running menu when number of codex processes is zero and codex is installed", async () => { 75 mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(0); 76 mockInstaller.isCodexInstalled.mockResolvedValue(true); 77 78 await mainmenu.promptMainMenu(); 79 80 expect(mainmenu.showRunningMenu).not.toHaveBeenCalled(); 81 expect(mainmenu.showNotRunningMenu).toHaveBeenCalled(); 82 expect(mainmenu.showNotInstalledMenu).not.toHaveBeenCalled(); 83 }); 84 85 it("shows not installed menu when number of codex processes is zero and codex is not installed", async () => { 86 mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(0); 87 mockInstaller.isCodexInstalled.mockResolvedValue(false); 88 89 await mainmenu.promptMainMenu(); 90 91 expect(mainmenu.showRunningMenu).not.toHaveBeenCalled(); 92 expect(mainmenu.showNotRunningMenu).not.toHaveBeenCalled(); 93 expect(mainmenu.showNotInstalledMenu).toHaveBeenCalled(); 94 }); 95 }); 96 97 describe("showNotInstalledMenu", () => { 98 it("shows a menu with options to install Codex or exit", async () => { 99 await mainmenu.showNotInstalledMenu(); 100 101 expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( 102 "Codex is not installed", 103 [ 104 { label: "Install Codex", action: mockInstallMenu.show }, 105 { label: "Exit", action: mockMenuLoop.stopLoop }, 106 ], 107 ); 108 }); 109 }); 110 111 describe("showRunningMenu", () => { 112 it("shows a menu with options to stop Codex, open Codex app, upload, download, or exit", async () => { 113 await mainmenu.showRunningMenu(); 114 115 expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( 116 "Codex is running", 117 [ 118 { label: "Open Codex app", action: mockCodexApp.openCodexApp }, 119 { label: "Stop Codex", action: mainmenu.stopCodex }, 120 { 121 label: "Show node status", 122 action: mockNodeStatusMenu.showNodeStatus, 123 }, 124 { label: "Upload a file", action: mockDataMenu.performUpload }, 125 { label: "Download a file", action: mockDataMenu.performDownload }, 126 { label: "Show local data", action: mockDataMenu.showLocalData }, 127 { 128 label: "Exit (Codex keeps running)", 129 action: mockMenuLoop.stopLoop, 130 }, 131 ], 132 ); 133 }); 134 }); 135 136 describe("showNotRunningMenu", () => { 137 it("shows a menu with options to start Codex, configure, uninstall, or exit", async () => { 138 await mainmenu.showNotRunningMenu(); 139 140 expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( 141 "Codex is installed but not running", 142 [ 143 { 144 label: "Start Codex", 145 action: mainmenu.startCodex, 146 }, 147 { label: "Edit Codex config", action: mockConfigMenu.show }, 148 { label: "Uninstall Codex", action: mockInstallMenu.show }, 149 { label: "Exit", action: mockMenuLoop.stopLoop }, 150 ], 151 ); 152 }); 153 }); 154 155 describe("process control", () => { 156 const mockSpinner = { 157 isMock: "yes", 158 }; 159 160 beforeEach(() => { 161 mockUiService.createAndStartSpinner.mockReturnValue(mockSpinner); 162 }); 163 164 describe("startCodex", () => { 165 it("starts codex", async () => { 166 await mainmenu.startCodex(); 167 168 expect(mockProcessControl.startCodexProcess).toHaveBeenCalled(); 169 }); 170 171 it("shows error message when process control throws", async () => { 172 mockProcessControl.startCodexProcess.mockRejectedValueOnce( 173 new Error("A!"), 174 ); 175 176 await mainmenu.startCodex(); 177 178 expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( 179 'Failed to start Codex. "Error: A!"', 180 ); 181 }); 182 183 it("starts spinner", async () => { 184 await mainmenu.startCodex(); 185 186 expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( 187 "Starting...", 188 ); 189 }); 190 191 it("stops spinner on success", async () => { 192 await mainmenu.startCodex(); 193 194 expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( 195 mockSpinner, 196 ); 197 }); 198 199 it("stops spinner on failure", async () => { 200 mockProcessControl.startCodexProcess.mockRejectedValueOnce( 201 new Error("A!"), 202 ); 203 204 await mainmenu.startCodex(); 205 206 expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith( 207 mockSpinner, 208 ); 209 }); 210 }); 211 212 describe("stopCodex", () => { 213 it("stops codex", async () => { 214 await mainmenu.stopCodex(); 215 216 expect(mockProcessControl.stopCodexProcess).toHaveBeenCalled(); 217 }); 218 219 it("shows error message when process control throws", async () => { 220 mockProcessControl.stopCodexProcess.mockRejectedValueOnce( 221 new Error("A!"), 222 ); 223 224 await mainmenu.stopCodex(); 225 226 expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( 227 'Failed to stop Codex. "Error: A!"', 228 ); 229 }); 230 231 it("starts spinner", async () => { 232 await mainmenu.stopCodex(); 233 234 expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( 235 "Stopping...", 236 ); 237 }); 238 239 it("stops spinner on success", async () => { 240 await mainmenu.stopCodex(); 241 242 expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( 243 mockSpinner, 244 ); 245 }); 246 247 it("stops spinner on failure", async () => { 248 mockProcessControl.stopCodexProcess.mockRejectedValueOnce( 249 new Error("A!"), 250 ); 251 252 await mainmenu.stopCodex(); 253 254 expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith( 255 mockSpinner, 256 ); 257 }); 258 }); 259 }); 260 });