Skip to content

Commit e7d84d5

Browse files
authored
Merge pull request #30 from flycran/feat/calendar-list-option
feat: 新增内部调试参数 calendarList,集成测试移除模块 mock
2 parents c01f744 + c4f8910 commit e7d84d5

6 files changed

Lines changed: 119 additions & 80 deletions

File tree

‎__tests__/fetchError.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* fetchContributions 失败时错误沿管道向上传播,且错误信息可读。
3+
*
4+
* 独立于 integration.test.ts:错误路径无法经 calendarList 注入,
5+
* 是整个测试套件中唯一需要 mock 贡献模块的地方。
6+
*/
7+
import { describe, expect, it, mock } from 'bun:test'
8+
9+
const mockFetchContributions = mock(async () => {
10+
throw new Error(
11+
'Failed to fetch contributions for "nonexistent": 404 Not Found'
12+
)
13+
})
14+
mock.module('../src/utils/githubContributions.ts', () => ({
15+
fetchContributions: mockFetchContributions,
16+
INTENSITY_COLORS: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39']
17+
}))
18+
19+
const { gensvg } = await import('../src/utils/gensvg.js')
20+
21+
describe('fetchContributions error propagation', () => {
22+
it('propagates the API error with a readable message', async () => {
23+
await expect(
24+
gensvg({ username: 'nonexistent', text: 'Test' })
25+
).rejects.toThrow('Failed to fetch contributions')
26+
})
27+
})

‎__tests__/integration.test.ts‎

Lines changed: 78 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
/**
2-
* 集成测试:来自gensvg的完整管道→ 物理学→ 字体加载→ SVG输出。
2+
* 集成测试:gensvg 的完整管道 → 物理模拟 → 字体加载 → SVG 输出。
3+
*
4+
* 贡献数据一律经 calendarList 注入,不 mock 贡献模块;
5+
* 全局 fetch 替换为守卫,实现一旦回退到网络请求测试即失败。
36
*/
4-
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
7+
import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
8+
import { gensvg } from '../src/utils/gensvg.js'
59

6-
function mockContributionsResponse() {
10+
const originalFetch = globalThis.fetch
11+
12+
/** 任何网络访问都意味着实现回退到了 fetchContributions,直接失败并给出原因 */
13+
async function fetchGuard(): Promise<Response> {
14+
throw new Error(
15+
'unexpected network access: contributions must come from calendarList in tests'
16+
)
17+
}
18+
19+
beforeAll(() => {
20+
globalThis.fetch = fetchGuard as unknown as typeof globalThis.fetch
21+
})
22+
23+
afterAll(() => {
24+
globalThis.fetch = originalFetch
25+
})
26+
27+
/** 一周的贡献数据(一列),count 为 0 的天不生成方块 */
28+
function makeCalendarList() {
729
return {
8-
total: 7,
30+
total: 46,
931
contributions: [
1032
[
1133
{ date: '2026-07-19', intensity: 1, count: 1 },
@@ -20,31 +42,14 @@ function mockContributionsResponse() {
2042
}
2143
}
2244

23-
const mockFetchContributions = mock(async () => mockContributionsResponse())
24-
mock.module('../src/utils/githubContributions.ts', () => ({
25-
fetchContributions: mockFetchContributions,
26-
INTENSITY_COLORS: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39']
27-
}))
28-
29-
const { gensvg } = await import('../src/utils/gensvg.js')
30-
3145
describe('full pipeline integration', () => {
32-
beforeEach(() => {
33-
mockFetchContributions.mockImplementation(async () =>
34-
mockContributionsResponse()
35-
)
36-
})
37-
38-
afterEach(() => {
39-
mockFetchContributions.mockReset()
40-
})
41-
4246
it('generates a valid SVG with contributions and text paths', async () => {
4347
const svg = await gensvg({
4448
username: 'testuser',
4549
text: 'Hi',
4650
fontSize: 40,
47-
shape: 'circle'
51+
shape: 'circle',
52+
calendarList: makeCalendarList()
4853
})
4954

5055
expect(typeof svg).toBe('string')
@@ -65,7 +70,8 @@ describe('full pipeline integration', () => {
6570
username: 'testuser',
6671
text: 'Hi',
6772
fontSize: 40,
68-
shape: 'square'
73+
shape: 'square',
74+
calendarList: makeCalendarList()
6975
})
7076

7177
expect(svg).toContain('<rect')
@@ -77,7 +83,8 @@ describe('full pipeline integration', () => {
7783
username: 'testuser',
7884
text: 'Hi',
7985
fontSize: 40,
80-
shape: 'square'
86+
shape: 'square',
87+
calendarList: makeCalendarList()
8188
})
8289
// 方形会翻滚:平移之外再叠加绕方块中心的自转
8390
expect(square).toContain('type="rotate"')
@@ -86,7 +93,8 @@ describe('full pipeline integration', () => {
8693
const circle = await gensvg({
8794
username: 'testuser',
8895
text: 'Hi',
89-
fontSize: 40
96+
fontSize: 40,
97+
calendarList: makeCalendarList()
9098
})
9199
expect(circle).not.toContain('type="rotate"')
92100
})
@@ -95,42 +103,29 @@ describe('full pipeline integration', () => {
95103
const svg = await gensvg({
96104
username: 'testuser',
97105
text: '',
98-
fontSize: 40
106+
fontSize: 40,
107+
calendarList: makeCalendarList()
99108
})
100109

101110
expect(svg).toContain('<svg')
102111
expect(svg).toContain('</svg')
103112
})
104113

105-
it('throws on invalid username (API error)', async () => {
106-
mockFetchContributions.mockImplementation(async () => {
107-
throw new Error(
108-
'Failed to fetch contributions for "nonexistent": 404 Not Found'
109-
)
110-
})
111-
112-
await expect(
113-
gensvg({ username: 'nonexistent', text: 'Test' })
114-
).rejects.toThrow('Failed to fetch contributions')
115-
})
116-
117114
it('produces deterministic SVG for same inputs', async () => {
118115
const svg1 = await gensvg({
119116
username: 'testuser',
120117
text: 'OK',
121118
fontSize: 30,
122-
shape: 'circle'
119+
shape: 'circle',
120+
calendarList: makeCalendarList()
123121
})
124122

125-
mockFetchContributions.mockImplementation(async () =>
126-
mockContributionsResponse()
127-
)
128-
129123
const svg2 = await gensvg({
130124
username: 'testuser',
131125
text: 'OK',
132126
fontSize: 30,
133-
shape: 'circle'
127+
shape: 'circle',
128+
calendarList: makeCalendarList()
134129
})
135130

136131
expect(svg1).toBe(svg2)
@@ -141,7 +136,8 @@ describe('full pipeline integration', () => {
141136
username: 'testuser',
142137
text: 'Hi',
143138
fontSize: 40,
144-
backgroundColor: '#1a1a2e'
139+
backgroundColor: '#1a1a2e',
140+
calendarList: makeCalendarList()
145141
})
146142

147143
expect(svg).toContain('fill="#1a1a2e"')
@@ -152,7 +148,8 @@ describe('full pipeline integration', () => {
152148
username: 'testuser',
153149
text: 'Hi',
154150
fontSize: 40,
155-
textColor: '#ff0000'
151+
textColor: '#ff0000',
152+
calendarList: makeCalendarList()
156153
})
157154

158155
expect(svg).toContain('fill="#ff0000"')
@@ -162,7 +159,8 @@ describe('full pipeline integration', () => {
162159
const svg = await gensvg({
163160
username: 'testuser',
164161
text: 'Hi',
165-
fontSize: 40
162+
fontSize: 40,
163+
calendarList: makeCalendarList()
166164
})
167165

168166
// 默认开启压缩,轨迹携带 span 并输出 keyTimes
@@ -174,7 +172,8 @@ describe('full pipeline integration', () => {
174172
username: 'testuser',
175173
text: 'Hi',
176174
fontSize: 40,
177-
compressionTolerance: 0
175+
compressionTolerance: 0,
176+
calendarList: makeCalendarList()
178177
})
179178

180179
// 容差 0 为无损压缩:匀速直线/静止段被并段,keyTimes 还原真实节奏
@@ -185,13 +184,15 @@ describe('full pipeline integration', () => {
185184
const plain = await gensvg({
186185
username: 'testuser',
187186
text: 'Hi',
188-
fontSize: 40
187+
fontSize: 40,
188+
calendarList: makeCalendarList()
189189
})
190190
const compressed = await gensvg({
191191
username: 'testuser',
192192
text: 'Hi',
193193
fontSize: 40,
194-
compressionTolerance: 1.5
194+
compressionTolerance: 1.5,
195+
calendarList: makeCalendarList()
195196
})
196197

197198
// keyTimes 首帧 0、末帧 1
@@ -205,7 +206,8 @@ describe('full pipeline integration', () => {
205206
text: 'Hi',
206207
fontSize: 40,
207208
compressionTolerance: 1.5,
208-
disableCompression: true
209+
disableCompression: true,
210+
calendarList: makeCalendarList()
209211
})
210212

211213
expect(svg).not.toContain('keyTimes')
@@ -215,7 +217,8 @@ describe('full pipeline integration', () => {
215217
const dflt = await gensvg({
216218
username: 'testuser',
217219
text: 'Hi',
218-
fontSize: 40
220+
fontSize: 40,
221+
calendarList: makeCalendarList()
219222
})
220223
// viewBox 使用物理坐标(SCALE=1),显示尺寸再乘 scale 选项放大
221224
expect(dflt).toContain('width="900"')
@@ -229,7 +232,8 @@ describe('full pipeline integration', () => {
229232
width: 400,
230233
height: 200,
231234
size: 6,
232-
interval: 2
235+
interval: 2,
236+
calendarList: makeCalendarList()
233237
})
234238

235239
expect(custom).toContain('width="1200"')
@@ -242,14 +246,16 @@ describe('full pipeline integration', () => {
242246
const omitted = await gensvg({
243247
username: 'testuser',
244248
text: 'Hi',
245-
fontSize: 40
249+
fontSize: 40,
250+
calendarList: makeCalendarList()
246251
})
247252
const explicitUndefined = await gensvg({
248253
username: 'testuser',
249254
text: 'Hi',
250255
fontSize: 40,
251256
compressionTolerance: undefined,
252-
maxSimulationTime: undefined
257+
maxSimulationTime: undefined,
258+
calendarList: makeCalendarList()
253259
})
254260

255261
// 键携带 undefined 与键不存在等效
@@ -261,7 +267,8 @@ describe('full pipeline integration', () => {
261267
username: 'testuser',
262268
text: 'Hi',
263269
fontSize: 40,
264-
maxSimulationTime: 0.2
270+
maxSimulationTime: 0.2,
271+
calendarList: makeCalendarList()
265272
})
266273

267274
// dur = 播放时长 = 模拟时长 / speed:0.2s × 3 倍速 = 0.6s 模拟时长 ≈ 18 步,
@@ -279,7 +286,8 @@ describe('full pipeline integration', () => {
279286
username: 'testuser',
280287
text: 'Hi',
281288
fontSize: 40,
282-
font
289+
font,
290+
calendarList: makeCalendarList()
283291
})
284292

285293
expect(svg).toContain('<path d="')
@@ -292,30 +300,22 @@ describe('full pipeline integration', () => {
292300
username: 'testuser',
293301
text: 'Hi',
294302
fontSize: 40,
295-
font: '/nonexistent/font.ttf'
303+
font: '/nonexistent/font.ttf',
304+
calendarList: makeCalendarList()
296305
})
297306
).rejects.toThrow('Failed to load font "/nonexistent/font.ttf"')
298307
})
299308
})
300309

301310
describe('css animation engine', () => {
302-
beforeEach(() => {
303-
mockFetchContributions.mockImplementation(async () =>
304-
mockContributionsResponse()
305-
)
306-
})
307-
308-
afterEach(() => {
309-
mockFetchContributions.mockReset()
310-
})
311-
312311
// 只冒烟引擎切换本身;动画的具体声明失效会直接表现为不动/走样,浏览器里肉眼可见
313312
it('renders with a self-contained style block instead of animateTransform', async () => {
314313
const svg = await gensvg({
315314
username: 'testuser',
316315
text: 'Hi',
317316
fontSize: 40,
318-
css: true
317+
css: true,
318+
calendarList: makeCalendarList()
319319
})
320320

321321
expect(svg).toContain('<style>')
@@ -327,7 +327,8 @@ describe('css animation engine', () => {
327327
username: 'testuser',
328328
text: 'Hi',
329329
fontSize: 40,
330-
css: true
330+
css: true,
331+
calendarList: makeCalendarList()
331332
})
332333

333334
// 方块只携带「颜色 + 动画名」class,fill、几何与动画绑定全由公共规则提供
@@ -353,7 +354,8 @@ describe('css animation engine', () => {
353354
text: 'Hi',
354355
fontSize: 40,
355356
css: true,
356-
shape: 'square'
357+
shape: 'square',
358+
calendarList: makeCalendarList()
357359
})
358360

359361
expect(rectSvg).toContain('<rect class="f0 g0"/>')
@@ -366,7 +368,8 @@ describe('css animation engine', () => {
366368
text: 'Hi',
367369
fontSize: 40,
368370
css: true,
369-
animation: 'none'
371+
animation: 'none',
372+
calendarList: makeCalendarList()
370373
})
371374

372375
expect(svg).toContain('animation:none')
@@ -379,7 +382,8 @@ describe('css animation engine', () => {
379382
text: 'Hi',
380383
fontSize: 40,
381384
css: true,
382-
animation: '2s;color:red'
385+
animation: '2s;color:red',
386+
calendarList: makeCalendarList()
383387
})
384388
).rejects.toThrow('Invalid animation')
385389
})

‎dist/index.js‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/index.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)