fontdue-js 2.19.0 → 2.19.1

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,1181 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var _utils = require("../utils");
5
+ var _reducer = _interopRequireWildcard(require("../reducer"));
6
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
7
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
8
+ // Mock react-relay's graphql tag since productState.ts uses it at import time
9
+ _vitest.vi.mock('react-relay', () => ({
10
+ graphql: () => null,
11
+ fetchQuery: _vitest.vi.fn()
12
+ }));
13
+ // Helper to build a minimal FontdueState for testing
14
+ function makeState() {
15
+ let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
16
+ return {
17
+ stylesheets: [],
18
+ cartOpen: false,
19
+ precartOpen: false,
20
+ selectedSkuIds: {},
21
+ licenseOptions: [],
22
+ collectionStyleSkus: {},
23
+ collectionSkus: {},
24
+ skuPrices: {},
25
+ fetchedCollectionIds: [],
26
+ storeModalRoute: {
27
+ name: 'index'
28
+ },
29
+ storeModalHistory: [],
30
+ orderVariableSelections: [],
31
+ licenseeIsBillingIdentity: null,
32
+ ...overrides
33
+ };
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Shared fixtures — reusable across tests that need the full
38
+ // "superfamily + 3 families + 3 bundles" topology
39
+ // ---------------------------------------------------------------------------
40
+ function makeThunderState() {
41
+ let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
42
+ return makeState({
43
+ collectionStyleSkus: {
44
+ // Superfamily
45
+ 'super-sku': {
46
+ fontStyleSkuIds: ['s1', 's2', 'ds1', 'ds2', 'cs1', 'cs2', 'vs1', 'vis1', 'dvs1', 'dvis1', 'cvs1', 'cvis1'],
47
+ fontStyleIds: ['f1', 'f2', 'df1', 'df2', 'cf1', 'cf2', 'vf1', 'vif1', 'dvf1', 'dvif1', 'cvf1', 'cvif1'],
48
+ childrenSkuIds: ['text-sku', 'display-sku', 'caption-sku', 'text-var-sku', 'text-var-italic-sku', 'display-var-sku', 'display-var-italic-sku', 'caption-var-sku', 'caption-var-italic-sku', 'text-bundle-sku', 'display-bundle-sku', 'caption-bundle-sku'],
49
+ name: 'Thunder Text Collection'
50
+ },
51
+ // Static families
52
+ 'text-sku': {
53
+ fontStyleSkuIds: ['s1', 's2'],
54
+ fontStyleIds: ['f1', 'f2'],
55
+ childrenSkuIds: [],
56
+ name: 'Thunder Text'
57
+ },
58
+ 'display-sku': {
59
+ fontStyleSkuIds: ['ds1', 'ds2'],
60
+ fontStyleIds: ['df1', 'df2'],
61
+ childrenSkuIds: [],
62
+ name: 'Thunder Display'
63
+ },
64
+ 'caption-sku': {
65
+ fontStyleSkuIds: ['cs1', 'cs2'],
66
+ fontStyleIds: ['cf1', 'cf2'],
67
+ childrenSkuIds: [],
68
+ name: 'Thunder Caption'
69
+ },
70
+ // Variable families
71
+ 'text-var-sku': {
72
+ fontStyleSkuIds: ['vs1'],
73
+ fontStyleIds: ['vf1'],
74
+ childrenSkuIds: [],
75
+ name: 'Thunder Text Variable'
76
+ },
77
+ 'text-var-italic-sku': {
78
+ fontStyleSkuIds: ['vis1'],
79
+ fontStyleIds: ['vif1'],
80
+ childrenSkuIds: [],
81
+ name: 'Thunder Text Italic Variable'
82
+ },
83
+ 'display-var-sku': {
84
+ fontStyleSkuIds: ['dvs1'],
85
+ fontStyleIds: ['dvf1'],
86
+ childrenSkuIds: [],
87
+ name: 'Thunder Display Variable'
88
+ },
89
+ 'display-var-italic-sku': {
90
+ fontStyleSkuIds: ['dvis1'],
91
+ fontStyleIds: ['dvif1'],
92
+ childrenSkuIds: [],
93
+ name: 'Thunder Display Italic Variable'
94
+ },
95
+ 'caption-var-sku': {
96
+ fontStyleSkuIds: ['cvs1'],
97
+ fontStyleIds: ['cvf1'],
98
+ childrenSkuIds: [],
99
+ name: 'Thunder Caption Variable'
100
+ },
101
+ 'caption-var-italic-sku': {
102
+ fontStyleSkuIds: ['cvis1'],
103
+ fontStyleIds: ['cvif1'],
104
+ childrenSkuIds: [],
105
+ name: 'Thunder Caption Italic Variable'
106
+ },
107
+ // Collection bundles
108
+ 'text-bundle-sku': {
109
+ fontStyleSkuIds: ['s1', 's2', 'vs1', 'vis1'],
110
+ fontStyleIds: ['f1', 'f2', 'vf1', 'vif1'],
111
+ childrenSkuIds: ['text-sku', 'text-var-sku', 'text-var-italic-sku'],
112
+ name: 'Thunder Text Family + Variable'
113
+ },
114
+ 'display-bundle-sku': {
115
+ fontStyleSkuIds: ['ds1', 'ds2', 'dvs1', 'dvis1'],
116
+ fontStyleIds: ['df1', 'df2', 'dvf1', 'dvif1'],
117
+ childrenSkuIds: ['display-sku', 'display-var-sku', 'display-var-italic-sku'],
118
+ name: 'Thunder Display Family + Variable'
119
+ },
120
+ 'caption-bundle-sku': {
121
+ fontStyleSkuIds: ['cs1', 'cs2', 'cvs1', 'cvis1'],
122
+ fontStyleIds: ['cf1', 'cf2', 'cvf1', 'cvif1'],
123
+ childrenSkuIds: ['caption-sku', 'caption-var-sku', 'caption-var-italic-sku'],
124
+ name: 'Thunder Caption Family + Variable'
125
+ }
126
+ },
127
+ skuPrices: {
128
+ 'super-sku': 750,
129
+ 'text-sku': 500,
130
+ 'display-sku': 500,
131
+ 'caption-sku': 500,
132
+ 'text-var-sku': 300,
133
+ 'text-var-italic-sku': 300,
134
+ 'display-var-sku': 300,
135
+ 'display-var-italic-sku': 300,
136
+ 'caption-var-sku': 300,
137
+ 'caption-var-italic-sku': 300,
138
+ 'text-bundle-sku': 500,
139
+ 'display-bundle-sku': 500,
140
+ 'caption-bundle-sku': 500,
141
+ s1: 40,
142
+ s2: 40,
143
+ ds1: 40,
144
+ ds2: 40,
145
+ cs1: 40,
146
+ cs2: 40,
147
+ vs1: 40,
148
+ vis1: 40,
149
+ dvs1: 40,
150
+ dvis1: 40,
151
+ cvs1: 40,
152
+ cvis1: 40
153
+ },
154
+ ...overrides
155
+ });
156
+ }
157
+
158
+ // =========================================================================
159
+ // collContainsSkuId
160
+ // =========================================================================
161
+ (0, _vitest.describe)('collContainsSkuId', () => {
162
+ (0, _vitest.it)('returns true when skuId is in childrenSkuIds', () => {
163
+ const coll = {
164
+ fontStyleSkuIds: [],
165
+ fontStyleIds: [],
166
+ childrenSkuIds: ['child-sku-1', 'child-sku-2'],
167
+ name: 'Test'
168
+ };
169
+ (0, _vitest.expect)((0, _utils.collContainsSkuId)(coll, 'child-sku-1')).toBe(true);
170
+ });
171
+ (0, _vitest.it)('returns true when skuId is in fontStyleSkuIds', () => {
172
+ const coll = {
173
+ fontStyleSkuIds: ['style-sku-1'],
174
+ fontStyleIds: [],
175
+ childrenSkuIds: [],
176
+ name: 'Test'
177
+ };
178
+ (0, _vitest.expect)((0, _utils.collContainsSkuId)(coll, 'style-sku-1')).toBe(true);
179
+ });
180
+ (0, _vitest.it)('returns true when id is in fontStyleIds', () => {
181
+ const coll = {
182
+ fontStyleSkuIds: [],
183
+ fontStyleIds: ['style-id-1'],
184
+ childrenSkuIds: [],
185
+ name: 'Test'
186
+ };
187
+ (0, _vitest.expect)((0, _utils.collContainsSkuId)(coll, 'style-id-1')).toBe(true);
188
+ });
189
+ (0, _vitest.it)('returns false for unknown skuId', () => {
190
+ const coll = {
191
+ fontStyleSkuIds: ['style-sku-1'],
192
+ fontStyleIds: ['style-1'],
193
+ childrenSkuIds: ['child-sku-1'],
194
+ name: 'Test'
195
+ };
196
+ (0, _vitest.expect)((0, _utils.collContainsSkuId)(coll, 'unknown-sku')).toBe(false);
197
+ });
198
+ (0, _vitest.it)('returns false for undefined collection', () => {
199
+ (0, _vitest.expect)((0, _utils.collContainsSkuId)(undefined, 'any-sku')).toBe(false);
200
+ });
201
+ });
202
+
203
+ // =========================================================================
204
+ // isSelected
205
+ // =========================================================================
206
+ (0, _vitest.describe)('isSelected', () => {
207
+ (0, _vitest.it)('returns true when the SKU is directly selected', () => {
208
+ const state = makeState({
209
+ selectedSkuIds: {
210
+ 'sku-1': true
211
+ }
212
+ });
213
+ (0, _vitest.expect)((0, _utils.isSelected)('sku-1')(state)).toBe(true);
214
+ });
215
+ (0, _vitest.it)('returns false when the SKU is explicitly deselected (false)', () => {
216
+ const state = makeState({
217
+ selectedSkuIds: {
218
+ 'sku-1': false
219
+ }
220
+ });
221
+ (0, _vitest.expect)((0, _utils.isSelected)('sku-1')(state)).toBe(false);
222
+ });
223
+ (0, _vitest.it)('returns true when a parent collection is selected', () => {
224
+ const state = makeState({
225
+ selectedSkuIds: {
226
+ 'bundle-sku': true
227
+ },
228
+ collectionStyleSkus: {
229
+ 'bundle-sku': {
230
+ fontStyleSkuIds: ['s1'],
231
+ fontStyleIds: ['f1'],
232
+ childrenSkuIds: ['family-sku'],
233
+ name: 'Bundle'
234
+ }
235
+ }
236
+ });
237
+ // Style SKU contained via fontStyleSkuIds
238
+ (0, _vitest.expect)((0, _utils.isSelected)('s1')(state)).toBe(true);
239
+ // Family SKU contained via childrenSkuIds
240
+ (0, _vitest.expect)((0, _utils.isSelected)('family-sku')(state)).toBe(true);
241
+ // Font style ID contained via fontStyleIds
242
+ (0, _vitest.expect)((0, _utils.isSelected)('f1')(state)).toBe(true);
243
+ });
244
+ (0, _vitest.it)('returns false when a parent collection is deselected', () => {
245
+ const state = makeState({
246
+ selectedSkuIds: {
247
+ 'bundle-sku': false,
248
+ 's1': false
249
+ },
250
+ collectionStyleSkus: {
251
+ 'bundle-sku': {
252
+ fontStyleSkuIds: ['s1'],
253
+ fontStyleIds: ['f1'],
254
+ childrenSkuIds: ['family-sku'],
255
+ name: 'Bundle'
256
+ }
257
+ }
258
+ });
259
+ (0, _vitest.expect)((0, _utils.isSelected)('s1')(state)).toBe(false);
260
+ (0, _vitest.expect)((0, _utils.isSelected)('family-sku')(state)).toBe(false);
261
+ });
262
+ (0, _vitest.it)('returns false for null skuId', () => {
263
+ const state = makeState();
264
+ (0, _vitest.expect)((0, _utils.isSelected)(null)(state)).toBe(false);
265
+ });
266
+ (0, _vitest.it)('returns true for member when bundle is selected, even if member is explicitly false', () => {
267
+ // After auto-selection, members are set to false but should still appear
268
+ // selected because the parent bundle is true
269
+ const state = makeState({
270
+ selectedSkuIds: {
271
+ 'bundle-sku': true,
272
+ 'family-sku': false,
273
+ 'var-sku': false
274
+ },
275
+ collectionStyleSkus: {
276
+ 'bundle-sku': {
277
+ fontStyleSkuIds: ['s1', 'vs1'],
278
+ fontStyleIds: ['f1', 'vf1'],
279
+ childrenSkuIds: ['family-sku', 'var-sku'],
280
+ name: 'Bundle'
281
+ }
282
+ }
283
+ });
284
+ (0, _vitest.expect)((0, _utils.isSelected)('family-sku')(state)).toBe(true);
285
+ (0, _vitest.expect)((0, _utils.isSelected)('var-sku')(state)).toBe(true);
286
+ (0, _vitest.expect)((0, _utils.isSelected)('s1')(state)).toBe(true);
287
+ });
288
+ });
289
+
290
+ // =========================================================================
291
+ // collectionSkuIdsDifferences
292
+ // =========================================================================
293
+ (0, _vitest.describe)('collectionSkuIdsDifferences', () => {
294
+ (0, _vitest.it)('includes self when skuId matches a collection', () => {
295
+ const state = makeState({
296
+ collectionStyleSkus: {
297
+ 'family-sku': {
298
+ fontStyleSkuIds: ['s1', 's2'],
299
+ fontStyleIds: ['f1', 'f2'],
300
+ childrenSkuIds: [],
301
+ name: 'Family'
302
+ }
303
+ },
304
+ skuPrices: {
305
+ 'family-sku': 500,
306
+ s1: 40,
307
+ s2: 40
308
+ }
309
+ });
310
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'family-sku');
311
+ (0, _vitest.expect)(diffs).toHaveProperty('family-sku');
312
+ (0, _vitest.expect)(diffs['family-sku']).toBe(500);
313
+ });
314
+ (0, _vitest.it)('includes parent collections that contain the skuId', () => {
315
+ const state = makeState({
316
+ collectionStyleSkus: {
317
+ 'family-sku': {
318
+ fontStyleSkuIds: ['s1', 's2'],
319
+ fontStyleIds: ['f1', 'f2'],
320
+ childrenSkuIds: [],
321
+ name: 'Family'
322
+ },
323
+ 'bundle-sku': {
324
+ fontStyleSkuIds: ['s1', 's2', 'vs1'],
325
+ fontStyleIds: ['f1', 'f2', 'vf1'],
326
+ childrenSkuIds: ['family-sku', 'var-family-sku'],
327
+ name: 'Bundle'
328
+ }
329
+ },
330
+ skuPrices: {
331
+ 'family-sku': 500,
332
+ 'bundle-sku': 500,
333
+ 'var-family-sku': 300,
334
+ s1: 40,
335
+ s2: 40,
336
+ vs1: 40
337
+ }
338
+ });
339
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'family-sku');
340
+ (0, _vitest.expect)(diffs).toHaveProperty('family-sku');
341
+ (0, _vitest.expect)(diffs).toHaveProperty('bundle-sku');
342
+ });
343
+ (0, _vitest.it)('subtracts already-selected items from the difference', () => {
344
+ const state = makeState({
345
+ selectedSkuIds: {
346
+ 'family-sku': true
347
+ },
348
+ collectionStyleSkus: {
349
+ 'family-sku': {
350
+ fontStyleSkuIds: ['s1', 's2'],
351
+ fontStyleIds: ['f1', 'f2'],
352
+ childrenSkuIds: [],
353
+ name: 'Family'
354
+ },
355
+ 'bundle-sku': {
356
+ fontStyleSkuIds: ['s1', 's2', 'vs1'],
357
+ fontStyleIds: ['f1', 'f2', 'vf1'],
358
+ childrenSkuIds: ['family-sku', 'var-family-sku'],
359
+ name: 'Bundle'
360
+ }
361
+ },
362
+ skuPrices: {
363
+ 'family-sku': 500,
364
+ 'bundle-sku': 500,
365
+ 'var-family-sku': 300
366
+ }
367
+ });
368
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'var-family-sku');
369
+ (0, _vitest.expect)(diffs['bundle-sku']).toBe(0);
370
+ });
371
+ (0, _vitest.it)('treats missing prices as 0 rather than producing NaN', () => {
372
+ const state = makeState({
373
+ selectedSkuIds: {
374
+ 'orphan-sku': true
375
+ },
376
+ collectionStyleSkus: {
377
+ 'family-sku': {
378
+ fontStyleSkuIds: ['orphan-sku'],
379
+ fontStyleIds: [],
380
+ childrenSkuIds: [],
381
+ name: 'Family'
382
+ }
383
+ },
384
+ skuPrices: {
385
+ 'family-sku': 500
386
+ // 'orphan-sku' deliberately missing
387
+ }
388
+ });
389
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'orphan-sku');
390
+ // Missing price treated as 0, so difference = 500 - 0 = 500
391
+ (0, _vitest.expect)(diffs['family-sku']).toBe(500);
392
+ });
393
+ (0, _vitest.it)('only counts items that are selected true, not false', () => {
394
+ const state = makeState({
395
+ selectedSkuIds: {
396
+ 'family-sku': false
397
+ },
398
+ collectionStyleSkus: {
399
+ 'bundle-sku': {
400
+ fontStyleSkuIds: [],
401
+ fontStyleIds: [],
402
+ childrenSkuIds: ['family-sku'],
403
+ name: 'Bundle'
404
+ },
405
+ 'family-sku': {
406
+ fontStyleSkuIds: [],
407
+ fontStyleIds: [],
408
+ childrenSkuIds: [],
409
+ name: 'Family'
410
+ }
411
+ },
412
+ skuPrices: {
413
+ 'bundle-sku': 500,
414
+ 'family-sku': 300
415
+ }
416
+ });
417
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'family-sku');
418
+ // family-sku is false, not true, so it shouldn't be subtracted
419
+ (0, _vitest.expect)(diffs['bundle-sku']).toBe(500);
420
+ });
421
+ (0, _vitest.it)('returns empty object when no collections contain the skuId', () => {
422
+ const state = makeState({
423
+ collectionStyleSkus: {
424
+ 'other-sku': {
425
+ fontStyleSkuIds: ['x1'],
426
+ fontStyleIds: ['xf1'],
427
+ childrenSkuIds: [],
428
+ name: 'Other'
429
+ }
430
+ },
431
+ skuPrices: {
432
+ 'other-sku': 100,
433
+ 'lonely-style': 40
434
+ }
435
+ });
436
+ const diffs = (0, _utils.collectionSkuIdsDifferences)(state, 'lonely-style');
437
+ (0, _vitest.expect)(Object.keys(diffs)).toHaveLength(0);
438
+ });
439
+ });
440
+
441
+ // =========================================================================
442
+ // collectionSkuIdWithDiscount
443
+ // =========================================================================
444
+ (0, _vitest.describe)('collectionSkuIdWithDiscount', () => {
445
+ (0, _vitest.it)('returns null when no collection is a better deal', () => {
446
+ const state = makeState({
447
+ collectionStyleSkus: {
448
+ 'family-sku': {
449
+ fontStyleSkuIds: ['s1'],
450
+ fontStyleIds: ['f1'],
451
+ childrenSkuIds: [],
452
+ name: 'Family'
453
+ }
454
+ },
455
+ skuPrices: {
456
+ s1: 40,
457
+ 'family-sku': 500
458
+ }
459
+ });
460
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 's1');
461
+ (0, _vitest.expect)(collId).toBe(null);
462
+ (0, _vitest.expect)(amount).toBe(null);
463
+ });
464
+ (0, _vitest.it)('returns parent collection when it is cheaper for individual style', () => {
465
+ const state = makeState({
466
+ selectedSkuIds: {
467
+ s1: true,
468
+ s2: true,
469
+ s3: true,
470
+ s4: true
471
+ },
472
+ collectionStyleSkus: {
473
+ 'family-sku': {
474
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
475
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
476
+ childrenSkuIds: [],
477
+ name: 'Family'
478
+ }
479
+ },
480
+ skuPrices: {
481
+ 'family-sku': 200,
482
+ s1: 40,
483
+ s2: 40,
484
+ s3: 40,
485
+ s4: 40,
486
+ s5: 40
487
+ }
488
+ });
489
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 's5');
490
+ (0, _vitest.expect)(collId).toBe('family-sku');
491
+ (0, _vitest.expect)(amount).toBe(40);
492
+ });
493
+ (0, _vitest.it)('prefers collection bundle over self when prices are equal', () => {
494
+ const state = makeState({
495
+ collectionStyleSkus: {
496
+ 'family-sku': {
497
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
498
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
499
+ childrenSkuIds: [],
500
+ name: 'Thunder Text'
501
+ },
502
+ 'var-family-sku': {
503
+ fontStyleSkuIds: ['vs1'],
504
+ fontStyleIds: ['vf1'],
505
+ childrenSkuIds: [],
506
+ name: 'Thunder Text Variable'
507
+ },
508
+ 'var-italic-sku': {
509
+ fontStyleSkuIds: ['vis1'],
510
+ fontStyleIds: ['vif1'],
511
+ childrenSkuIds: [],
512
+ name: 'Thunder Text Italic Variable'
513
+ },
514
+ 'bundle-sku': {
515
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'vs1', 'vis1'],
516
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'vf1', 'vif1'],
517
+ childrenSkuIds: ['family-sku', 'var-family-sku', 'var-italic-sku'],
518
+ name: 'Thunder Text Family + Variable'
519
+ }
520
+ },
521
+ skuPrices: {
522
+ 'family-sku': 500,
523
+ 'bundle-sku': 500,
524
+ 'var-family-sku': 300,
525
+ 'var-italic-sku': 300,
526
+ s1: 40,
527
+ s2: 40,
528
+ s3: 40,
529
+ s4: 40,
530
+ s5: 40,
531
+ vs1: 40,
532
+ vis1: 40
533
+ }
534
+ });
535
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
536
+ (0, _vitest.expect)(collId).toBe('bundle-sku');
537
+ (0, _vitest.expect)(amount).toBe(500);
538
+ });
539
+ (0, _vitest.it)('prefers collection bundle regardless of object key order', () => {
540
+ const state = makeState({
541
+ collectionStyleSkus: {
542
+ 'bundle-sku': {
543
+ fontStyleSkuIds: ['s1', 's2', 'vs1'],
544
+ fontStyleIds: ['f1', 'f2', 'vf1'],
545
+ childrenSkuIds: ['family-sku', 'var-family-sku'],
546
+ name: 'Bundle'
547
+ },
548
+ 'family-sku': {
549
+ fontStyleSkuIds: ['s1', 's2'],
550
+ fontStyleIds: ['f1', 'f2'],
551
+ childrenSkuIds: [],
552
+ name: 'Family'
553
+ }
554
+ },
555
+ skuPrices: {
556
+ 'family-sku': 500,
557
+ 'bundle-sku': 500,
558
+ 'var-family-sku': 300
559
+ }
560
+ });
561
+ const [collId] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
562
+ (0, _vitest.expect)(collId).toBe('bundle-sku');
563
+ });
564
+ (0, _vitest.it)('still returns self for display pricing (superfamily with no parent)', () => {
565
+ const state = makeState({
566
+ selectedSkuIds: {
567
+ 'family-sku': true
568
+ },
569
+ collectionStyleSkus: {
570
+ 'superfamily-sku': {
571
+ fontStyleSkuIds: ['s1', 's2', 'ds1', 'ds2'],
572
+ fontStyleIds: ['f1', 'f2', 'df1', 'df2'],
573
+ childrenSkuIds: ['family-sku', 'display-family-sku'],
574
+ name: 'Superfamily'
575
+ },
576
+ 'family-sku': {
577
+ fontStyleSkuIds: ['s1', 's2'],
578
+ fontStyleIds: ['f1', 'f2'],
579
+ childrenSkuIds: [],
580
+ name: 'Family'
581
+ },
582
+ 'display-family-sku': {
583
+ fontStyleSkuIds: ['ds1', 'ds2'],
584
+ fontStyleIds: ['df1', 'df2'],
585
+ childrenSkuIds: [],
586
+ name: 'Display'
587
+ }
588
+ },
589
+ skuPrices: {
590
+ 'superfamily-sku': 750,
591
+ 'family-sku': 500,
592
+ 'display-family-sku': 500
593
+ }
594
+ });
595
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'superfamily-sku');
596
+ (0, _vitest.expect)(collId).toBe('superfamily-sku');
597
+ (0, _vitest.expect)(amount).toBe(250);
598
+ });
599
+ (0, _vitest.it)('prefers bundle at $0 when family is already selected', () => {
600
+ const state = makeState({
601
+ selectedSkuIds: {
602
+ 'family-sku': true
603
+ },
604
+ collectionStyleSkus: {
605
+ 'family-sku': {
606
+ fontStyleSkuIds: ['s1', 's2'],
607
+ fontStyleIds: ['f1', 'f2'],
608
+ childrenSkuIds: [],
609
+ name: 'Family'
610
+ },
611
+ 'var-family-sku': {
612
+ fontStyleSkuIds: ['vs1'],
613
+ fontStyleIds: ['vf1'],
614
+ childrenSkuIds: [],
615
+ name: 'Variable'
616
+ },
617
+ 'bundle-sku': {
618
+ fontStyleSkuIds: ['s1', 's2', 'vs1'],
619
+ fontStyleIds: ['f1', 'f2', 'vf1'],
620
+ childrenSkuIds: ['family-sku', 'var-family-sku'],
621
+ name: 'Bundle'
622
+ }
623
+ },
624
+ skuPrices: {
625
+ 'family-sku': 500,
626
+ 'var-family-sku': 300,
627
+ 'bundle-sku': 500
628
+ }
629
+ });
630
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'var-family-sku');
631
+ (0, _vitest.expect)(collId).toBe('bundle-sku');
632
+ (0, _vitest.expect)(amount).toBe(0);
633
+ });
634
+ (0, _vitest.it)('does not upgrade when collection bundle is more expensive', () => {
635
+ const state = makeState({
636
+ collectionStyleSkus: {
637
+ 'family-sku': {
638
+ fontStyleSkuIds: ['s1', 's2'],
639
+ fontStyleIds: ['f1', 'f2'],
640
+ childrenSkuIds: [],
641
+ name: 'Family'
642
+ },
643
+ 'bundle-sku': {
644
+ fontStyleSkuIds: ['s1', 's2', 'vs1'],
645
+ fontStyleIds: ['f1', 'f2', 'vf1'],
646
+ childrenSkuIds: ['family-sku', 'var-family-sku'],
647
+ name: 'Bundle'
648
+ }
649
+ },
650
+ skuPrices: {
651
+ 'family-sku': 500,
652
+ 'bundle-sku': 600
653
+ }
654
+ });
655
+ const [collId] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
656
+ (0, _vitest.expect)(collId).toBe('family-sku');
657
+ });
658
+ (0, _vitest.it)('returns null when the selected SKU has no price (undefined)', () => {
659
+ // If skuPrices[skuId] is undefined, the filter comparison
660
+ // `difference <= undefined` is always false → no collection found
661
+ const state = makeState({
662
+ collectionStyleSkus: {
663
+ 'family-sku': {
664
+ fontStyleSkuIds: ['s1'],
665
+ fontStyleIds: ['f1'],
666
+ childrenSkuIds: [],
667
+ name: 'Family'
668
+ }
669
+ },
670
+ skuPrices: {
671
+ 'family-sku': 500
672
+ } // s1 deliberately missing
673
+ });
674
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 's1');
675
+ (0, _vitest.expect)(collId).toBe(null);
676
+ (0, _vitest.expect)(amount).toBe(null);
677
+ });
678
+ (0, _vitest.it)('treats collections with missing price as price 0', () => {
679
+ // A collection with no price entry is treated as $0, meaning its
680
+ // difference = 0 - sum_selected. With nothing selected, diff = 0.
681
+ const state = makeState({
682
+ collectionStyleSkus: {
683
+ 'ghost-bundle': {
684
+ fontStyleSkuIds: ['s1'],
685
+ fontStyleIds: ['f1'],
686
+ childrenSkuIds: ['family-sku'],
687
+ name: 'Ghost Bundle'
688
+ },
689
+ 'family-sku': {
690
+ fontStyleSkuIds: ['s1'],
691
+ fontStyleIds: ['f1'],
692
+ childrenSkuIds: [],
693
+ name: 'Family'
694
+ }
695
+ },
696
+ skuPrices: {
697
+ 'family-sku': 500,
698
+ s1: 40
699
+ }
700
+ // 'ghost-bundle' price deliberately missing → treated as 0
701
+ });
702
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
703
+ // ghost-bundle diff = 0, family diff = 500. ghost-bundle is cheaper.
704
+ // 0 <= 500 ✓ → ghost-bundle wins
705
+ (0, _vitest.expect)(collId).toBe('ghost-bundle');
706
+ (0, _vitest.expect)(amount).toBe(0);
707
+ });
708
+ (0, _vitest.it)('handles zero-price SKUs without errors', () => {
709
+ const state = makeState({
710
+ collectionStyleSkus: {
711
+ 'free-family': {
712
+ fontStyleSkuIds: ['fs1'],
713
+ fontStyleIds: ['ff1'],
714
+ childrenSkuIds: [],
715
+ name: 'Free Family'
716
+ },
717
+ 'free-bundle': {
718
+ fontStyleSkuIds: ['fs1', 'fvs1'],
719
+ fontStyleIds: ['ff1', 'fvf1'],
720
+ childrenSkuIds: ['free-family', 'free-var'],
721
+ name: 'Free Bundle'
722
+ }
723
+ },
724
+ skuPrices: {
725
+ 'free-family': 0,
726
+ 'free-bundle': 0,
727
+ 'free-var': 0,
728
+ fs1: 0,
729
+ fvs1: 0
730
+ }
731
+ });
732
+
733
+ // Both have diff = 0, filter: 0 <= 0 ✓, tie-break prefers bundle
734
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'free-family');
735
+ (0, _vitest.expect)(collId).toBe('free-bundle');
736
+ (0, _vitest.expect)(amount).toBe(0);
737
+ });
738
+ (0, _vitest.it)('handles negative difference (collection cheaper than sum of selected)', () => {
739
+ // Unusual but possible: if prices change or a collection is deeply discounted
740
+ const state = makeState({
741
+ selectedSkuIds: {
742
+ s1: true,
743
+ s2: true,
744
+ s3: true
745
+ },
746
+ collectionStyleSkus: {
747
+ 'family-sku': {
748
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4'],
749
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4'],
750
+ childrenSkuIds: [],
751
+ name: 'Family'
752
+ }
753
+ },
754
+ skuPrices: {
755
+ 'family-sku': 100,
756
+ s1: 40,
757
+ s2: 40,
758
+ s3: 40,
759
+ s4: 40
760
+ }
761
+ });
762
+
763
+ // Family costs $100, 3 styles selected = $120, diff = $100 - $120 = -$20
764
+ // This means the collection is $20 cheaper than what's already selected
765
+ const [collId, amount] = (0, _reducer.collectionSkuIdWithDiscount)(state, 's4');
766
+ (0, _vitest.expect)(collId).toBe('family-sku');
767
+ (0, _vitest.expect)(amount).toBe(-20);
768
+ });
769
+ });
770
+
771
+ // =========================================================================
772
+ // reducer SELECT_SKU_ID — core auto-selection
773
+ // =========================================================================
774
+ (0, _vitest.describe)('reducer SELECT_SKU_ID with collection bundles', () => {
775
+ (0, _vitest.it)('auto-selects collection bundle when selecting a family at the same price', () => {
776
+ const state = makeThunderState();
777
+ const newState = (0, _reducer.default)(state, {
778
+ type: 'SELECT_SKU_ID',
779
+ skuId: 'text-sku',
780
+ selected: true
781
+ });
782
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
783
+ (0, _vitest.expect)(newState.selectedSkuIds['text-sku']).toBe(false);
784
+ (0, _vitest.expect)(newState.selectedSkuIds['text-var-sku']).toBe(false);
785
+ (0, _vitest.expect)(newState.selectedSkuIds['text-var-italic-sku']).toBe(false);
786
+ });
787
+ (0, _vitest.it)('does not auto-select bundle when bundle is more expensive', () => {
788
+ const state = makeThunderState({
789
+ skuPrices: {
790
+ ...makeThunderState().skuPrices,
791
+ 'text-bundle-sku': 600 // more expensive than text-sku ($500)
792
+ }
793
+ });
794
+ const newState = (0, _reducer.default)(state, {
795
+ type: 'SELECT_SKU_ID',
796
+ skuId: 'text-sku',
797
+ selected: true
798
+ });
799
+ (0, _vitest.expect)(newState.selectedSkuIds['text-sku']).toBe(true);
800
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBeUndefined();
801
+ });
802
+ (0, _vitest.it)('auto-selects bundle when clicking variable family with static already selected', () => {
803
+ const state = makeThunderState({
804
+ selectedSkuIds: {
805
+ 'text-sku': true
806
+ }
807
+ });
808
+ const newState = (0, _reducer.default)(state, {
809
+ type: 'SELECT_SKU_ID',
810
+ skuId: 'text-var-sku',
811
+ selected: true
812
+ });
813
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
814
+ (0, _vitest.expect)(newState.selectedSkuIds['text-sku']).toBe(false);
815
+ (0, _vitest.expect)(newState.selectedSkuIds['text-var-sku']).toBe(false);
816
+ });
817
+ });
818
+
819
+ // =========================================================================
820
+ // reducer SELECT_SKU_ID — multiple bundles don't interfere
821
+ // =========================================================================
822
+ (0, _vitest.describe)('reducer SELECT_SKU_ID with multiple bundles', () => {
823
+ (0, _vitest.it)('only selects the correct bundle for the family being clicked', () => {
824
+ const state = makeThunderState();
825
+ const newState = (0, _reducer.default)(state, {
826
+ type: 'SELECT_SKU_ID',
827
+ skuId: 'display-sku',
828
+ selected: true
829
+ });
830
+
831
+ // Display bundle should be selected
832
+ (0, _vitest.expect)(newState.selectedSkuIds['display-bundle-sku']).toBe(true);
833
+ (0, _vitest.expect)(newState.selectedSkuIds['display-sku']).toBe(false);
834
+ // Text and caption bundles should be untouched
835
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBeUndefined();
836
+ (0, _vitest.expect)(newState.selectedSkuIds['caption-bundle-sku']).toBeUndefined();
837
+ });
838
+ (0, _vitest.it)('upgrades to superfamily when selecting a second family bundle', () => {
839
+ // Selecting two families ($500+$500) costs more than the superfamily ($750),
840
+ // so the system correctly upgrades to the superfamily on the second click
841
+ const state = makeThunderState();
842
+ let newState = (0, _reducer.default)(state, {
843
+ type: 'SELECT_SKU_ID',
844
+ skuId: 'text-sku',
845
+ selected: true
846
+ });
847
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
848
+ newState = (0, _reducer.default)(newState, {
849
+ type: 'SELECT_SKU_ID',
850
+ skuId: 'display-sku',
851
+ selected: true
852
+ });
853
+
854
+ // Superfamily ($750) beats two bundles ($500 + $500 = $1000)
855
+ (0, _vitest.expect)(newState.selectedSkuIds['super-sku']).toBe(true);
856
+ // Previous bundle should be deselected (subsumed by superfamily)
857
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
858
+ (0, _vitest.expect)(newState.selectedSkuIds['display-bundle-sku']).toBe(false);
859
+ });
860
+ });
861
+
862
+ // =========================================================================
863
+ // reducer SELECT_SKU_ID — deselection flow
864
+ // =========================================================================
865
+ (0, _vitest.describe)('reducer deselection', () => {
866
+ (0, _vitest.it)('deselecting a bundle member deselects the entire bundle', () => {
867
+ // Start with the text bundle selected (members are false)
868
+ const state = makeThunderState({
869
+ selectedSkuIds: {
870
+ 'text-bundle-sku': true,
871
+ 'text-sku': false,
872
+ 'text-var-sku': false,
873
+ 'text-var-italic-sku': false,
874
+ s1: false,
875
+ s2: false,
876
+ vs1: false,
877
+ vis1: false
878
+ }
879
+ });
880
+
881
+ // User clicks to deselect "Thunder Text Variable"
882
+ const newState = (0, _reducer.default)(state, {
883
+ type: 'SELECT_SKU_ID',
884
+ skuId: 'text-var-sku',
885
+ selected: false
886
+ });
887
+
888
+ // The bundle should be deselected
889
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
890
+ });
891
+ (0, _vitest.it)('deselecting a font style deselects the parent bundle', () => {
892
+ const state = makeThunderState({
893
+ selectedSkuIds: {
894
+ 'text-bundle-sku': true,
895
+ 'text-sku': false,
896
+ 'text-var-sku': false,
897
+ 'text-var-italic-sku': false,
898
+ s1: false,
899
+ s2: false,
900
+ vs1: false,
901
+ vis1: false
902
+ }
903
+ });
904
+
905
+ // User clicks to deselect individual style "s1"
906
+ const newState = (0, _reducer.default)(state, {
907
+ type: 'SELECT_SKU_ID',
908
+ skuId: 's1',
909
+ selected: false
910
+ });
911
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
912
+ });
913
+ (0, _vitest.it)('deselecting a bundle does not affect other bundles', () => {
914
+ const state = makeThunderState({
915
+ selectedSkuIds: {
916
+ 'text-bundle-sku': true,
917
+ 'text-sku': false,
918
+ 'text-var-sku': false,
919
+ 'text-var-italic-sku': false,
920
+ s1: false,
921
+ s2: false,
922
+ vs1: false,
923
+ vis1: false,
924
+ 'display-bundle-sku': true,
925
+ 'display-sku': false,
926
+ 'display-var-sku': false,
927
+ 'display-var-italic-sku': false,
928
+ ds1: false,
929
+ ds2: false,
930
+ dvs1: false,
931
+ dvis1: false
932
+ }
933
+ });
934
+ const newState = (0, _reducer.default)(state, {
935
+ type: 'SELECT_SKU_ID',
936
+ skuId: 'text-var-sku',
937
+ selected: false
938
+ });
939
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
940
+ // Display bundle should be unaffected
941
+ (0, _vitest.expect)(newState.selectedSkuIds['display-bundle-sku']).toBe(true);
942
+ });
943
+ });
944
+
945
+ // =========================================================================
946
+ // reducer SELECT_SKU_ID — re-selection after deselection
947
+ // =========================================================================
948
+ (0, _vitest.describe)('reducer re-selection cycle', () => {
949
+ (0, _vitest.it)('re-selecting family after bundle was deselected re-selects the bundle', () => {
950
+ // 1. Start with bundle selected
951
+ const state = makeThunderState({
952
+ selectedSkuIds: {
953
+ 'text-bundle-sku': true,
954
+ 'text-sku': false,
955
+ 'text-var-sku': false,
956
+ 'text-var-italic-sku': false,
957
+ s1: false,
958
+ s2: false,
959
+ vs1: false,
960
+ vis1: false
961
+ }
962
+ });
963
+
964
+ // 2. Deselect a member → bundle goes away
965
+ const afterDeselect = (0, _reducer.default)(state, {
966
+ type: 'SELECT_SKU_ID',
967
+ skuId: 'text-var-sku',
968
+ selected: false
969
+ });
970
+ (0, _vitest.expect)(afterDeselect.selectedSkuIds['text-bundle-sku']).toBe(false);
971
+
972
+ // 3. Re-select the family → bundle should come back
973
+ const afterReselect = (0, _reducer.default)(afterDeselect, {
974
+ type: 'SELECT_SKU_ID',
975
+ skuId: 'text-sku',
976
+ selected: true
977
+ });
978
+ (0, _vitest.expect)(afterReselect.selectedSkuIds['text-bundle-sku']).toBe(true);
979
+ (0, _vitest.expect)(afterReselect.selectedSkuIds['text-sku']).toBe(false);
980
+ (0, _vitest.expect)(afterReselect.selectedSkuIds['text-var-sku']).toBe(false);
981
+ });
982
+ });
983
+
984
+ // =========================================================================
985
+ // reducer SELECT_SKU_ID — superfamily upgrade
986
+ // =========================================================================
987
+ (0, _vitest.describe)('reducer superfamily upgrade', () => {
988
+ (0, _vitest.it)('upgrades to superfamily when cumulative selections make it cheaper', () => {
989
+ // With two bundles selected ($500 each = $1000 total), the superfamily
990
+ // at $750 should be a better deal when selecting the third family
991
+ const state = makeThunderState({
992
+ selectedSkuIds: {
993
+ 'text-bundle-sku': true,
994
+ 'text-sku': false,
995
+ 'text-var-sku': false,
996
+ 'text-var-italic-sku': false,
997
+ s1: false,
998
+ s2: false,
999
+ vs1: false,
1000
+ vis1: false,
1001
+ 'display-bundle-sku': true,
1002
+ 'display-sku': false,
1003
+ 'display-var-sku': false,
1004
+ 'display-var-italic-sku': false,
1005
+ ds1: false,
1006
+ ds2: false,
1007
+ dvs1: false,
1008
+ dvis1: false
1009
+ }
1010
+ });
1011
+ const newState = (0, _reducer.default)(state, {
1012
+ type: 'SELECT_SKU_ID',
1013
+ skuId: 'caption-sku',
1014
+ selected: true
1015
+ });
1016
+
1017
+ // Superfamily ($750) - two bundles ($500+$500) = -$250 difference
1018
+ // Caption-sku costs $500, so -$250 <= $500 → superfamily is preferred
1019
+ (0, _vitest.expect)(newState.selectedSkuIds['super-sku']).toBe(true);
1020
+ });
1021
+ });
1022
+
1023
+ // =========================================================================
1024
+ // reducer SELECT_SKU_ID — individual style to family upgrade
1025
+ // =========================================================================
1026
+ (0, _vitest.describe)('reducer individual style upgrade', () => {
1027
+ (0, _vitest.it)('upgrades to family when enough styles are individually selected', () => {
1028
+ const state = makeState({
1029
+ selectedSkuIds: {
1030
+ s1: true,
1031
+ s2: true,
1032
+ s3: true,
1033
+ s4: true
1034
+ },
1035
+ collectionStyleSkus: {
1036
+ 'family-sku': {
1037
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
1038
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
1039
+ childrenSkuIds: [],
1040
+ name: 'Family'
1041
+ }
1042
+ },
1043
+ skuPrices: {
1044
+ 'family-sku': 200,
1045
+ s1: 40,
1046
+ s2: 40,
1047
+ s3: 40,
1048
+ s4: 40,
1049
+ s5: 40
1050
+ }
1051
+ });
1052
+
1053
+ // Family is $200, 4 selected = $160, diff = $40. Style s5 costs $40.
1054
+ // $40 <= $40 → family is a better deal
1055
+ const newState = (0, _reducer.default)(state, {
1056
+ type: 'SELECT_SKU_ID',
1057
+ skuId: 's5',
1058
+ selected: true
1059
+ });
1060
+ (0, _vitest.expect)(newState.selectedSkuIds['family-sku']).toBe(true);
1061
+ // Individual styles should be deselected (included via family)
1062
+ (0, _vitest.expect)(newState.selectedSkuIds['s1']).toBe(false);
1063
+ (0, _vitest.expect)(newState.selectedSkuIds['s5']).toBe(false);
1064
+ });
1065
+ (0, _vitest.it)('does not upgrade to family when not yet cost-effective', () => {
1066
+ const state = makeState({
1067
+ selectedSkuIds: {
1068
+ s1: true
1069
+ },
1070
+ collectionStyleSkus: {
1071
+ 'family-sku': {
1072
+ fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
1073
+ fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
1074
+ childrenSkuIds: [],
1075
+ name: 'Family'
1076
+ }
1077
+ },
1078
+ skuPrices: {
1079
+ 'family-sku': 200,
1080
+ s1: 40,
1081
+ s2: 40,
1082
+ s3: 40,
1083
+ s4: 40,
1084
+ s5: 40
1085
+ }
1086
+ });
1087
+
1088
+ // Family is $200, 1 selected = $40, diff = $160. Style s2 costs $40.
1089
+ // $160 > $40 → not worth upgrading
1090
+ const newState = (0, _reducer.default)(state, {
1091
+ type: 'SELECT_SKU_ID',
1092
+ skuId: 's2',
1093
+ selected: true
1094
+ });
1095
+ (0, _vitest.expect)(newState.selectedSkuIds['family-sku']).toBeUndefined();
1096
+ (0, _vitest.expect)(newState.selectedSkuIds['s1']).toBe(true);
1097
+ (0, _vitest.expect)(newState.selectedSkuIds['s2']).toBe(true);
1098
+ });
1099
+ });
1100
+
1101
+ // =========================================================================
1102
+ // reducer SELECT_SKU_ID — edge cases with missing data
1103
+ // =========================================================================
1104
+ (0, _vitest.describe)('reducer edge cases', () => {
1105
+ (0, _vitest.it)('handles selection when collectionStyleSkus is empty', () => {
1106
+ const state = makeState({
1107
+ skuPrices: {
1108
+ 'some-sku': 100
1109
+ }
1110
+ });
1111
+ const newState = (0, _reducer.default)(state, {
1112
+ type: 'SELECT_SKU_ID',
1113
+ skuId: 'some-sku',
1114
+ selected: true
1115
+ });
1116
+ (0, _vitest.expect)(newState.selectedSkuIds['some-sku']).toBe(true);
1117
+ });
1118
+ (0, _vitest.it)('handles selection when skuPrices is empty', () => {
1119
+ const state = makeState({
1120
+ collectionStyleSkus: {
1121
+ 'family-sku': {
1122
+ fontStyleSkuIds: ['s1'],
1123
+ fontStyleIds: ['f1'],
1124
+ childrenSkuIds: [],
1125
+ name: 'Family'
1126
+ }
1127
+ }
1128
+ });
1129
+
1130
+ // No prices → no collection upgrade possible, just select directly
1131
+ const newState = (0, _reducer.default)(state, {
1132
+ type: 'SELECT_SKU_ID',
1133
+ skuId: 's1',
1134
+ selected: true
1135
+ });
1136
+ (0, _vitest.expect)(newState.selectedSkuIds['s1']).toBe(true);
1137
+ (0, _vitest.expect)(newState.selectedSkuIds['family-sku']).toBeUndefined();
1138
+ });
1139
+ (0, _vitest.it)('re-selecting a bundle member may upgrade to superfamily', () => {
1140
+ // When the text bundle is selected and the user clicks text-sku (a member),
1141
+ // the system re-evaluates: the superfamily sees the text bundle ($500) as
1142
+ // already selected, making its difference only $250 — cheaper than text-sku
1143
+ // at $500. So it upgrades to the superfamily.
1144
+ //
1145
+ // This is a documented consequence of the greedy price-optimization: every
1146
+ // SELECT_SKU_ID re-evaluates the cheapest collection, even for members
1147
+ // that are already effectively selected.
1148
+ const state = makeThunderState({
1149
+ selectedSkuIds: {
1150
+ 'text-bundle-sku': true,
1151
+ 'text-sku': false,
1152
+ 'text-var-sku': false,
1153
+ 'text-var-italic-sku': false,
1154
+ s1: false,
1155
+ s2: false,
1156
+ vs1: false,
1157
+ vis1: false
1158
+ }
1159
+ });
1160
+ const newState = (0, _reducer.default)(state, {
1161
+ type: 'SELECT_SKU_ID',
1162
+ skuId: 'text-sku',
1163
+ selected: true
1164
+ });
1165
+ (0, _vitest.expect)(newState.selectedSkuIds['super-sku']).toBe(true);
1166
+ (0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
1167
+ });
1168
+ (0, _vitest.it)('UNSELECT_SKUS clears all selections cleanly', () => {
1169
+ const state = makeThunderState({
1170
+ selectedSkuIds: {
1171
+ 'text-bundle-sku': true,
1172
+ 'text-sku': false,
1173
+ 'text-var-sku': false
1174
+ }
1175
+ });
1176
+ const newState = (0, _reducer.default)(state, {
1177
+ type: 'UNSELECT_SKUS'
1178
+ });
1179
+ (0, _vitest.expect)(Object.keys(newState.selectedSkuIds)).toHaveLength(0);
1180
+ });
1181
+ });