@scratch/scratch-media-lib-scripts 13.2.0-build-media-scripts

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.
@@ -0,0 +1,929 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const { buildLibraries } = require('../build_media_libraries');
5
+ const fixturesDir = path.resolve(__dirname, 'fixtures'); // Directory for test fixtures
6
+
7
+ const { generateAsset, generateSprite, createNewBackdrop, createNewSound, createNewCostume } = require('./helpers/input-helpers');
8
+
9
+ const tempInputDir = path.join(os.tmpdir(), 'media-lib-test-input');
10
+
11
+ describe('buildLibraries', () => {
12
+ beforeEach(() => {
13
+ // Clean and recreate temp input directory before each test
14
+ if (fs.existsSync(tempInputDir)) {
15
+ fs.rmSync(tempInputDir, { recursive: true, force: true });
16
+ }
17
+ fs.mkdirSync(tempInputDir, { recursive: true });
18
+ });
19
+
20
+ afterAll(() => {
21
+ // Cleanup temp input and output directories after all tests
22
+ if (fs.existsSync(tempInputDir)) {
23
+ fs.rmSync(tempInputDir, { recursive: true, force: true });
24
+ }
25
+ });
26
+
27
+ test('Should return empty collections for empty input directory', () => {
28
+ const result = buildLibraries({ inputDir: tempInputDir });
29
+ expect(result).toEqual({
30
+ sprites: [],
31
+ backdrops: [],
32
+ costumes: [],
33
+ sounds: []
34
+ });
35
+ });
36
+
37
+ test('Should process a backdrop correctly', () => {
38
+ const {
39
+ backdropName,
40
+ backdrop,
41
+ backdropMd5ext,
42
+ backdropTags,
43
+ backdropExt
44
+ } = createNewBackdrop();
45
+
46
+ generateAsset(
47
+ tempInputDir,
48
+ 'backdrops',
49
+ backdrop, {
50
+ name: backdropName,
51
+ ext: backdropExt,
52
+ tags: backdropTags
53
+ }
54
+ );
55
+
56
+ const result = buildLibraries({ inputDir: tempInputDir });
57
+
58
+ expect(result.backdrops).toHaveLength(1);
59
+ expect(result.backdrops[0].name).toBe(backdropName);
60
+ expect(result.backdrops[0].md5ext).toBe(backdropMd5ext);
61
+ expect(result.backdrops[0].tags).toEqual(backdropTags);
62
+ expect(result.backdrops[0].dataFormat).toBe(backdropExt);
63
+ });
64
+
65
+ test('Should process a sound correctly', () => {
66
+ const {
67
+ soundName,
68
+ sound,
69
+ soundMd5ext,
70
+ soundTags,
71
+ soundExt,
72
+ soundRate,
73
+ soundSampleCount
74
+ } = createNewSound();
75
+
76
+ generateAsset(
77
+ tempInputDir,
78
+ 'sounds',
79
+ sound, {
80
+ name: soundName,
81
+ ext: soundExt,
82
+ tags: soundTags,
83
+ rate: soundRate,
84
+ sampleCount: soundSampleCount
85
+ }
86
+ );
87
+
88
+ const result = buildLibraries({ inputDir: tempInputDir });
89
+
90
+ expect(result.sounds).toHaveLength(1);
91
+ expect(result.sounds[0].name).toBe(soundName);
92
+ expect(result.sounds[0].md5ext).toBe(soundMd5ext);
93
+ expect(result.sounds[0].tags).toEqual(soundTags);
94
+ expect(result.sounds[0].rate).toBe(soundRate);
95
+ expect(result.sounds[0].sampleCount).toBe(soundSampleCount);
96
+ expect(result.sounds[0].dataFormat).toBe(soundExt);
97
+ });
98
+
99
+ test('Should process a costume correctly', () => {
100
+ const {
101
+ costumeName,
102
+ costume,
103
+ costumeMd5ext,
104
+ costumeTags,
105
+ costumeExt
106
+ } = createNewCostume();
107
+
108
+ generateAsset(
109
+ tempInputDir,
110
+ 'costumes',
111
+ costume, {
112
+ name: costumeName,
113
+ ext: costumeExt,
114
+ tags: costumeTags
115
+ }
116
+ );
117
+
118
+ const result = buildLibraries({ inputDir: tempInputDir });
119
+
120
+ expect(result.costumes).toHaveLength(1);
121
+ expect(result.costumes[0].name).toBe(costumeName);
122
+ expect(result.costumes[0].md5ext).toBe(costumeMd5ext);
123
+ expect(result.costumes[0].tags).toEqual(costumeTags);
124
+ expect(result.costumes[0].dataFormat).toBe(costumeExt);
125
+ });
126
+
127
+ test('Should process a sprite and fill costumes correctly', () => {
128
+ const spriteName = 'Abby';
129
+ const {
130
+ costumeName,
131
+ costume,
132
+ costumeMd5ext,
133
+ costumeExt,
134
+ costumeBitmapResolution,
135
+ costumeRotationCenterX,
136
+ costumeRotationCenterY
137
+ } = createNewCostume();
138
+
139
+ generateSprite(
140
+ tempInputDir,
141
+ {
142
+ [costumeName]: {
143
+ ext: costumeExt,
144
+ data: costume,
145
+ jsonOptions: {
146
+ bitmapResolution: costumeBitmapResolution,
147
+ rotationCenterX: costumeRotationCenterX,
148
+ rotationCenterY: costumeRotationCenterY
149
+ }
150
+ }
151
+ },
152
+ {},
153
+ {
154
+ name: spriteName
155
+ }
156
+ );
157
+
158
+ const result = buildLibraries({ inputDir: tempInputDir });
159
+
160
+ expect(result.sprites).toHaveLength(1);
161
+ expect(result.sprites[0].name).toBe(spriteName);
162
+ expect(result.sprites[0].costumes).toHaveLength(1);
163
+ expect(result.sprites[0].costumes[0].name).toBe(costumeName);
164
+ expect(result.sprites[0].costumes[0].md5ext).toBe(costumeMd5ext);
165
+ expect(result.sprites[0].costumes[0].bitmapResolution).toBe(costumeBitmapResolution);
166
+ expect(result.sprites[0].costumes[0].rotationCenterX).toBe(costumeRotationCenterX);
167
+ expect(result.sprites[0].costumes[0].rotationCenterY).toBe(costumeRotationCenterY);
168
+ });
169
+
170
+ test('Should process a sprite and fill sounds correctly', () => {
171
+ const spriteName = 'Abby';
172
+
173
+ const {
174
+ soundName,
175
+ sound,
176
+ soundMd5ext,
177
+ soundExt,
178
+ soundRate,
179
+ soundSampleCount
180
+ } = createNewSound();
181
+
182
+ generateSprite(
183
+ tempInputDir,
184
+ {},
185
+ {
186
+ [soundName]: {
187
+ ext: soundExt,
188
+ data: sound,
189
+ jsonOptions: {
190
+ rate: soundRate,
191
+ sampleCount: soundSampleCount
192
+ }
193
+ }
194
+ },
195
+ {
196
+ name: spriteName
197
+ }
198
+ );
199
+
200
+ const result = buildLibraries({ inputDir: tempInputDir });
201
+
202
+ expect(result.sprites).toHaveLength(1);
203
+ expect(result.sprites[0].name).toBe(spriteName);
204
+
205
+ expect(result.sprites[0].sounds).toHaveLength(1);
206
+ expect(result.sprites[0].sounds[0].name).toBe(soundName);
207
+ expect(result.sprites[0].sounds[0].md5ext).toBe(soundMd5ext);
208
+ expect(result.sprites[0].sounds[0].rate).toBe(soundRate);
209
+ expect(result.sprites[0].sounds[0].sampleCount).toBe(soundSampleCount);
210
+ expect(result.sprites[0].sounds[0].dataFormat).toBe(soundExt);
211
+ });
212
+
213
+ test('Should preserve additional properties on sprites', () => {
214
+ const spriteName = 'Abby';
215
+ const variables = {
216
+ myVariable: {
217
+ value: 10
218
+ }
219
+ };
220
+
221
+ const blocks = {
222
+ moveSteps: {
223
+ inputs: {
224
+ STEPS: {
225
+ name: 'steps',
226
+ value: 10
227
+ }
228
+ }
229
+ }
230
+ };
231
+
232
+ generateSprite(
233
+ tempInputDir,
234
+ {},
235
+ {},
236
+ {
237
+ name: spriteName,
238
+ isStage: true,
239
+ tags: ['people', 'person', 'drawing'],
240
+ variables,
241
+ blocks
242
+ },
243
+ );
244
+
245
+ const result = buildLibraries({ inputDir: tempInputDir });
246
+
247
+ expect(result.sprites).toHaveLength(1);
248
+ expect(result.sprites[0].name).toBe(spriteName);
249
+ expect(result.sprites[0].isStage).toBe(true);
250
+ expect(result.sprites[0].tags).toEqual(['people', 'person', 'drawing']);
251
+ expect(result.sprites[0].variables).toEqual(variables);
252
+ expect(result.sprites[0].blocks).toEqual(blocks);
253
+ });
254
+
255
+ test('Should inherit costume tags from sprites', () => {
256
+ const spriteName = 'Abby';
257
+ const spriteTags = [
258
+ 'people',
259
+ 'person',
260
+ 'drawing'
261
+ ];
262
+
263
+ const {
264
+ costumeName,
265
+ costume,
266
+ costumeExt,
267
+ costumeBitmapResolution,
268
+ costumeRotationCenterX,
269
+ costumeRotationCenterY
270
+ } = createNewCostume();
271
+
272
+ generateSprite(
273
+ tempInputDir,
274
+ {
275
+ [costumeName]: {
276
+ ext: costumeExt,
277
+ data: costume,
278
+ jsonOptions: {
279
+ bitmapResolution: costumeBitmapResolution,
280
+ rotationCenterX: costumeRotationCenterX,
281
+ rotationCenterY: costumeRotationCenterY
282
+ }
283
+ }
284
+ },
285
+ {},
286
+ {
287
+ name: spriteName,
288
+ tags: spriteTags
289
+ }
290
+ );
291
+
292
+ const result = buildLibraries({ inputDir: tempInputDir });
293
+
294
+ expect(result.sprites).toHaveLength(1);
295
+ expect(result.sprites[0].name).toBe(spriteName);
296
+ expect(result.sprites[0].tags).toEqual(spriteTags);
297
+ expect(result.sprites[0].costumes).toHaveLength(1);
298
+ expect(result.sprites[0].costumes[0].name).toBe(costumeName);
299
+ expect(result.sprites[0].costumes[0].tags).toEqual(spriteTags); // Inherited from sprite
300
+ });
301
+
302
+ test('Should NOT inherit costume tags from sprites if costume already has tags', () => {
303
+ const spriteName = 'Abby';
304
+ const spriteTags = [
305
+ 'people',
306
+ 'person',
307
+ 'drawing'
308
+ ];
309
+
310
+ const costumeTags = [
311
+ 'cat',
312
+ 'pet'
313
+ ];
314
+
315
+ const {
316
+ costumeName,
317
+ costume,
318
+ costumeExt,
319
+ costumeBitmapResolution,
320
+ costumeRotationCenterX,
321
+ costumeRotationCenterY
322
+ } = createNewCostume({ tags: costumeTags });
323
+
324
+ generateSprite(
325
+ tempInputDir,
326
+ {
327
+ [costumeName]: {
328
+ ext: costumeExt,
329
+ data: costume,
330
+ jsonOptions: {
331
+ bitmapResolution: costumeBitmapResolution,
332
+ rotationCenterX: costumeRotationCenterX,
333
+ rotationCenterY: costumeRotationCenterY,
334
+ tags: costumeTags
335
+ }
336
+ }
337
+ },
338
+ {},
339
+ {
340
+ name: spriteName,
341
+ tags: spriteTags
342
+ }
343
+ );
344
+
345
+ const result = buildLibraries({ inputDir: tempInputDir });
346
+ expect(result.sprites).toHaveLength(1);
347
+ expect(result.sprites[0].name).toBe(spriteName);
348
+ expect(result.sprites[0].tags).toEqual(spriteTags);
349
+ expect(result.sprites[0].costumes).toHaveLength(1);
350
+ expect(result.sprites[0].costumes[0].name).toBe(costumeName);
351
+ expect(result.sprites[0].costumes[0].tags).toEqual(costumeTags); // Should NOT inherit tags since costume already has its own tags
352
+ });
353
+
354
+ test('Should NOT inherit sound tags from sprites', () => {
355
+ const spriteName = 'Abby';
356
+ const spriteTags = [
357
+ 'people',
358
+ 'person',
359
+ 'drawing'
360
+ ];
361
+
362
+ const {
363
+ soundName,
364
+ sound,
365
+ soundExt,
366
+ soundRate,
367
+ soundSampleCount
368
+ } = createNewSound();
369
+
370
+ generateSprite(
371
+ tempInputDir,
372
+ {},
373
+ {
374
+ [soundName]: {
375
+ ext: soundExt,
376
+ data: sound,
377
+ jsonOptions: {
378
+ rate: soundRate,
379
+ sampleCount: soundSampleCount
380
+ }
381
+ }
382
+ },
383
+ {
384
+ name: spriteName,
385
+ tags: spriteTags
386
+ }
387
+ );
388
+
389
+ const result = buildLibraries({ inputDir: tempInputDir });
390
+
391
+ expect(result.sprites).toHaveLength(1);
392
+ expect(result.sprites[0].name).toBe(spriteName);
393
+ expect(result.sprites[0].tags).toEqual(spriteTags);
394
+ expect(result.sprites[0].sounds).toHaveLength(1);
395
+ expect(result.sprites[0].sounds[0].name).toBe(soundName);
396
+ expect(result.sprites[0].sounds[0].tags).toEqual([]); // Should NOT inherit tags
397
+ });
398
+
399
+ test('Should fill global costumes from sprites if fillAssetsFromSprites is true', () => {
400
+ const spriteName = 'Abby';
401
+ const spriteTags = [
402
+ 'people',
403
+ 'person',
404
+ 'drawing'
405
+ ];
406
+ const {
407
+ costumeName,
408
+ costume,
409
+ costumeExt,
410
+ costumeMd5ext,
411
+ costumeBitmapResolution,
412
+ costumeRotationCenterX,
413
+ costumeRotationCenterY
414
+ } = createNewCostume();
415
+
416
+ const {
417
+ soundName,
418
+ sound,
419
+ soundExt,
420
+ soundMd5ext,
421
+ soundRate,
422
+ soundSampleCount
423
+ } = createNewSound();
424
+
425
+ generateSprite(
426
+ tempInputDir,
427
+ {
428
+ [costumeName]: {
429
+ ext: costumeExt,
430
+ data: costume,
431
+ jsonOptions: {
432
+ bitmapResolution: costumeBitmapResolution,
433
+ rotationCenterX: costumeRotationCenterX,
434
+ rotationCenterY: costumeRotationCenterY
435
+ }
436
+ }
437
+ },
438
+ {
439
+ [soundName]: {
440
+ ext: soundExt,
441
+ data: sound,
442
+ jsonOptions: {
443
+ rate: soundRate,
444
+ sampleCount: soundSampleCount
445
+ }
446
+ }
447
+ },
448
+ {
449
+ name: spriteName,
450
+ tags: spriteTags
451
+ }
452
+ );
453
+
454
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
455
+
456
+ expect(result.sprites).toHaveLength(1);
457
+ expect(result.costumes).toHaveLength(1); // Should be added to global costumes
458
+ expect(result.sounds).toHaveLength(1); // Should be added to global sounds
459
+ expect(result.costumes[0].name).toBe(costumeName);
460
+ expect(result.costumes[0].md5ext).toBe(costumeMd5ext);
461
+ expect(result.costumes[0].bitmapResolution).toBe(costumeBitmapResolution);
462
+ expect(result.costumes[0].rotationCenterX).toBe(costumeRotationCenterX);
463
+ expect(result.costumes[0].rotationCenterY).toBe(costumeRotationCenterY);
464
+ expect(result.costumes[0].tags).toEqual(spriteTags); // Inherited from sprite
465
+ expect(result.sounds[0].name).toBe(soundName);
466
+ expect(result.sounds[0].md5ext).toBe(soundMd5ext);
467
+ expect(result.sounds[0].rate).toBe(soundRate);
468
+ expect(result.sounds[0].sampleCount).toBe(soundSampleCount);
469
+ expect(result.sounds[0].dataFormat).toBe(soundExt);
470
+ });
471
+
472
+ test('Should NOT fill global assets from sprites if fillAssetsFromSprites is false', () => {
473
+ const spriteName = 'Abby';
474
+ const spriteTags = [
475
+ 'people',
476
+ 'person',
477
+ 'drawing'
478
+ ];
479
+ const {
480
+ costumeName,
481
+ costume,
482
+ costumeExt,
483
+ costumeBitmapResolution,
484
+ costumeRotationCenterX,
485
+ costumeRotationCenterY
486
+ } = createNewCostume();
487
+
488
+ const {
489
+ soundName,
490
+ sound,
491
+ soundExt,
492
+ soundRate,
493
+ soundSampleCount
494
+ } = createNewSound();
495
+
496
+ generateSprite(
497
+ tempInputDir,
498
+ {
499
+ [costumeName]: {
500
+ ext: costumeExt,
501
+ data: costume,
502
+ jsonOptions: {
503
+ bitmapResolution: costumeBitmapResolution,
504
+ rotationCenterX: costumeRotationCenterX,
505
+ rotationCenterY: costumeRotationCenterY
506
+ }
507
+ }
508
+ },
509
+ {
510
+ [soundName]: {
511
+ ext: soundExt,
512
+ data: sound,
513
+ jsonOptions: {
514
+ rate: soundRate,
515
+ sampleCount: soundSampleCount
516
+ }
517
+ }
518
+ },
519
+ {
520
+ name: spriteName,
521
+ tags: spriteTags
522
+ }
523
+ );
524
+
525
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: false });
526
+
527
+ expect(result.sprites).toHaveLength(1);
528
+ expect(result.sounds).toHaveLength(0); // Should be empty
529
+ expect(result.costumes).toHaveLength(0); // Should be empty
530
+ });
531
+
532
+ test('Should fill sounds from global libraries even when fillAssetsFromSprites is false', () => {
533
+ const spriteName = 'Abby';
534
+ const soundNameInSprite = 'Pop-sprite';
535
+ const {
536
+ soundName: soundNameInAsset,
537
+ sound,
538
+ soundExt,
539
+ soundMd5ext,
540
+ soundRate,
541
+ soundSampleCount
542
+ } = createNewSound({ name: 'Pop-asset' });
543
+
544
+ generateAsset(
545
+ tempInputDir,
546
+ 'sounds',
547
+ sound,
548
+ {
549
+ name: soundNameInAsset,
550
+ ext: soundExt,
551
+ tags: [],
552
+ rate: soundRate,
553
+ sampleCount: soundSampleCount,
554
+ }
555
+ );
556
+
557
+ generateSprite(
558
+ tempInputDir,
559
+ {},
560
+ {
561
+ [soundNameInSprite]: {
562
+ ext: soundExt,
563
+ data: sound,
564
+ jsonOptions: {
565
+ rate: soundRate,
566
+ sampleCount: soundSampleCount
567
+ }
568
+ }
569
+ },
570
+ {
571
+ name: spriteName,
572
+ }
573
+ );
574
+
575
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: false });
576
+
577
+ expect(result.sprites).toHaveLength(1);
578
+ expect(result.sprites[0].sounds).toHaveLength(1);
579
+ expect(result.sprites[0].sounds[0].name).toBe(soundNameInSprite);
580
+ expect(result.sprites[0].sounds[0].md5ext).toBe(soundMd5ext);
581
+ expect(result.sounds).toHaveLength(1); // Should still include sound from global library
582
+ expect(result.sounds[0].name).toBe(soundNameInAsset);
583
+ expect(result.sounds[0].md5ext).toBe(soundMd5ext);
584
+ });
585
+
586
+ test('Should handle duplicate asset names in different sprites by including only one in the global library', () => {
587
+ const spriteName1 = 'Abby 1';
588
+ const spriteName2 = 'Abby 2';
589
+
590
+ const {
591
+ costumeName,
592
+ costume,
593
+ costumeMd5ext
594
+ } = createNewCostume();
595
+
596
+ generateSprite(
597
+ tempInputDir,
598
+ {
599
+ [costumeName]: {
600
+ ext: costumeMd5ext,
601
+ data: costume,
602
+ jsonOptions: {}
603
+ }
604
+ },
605
+ {},
606
+ {
607
+ name: spriteName1
608
+ }
609
+ );
610
+
611
+ generateSprite(
612
+ tempInputDir,
613
+ {
614
+ [costumeName]: {
615
+ ext: costumeMd5ext,
616
+ data: costume,
617
+ jsonOptions: {}
618
+ }
619
+ },
620
+ {},
621
+ {
622
+ name: spriteName2
623
+ }
624
+ );
625
+
626
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
627
+
628
+ expect(result.sprites).toHaveLength(2);
629
+ expect(result.costumes).toHaveLength(1); // Only one costume should be in global lib since names and files match
630
+ expect(result.costumes[0].name).toBe(costumeName);
631
+ expect(result.costumes[0].md5ext).toBe(costumeMd5ext);
632
+ });
633
+
634
+ test('Should deduplicate assets with the same name, ignoring casing', () => {
635
+ const spriteName1 = 'Abby 1';
636
+ const spriteName2 = 'Abby 2';
637
+
638
+ const {
639
+ soundName: soundName1,
640
+ sound,
641
+ soundExt,
642
+ soundRate,
643
+ soundSampleCount
644
+ } = createNewSound({ name: 'Pop' });
645
+
646
+ const soundName2 = 'pop';
647
+
648
+ generateSprite(
649
+ tempInputDir,
650
+ {},
651
+ {
652
+ [soundName1]: {
653
+ ext: soundExt,
654
+ data: sound,
655
+ jsonOptions: {
656
+ rate: soundRate,
657
+ sampleCount: soundSampleCount
658
+ }
659
+ }
660
+ },
661
+ {
662
+ name: spriteName1
663
+ }
664
+ );
665
+
666
+ generateSprite(
667
+ tempInputDir,
668
+ {},
669
+ {
670
+ [soundName2]: {
671
+ ext: soundExt,
672
+ data: sound,
673
+ jsonOptions: {
674
+ rate: soundRate,
675
+ sampleCount: soundSampleCount
676
+ }
677
+ }
678
+ },
679
+ {
680
+ name: spriteName2
681
+ }
682
+ );
683
+
684
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
685
+
686
+ expect(result.sprites).toHaveLength(2);
687
+ expect(result.sounds).toHaveLength(1); // Only one sound should be in global lib since names match ignoring case
688
+ expect(result.sounds[0].name).toBe(soundName1); // Should keep the first one encountered
689
+ expect(result.sprites[0].sounds[0].name).toBe(soundName1); // Sprites should still refer to the original name
690
+ expect(result.sprites[1].sounds[0].name).toBe(soundName2);
691
+ });
692
+
693
+ test('Should handle multiple sprites with same costume name but different files', () => {
694
+ const spriteName1 = 'Abby 1';
695
+ const spriteName2 = 'Abby 2';
696
+
697
+ const {
698
+ costumeName,
699
+ costumeExt,
700
+ costumeMd5ext: costumeMd5ext1,
701
+ costume: costume1
702
+ } = createNewCostume({ name: 'commonCostume' });
703
+
704
+ const {
705
+ costumeMd5ext: costumeMd5ext2,
706
+ costume: costume2
707
+ } = createNewCostume({
708
+ name: 'commonCostume',
709
+ path: path.join(fixturesDir, 'Abby-b.svg'),
710
+ md5ext: '920f14335615fff9b8c55fccb8971984.svg'
711
+ });
712
+
713
+ generateSprite(
714
+ tempInputDir,
715
+ {
716
+ [costumeName]: {
717
+ ext: costumeExt,
718
+ data: costume1,
719
+ jsonOptions: {}
720
+ }
721
+ },
722
+ {},
723
+ {
724
+ name: spriteName1
725
+ }
726
+ );
727
+
728
+ generateSprite(
729
+ tempInputDir,
730
+ {
731
+ [costumeName]: {
732
+ ext: costumeExt,
733
+ data: costume2,
734
+ jsonOptions: {}
735
+ }
736
+ },
737
+ {},
738
+ {
739
+ name: spriteName2
740
+ }
741
+ );
742
+
743
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
744
+
745
+ expect(result.sprites).toHaveLength(2);
746
+ expect(result.sprites[0].costumes).toHaveLength(1);
747
+ expect(result.sprites[0].costumes[0].name).toBe(costumeName);
748
+ expect(result.sprites[0].costumes[0].md5ext).toBe(costumeMd5ext1);
749
+ expect(result.sprites[1].costumes).toHaveLength(1);
750
+ expect(result.sprites[1].costumes[0].name).toBe(costumeName);
751
+ expect(result.sprites[1].costumes[0].md5ext).toBe(costumeMd5ext2);
752
+
753
+ expect(result.costumes).toHaveLength(2); // Both costumes should be in global lib since the key-name pair differs due to different md5ext, even though names are the same
754
+
755
+ const costumeMd5exts = result.costumes.map(c => c.md5ext);
756
+
757
+ expect(costumeMd5exts).toContain(costumeMd5ext1);
758
+ expect(costumeMd5exts).toContain(costumeMd5ext2);
759
+
760
+ result.costumes.forEach(c => {
761
+ expect(c.name).toBe(costumeName);
762
+ });
763
+ });
764
+
765
+ test('Should handle multiple sprites with same asset md5key but different names', () => {
766
+ const spriteName1 = 'Abby 1';
767
+ const spriteName2 = 'Abby 2';
768
+
769
+ const {
770
+ soundName: soundName1,
771
+ soundExt,
772
+ sound,
773
+ soundMd5ext,
774
+ soundRate,
775
+ soundSampleCount
776
+ } = createNewSound({ name: 'Pop-a' });
777
+
778
+ const soundName2 = 'Pop-b';
779
+
780
+ generateSprite(
781
+ tempInputDir,
782
+ {},
783
+ {
784
+ [soundName1]: {
785
+ ext: soundExt,
786
+ data: sound,
787
+ jsonOptions: {
788
+ rate: soundRate,
789
+ sampleCount: soundSampleCount
790
+ }
791
+ }
792
+ },
793
+ {
794
+ name: spriteName1
795
+ }
796
+ );
797
+
798
+ generateSprite(
799
+ tempInputDir,
800
+ {},
801
+ {
802
+ [soundName2]: {
803
+ ext: soundExt,
804
+ data: sound,
805
+ jsonOptions: {
806
+ rate: soundRate,
807
+ sampleCount: soundSampleCount
808
+ }
809
+ }
810
+ },
811
+ {
812
+ name: spriteName2
813
+ }
814
+ );
815
+
816
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
817
+
818
+ expect(result.sprites).toHaveLength(2);
819
+ expect(result.sprites[0].sounds).toHaveLength(1);
820
+ expect(result.sprites[0].sounds[0].name).toBe(soundName1);
821
+ expect(result.sprites[0].sounds[0].md5ext).toBe(soundMd5ext);
822
+ expect(result.sprites[1].sounds).toHaveLength(1);
823
+ expect(result.sprites[1].sounds[0].name).toBe(soundName2);
824
+ expect(result.sprites[1].sounds[0].md5ext).toBe(soundMd5ext);
825
+
826
+ expect(result.sounds).toHaveLength(2); // Both sounds should be in global lib since names are different, even though files are the same
827
+ const soundNames = result.sounds.map(s => s.name);
828
+ expect(soundNames).toContain(soundName1);
829
+ expect(soundNames).toContain(soundName2);
830
+ });
831
+
832
+ test('Should always extract dataFormat from file extension', () => {
833
+ const spriteName = 'Abby';
834
+ const {
835
+ sound,
836
+ soundName,
837
+ soundExt,
838
+ soundMd5ext,
839
+ soundRate,
840
+ soundSampleCount
841
+ } = createNewSound({ name: 'Pop' });
842
+
843
+ generateSprite(
844
+ tempInputDir,
845
+ {},
846
+ {
847
+ [soundName]: {
848
+ ext: soundExt,
849
+ data: sound,
850
+ jsonOptions: {
851
+ rate: soundRate,
852
+ sampleCount: soundSampleCount,
853
+ dataFormat: 'adpcm',
854
+ format: 'adpcm' // Also include format for good measure to test that we ignore it as well
855
+ }
856
+ }
857
+ },
858
+ {
859
+ name: spriteName
860
+ }
861
+ );
862
+
863
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
864
+
865
+ expect(result.sprites).toHaveLength(1);
866
+ expect(result.sprites[0].name).toBe(spriteName);
867
+ expect(result.sprites[0].sounds).toHaveLength(1);
868
+ expect(result.sprites[0].sounds[0].name).toBe(soundName);
869
+ expect(result.sprites[0].sounds[0].md5ext).toBe(soundMd5ext);
870
+ expect(result.sprites[0].sounds[0].rate).toBe(soundRate);
871
+ expect(result.sprites[0].sounds[0].sampleCount).toBe(soundSampleCount);
872
+ expect(result.sprites[0].sounds[0].dataFormat).toBe(soundExt); // Should use file extension, not provided dataFormat or format
873
+ expect(result.sprites[0].sounds[0].format).toBeUndefined(); // format should be removed from final output
874
+
875
+ expect(result.sounds).toHaveLength(1);
876
+ expect(result.sounds[0].name).toBe(soundName);
877
+ expect(result.sounds[0].md5ext).toBe(soundMd5ext);
878
+ expect(result.sounds[0].rate).toBe(soundRate);
879
+ expect(result.sounds[0].sampleCount).toBe(soundSampleCount);
880
+ expect(result.sounds[0].dataFormat).toBe(soundExt); // Should use file extension, not provided dataFormat or format
881
+ expect(result.sounds[0].format).toBeUndefined(); // format should be removed from final output
882
+ });
883
+
884
+ test('Should handle invalid JSON files gracefully', () => {
885
+ const spriteName = 'InvalidSprite';
886
+ const spriteDir = path.join(tempInputDir, 'sprites', spriteName);
887
+
888
+ fs.mkdirSync(spriteDir, { recursive: true });
889
+ fs.writeFileSync(path.join(spriteDir, `${spriteName}.json5`), '{ invalid json }'); // Invalid JSON
890
+
891
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
892
+
893
+ expect(result.sprites).toHaveLength(0); // Should skip invalid sprite
894
+ });
895
+
896
+ test('Should handle missing files gracefully', () => {
897
+ const spriteName = 'MissingFilesSprite';
898
+ const costumeName = 'NonExistentCostume';
899
+ const soundName = 'NonExistentSound';
900
+
901
+ const spriteDir = path.join(tempInputDir, 'sprites', spriteName);
902
+ fs.mkdirSync(spriteDir, { recursive: true });
903
+ const jsonContent = {
904
+ '$schema': '../../../schemas/media-collection-schema.json',
905
+ sprites: [{
906
+ name: spriteName,
907
+ costumes: [costumeName],
908
+ sounds: [soundName]
909
+ }],
910
+ costumes: [{
911
+ name: costumeName,
912
+ file: `${costumeName}.svg`
913
+ }],
914
+ sounds: [{
915
+ name: soundName,
916
+ file: `${soundName}.wav`
917
+ }]
918
+ };
919
+ fs.writeFileSync(path.join(spriteDir, `${spriteName}.json5`), JSON.stringify(jsonContent));
920
+
921
+ const result = buildLibraries({ inputDir: tempInputDir, fillAssetsFromSprites: true });
922
+ expect(result.sprites).toHaveLength(1);
923
+ expect(result.sprites[0].name).toBe(spriteName);
924
+ expect(result.sprites[0].costumes).toHaveLength(0); // Should skip missing costume
925
+ expect(result.sprites[0].sounds).toHaveLength(0); // Should skip missing sound
926
+ expect(result.costumes).toHaveLength(0); // Should not include missing costume in global library
927
+ expect(result.sounds).toHaveLength(0); // Should not include missing sound in global library
928
+ });
929
+ });