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-
3145describe ( '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
301310describe ( '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 } )
0 commit comments