a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { downloadCommand } from "$commands/download.js";
2import { initCommand } from "$commands/init.js";
3import * as downloadUtils from "$utils/download.js";
4import * as filesUtils from "$utils/files.js";
5import { mkdtemp, rm } from "node:fs/promises";
6import { tmpdir } from "node:os";
7import path from "node:path";
8import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
10// Mock inquirer prompts
11vi.mock("@inquirer/prompts", () => ({ input: vi.fn(), select: vi.fn() }));
12
13describe("CLI commands", () => {
14 let tempDir: string;
15
16 beforeEach(async () => {
17 tempDir = await mkdtemp(path.join(tmpdir(), "voltx-cmd-test-"));
18 vi.spyOn(downloadUtils, "downloadFile").mockResolvedValue();
19 vi.spyOn(filesUtils, "createFile").mockResolvedValue();
20 });
21
22 afterEach(async () => {
23 await rm(tempDir, { recursive: true, force: true }).catch(() => {});
24 vi.clearAllMocks();
25 vi.restoreAllMocks();
26 });
27
28 describe("downloadCommand", () => {
29 it("should download JS and CSS by default", async () => {
30 await downloadCommand({ output: tempDir });
31
32 const downloadFileSpy = vi.mocked(downloadUtils.downloadFile);
33 expect(downloadFileSpy).toHaveBeenCalledTimes(2);
34 expect(downloadFileSpy).toHaveBeenCalledWith(
35 "https://cdn.jsdelivr.net/npm/voltx.js@latest/dist/voltx.min.js",
36 expect.stringContaining("voltx.min.js"),
37 );
38 expect(downloadFileSpy).toHaveBeenCalledWith(
39 "https://cdn.jsdelivr.net/npm/voltx.js@latest/dist/voltx.min.css",
40 expect.stringContaining("voltx.min.css"),
41 );
42 });
43
44 it("should download only JS when css is disabled", async () => {
45 await downloadCommand({ output: tempDir, css: false });
46
47 const downloadFileSpy = vi.mocked(downloadUtils.downloadFile);
48 expect(downloadFileSpy).toHaveBeenCalledTimes(1);
49 expect(downloadFileSpy).toHaveBeenCalledWith(
50 "https://cdn.jsdelivr.net/npm/voltx.js@latest/dist/voltx.min.js",
51 expect.stringContaining("voltx.min.js"),
52 );
53 });
54
55 it("should download only CSS when js is disabled", async () => {
56 await downloadCommand({ output: tempDir, js: false });
57
58 const downloadFileSpy = vi.mocked(downloadUtils.downloadFile);
59 expect(downloadFileSpy).toHaveBeenCalledTimes(1);
60 expect(downloadFileSpy).toHaveBeenCalledWith(
61 "https://cdn.jsdelivr.net/npm/voltx.js@latest/dist/voltx.min.css",
62 expect.stringContaining("voltx.min.css"),
63 );
64 });
65
66 it("should download specific version when specified", async () => {
67 await downloadCommand({ output: tempDir, version: "0.5.0" });
68
69 const downloadFileSpy = vi.mocked(downloadUtils.downloadFile);
70 expect(downloadFileSpy).toHaveBeenCalledWith(
71 "https://cdn.jsdelivr.net/npm/voltx.js@0.5.0/dist/voltx.min.js",
72 expect.stringContaining("voltx.min.js"),
73 );
74 expect(downloadFileSpy).toHaveBeenCalledWith(
75 "https://cdn.jsdelivr.net/npm/voltx.js@0.5.0/dist/voltx.min.css",
76 expect.stringContaining("voltx.min.css"),
77 );
78 });
79
80 it("should handle download errors and exit with code 1", async () => {
81 const downloadFileSpy = vi.spyOn(downloadUtils, "downloadFile").mockRejectedValue(new Error("Network error"));
82 const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
83
84 await downloadCommand({ output: tempDir });
85
86 expect(downloadFileSpy).toHaveBeenCalled();
87 expect(exitSpy).toHaveBeenCalledWith(1);
88
89 exitSpy.mockRestore();
90 });
91 });
92
93 describe("initCommand", () => {
94 it("should check for existing non-empty directory", async () => {
95 const { input, select } = await import("@inquirer/prompts");
96 vi.mocked(input).mockResolvedValue("test-project");
97 vi.mocked(select).mockResolvedValue("minimal" as any);
98
99 const isEmptyOrMissingSpy = vi.spyOn(filesUtils, "isEmptyOrMissing").mockResolvedValue(false);
100 const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
101
102 await initCommand();
103
104 expect(isEmptyOrMissingSpy).toHaveBeenCalled();
105 expect(exitSpy).toHaveBeenCalledWith(1);
106
107 exitSpy.mockRestore();
108 });
109
110 it("should create minimal template", async () => {
111 const { select } = await import("@inquirer/prompts");
112 vi.mocked(select).mockResolvedValue("minimal" as any);
113
114 vi.spyOn(filesUtils, "isEmptyOrMissing").mockResolvedValue(true);
115
116 await initCommand("minimal-app");
117
118 expect(vi.mocked(filesUtils.createFile)).toHaveBeenCalled();
119 expect(vi.mocked(downloadUtils.downloadFile)).toHaveBeenCalled();
120 });
121
122 it("should create styles template without JS", async () => {
123 const { select } = await import("@inquirer/prompts");
124 vi.mocked(select).mockResolvedValue("styles" as any);
125
126 vi.spyOn(filesUtils, "isEmptyOrMissing").mockResolvedValue(true);
127
128 await initCommand("styles-app");
129
130 const downloadSpy = vi.mocked(downloadUtils.downloadFile);
131 const calls = downloadSpy.mock.calls;
132 expect(calls.some((call) => call[0].includes("voltx.min.css"))).toBe(true);
133 expect(calls.some((call) => call[0].includes("voltx.min.js"))).toBe(false);
134 });
135
136 it("should create with-router template", async () => {
137 const { select } = await import("@inquirer/prompts");
138 vi.mocked(select).mockResolvedValue("with-router" as any);
139
140 vi.spyOn(filesUtils, "isEmptyOrMissing").mockResolvedValue(true);
141
142 await initCommand("router-app");
143
144 expect(vi.mocked(filesUtils.createFile)).toHaveBeenCalled();
145 });
146
147 it("should create with-plugins template", async () => {
148 const { select } = await import("@inquirer/prompts");
149 vi.mocked(select).mockResolvedValue("with-plugins" as any);
150
151 vi.spyOn(filesUtils, "isEmptyOrMissing").mockResolvedValue(true);
152
153 await initCommand("plugins-app");
154
155 expect(vi.mocked(filesUtils.createFile)).toHaveBeenCalled();
156 });
157 });
158});