@zjex/git-workflow 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +33 -0
- package/.husky/pre-commit +5 -0
- package/README.md +55 -0
- package/TESTING.md +436 -0
- package/dist/index.js +65 -7
- package/package.json +9 -2
- package/src/commands/tag.ts +94 -5
- package/tests/COVERAGE_REPORT.md +222 -0
- package/tests/QUICK_START.md +242 -0
- package/tests/README.md +119 -0
- package/tests/TEST_SUMMARY.md +330 -0
- package/tests/ai-service.test.ts +470 -0
- package/tests/branch.test.ts +255 -0
- package/tests/commit.test.ts +85 -0
- package/tests/config.test.ts +311 -0
- package/tests/tag.test.ts +396 -0
- package/tests/update-notifier.test.ts +384 -0
- package/tests/utils.test.ts +229 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { checkForUpdates, clearUpdateCache } from "../src/update-notifier";
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
vi.mock("child_process");
|
|
9
|
+
vi.mock("fs");
|
|
10
|
+
vi.mock("os");
|
|
11
|
+
vi.mock("boxen", () => ({
|
|
12
|
+
default: (content: string) => content,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("ora", () => ({
|
|
15
|
+
default: () => ({
|
|
16
|
+
start: () => ({
|
|
17
|
+
succeed: vi.fn(),
|
|
18
|
+
fail: vi.fn(),
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("@inquirer/prompts", () => ({
|
|
23
|
+
select: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe("Update Notifier 模块测试", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
vi.mocked(homedir).mockReturnValue("/home/user");
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
vi.useRealTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("clearUpdateCache 函数", () => {
|
|
39
|
+
it("应该删除缓存文件", () => {
|
|
40
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
41
|
+
vi.mocked(unlinkSync).mockImplementation(() => {});
|
|
42
|
+
|
|
43
|
+
clearUpdateCache();
|
|
44
|
+
|
|
45
|
+
expect(unlinkSync).toHaveBeenCalledWith("/home/user/.gw-update-check");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("缓存文件不存在时不应该报错", () => {
|
|
49
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
50
|
+
|
|
51
|
+
expect(() => clearUpdateCache()).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("删除失败时应该静默处理", () => {
|
|
55
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
56
|
+
vi.mocked(unlinkSync).mockImplementation(() => {
|
|
57
|
+
throw new Error("Permission denied");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(() => clearUpdateCache()).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("checkForUpdates 函数", () => {
|
|
65
|
+
it("没有缓存时应该后台检查", async () => {
|
|
66
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
67
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
68
|
+
|
|
69
|
+
await checkForUpdates("1.0.0");
|
|
70
|
+
|
|
71
|
+
// 等待异步操作
|
|
72
|
+
await vi.runAllTimersAsync();
|
|
73
|
+
|
|
74
|
+
expect(writeFileSync).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("版本相同时不应该显示提示", async () => {
|
|
78
|
+
const mockCache = {
|
|
79
|
+
lastCheck: Date.now(),
|
|
80
|
+
latestVersion: "1.0.0",
|
|
81
|
+
checkedVersion: "1.0.0",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
85
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
86
|
+
|
|
87
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
88
|
+
|
|
89
|
+
await checkForUpdates("1.0.0");
|
|
90
|
+
|
|
91
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
92
|
+
|
|
93
|
+
consoleSpy.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("有新版本时应该显示简单通知(非交互式)", async () => {
|
|
97
|
+
const mockCache = {
|
|
98
|
+
lastCheck: Date.now(),
|
|
99
|
+
latestVersion: "1.0.1",
|
|
100
|
+
checkedVersion: "1.0.0",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
104
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
105
|
+
|
|
106
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
107
|
+
|
|
108
|
+
await checkForUpdates("1.0.0", "@zjex/git-workflow", false);
|
|
109
|
+
|
|
110
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
111
|
+
|
|
112
|
+
consoleSpy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("24小时内关闭过提示应该跳过", async () => {
|
|
116
|
+
const mockCache = {
|
|
117
|
+
lastCheck: Date.now(),
|
|
118
|
+
lastDismiss: Date.now(),
|
|
119
|
+
latestVersion: "1.0.1",
|
|
120
|
+
checkedVersion: "1.0.0",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
124
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
125
|
+
|
|
126
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
127
|
+
|
|
128
|
+
await checkForUpdates("1.0.0");
|
|
129
|
+
|
|
130
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
131
|
+
|
|
132
|
+
consoleSpy.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("超过24小时后应该再次提示", async () => {
|
|
136
|
+
const oneDayAgo = Date.now() - 25 * 60 * 60 * 1000; // 25小时前
|
|
137
|
+
const mockCache = {
|
|
138
|
+
lastCheck: Date.now(),
|
|
139
|
+
lastDismiss: oneDayAgo,
|
|
140
|
+
latestVersion: "1.0.1",
|
|
141
|
+
checkedVersion: "1.0.0",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
145
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
146
|
+
|
|
147
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
148
|
+
|
|
149
|
+
await checkForUpdates("1.0.0", "@zjex/git-workflow", false);
|
|
150
|
+
|
|
151
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
152
|
+
|
|
153
|
+
consoleSpy.mockRestore();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("4小时内不应该重复检查", async () => {
|
|
157
|
+
const mockCache = {
|
|
158
|
+
lastCheck: Date.now(),
|
|
159
|
+
latestVersion: "1.0.0",
|
|
160
|
+
checkedVersion: "1.0.0",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
164
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
165
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
166
|
+
|
|
167
|
+
await checkForUpdates("1.0.0");
|
|
168
|
+
await vi.runAllTimersAsync();
|
|
169
|
+
|
|
170
|
+
// writeFileSync 不应该被调用(因为在4小时内)
|
|
171
|
+
expect(writeFileSync).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("超过4小时应该重新检查", async () => {
|
|
175
|
+
const fourHoursAgo = Date.now() - 5 * 60 * 60 * 1000; // 5小时前
|
|
176
|
+
const mockCache = {
|
|
177
|
+
lastCheck: fourHoursAgo,
|
|
178
|
+
latestVersion: "1.0.0",
|
|
179
|
+
checkedVersion: "1.0.0",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
183
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
184
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
185
|
+
|
|
186
|
+
await checkForUpdates("1.0.0");
|
|
187
|
+
await vi.runAllTimersAsync();
|
|
188
|
+
|
|
189
|
+
expect(writeFileSync).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("缓存文件损坏时应该静默处理", async () => {
|
|
193
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
194
|
+
vi.mocked(readFileSync).mockReturnValue("invalid json");
|
|
195
|
+
|
|
196
|
+
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("ExitPromptError 应该重新抛出", async () => {
|
|
200
|
+
const mockCache = {
|
|
201
|
+
lastCheck: Date.now(),
|
|
202
|
+
latestVersion: "1.0.1",
|
|
203
|
+
checkedVersion: "1.0.0",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
207
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
208
|
+
|
|
209
|
+
const { select } = await import("@inquirer/prompts");
|
|
210
|
+
|
|
211
|
+
// 创建一个自定义错误类
|
|
212
|
+
class ExitPromptError extends Error {
|
|
213
|
+
constructor(message: string) {
|
|
214
|
+
super(message);
|
|
215
|
+
this.name = "ExitPromptError";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const exitError = new ExitPromptError("User cancelled");
|
|
220
|
+
vi.mocked(select).mockRejectedValue(exitError);
|
|
221
|
+
|
|
222
|
+
await expect(
|
|
223
|
+
checkForUpdates("1.0.0", "@zjex/git-workflow", true)
|
|
224
|
+
).rejects.toThrow();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Volta 检测", () => {
|
|
229
|
+
it("应该检测 Volta 环境", async () => {
|
|
230
|
+
vi.mocked(execSync).mockReturnValue("/home/user/.volta/bin/gw" as any);
|
|
231
|
+
|
|
232
|
+
const mockCache = {
|
|
233
|
+
lastCheck: Date.now(),
|
|
234
|
+
latestVersion: "1.0.1",
|
|
235
|
+
checkedVersion: "1.0.0",
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
239
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
240
|
+
|
|
241
|
+
const { select } = await import("@inquirer/prompts");
|
|
242
|
+
vi.mocked(select).mockResolvedValue("continue");
|
|
243
|
+
|
|
244
|
+
await checkForUpdates("1.0.0", "@zjex/git-workflow", true);
|
|
245
|
+
|
|
246
|
+
expect(select).toHaveBeenCalledWith(
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
choices: expect.arrayContaining([
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
description: expect.stringContaining("volta install"),
|
|
251
|
+
}),
|
|
252
|
+
]),
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("应该检测非 Volta 环境", async () => {
|
|
258
|
+
vi.mocked(execSync).mockReturnValue("/usr/local/bin/gw" as any);
|
|
259
|
+
|
|
260
|
+
const mockCache = {
|
|
261
|
+
lastCheck: Date.now(),
|
|
262
|
+
latestVersion: "1.0.1",
|
|
263
|
+
checkedVersion: "1.0.0",
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
267
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
268
|
+
|
|
269
|
+
const { select } = await import("@inquirer/prompts");
|
|
270
|
+
vi.mocked(select).mockResolvedValue("continue");
|
|
271
|
+
|
|
272
|
+
await checkForUpdates("1.0.0", "@zjex/git-workflow", true);
|
|
273
|
+
|
|
274
|
+
expect(select).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
choices: expect.arrayContaining([
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
description: expect.stringContaining("npm install -g"),
|
|
279
|
+
}),
|
|
280
|
+
]),
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("版本比较", () => {
|
|
287
|
+
it("应该正确比较 semver 版本", async () => {
|
|
288
|
+
const testCases = [
|
|
289
|
+
{ current: "1.0.0", latest: "1.0.1", shouldShow: true },
|
|
290
|
+
{ current: "1.0.0", latest: "1.1.0", shouldShow: true },
|
|
291
|
+
{ current: "1.0.0", latest: "2.0.0", shouldShow: true },
|
|
292
|
+
{ current: "1.0.1", latest: "1.0.0", shouldShow: false },
|
|
293
|
+
{ current: "1.0.0", latest: "1.0.0", shouldShow: false },
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
for (const { current, latest, shouldShow } of testCases) {
|
|
297
|
+
const mockCache = {
|
|
298
|
+
lastCheck: Date.now(),
|
|
299
|
+
latestVersion: latest,
|
|
300
|
+
checkedVersion: current,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
304
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
305
|
+
|
|
306
|
+
const consoleSpy = vi
|
|
307
|
+
.spyOn(console, "log")
|
|
308
|
+
.mockImplementation(() => {});
|
|
309
|
+
|
|
310
|
+
await checkForUpdates(current, "@zjex/git-workflow", false);
|
|
311
|
+
|
|
312
|
+
if (shouldShow) {
|
|
313
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
314
|
+
} else {
|
|
315
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
consoleSpy.mockRestore();
|
|
319
|
+
vi.clearAllMocks();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("缓存读写", () => {
|
|
325
|
+
it("应该正确写入缓存", async () => {
|
|
326
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
327
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
328
|
+
|
|
329
|
+
await checkForUpdates("1.0.0");
|
|
330
|
+
await vi.runAllTimersAsync();
|
|
331
|
+
|
|
332
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
333
|
+
"/home/user/.gw-update-check",
|
|
334
|
+
expect.stringContaining("1.0.1"),
|
|
335
|
+
"utf-8"
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("写入缓存失败时应该静默处理", async () => {
|
|
340
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
341
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
342
|
+
vi.mocked(writeFileSync).mockImplementation(() => {
|
|
343
|
+
throw new Error("Write failed");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("读取缓存失败时应该返回 null", async () => {
|
|
350
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
351
|
+
vi.mocked(readFileSync).mockImplementation(() => {
|
|
352
|
+
throw new Error("Read failed");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("网络请求", () => {
|
|
360
|
+
it("获取最新版本失败时应该静默处理", async () => {
|
|
361
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
362
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
363
|
+
throw new Error("Network error");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("应该使用正确的 npm 命令", async () => {
|
|
370
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
371
|
+
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
372
|
+
|
|
373
|
+
await checkForUpdates("1.0.0", "@zjex/git-workflow");
|
|
374
|
+
await vi.runAllTimersAsync();
|
|
375
|
+
|
|
376
|
+
expect(execSync).toHaveBeenCalledWith(
|
|
377
|
+
"npm view @zjex/git-workflow version",
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
timeout: 3000,
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import {
|
|
4
|
+
colors,
|
|
5
|
+
TODAY,
|
|
6
|
+
theme,
|
|
7
|
+
exec,
|
|
8
|
+
execOutput,
|
|
9
|
+
checkGitRepo,
|
|
10
|
+
getMainBranch,
|
|
11
|
+
divider,
|
|
12
|
+
} from "../src/utils";
|
|
13
|
+
|
|
14
|
+
// Mock child_process
|
|
15
|
+
vi.mock("child_process");
|
|
16
|
+
|
|
17
|
+
describe("Utils 模块测试", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("colors 工具", () => {
|
|
27
|
+
it("应该正确添加红色", () => {
|
|
28
|
+
const result = colors.red("error");
|
|
29
|
+
expect(result).toContain("error");
|
|
30
|
+
expect(result).toMatch(/\x1b\[31m.*\x1b\[0m/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("应该正确添加绿色", () => {
|
|
34
|
+
const result = colors.green("success");
|
|
35
|
+
expect(result).toContain("success");
|
|
36
|
+
expect(result).toMatch(/\x1b\[32m.*\x1b\[0m/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("应该正确添加黄色", () => {
|
|
40
|
+
const result = colors.yellow("warning");
|
|
41
|
+
expect(result).toContain("warning");
|
|
42
|
+
expect(result).toMatch(/\x1b\[33m.*\x1b\[0m/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("应该正确添加青色", () => {
|
|
46
|
+
const result = colors.cyan("info");
|
|
47
|
+
expect(result).toContain("info");
|
|
48
|
+
expect(result).toMatch(/\x1b\[36m.*\x1b\[0m/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("应该正确添加暗淡效果", () => {
|
|
52
|
+
const result = colors.dim("dimmed");
|
|
53
|
+
expect(result).toContain("dimmed");
|
|
54
|
+
expect(result).toMatch(/\x1b\[2m.*\x1b\[0m/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("应该正确添加粗体", () => {
|
|
58
|
+
const result = colors.bold("bold");
|
|
59
|
+
expect(result).toContain("bold");
|
|
60
|
+
expect(result).toMatch(/\x1b\[1m.*\x1b\[0m/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("应该有 reset 代码", () => {
|
|
64
|
+
expect(colors.reset).toBe("\x1b[0m");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("TODAY 常量", () => {
|
|
69
|
+
it("应该是 YYYYMMDD 格式", () => {
|
|
70
|
+
expect(TODAY).toMatch(/^\d{8}$/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("应该是今天的日期", () => {
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
75
|
+
expect(TODAY).toBe(today);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("exec 函数", () => {
|
|
80
|
+
it("应该执行命令并返回输出", () => {
|
|
81
|
+
const mockExecSync = vi.mocked(execSync);
|
|
82
|
+
mockExecSync.mockReturnValue("output" as any);
|
|
83
|
+
|
|
84
|
+
const result = exec("git status", true);
|
|
85
|
+
|
|
86
|
+
expect(result).toBe("output");
|
|
87
|
+
expect(mockExecSync).toHaveBeenCalledWith("git status", {
|
|
88
|
+
encoding: "utf-8",
|
|
89
|
+
stdio: "pipe",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("静默模式失败时应该返回空字符串", () => {
|
|
94
|
+
const mockExecSync = vi.mocked(execSync);
|
|
95
|
+
mockExecSync.mockImplementation(() => {
|
|
96
|
+
throw new Error("Command failed");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = exec("git status", true);
|
|
100
|
+
|
|
101
|
+
expect(result).toBe("");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("非静默模式失败时应该抛出错误", () => {
|
|
105
|
+
const mockExecSync = vi.mocked(execSync);
|
|
106
|
+
mockExecSync.mockImplementation(() => {
|
|
107
|
+
throw new Error("Command failed");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(() => exec("git status", false)).toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("execOutput 函数", () => {
|
|
115
|
+
it("应该执行命令并返回 trim 后的输出", () => {
|
|
116
|
+
const mockExecSync = vi.mocked(execSync);
|
|
117
|
+
mockExecSync.mockReturnValue(" output \n" as any);
|
|
118
|
+
|
|
119
|
+
const result = execOutput("git branch");
|
|
120
|
+
|
|
121
|
+
expect(result).toBe("output");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("失败时应该返回空字符串", () => {
|
|
125
|
+
const mockExecSync = vi.mocked(execSync);
|
|
126
|
+
mockExecSync.mockImplementation(() => {
|
|
127
|
+
throw new Error("Command failed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = execOutput("git branch");
|
|
131
|
+
|
|
132
|
+
expect(result).toBe("");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("checkGitRepo 函数", () => {
|
|
137
|
+
it("在 git 仓库中应该正常执行", () => {
|
|
138
|
+
const mockExecSync = vi.mocked(execSync);
|
|
139
|
+
mockExecSync.mockReturnValue("true" as any);
|
|
140
|
+
|
|
141
|
+
expect(() => checkGitRepo()).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("不在 git 仓库中应该退出", () => {
|
|
145
|
+
const mockExecSync = vi.mocked(execSync);
|
|
146
|
+
mockExecSync.mockImplementation(() => {
|
|
147
|
+
throw new Error("Not a git repository");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
151
|
+
throw new Error("process.exit");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(() => checkGitRepo()).toThrow("process.exit");
|
|
155
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
156
|
+
|
|
157
|
+
mockExit.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("getMainBranch 函数", () => {
|
|
162
|
+
it("应该返回 origin/main 如果存在", () => {
|
|
163
|
+
const mockExecSync = vi.mocked(execSync);
|
|
164
|
+
mockExecSync.mockReturnValue(" origin/main\n origin/develop\n" as any);
|
|
165
|
+
|
|
166
|
+
const result = getMainBranch();
|
|
167
|
+
|
|
168
|
+
expect(result).toBe("origin/main");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("应该返回 origin/master 如果 main 不存在", () => {
|
|
172
|
+
const mockExecSync = vi.mocked(execSync);
|
|
173
|
+
mockExecSync.mockReturnValue(
|
|
174
|
+
" origin/master\n origin/develop\n" as any
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = getMainBranch();
|
|
178
|
+
|
|
179
|
+
expect(result).toBe("origin/master");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("都不存在时应该默认返回 origin/main", () => {
|
|
183
|
+
const mockExecSync = vi.mocked(execSync);
|
|
184
|
+
mockExecSync.mockReturnValue(" origin/develop\n" as any);
|
|
185
|
+
|
|
186
|
+
const result = getMainBranch();
|
|
187
|
+
|
|
188
|
+
expect(result).toBe("origin/main");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("divider 函数", () => {
|
|
193
|
+
it("应该输出分隔线", () => {
|
|
194
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
195
|
+
|
|
196
|
+
divider();
|
|
197
|
+
|
|
198
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining("─".repeat(40))
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
consoleSpy.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("theme 对象", () => {
|
|
207
|
+
it("应该有 helpMode 属性", () => {
|
|
208
|
+
expect(theme).toHaveProperty("helpMode");
|
|
209
|
+
expect(theme.helpMode).toBe("always");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("应该有 style.keysHelpTip 方法", () => {
|
|
213
|
+
expect(theme.style).toHaveProperty("keysHelpTip");
|
|
214
|
+
expect(typeof theme.style.keysHelpTip).toBe("function");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("keysHelpTip 应该格式化按键提示", () => {
|
|
218
|
+
const keys: [string, string][] = [
|
|
219
|
+
["↑↓", "选择"],
|
|
220
|
+
["space", "确认"],
|
|
221
|
+
];
|
|
222
|
+
const result = theme.style.keysHelpTip(keys);
|
|
223
|
+
|
|
224
|
+
expect(result).toContain("↑↓ 选择");
|
|
225
|
+
expect(result).toContain("space 确认");
|
|
226
|
+
expect(result).toContain("Ctrl+C quit");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: "v8",
|
|
9
|
+
reporter: ["text", "json", "html"],
|
|
10
|
+
exclude: [
|
|
11
|
+
"node_modules/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"**/*.config.ts",
|
|
14
|
+
"scripts/",
|
|
15
|
+
"**/*.test.ts",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|