fontdue-js 3.6.0 → 3.7.0
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/CHANGELOG.md +8 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +78 -77
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +78 -77
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/components/Cart/Checkout.js +11 -1
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.js +16 -9
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TypeTester/useTypeTesterStyler.js +1 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.js +12 -0
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +33 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -5,21 +5,30 @@ vi.mock('react-relay', () => ({
|
|
|
5
5
|
graphql: () => null,
|
|
6
6
|
fetchQuery: vi.fn()
|
|
7
7
|
}));
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
8
|
+
import reducer, { isSelected } from '../reducer.js';
|
|
9
|
+
import { additionalCost, cheapestSkuIds } from '../selection.js';
|
|
10
|
+
|
|
11
|
+
// A catalog as the selection sees it: each SKU's [price, styles it grants],
|
|
12
|
+
// the price undefined when not known.
|
|
11
13
|
|
|
12
|
-
// Helper to build a minimal FontdueState for testing
|
|
13
14
|
function makeState() {
|
|
14
|
-
let
|
|
15
|
+
let catalog = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
16
|
+
let overrides = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17
|
+
const skuStyleIds = {};
|
|
18
|
+
const skuPrices = {};
|
|
19
|
+
for (const [skuId, [price, styleIds]] of Object.entries(catalog)) {
|
|
20
|
+
skuStyleIds[skuId] = styleIds;
|
|
21
|
+
if (price !== undefined) skuPrices[skuId] = price;
|
|
22
|
+
}
|
|
15
23
|
return {
|
|
16
24
|
cartOpen: false,
|
|
17
25
|
precartOpen: false,
|
|
18
26
|
selectedSkuIds: {},
|
|
27
|
+
requestedSkuIds: [],
|
|
19
28
|
licenseOptions: [],
|
|
20
|
-
|
|
29
|
+
skuStyleIds,
|
|
21
30
|
collectionSkus: {},
|
|
22
|
-
skuPrices
|
|
31
|
+
skuPrices,
|
|
23
32
|
fetchedCollectionIds: [],
|
|
24
33
|
storeModalRoute: {
|
|
25
34
|
name: 'index'
|
|
@@ -30,739 +39,162 @@ function makeState() {
|
|
|
30
39
|
...overrides
|
|
31
40
|
};
|
|
32
41
|
}
|
|
42
|
+
const select = function (state) {
|
|
43
|
+
for (var _len = arguments.length, skuIds = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
44
|
+
skuIds[_key - 1] = arguments[_key];
|
|
45
|
+
}
|
|
46
|
+
return skuIds.reduce((current, skuId) => reducer(current, {
|
|
47
|
+
type: 'SELECT_SKU_ID',
|
|
48
|
+
skuId,
|
|
49
|
+
selected: true
|
|
50
|
+
}), state);
|
|
51
|
+
};
|
|
52
|
+
const deselect = (state, skuId) => reducer(state, {
|
|
53
|
+
type: 'SELECT_SKU_ID',
|
|
54
|
+
skuId,
|
|
55
|
+
selected: false
|
|
56
|
+
});
|
|
57
|
+
const selected = state => Object.keys(state.selectedSkuIds).filter(skuId => state.selectedSkuIds[skuId] === true).sort();
|
|
33
58
|
|
|
34
59
|
// ---------------------------------------------------------------------------
|
|
35
|
-
// Shared
|
|
36
|
-
//
|
|
60
|
+
// Shared fixture – a superfamily of three families, each with a variable
|
|
61
|
+
// upright and italic, and a collection bundle of the family and its variables
|
|
62
|
+
// priced like the family alone.
|
|
37
63
|
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
childrenSkuIds: [],
|
|
91
|
-
name: 'Thunder Display Italic Variable'
|
|
92
|
-
},
|
|
93
|
-
'caption-var-sku': {
|
|
94
|
-
fontStyleSkuIds: ['cvs1'],
|
|
95
|
-
fontStyleIds: ['cvf1'],
|
|
96
|
-
childrenSkuIds: [],
|
|
97
|
-
name: 'Thunder Caption Variable'
|
|
98
|
-
},
|
|
99
|
-
'caption-var-italic-sku': {
|
|
100
|
-
fontStyleSkuIds: ['cvis1'],
|
|
101
|
-
fontStyleIds: ['cvif1'],
|
|
102
|
-
childrenSkuIds: [],
|
|
103
|
-
name: 'Thunder Caption Italic Variable'
|
|
104
|
-
},
|
|
105
|
-
// Collection bundles
|
|
106
|
-
'text-bundle-sku': {
|
|
107
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1', 'vis1'],
|
|
108
|
-
fontStyleIds: ['f1', 'f2', 'vf1', 'vif1'],
|
|
109
|
-
childrenSkuIds: ['text-sku', 'text-var-sku', 'text-var-italic-sku'],
|
|
110
|
-
name: 'Thunder Text Family + Variable'
|
|
111
|
-
},
|
|
112
|
-
'display-bundle-sku': {
|
|
113
|
-
fontStyleSkuIds: ['ds1', 'ds2', 'dvs1', 'dvis1'],
|
|
114
|
-
fontStyleIds: ['df1', 'df2', 'dvf1', 'dvif1'],
|
|
115
|
-
childrenSkuIds: ['display-sku', 'display-var-sku', 'display-var-italic-sku'],
|
|
116
|
-
name: 'Thunder Display Family + Variable'
|
|
117
|
-
},
|
|
118
|
-
'caption-bundle-sku': {
|
|
119
|
-
fontStyleSkuIds: ['cs1', 'cs2', 'cvs1', 'cvis1'],
|
|
120
|
-
fontStyleIds: ['cf1', 'cf2', 'cvf1', 'cvif1'],
|
|
121
|
-
childrenSkuIds: ['caption-sku', 'caption-var-sku', 'caption-var-italic-sku'],
|
|
122
|
-
name: 'Thunder Caption Family + Variable'
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
skuPrices: {
|
|
126
|
-
'super-sku': 750,
|
|
127
|
-
'text-sku': 500,
|
|
128
|
-
'display-sku': 500,
|
|
129
|
-
'caption-sku': 500,
|
|
130
|
-
'text-var-sku': 300,
|
|
131
|
-
'text-var-italic-sku': 300,
|
|
132
|
-
'display-var-sku': 300,
|
|
133
|
-
'display-var-italic-sku': 300,
|
|
134
|
-
'caption-var-sku': 300,
|
|
135
|
-
'caption-var-italic-sku': 300,
|
|
136
|
-
'text-bundle-sku': 500,
|
|
137
|
-
'display-bundle-sku': 500,
|
|
138
|
-
'caption-bundle-sku': 500,
|
|
139
|
-
s1: 40,
|
|
140
|
-
s2: 40,
|
|
141
|
-
ds1: 40,
|
|
142
|
-
ds2: 40,
|
|
143
|
-
cs1: 40,
|
|
144
|
-
cs2: 40,
|
|
145
|
-
vs1: 40,
|
|
146
|
-
vis1: 40,
|
|
147
|
-
dvs1: 40,
|
|
148
|
-
dvis1: 40,
|
|
149
|
-
cvs1: 40,
|
|
150
|
-
cvis1: 40
|
|
151
|
-
},
|
|
152
|
-
...overrides
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// =========================================================================
|
|
157
|
-
// collContainsSkuId
|
|
158
|
-
// =========================================================================
|
|
159
|
-
describe('collContainsSkuId', () => {
|
|
160
|
-
it('returns true when skuId is in childrenSkuIds', () => {
|
|
161
|
-
const coll = {
|
|
162
|
-
fontStyleSkuIds: [],
|
|
163
|
-
fontStyleIds: [],
|
|
164
|
-
childrenSkuIds: ['child-sku-1', 'child-sku-2'],
|
|
165
|
-
name: 'Test'
|
|
166
|
-
};
|
|
167
|
-
expect(collContainsSkuId(coll, 'child-sku-1')).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
it('returns true when skuId is in fontStyleSkuIds', () => {
|
|
170
|
-
const coll = {
|
|
171
|
-
fontStyleSkuIds: ['style-sku-1'],
|
|
172
|
-
fontStyleIds: [],
|
|
173
|
-
childrenSkuIds: [],
|
|
174
|
-
name: 'Test'
|
|
175
|
-
};
|
|
176
|
-
expect(collContainsSkuId(coll, 'style-sku-1')).toBe(true);
|
|
177
|
-
});
|
|
178
|
-
it('returns true when id is in fontStyleIds', () => {
|
|
179
|
-
const coll = {
|
|
180
|
-
fontStyleSkuIds: [],
|
|
181
|
-
fontStyleIds: ['style-id-1'],
|
|
182
|
-
childrenSkuIds: [],
|
|
183
|
-
name: 'Test'
|
|
184
|
-
};
|
|
185
|
-
expect(collContainsSkuId(coll, 'style-id-1')).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
it('returns false for unknown skuId', () => {
|
|
188
|
-
const coll = {
|
|
189
|
-
fontStyleSkuIds: ['style-sku-1'],
|
|
190
|
-
fontStyleIds: ['style-1'],
|
|
191
|
-
childrenSkuIds: ['child-sku-1'],
|
|
192
|
-
name: 'Test'
|
|
193
|
-
};
|
|
194
|
-
expect(collContainsSkuId(coll, 'unknown-sku')).toBe(false);
|
|
195
|
-
});
|
|
196
|
-
it('returns false for undefined collection', () => {
|
|
197
|
-
expect(collContainsSkuId(undefined, 'any-sku')).toBe(false);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
64
|
+
const THUNDER = {
|
|
65
|
+
'super-sku': [750, ['f1', 'f2', 'df1', 'df2', 'cf1', 'cf2', 'vf1', 'vif1', 'dvf1', 'dvif1', 'cvf1', 'cvif1']],
|
|
66
|
+
'text-sku': [500, ['f1', 'f2']],
|
|
67
|
+
'display-sku': [500, ['df1', 'df2']],
|
|
68
|
+
'caption-sku': [500, ['cf1', 'cf2']],
|
|
69
|
+
'text-var-sku': [300, ['vf1']],
|
|
70
|
+
'text-var-italic-sku': [300, ['vif1']],
|
|
71
|
+
'display-var-sku': [300, ['dvf1']],
|
|
72
|
+
'display-var-italic-sku': [300, ['dvif1']],
|
|
73
|
+
'caption-var-sku': [300, ['cvf1']],
|
|
74
|
+
'caption-var-italic-sku': [300, ['cvif1']],
|
|
75
|
+
'text-bundle-sku': [500, ['f1', 'f2', 'vf1', 'vif1']],
|
|
76
|
+
'display-bundle-sku': [500, ['df1', 'df2', 'dvf1', 'dvif1']],
|
|
77
|
+
'caption-bundle-sku': [500, ['cf1', 'cf2', 'cvf1', 'cvif1']],
|
|
78
|
+
s1: [40, ['f1']],
|
|
79
|
+
s2: [40, ['f2']],
|
|
80
|
+
ds1: [40, ['df1']],
|
|
81
|
+
ds2: [40, ['df2']],
|
|
82
|
+
cs1: [40, ['cf1']],
|
|
83
|
+
cs2: [40, ['cf2']],
|
|
84
|
+
vs1: [40, ['vf1']],
|
|
85
|
+
vis1: [40, ['vif1']],
|
|
86
|
+
dvs1: [40, ['dvf1']],
|
|
87
|
+
dvis1: [40, ['dvif1']],
|
|
88
|
+
cvs1: [40, ['cvf1']],
|
|
89
|
+
cvis1: [40, ['cvif1']]
|
|
90
|
+
};
|
|
91
|
+
const makeThunderState = function () {
|
|
92
|
+
let prices = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
93
|
+
return makeState(Object.fromEntries(Object.entries(THUNDER).map(_ref => {
|
|
94
|
+
let [skuId, [price, styleIds]] = _ref;
|
|
95
|
+
return [skuId, [prices[skuId] ?? price, styleIds]];
|
|
96
|
+
})));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// A family of five uprights and five italics, sold whole, as upright and
|
|
100
|
+
// italic packages, or by the style.
|
|
101
|
+
const FAMILY_WITH_PACKAGES = {
|
|
102
|
+
'family-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
103
|
+
'pkg-upright-sku': [250, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
104
|
+
'pkg-italic-sku': [250, ['fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
105
|
+
s1: [100, ['f1']],
|
|
106
|
+
s2: [100, ['f2']],
|
|
107
|
+
s3: [100, ['f3']],
|
|
108
|
+
s4: [100, ['f4']],
|
|
109
|
+
s5: [100, ['f5']],
|
|
110
|
+
si1: [100, ['fi1']],
|
|
111
|
+
si2: [100, ['fi2']],
|
|
112
|
+
si3: [100, ['fi3']],
|
|
113
|
+
si4: [100, ['fi4']],
|
|
114
|
+
si5: [100, ['fi5']]
|
|
115
|
+
};
|
|
200
116
|
|
|
201
117
|
// =========================================================================
|
|
202
118
|
// isSelected
|
|
203
119
|
// =========================================================================
|
|
204
120
|
describe('isSelected', () => {
|
|
205
121
|
it('returns true when the SKU is directly selected', () => {
|
|
206
|
-
const state = makeState({
|
|
122
|
+
const state = makeState({}, {
|
|
207
123
|
selectedSkuIds: {
|
|
208
124
|
'sku-1': true
|
|
209
125
|
}
|
|
210
126
|
});
|
|
211
127
|
expect(isSelected('sku-1')(state)).toBe(true);
|
|
212
128
|
});
|
|
213
|
-
it('returns false when the SKU is
|
|
214
|
-
|
|
215
|
-
selectedSkuIds: {
|
|
216
|
-
'sku-1': false
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
expect(isSelected('sku-1')(state)).toBe(false);
|
|
129
|
+
it('returns false when the SKU is not selected', () => {
|
|
130
|
+
expect(isSelected('sku-1')(makeState())).toBe(false);
|
|
220
131
|
});
|
|
221
|
-
it('returns true when
|
|
132
|
+
it('returns true when every style it grants is granted by the selection', () => {
|
|
222
133
|
const state = makeState({
|
|
134
|
+
'bundle-sku': [300, ['f1', 'f2']],
|
|
135
|
+
'family-sku': [500, ['f1']],
|
|
136
|
+
s1: [100, ['f1']]
|
|
137
|
+
}, {
|
|
223
138
|
selectedSkuIds: {
|
|
224
139
|
'bundle-sku': true
|
|
225
|
-
},
|
|
226
|
-
collectionStyleSkus: {
|
|
227
|
-
'bundle-sku': {
|
|
228
|
-
fontStyleSkuIds: ['s1'],
|
|
229
|
-
fontStyleIds: ['f1'],
|
|
230
|
-
childrenSkuIds: ['family-sku'],
|
|
231
|
-
name: 'Bundle'
|
|
232
|
-
}
|
|
233
140
|
}
|
|
234
141
|
});
|
|
235
|
-
// Style SKU contained via fontStyleSkuIds
|
|
236
142
|
expect(isSelected('s1')(state)).toBe(true);
|
|
237
|
-
// Family SKU contained via childrenSkuIds
|
|
238
143
|
expect(isSelected('family-sku')(state)).toBe(true);
|
|
239
|
-
//
|
|
144
|
+
// A style with no SKU of its own, by its style id
|
|
240
145
|
expect(isSelected('f1')(state)).toBe(true);
|
|
241
146
|
});
|
|
242
|
-
it('returns false when
|
|
147
|
+
it('returns false when only some of its styles are selected', () => {
|
|
243
148
|
const state = makeState({
|
|
149
|
+
'family-sku': [500, ['f1', 'f2']],
|
|
150
|
+
s1: [100, ['f1']]
|
|
151
|
+
}, {
|
|
244
152
|
selectedSkuIds: {
|
|
245
|
-
|
|
246
|
-
's1': false
|
|
247
|
-
},
|
|
248
|
-
collectionStyleSkus: {
|
|
249
|
-
'bundle-sku': {
|
|
250
|
-
fontStyleSkuIds: ['s1'],
|
|
251
|
-
fontStyleIds: ['f1'],
|
|
252
|
-
childrenSkuIds: ['family-sku'],
|
|
253
|
-
name: 'Bundle'
|
|
254
|
-
}
|
|
153
|
+
s1: true
|
|
255
154
|
}
|
|
256
155
|
});
|
|
257
|
-
expect(isSelected('s1')(state)).toBe(false);
|
|
258
156
|
expect(isSelected('family-sku')(state)).toBe(false);
|
|
259
157
|
});
|
|
260
158
|
it('returns false for null skuId', () => {
|
|
261
|
-
|
|
262
|
-
expect(isSelected(null)(state)).toBe(false);
|
|
263
|
-
});
|
|
264
|
-
it('returns true for member when bundle is selected, even if member is explicitly false', () => {
|
|
265
|
-
// After auto-selection, members are set to false but should still appear
|
|
266
|
-
// selected because the parent bundle is true
|
|
267
|
-
const state = makeState({
|
|
268
|
-
selectedSkuIds: {
|
|
269
|
-
'bundle-sku': true,
|
|
270
|
-
'family-sku': false,
|
|
271
|
-
'var-sku': false
|
|
272
|
-
},
|
|
273
|
-
collectionStyleSkus: {
|
|
274
|
-
'bundle-sku': {
|
|
275
|
-
fontStyleSkuIds: ['s1', 'vs1'],
|
|
276
|
-
fontStyleIds: ['f1', 'vf1'],
|
|
277
|
-
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
278
|
-
name: 'Bundle'
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
expect(isSelected('family-sku')(state)).toBe(true);
|
|
283
|
-
expect(isSelected('var-sku')(state)).toBe(true);
|
|
284
|
-
expect(isSelected('s1')(state)).toBe(true);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// =========================================================================
|
|
289
|
-
// collectionSkuIdsDifferences
|
|
290
|
-
// =========================================================================
|
|
291
|
-
describe('collectionSkuIdsDifferences', () => {
|
|
292
|
-
it('includes self when skuId matches a collection', () => {
|
|
293
|
-
const state = makeState({
|
|
294
|
-
collectionStyleSkus: {
|
|
295
|
-
'family-sku': {
|
|
296
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
297
|
-
fontStyleIds: ['f1', 'f2'],
|
|
298
|
-
childrenSkuIds: [],
|
|
299
|
-
name: 'Family'
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
skuPrices: {
|
|
303
|
-
'family-sku': 500,
|
|
304
|
-
s1: 40,
|
|
305
|
-
s2: 40
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
const diffs = collectionSkuIdsDifferences(state, 'family-sku');
|
|
309
|
-
expect(diffs).toHaveProperty('family-sku');
|
|
310
|
-
expect(diffs['family-sku']).toBe(500);
|
|
311
|
-
});
|
|
312
|
-
it('includes parent collections that contain the skuId', () => {
|
|
313
|
-
const state = makeState({
|
|
314
|
-
collectionStyleSkus: {
|
|
315
|
-
'family-sku': {
|
|
316
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
317
|
-
fontStyleIds: ['f1', 'f2'],
|
|
318
|
-
childrenSkuIds: [],
|
|
319
|
-
name: 'Family'
|
|
320
|
-
},
|
|
321
|
-
'bundle-sku': {
|
|
322
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1'],
|
|
323
|
-
fontStyleIds: ['f1', 'f2', 'vf1'],
|
|
324
|
-
childrenSkuIds: ['family-sku', 'var-family-sku'],
|
|
325
|
-
name: 'Bundle'
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
skuPrices: {
|
|
329
|
-
'family-sku': 500,
|
|
330
|
-
'bundle-sku': 500,
|
|
331
|
-
'var-family-sku': 300,
|
|
332
|
-
s1: 40,
|
|
333
|
-
s2: 40,
|
|
334
|
-
vs1: 40
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
const diffs = collectionSkuIdsDifferences(state, 'family-sku');
|
|
338
|
-
expect(diffs).toHaveProperty('family-sku');
|
|
339
|
-
expect(diffs).toHaveProperty('bundle-sku');
|
|
340
|
-
});
|
|
341
|
-
it('subtracts already-selected items from the difference', () => {
|
|
342
|
-
const state = makeState({
|
|
343
|
-
selectedSkuIds: {
|
|
344
|
-
'family-sku': true
|
|
345
|
-
},
|
|
346
|
-
collectionStyleSkus: {
|
|
347
|
-
'family-sku': {
|
|
348
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
349
|
-
fontStyleIds: ['f1', 'f2'],
|
|
350
|
-
childrenSkuIds: [],
|
|
351
|
-
name: 'Family'
|
|
352
|
-
},
|
|
353
|
-
'bundle-sku': {
|
|
354
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1'],
|
|
355
|
-
fontStyleIds: ['f1', 'f2', 'vf1'],
|
|
356
|
-
childrenSkuIds: ['family-sku', 'var-family-sku'],
|
|
357
|
-
name: 'Bundle'
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
skuPrices: {
|
|
361
|
-
'family-sku': 500,
|
|
362
|
-
'bundle-sku': 500,
|
|
363
|
-
'var-family-sku': 300
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
const diffs = collectionSkuIdsDifferences(state, 'var-family-sku');
|
|
367
|
-
expect(diffs['bundle-sku']).toBe(0);
|
|
368
|
-
});
|
|
369
|
-
it('treats missing prices as 0 rather than producing NaN', () => {
|
|
370
|
-
const state = makeState({
|
|
371
|
-
selectedSkuIds: {
|
|
372
|
-
'orphan-sku': true
|
|
373
|
-
},
|
|
374
|
-
collectionStyleSkus: {
|
|
375
|
-
'family-sku': {
|
|
376
|
-
fontStyleSkuIds: ['orphan-sku'],
|
|
377
|
-
fontStyleIds: [],
|
|
378
|
-
childrenSkuIds: [],
|
|
379
|
-
name: 'Family'
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
skuPrices: {
|
|
383
|
-
'family-sku': 500
|
|
384
|
-
// 'orphan-sku' deliberately missing
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
const diffs = collectionSkuIdsDifferences(state, 'orphan-sku');
|
|
388
|
-
// Missing price treated as 0, so difference = 500 - 0 = 500
|
|
389
|
-
expect(diffs['family-sku']).toBe(500);
|
|
390
|
-
});
|
|
391
|
-
it('only counts items that are selected true, not false', () => {
|
|
392
|
-
const state = makeState({
|
|
393
|
-
selectedSkuIds: {
|
|
394
|
-
'family-sku': false
|
|
395
|
-
},
|
|
396
|
-
collectionStyleSkus: {
|
|
397
|
-
'bundle-sku': {
|
|
398
|
-
fontStyleSkuIds: [],
|
|
399
|
-
fontStyleIds: [],
|
|
400
|
-
childrenSkuIds: ['family-sku'],
|
|
401
|
-
name: 'Bundle'
|
|
402
|
-
},
|
|
403
|
-
'family-sku': {
|
|
404
|
-
fontStyleSkuIds: [],
|
|
405
|
-
fontStyleIds: [],
|
|
406
|
-
childrenSkuIds: [],
|
|
407
|
-
name: 'Family'
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
skuPrices: {
|
|
411
|
-
'bundle-sku': 500,
|
|
412
|
-
'family-sku': 300
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
const diffs = collectionSkuIdsDifferences(state, 'family-sku');
|
|
416
|
-
// family-sku is false, not true, so it shouldn't be subtracted
|
|
417
|
-
expect(diffs['bundle-sku']).toBe(500);
|
|
418
|
-
});
|
|
419
|
-
it('returns empty object when no collections contain the skuId', () => {
|
|
420
|
-
const state = makeState({
|
|
421
|
-
collectionStyleSkus: {
|
|
422
|
-
'other-sku': {
|
|
423
|
-
fontStyleSkuIds: ['x1'],
|
|
424
|
-
fontStyleIds: ['xf1'],
|
|
425
|
-
childrenSkuIds: [],
|
|
426
|
-
name: 'Other'
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
skuPrices: {
|
|
430
|
-
'other-sku': 100,
|
|
431
|
-
'lonely-style': 40
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
const diffs = collectionSkuIdsDifferences(state, 'lonely-style');
|
|
435
|
-
expect(Object.keys(diffs)).toHaveLength(0);
|
|
159
|
+
expect(isSelected(null)(makeState())).toBe(false);
|
|
436
160
|
});
|
|
437
161
|
});
|
|
438
162
|
|
|
439
163
|
// =========================================================================
|
|
440
|
-
//
|
|
164
|
+
// additionalCost – the “+$X” next to what isn't selected
|
|
441
165
|
// =========================================================================
|
|
442
|
-
describe('
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
skuPrices: {
|
|
454
|
-
s1: 40,
|
|
455
|
-
'family-sku': 500
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 's1');
|
|
459
|
-
expect(collId).toBe(null);
|
|
460
|
-
expect(amount).toBe(null);
|
|
461
|
-
});
|
|
462
|
-
it('returns parent collection when it is cheaper for individual style', () => {
|
|
463
|
-
const state = makeState({
|
|
464
|
-
selectedSkuIds: {
|
|
465
|
-
s1: true,
|
|
466
|
-
s2: true,
|
|
467
|
-
s3: true,
|
|
468
|
-
s4: true
|
|
469
|
-
},
|
|
470
|
-
collectionStyleSkus: {
|
|
471
|
-
'family-sku': {
|
|
472
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
473
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
474
|
-
childrenSkuIds: [],
|
|
475
|
-
name: 'Family'
|
|
476
|
-
}
|
|
477
|
-
},
|
|
478
|
-
skuPrices: {
|
|
479
|
-
'family-sku': 200,
|
|
480
|
-
s1: 40,
|
|
481
|
-
s2: 40,
|
|
482
|
-
s3: 40,
|
|
483
|
-
s4: 40,
|
|
484
|
-
s5: 40
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 's5');
|
|
488
|
-
expect(collId).toBe('family-sku');
|
|
489
|
-
expect(amount).toBe(40);
|
|
490
|
-
});
|
|
491
|
-
it('prefers collection bundle over self when prices are equal', () => {
|
|
492
|
-
const state = makeState({
|
|
493
|
-
collectionStyleSkus: {
|
|
494
|
-
'family-sku': {
|
|
495
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
496
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
497
|
-
childrenSkuIds: [],
|
|
498
|
-
name: 'Thunder Text'
|
|
499
|
-
},
|
|
500
|
-
'var-family-sku': {
|
|
501
|
-
fontStyleSkuIds: ['vs1'],
|
|
502
|
-
fontStyleIds: ['vf1'],
|
|
503
|
-
childrenSkuIds: [],
|
|
504
|
-
name: 'Thunder Text Variable'
|
|
505
|
-
},
|
|
506
|
-
'var-italic-sku': {
|
|
507
|
-
fontStyleSkuIds: ['vis1'],
|
|
508
|
-
fontStyleIds: ['vif1'],
|
|
509
|
-
childrenSkuIds: [],
|
|
510
|
-
name: 'Thunder Text Italic Variable'
|
|
511
|
-
},
|
|
512
|
-
'bundle-sku': {
|
|
513
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'vs1', 'vis1'],
|
|
514
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'vf1', 'vif1'],
|
|
515
|
-
childrenSkuIds: ['family-sku', 'var-family-sku', 'var-italic-sku'],
|
|
516
|
-
name: 'Thunder Text Family + Variable'
|
|
517
|
-
}
|
|
518
|
-
},
|
|
519
|
-
skuPrices: {
|
|
520
|
-
'family-sku': 500,
|
|
521
|
-
'bundle-sku': 500,
|
|
522
|
-
'var-family-sku': 300,
|
|
523
|
-
'var-italic-sku': 300,
|
|
524
|
-
s1: 40,
|
|
525
|
-
s2: 40,
|
|
526
|
-
s3: 40,
|
|
527
|
-
s4: 40,
|
|
528
|
-
s5: 40,
|
|
529
|
-
vs1: 40,
|
|
530
|
-
vis1: 40
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 'family-sku');
|
|
534
|
-
expect(collId).toBe('bundle-sku');
|
|
535
|
-
expect(amount).toBe(500);
|
|
536
|
-
});
|
|
537
|
-
it('prefers collection bundle regardless of object key order', () => {
|
|
538
|
-
const state = makeState({
|
|
539
|
-
collectionStyleSkus: {
|
|
540
|
-
'bundle-sku': {
|
|
541
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1'],
|
|
542
|
-
fontStyleIds: ['f1', 'f2', 'vf1'],
|
|
543
|
-
childrenSkuIds: ['family-sku', 'var-family-sku'],
|
|
544
|
-
name: 'Bundle'
|
|
545
|
-
},
|
|
546
|
-
'family-sku': {
|
|
547
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
548
|
-
fontStyleIds: ['f1', 'f2'],
|
|
549
|
-
childrenSkuIds: [],
|
|
550
|
-
name: 'Family'
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
|
-
skuPrices: {
|
|
554
|
-
'family-sku': 500,
|
|
555
|
-
'bundle-sku': 500,
|
|
556
|
-
'var-family-sku': 300
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
const [collId] = collectionSkuIdWithDiscount(state, 'family-sku');
|
|
560
|
-
expect(collId).toBe('bundle-sku');
|
|
561
|
-
});
|
|
562
|
-
it('still returns self for display pricing (superfamily with no parent)', () => {
|
|
563
|
-
const state = makeState({
|
|
564
|
-
selectedSkuIds: {
|
|
565
|
-
'family-sku': true
|
|
566
|
-
},
|
|
567
|
-
collectionStyleSkus: {
|
|
568
|
-
'superfamily-sku': {
|
|
569
|
-
fontStyleSkuIds: ['s1', 's2', 'ds1', 'ds2'],
|
|
570
|
-
fontStyleIds: ['f1', 'f2', 'df1', 'df2'],
|
|
571
|
-
childrenSkuIds: ['family-sku', 'display-family-sku'],
|
|
572
|
-
name: 'Superfamily'
|
|
573
|
-
},
|
|
574
|
-
'family-sku': {
|
|
575
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
576
|
-
fontStyleIds: ['f1', 'f2'],
|
|
577
|
-
childrenSkuIds: [],
|
|
578
|
-
name: 'Family'
|
|
579
|
-
},
|
|
580
|
-
'display-family-sku': {
|
|
581
|
-
fontStyleSkuIds: ['ds1', 'ds2'],
|
|
582
|
-
fontStyleIds: ['df1', 'df2'],
|
|
583
|
-
childrenSkuIds: [],
|
|
584
|
-
name: 'Display'
|
|
585
|
-
}
|
|
586
|
-
},
|
|
587
|
-
skuPrices: {
|
|
588
|
-
'superfamily-sku': 750,
|
|
589
|
-
'family-sku': 500,
|
|
590
|
-
'display-family-sku': 500
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 'superfamily-sku');
|
|
594
|
-
expect(collId).toBe('superfamily-sku');
|
|
595
|
-
expect(amount).toBe(250);
|
|
596
|
-
});
|
|
597
|
-
it('prefers bundle at $0 when family is already selected', () => {
|
|
598
|
-
const state = makeState({
|
|
599
|
-
selectedSkuIds: {
|
|
600
|
-
'family-sku': true
|
|
601
|
-
},
|
|
602
|
-
collectionStyleSkus: {
|
|
603
|
-
'family-sku': {
|
|
604
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
605
|
-
fontStyleIds: ['f1', 'f2'],
|
|
606
|
-
childrenSkuIds: [],
|
|
607
|
-
name: 'Family'
|
|
608
|
-
},
|
|
609
|
-
'var-family-sku': {
|
|
610
|
-
fontStyleSkuIds: ['vs1'],
|
|
611
|
-
fontStyleIds: ['vf1'],
|
|
612
|
-
childrenSkuIds: [],
|
|
613
|
-
name: 'Variable'
|
|
614
|
-
},
|
|
615
|
-
'bundle-sku': {
|
|
616
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1'],
|
|
617
|
-
fontStyleIds: ['f1', 'f2', 'vf1'],
|
|
618
|
-
childrenSkuIds: ['family-sku', 'var-family-sku'],
|
|
619
|
-
name: 'Bundle'
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
skuPrices: {
|
|
623
|
-
'family-sku': 500,
|
|
624
|
-
'var-family-sku': 300,
|
|
625
|
-
'bundle-sku': 500
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 'var-family-sku');
|
|
629
|
-
expect(collId).toBe('bundle-sku');
|
|
630
|
-
expect(amount).toBe(0);
|
|
166
|
+
describe('additionalCost', () => {
|
|
167
|
+
const FIVE_STYLES = {
|
|
168
|
+
'family-sku': [150, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
169
|
+
s1: [40, ['f1']],
|
|
170
|
+
s2: [40, ['f2']],
|
|
171
|
+
s3: [40, ['f3']],
|
|
172
|
+
s4: [40, ['f4']],
|
|
173
|
+
s5: [40, ['f5']]
|
|
174
|
+
};
|
|
175
|
+
it('is the SKU’s own price when nothing cheaper holds it', () => {
|
|
176
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 's1'), 's2')).toBe(40);
|
|
631
177
|
});
|
|
632
|
-
it('
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
'family-sku': {
|
|
636
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
637
|
-
fontStyleIds: ['f1', 'f2'],
|
|
638
|
-
childrenSkuIds: [],
|
|
639
|
-
name: 'Family'
|
|
640
|
-
},
|
|
641
|
-
'bundle-sku': {
|
|
642
|
-
fontStyleSkuIds: ['s1', 's2', 'vs1'],
|
|
643
|
-
fontStyleIds: ['f1', 'f2', 'vf1'],
|
|
644
|
-
childrenSkuIds: ['family-sku', 'var-family-sku'],
|
|
645
|
-
name: 'Bundle'
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
skuPrices: {
|
|
649
|
-
'family-sku': 500,
|
|
650
|
-
'bundle-sku': 600
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
const [collId] = collectionSkuIdWithDiscount(state, 'family-sku');
|
|
654
|
-
expect(collId).toBe('family-sku');
|
|
178
|
+
it('is what completing a cheaper bundle adds', () => {
|
|
179
|
+
// Three styles selected (120); the family (150) holds a fourth for 30 more
|
|
180
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 's1', 's2', 's3'), 's4')).toBe(30);
|
|
655
181
|
});
|
|
656
|
-
it('
|
|
657
|
-
|
|
658
|
-
// `difference <= undefined` is always false → no collection found
|
|
659
|
-
const state = makeState({
|
|
660
|
-
collectionStyleSkus: {
|
|
661
|
-
'family-sku': {
|
|
662
|
-
fontStyleSkuIds: ['s1'],
|
|
663
|
-
fontStyleIds: ['f1'],
|
|
664
|
-
childrenSkuIds: [],
|
|
665
|
-
name: 'Family'
|
|
666
|
-
}
|
|
667
|
-
},
|
|
668
|
-
skuPrices: {
|
|
669
|
-
'family-sku': 500
|
|
670
|
-
} // s1 deliberately missing
|
|
671
|
-
});
|
|
672
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 's1');
|
|
673
|
-
expect(collId).toBe(null);
|
|
674
|
-
expect(amount).toBe(null);
|
|
182
|
+
it('is zero for what the selection already holds', () => {
|
|
183
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 'family-sku'), 's1')).toBe(0);
|
|
675
184
|
});
|
|
676
|
-
it('
|
|
677
|
-
// A collection with no price entry is treated as $0, meaning its
|
|
678
|
-
// difference = 0 - sum_selected. With nothing selected, diff = 0.
|
|
185
|
+
it('is null when the SKU’s price or styles are unknown', () => {
|
|
679
186
|
const state = makeState({
|
|
680
|
-
|
|
681
|
-
'ghost-bundle': {
|
|
682
|
-
fontStyleSkuIds: ['s1'],
|
|
683
|
-
fontStyleIds: ['f1'],
|
|
684
|
-
childrenSkuIds: ['family-sku'],
|
|
685
|
-
name: 'Ghost Bundle'
|
|
686
|
-
},
|
|
687
|
-
'family-sku': {
|
|
688
|
-
fontStyleSkuIds: ['s1'],
|
|
689
|
-
fontStyleIds: ['f1'],
|
|
690
|
-
childrenSkuIds: [],
|
|
691
|
-
name: 'Family'
|
|
692
|
-
}
|
|
693
|
-
},
|
|
694
|
-
skuPrices: {
|
|
695
|
-
'family-sku': 500,
|
|
696
|
-
s1: 40
|
|
697
|
-
}
|
|
698
|
-
// 'ghost-bundle' price deliberately missing → treated as 0
|
|
187
|
+
'unpriced-sku': [undefined, ['f1']]
|
|
699
188
|
});
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// 0 <= 500 ✓ → ghost-bundle wins
|
|
703
|
-
expect(collId).toBe('ghost-bundle');
|
|
704
|
-
expect(amount).toBe(0);
|
|
189
|
+
expect(additionalCost(state, 'unpriced-sku')).toBeNull();
|
|
190
|
+
expect(additionalCost(state, 'unknown-sku')).toBeNull();
|
|
705
191
|
});
|
|
706
|
-
it('handles zero-price SKUs
|
|
192
|
+
it('handles zero-price SKUs', () => {
|
|
707
193
|
const state = makeState({
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
fontStyleSkuIds: ['fs1'],
|
|
711
|
-
fontStyleIds: ['ff1'],
|
|
712
|
-
childrenSkuIds: [],
|
|
713
|
-
name: 'Free Family'
|
|
714
|
-
},
|
|
715
|
-
'free-bundle': {
|
|
716
|
-
fontStyleSkuIds: ['fs1', 'fvs1'],
|
|
717
|
-
fontStyleIds: ['ff1', 'fvf1'],
|
|
718
|
-
childrenSkuIds: ['free-family', 'free-var'],
|
|
719
|
-
name: 'Free Bundle'
|
|
720
|
-
}
|
|
721
|
-
},
|
|
722
|
-
skuPrices: {
|
|
723
|
-
'free-family': 0,
|
|
724
|
-
'free-bundle': 0,
|
|
725
|
-
'free-var': 0,
|
|
726
|
-
fs1: 0,
|
|
727
|
-
fvs1: 0
|
|
728
|
-
}
|
|
194
|
+
'free-sku': [0, ['f1']],
|
|
195
|
+
s2: [40, ['f2']]
|
|
729
196
|
});
|
|
730
|
-
|
|
731
|
-
// Both have diff = 0, filter: 0 <= 0 ✓, tie-break prefers bundle
|
|
732
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 'free-family');
|
|
733
|
-
expect(collId).toBe('free-bundle');
|
|
734
|
-
expect(amount).toBe(0);
|
|
735
|
-
});
|
|
736
|
-
it('handles negative difference (collection cheaper than sum of selected)', () => {
|
|
737
|
-
// Unusual but possible: if prices change or a collection is deeply discounted
|
|
738
|
-
const state = makeState({
|
|
739
|
-
selectedSkuIds: {
|
|
740
|
-
s1: true,
|
|
741
|
-
s2: true,
|
|
742
|
-
s3: true
|
|
743
|
-
},
|
|
744
|
-
collectionStyleSkus: {
|
|
745
|
-
'family-sku': {
|
|
746
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4'],
|
|
747
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4'],
|
|
748
|
-
childrenSkuIds: [],
|
|
749
|
-
name: 'Family'
|
|
750
|
-
}
|
|
751
|
-
},
|
|
752
|
-
skuPrices: {
|
|
753
|
-
'family-sku': 100,
|
|
754
|
-
s1: 40,
|
|
755
|
-
s2: 40,
|
|
756
|
-
s3: 40,
|
|
757
|
-
s4: 40
|
|
758
|
-
}
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
// Family costs $100, 3 styles selected = $120, diff = $100 - $120 = -$20
|
|
762
|
-
// This means the collection is $20 cheaper than what's already selected
|
|
763
|
-
const [collId, amount] = collectionSkuIdWithDiscount(state, 's4');
|
|
764
|
-
expect(collId).toBe('family-sku');
|
|
765
|
-
expect(amount).toBe(-20);
|
|
197
|
+
expect(additionalCost(select(state, 's2'), 'free-sku')).toBe(0);
|
|
766
198
|
});
|
|
767
199
|
});
|
|
768
200
|
|
|
@@ -771,46 +203,25 @@ describe('collectionSkuIdWithDiscount', () => {
|
|
|
771
203
|
// =========================================================================
|
|
772
204
|
describe('reducer SELECT_SKU_ID with collection bundles', () => {
|
|
773
205
|
it('auto-selects collection bundle when selecting a family at the same price', () => {
|
|
774
|
-
|
|
775
|
-
const newState = reducer(state, {
|
|
776
|
-
type: 'SELECT_SKU_ID',
|
|
777
|
-
skuId: 'text-sku',
|
|
778
|
-
selected: true
|
|
779
|
-
});
|
|
780
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
|
|
781
|
-
expect(newState.selectedSkuIds['text-sku']).toBe(false);
|
|
782
|
-
expect(newState.selectedSkuIds['text-var-sku']).toBe(false);
|
|
783
|
-
expect(newState.selectedSkuIds['text-var-italic-sku']).toBe(false);
|
|
206
|
+
expect(selected(select(makeThunderState(), 'text-sku'))).toEqual(['text-bundle-sku']);
|
|
784
207
|
});
|
|
785
208
|
it('does not auto-select bundle when bundle is more expensive', () => {
|
|
786
209
|
const state = makeThunderState({
|
|
787
|
-
|
|
788
|
-
...makeThunderState().skuPrices,
|
|
789
|
-
'text-bundle-sku': 600 // more expensive than text-sku ($500)
|
|
790
|
-
}
|
|
210
|
+
'text-bundle-sku': 600
|
|
791
211
|
});
|
|
792
|
-
|
|
793
|
-
type: 'SELECT_SKU_ID',
|
|
794
|
-
skuId: 'text-sku',
|
|
795
|
-
selected: true
|
|
796
|
-
});
|
|
797
|
-
expect(newState.selectedSkuIds['text-sku']).toBe(true);
|
|
798
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBeUndefined();
|
|
212
|
+
expect(selected(select(state, 'text-sku'))).toEqual(['text-sku']);
|
|
799
213
|
});
|
|
800
214
|
it('auto-selects bundle when clicking variable family with static already selected', () => {
|
|
215
|
+
expect(selected(select(makeThunderState(), 'text-sku', 'text-var-sku'))).toEqual(['text-bundle-sku']);
|
|
216
|
+
});
|
|
217
|
+
it('never splits a pick into cheaper parts', () => {
|
|
218
|
+
// The styles cost 80 together, but a family bought is not the same
|
|
219
|
+
// purchase as today's styles bought one by one
|
|
801
220
|
const state = makeThunderState({
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
221
|
+
'text-bundle-sku': 600,
|
|
222
|
+
'super-sku': 2000
|
|
805
223
|
});
|
|
806
|
-
|
|
807
|
-
type: 'SELECT_SKU_ID',
|
|
808
|
-
skuId: 'text-var-sku',
|
|
809
|
-
selected: true
|
|
810
|
-
});
|
|
811
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
|
|
812
|
-
expect(newState.selectedSkuIds['text-sku']).toBe(false);
|
|
813
|
-
expect(newState.selectedSkuIds['text-var-sku']).toBe(false);
|
|
224
|
+
expect(selected(select(state, 'text-sku'))).toEqual(['text-sku']);
|
|
814
225
|
});
|
|
815
226
|
});
|
|
816
227
|
|
|
@@ -819,41 +230,45 @@ describe('reducer SELECT_SKU_ID with collection bundles', () => {
|
|
|
819
230
|
// =========================================================================
|
|
820
231
|
describe('reducer SELECT_SKU_ID with multiple bundles', () => {
|
|
821
232
|
it('only selects the correct bundle for the family being clicked', () => {
|
|
822
|
-
|
|
823
|
-
const newState = reducer(state, {
|
|
824
|
-
type: 'SELECT_SKU_ID',
|
|
825
|
-
skuId: 'display-sku',
|
|
826
|
-
selected: true
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
// Display bundle should be selected
|
|
830
|
-
expect(newState.selectedSkuIds['display-bundle-sku']).toBe(true);
|
|
831
|
-
expect(newState.selectedSkuIds['display-sku']).toBe(false);
|
|
832
|
-
// Text and caption bundles should be untouched
|
|
833
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBeUndefined();
|
|
834
|
-
expect(newState.selectedSkuIds['caption-bundle-sku']).toBeUndefined();
|
|
233
|
+
expect(selected(select(makeThunderState(), 'display-sku'))).toEqual(['display-bundle-sku']);
|
|
835
234
|
});
|
|
836
235
|
it('upgrades to superfamily when selecting a second family bundle', () => {
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
type: 'SELECT_SKU_ID',
|
|
842
|
-
skuId: 'text-sku',
|
|
843
|
-
selected: true
|
|
844
|
-
});
|
|
845
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(true);
|
|
846
|
-
newState = reducer(newState, {
|
|
847
|
-
type: 'SELECT_SKU_ID',
|
|
848
|
-
skuId: 'display-sku',
|
|
849
|
-
selected: true
|
|
850
|
-
});
|
|
236
|
+
// Two families ($500 + $500) cost more than the superfamily ($750)
|
|
237
|
+
expect(selected(select(makeThunderState(), 'text-sku', 'display-sku'))).toEqual(['super-sku']);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
851
240
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
241
|
+
// =========================================================================
|
|
242
|
+
// reducer SELECT_SKU_ID — order independence (FD-1238)
|
|
243
|
+
// =========================================================================
|
|
244
|
+
describe('reducer selection does not depend on click order', () => {
|
|
245
|
+
// Overlapping style bundles: every pair of the three uprights, each upright
|
|
246
|
+
// with its italic, and the three uprights together
|
|
247
|
+
const OVERLAPPING = {
|
|
248
|
+
light: [90, ['light']],
|
|
249
|
+
regular: [90, ['regular']],
|
|
250
|
+
bold: [90, ['bold']],
|
|
251
|
+
italic: [90, ['italic']],
|
|
252
|
+
'bold-italic': [90, ['bold-italic']],
|
|
253
|
+
'light+regular': [153, ['light', 'regular']],
|
|
254
|
+
'light+bold': [153, ['light', 'bold']],
|
|
255
|
+
'regular+bold': [153, ['regular', 'bold']],
|
|
256
|
+
'regular+italic': [144, ['regular', 'italic']],
|
|
257
|
+
'bold+bold-italic': [144, ['bold', 'bold-italic']],
|
|
258
|
+
'light+regular+bold': [202.5, ['light', 'regular', 'bold']]
|
|
259
|
+
};
|
|
260
|
+
it('reaches a triple that every pair inside it would otherwise snap to first', () => {
|
|
261
|
+
const state = makeState(OVERLAPPING);
|
|
262
|
+
expect(selected(select(state, 'bold', 'regular', 'light'))).toEqual(['light+regular+bold']);
|
|
263
|
+
expect(selected(select(state, 'light', 'regular', 'bold'))).toEqual(['light+regular+bold']);
|
|
264
|
+
});
|
|
265
|
+
it('re-partitions a pair when a cheaper split appears', () => {
|
|
266
|
+
const state = makeState(OVERLAPPING);
|
|
267
|
+
expect(selected(select(state, 'bold', 'regular', 'bold-italic', 'italic'))).toEqual(['bold+bold-italic', 'regular+italic']);
|
|
268
|
+
});
|
|
269
|
+
it('deselecting a style keeps the rest of its bundle', () => {
|
|
270
|
+
const state = select(makeState(OVERLAPPING), 'bold', 'regular', 'light');
|
|
271
|
+
expect(selected(deselect(state, 'regular'))).toEqual(['light+bold']);
|
|
857
272
|
});
|
|
858
273
|
});
|
|
859
274
|
|
|
@@ -862,161 +277,61 @@ describe('reducer SELECT_SKU_ID with multiple bundles', () => {
|
|
|
862
277
|
// =========================================================================
|
|
863
278
|
describe('reducer deselection', () => {
|
|
864
279
|
it('deselecting a bundle member deselects the entire bundle', () => {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
selectedSkuIds: {
|
|
868
|
-
'text-bundle-sku': true,
|
|
869
|
-
'text-sku': false,
|
|
870
|
-
'text-var-sku': false,
|
|
871
|
-
'text-var-italic-sku': false,
|
|
872
|
-
s1: false,
|
|
873
|
-
s2: false,
|
|
874
|
-
vs1: false,
|
|
875
|
-
vis1: false
|
|
876
|
-
}
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
// User clicks to deselect "Thunder Text Variable"
|
|
880
|
-
const newState = reducer(state, {
|
|
881
|
-
type: 'SELECT_SKU_ID',
|
|
882
|
-
skuId: 'text-var-sku',
|
|
883
|
-
selected: false
|
|
884
|
-
});
|
|
280
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
281
|
+
expect(selected(state)).toEqual(['text-bundle-sku']);
|
|
885
282
|
|
|
886
|
-
// The bundle
|
|
887
|
-
|
|
283
|
+
// The variable comes free with the bundle, so taking it out alone would
|
|
284
|
+
// change nothing: the bundle goes
|
|
285
|
+
expect(selected(deselect(state, 'text-var-sku'))).toEqual([]);
|
|
888
286
|
});
|
|
889
287
|
it('deselecting a font style deselects the parent bundle', () => {
|
|
890
|
-
const state = makeThunderState(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
|
|
288
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
289
|
+
const after = deselect(state, 's1');
|
|
290
|
+
expect(selected(after)).toEqual([]);
|
|
291
|
+
expect(isSelected('s1')(after)).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
it('deselecting a style a cheaper family still holds deselects the family', () => {
|
|
294
|
+
const catalog = {
|
|
295
|
+
'family-sku': [200, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
296
|
+
s1: [50, ['f1']],
|
|
297
|
+
s2: [50, ['f2']],
|
|
298
|
+
s3: [50, ['f3']],
|
|
299
|
+
s4: [50, ['f4']],
|
|
300
|
+
s5: [50, ['f5']]
|
|
301
|
+
};
|
|
302
|
+
const state = select(makeState(catalog), 's1', 's2', 's3', 's4', 's5');
|
|
303
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
304
|
+
// Without s5 the family still costs no more than four styles, so it would
|
|
305
|
+
// stay selected – the click takes the family out instead
|
|
306
|
+
expect(selected(deselect(state, 's5'))).toEqual([]);
|
|
910
307
|
});
|
|
911
308
|
it('deselecting a family bundle deselects the parent collection bundle (transitive)', () => {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
si2: false,
|
|
926
|
-
vs1: false
|
|
927
|
-
},
|
|
928
|
-
collectionStyleSkus: {
|
|
929
|
-
'coll-bundle-sku': {
|
|
930
|
-
fontStyleSkuIds: ['s1', 's2', 'si1', 'si2', 'vs1'],
|
|
931
|
-
fontStyleIds: ['f1', 'f2', 'fi1', 'fi2', 'vf1'],
|
|
932
|
-
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
933
|
-
name: 'Text + Variable'
|
|
934
|
-
},
|
|
935
|
-
'family-sku': {
|
|
936
|
-
fontStyleSkuIds: ['s1', 's2', 'si1', 'si2'],
|
|
937
|
-
fontStyleIds: ['f1', 'f2', 'fi1', 'fi2'],
|
|
938
|
-
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
939
|
-
name: 'Text Family'
|
|
940
|
-
},
|
|
941
|
-
'var-sku': {
|
|
942
|
-
fontStyleSkuIds: ['vs1'],
|
|
943
|
-
fontStyleIds: ['vf1'],
|
|
944
|
-
childrenSkuIds: [],
|
|
945
|
-
name: 'Variable'
|
|
946
|
-
},
|
|
947
|
-
'pkg-upright-sku': {
|
|
948
|
-
fontStyleSkuIds: ['s1', 's2'],
|
|
949
|
-
fontStyleIds: ['f1', 'f2'],
|
|
950
|
-
childrenSkuIds: [],
|
|
951
|
-
name: 'Package Text'
|
|
952
|
-
},
|
|
953
|
-
'pkg-italic-sku': {
|
|
954
|
-
fontStyleSkuIds: ['si1', 'si2'],
|
|
955
|
-
fontStyleIds: ['fi1', 'fi2'],
|
|
956
|
-
childrenSkuIds: [],
|
|
957
|
-
name: 'Package Italic'
|
|
958
|
-
}
|
|
959
|
-
},
|
|
960
|
-
skuPrices: {
|
|
961
|
-
'coll-bundle-sku': 500,
|
|
962
|
-
'family-sku': 500,
|
|
963
|
-
'var-sku': 300,
|
|
964
|
-
'pkg-upright-sku': 250,
|
|
965
|
-
'pkg-italic-sku': 250,
|
|
966
|
-
s1: 100,
|
|
967
|
-
s2: 100,
|
|
968
|
-
si1: 100,
|
|
969
|
-
si2: 100,
|
|
970
|
-
vs1: 100
|
|
971
|
-
}
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
// Bundle shows as selected via transitive isSelected
|
|
309
|
+
const state = select(makeState({
|
|
310
|
+
'coll-bundle-sku': [500, ['f1', 'f2', 'fi1', 'fi2', 'vf1']],
|
|
311
|
+
'family-sku': [500, ['f1', 'f2', 'fi1', 'fi2']],
|
|
312
|
+
'var-sku': [300, ['vf1']],
|
|
313
|
+
'pkg-upright-sku': [250, ['f1', 'f2']],
|
|
314
|
+
'pkg-italic-sku': [250, ['fi1', 'fi2']],
|
|
315
|
+
s1: [100, ['f1']],
|
|
316
|
+
s2: [100, ['f2']],
|
|
317
|
+
si1: [100, ['fi1']],
|
|
318
|
+
si2: [100, ['fi2']],
|
|
319
|
+
vs1: [100, ['vf1']]
|
|
320
|
+
}), 'family-sku');
|
|
321
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
975
322
|
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
selected: false
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
// Collection bundle should be deselected
|
|
985
|
-
expect(newState.selectedSkuIds['coll-bundle-sku']).toBe(false);
|
|
986
|
-
// Nothing should be selected anymore
|
|
987
|
-
expect(isSelected('pkg-upright-sku')(newState)).toBe(false);
|
|
988
|
-
expect(isSelected('family-sku')(newState)).toBe(false);
|
|
989
|
-
expect(isSelected('s1')(newState)).toBe(false);
|
|
323
|
+
const after = deselect(state, 'pkg-upright-sku');
|
|
324
|
+
expect(selected(after)).toEqual([]);
|
|
325
|
+
expect(isSelected('pkg-upright-sku')(after)).toBe(false);
|
|
326
|
+
expect(isSelected('family-sku')(after)).toBe(false);
|
|
327
|
+
expect(isSelected('s1')(after)).toBe(false);
|
|
990
328
|
});
|
|
991
329
|
it('deselecting a bundle does not affect other bundles', () => {
|
|
992
|
-
const state = makeThunderState({
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
'text-var-italic-sku': false,
|
|
998
|
-
s1: false,
|
|
999
|
-
s2: false,
|
|
1000
|
-
vs1: false,
|
|
1001
|
-
vis1: false,
|
|
1002
|
-
'display-bundle-sku': true,
|
|
1003
|
-
'display-sku': false,
|
|
1004
|
-
'display-var-sku': false,
|
|
1005
|
-
'display-var-italic-sku': false,
|
|
1006
|
-
ds1: false,
|
|
1007
|
-
ds2: false,
|
|
1008
|
-
dvs1: false,
|
|
1009
|
-
dvis1: false
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
const newState = reducer(state, {
|
|
1013
|
-
type: 'SELECT_SKU_ID',
|
|
1014
|
-
skuId: 'text-var-sku',
|
|
1015
|
-
selected: false
|
|
1016
|
-
});
|
|
1017
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
|
|
1018
|
-
// Display bundle should be unaffected
|
|
1019
|
-
expect(newState.selectedSkuIds['display-bundle-sku']).toBe(true);
|
|
330
|
+
const state = select(makeThunderState({
|
|
331
|
+
'super-sku': 2000
|
|
332
|
+
}), 'text-sku', 'display-sku');
|
|
333
|
+
expect(selected(state)).toEqual(['display-bundle-sku', 'text-bundle-sku']);
|
|
334
|
+
expect(selected(deselect(state, 'text-var-sku'))).toEqual(['display-bundle-sku']);
|
|
1020
335
|
});
|
|
1021
336
|
});
|
|
1022
337
|
|
|
@@ -1025,37 +340,10 @@ describe('reducer deselection', () => {
|
|
|
1025
340
|
// =========================================================================
|
|
1026
341
|
describe('reducer re-selection cycle', () => {
|
|
1027
342
|
it('re-selecting family after bundle was deselected re-selects the bundle', () => {
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
'text-sku': false,
|
|
1033
|
-
'text-var-sku': false,
|
|
1034
|
-
'text-var-italic-sku': false,
|
|
1035
|
-
s1: false,
|
|
1036
|
-
s2: false,
|
|
1037
|
-
vs1: false,
|
|
1038
|
-
vis1: false
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
// 2. Deselect a member → bundle goes away
|
|
1043
|
-
const afterDeselect = reducer(state, {
|
|
1044
|
-
type: 'SELECT_SKU_ID',
|
|
1045
|
-
skuId: 'text-var-sku',
|
|
1046
|
-
selected: false
|
|
1047
|
-
});
|
|
1048
|
-
expect(afterDeselect.selectedSkuIds['text-bundle-sku']).toBe(false);
|
|
1049
|
-
|
|
1050
|
-
// 3. Re-select the family → bundle should come back
|
|
1051
|
-
const afterReselect = reducer(afterDeselect, {
|
|
1052
|
-
type: 'SELECT_SKU_ID',
|
|
1053
|
-
skuId: 'text-sku',
|
|
1054
|
-
selected: true
|
|
1055
|
-
});
|
|
1056
|
-
expect(afterReselect.selectedSkuIds['text-bundle-sku']).toBe(true);
|
|
1057
|
-
expect(afterReselect.selectedSkuIds['text-sku']).toBe(false);
|
|
1058
|
-
expect(afterReselect.selectedSkuIds['text-var-sku']).toBe(false);
|
|
343
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
344
|
+
const afterDeselect = deselect(state, 'text-var-sku');
|
|
345
|
+
expect(selected(afterDeselect)).toEqual([]);
|
|
346
|
+
expect(selected(select(afterDeselect, 'text-sku'))).toEqual(['text-bundle-sku']);
|
|
1059
347
|
});
|
|
1060
348
|
});
|
|
1061
349
|
|
|
@@ -1064,37 +352,12 @@ describe('reducer re-selection cycle', () => {
|
|
|
1064
352
|
// =========================================================================
|
|
1065
353
|
describe('reducer superfamily upgrade', () => {
|
|
1066
354
|
it('upgrades to superfamily when cumulative selections make it cheaper', () => {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
'text-var-sku': false,
|
|
1074
|
-
'text-var-italic-sku': false,
|
|
1075
|
-
s1: false,
|
|
1076
|
-
s2: false,
|
|
1077
|
-
vs1: false,
|
|
1078
|
-
vis1: false,
|
|
1079
|
-
'display-bundle-sku': true,
|
|
1080
|
-
'display-sku': false,
|
|
1081
|
-
'display-var-sku': false,
|
|
1082
|
-
'display-var-italic-sku': false,
|
|
1083
|
-
ds1: false,
|
|
1084
|
-
ds2: false,
|
|
1085
|
-
dvs1: false,
|
|
1086
|
-
dvis1: false
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1089
|
-
const newState = reducer(state, {
|
|
1090
|
-
type: 'SELECT_SKU_ID',
|
|
1091
|
-
skuId: 'caption-sku',
|
|
1092
|
-
selected: true
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// Superfamily ($750) - two bundles ($500+$500) = -$250 difference
|
|
1096
|
-
// Caption-sku costs $500, so -$250 <= $500 → superfamily is preferred
|
|
1097
|
-
expect(newState.selectedSkuIds['super-sku']).toBe(true);
|
|
355
|
+
const state = select(makeThunderState({
|
|
356
|
+
'super-sku': 1200
|
|
357
|
+
}), 'text-sku', 'display-sku');
|
|
358
|
+
expect(selected(state)).toEqual(['display-bundle-sku', 'text-bundle-sku']);
|
|
359
|
+
// A third $500 bundle would make $1,500
|
|
360
|
+
expect(selected(select(state, 'caption-sku'))).toEqual(['super-sku']);
|
|
1098
361
|
});
|
|
1099
362
|
});
|
|
1100
363
|
|
|
@@ -1102,77 +365,21 @@ describe('reducer superfamily upgrade', () => {
|
|
|
1102
365
|
// reducer SELECT_SKU_ID — individual style to family upgrade
|
|
1103
366
|
// =========================================================================
|
|
1104
367
|
describe('reducer individual style upgrade', () => {
|
|
368
|
+
const FAMILY = {
|
|
369
|
+
'family-sku': [200, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
370
|
+
s1: [40, ['f1']],
|
|
371
|
+
s2: [40, ['f2']],
|
|
372
|
+
s3: [40, ['f3']],
|
|
373
|
+
s4: [40, ['f4']],
|
|
374
|
+
s5: [40, ['f5']]
|
|
375
|
+
};
|
|
1105
376
|
it('upgrades to family when enough styles are individually selected', () => {
|
|
1106
|
-
const state = makeState(
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
s2: true,
|
|
1110
|
-
s3: true,
|
|
1111
|
-
s4: true
|
|
1112
|
-
},
|
|
1113
|
-
collectionStyleSkus: {
|
|
1114
|
-
'family-sku': {
|
|
1115
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1116
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1117
|
-
childrenSkuIds: [],
|
|
1118
|
-
name: 'Family'
|
|
1119
|
-
}
|
|
1120
|
-
},
|
|
1121
|
-
skuPrices: {
|
|
1122
|
-
'family-sku': 200,
|
|
1123
|
-
s1: 40,
|
|
1124
|
-
s2: 40,
|
|
1125
|
-
s3: 40,
|
|
1126
|
-
s4: 40,
|
|
1127
|
-
s5: 40
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
// Family is $200, 4 selected = $160, diff = $40. Style s5 costs $40.
|
|
1132
|
-
// $40 <= $40 → family is a better deal
|
|
1133
|
-
const newState = reducer(state, {
|
|
1134
|
-
type: 'SELECT_SKU_ID',
|
|
1135
|
-
skuId: 's5',
|
|
1136
|
-
selected: true
|
|
1137
|
-
});
|
|
1138
|
-
expect(newState.selectedSkuIds['family-sku']).toBe(true);
|
|
1139
|
-
// Individual styles should be deselected (included via family)
|
|
1140
|
-
expect(newState.selectedSkuIds['s1']).toBe(false);
|
|
1141
|
-
expect(newState.selectedSkuIds['s5']).toBe(false);
|
|
377
|
+
const state = select(makeState(FAMILY), 's1', 's2', 's3', 's4', 's5');
|
|
378
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
379
|
+
expect(isSelected('s1')(state)).toBe(true);
|
|
1142
380
|
});
|
|
1143
381
|
it('does not upgrade to family when not yet cost-effective', () => {
|
|
1144
|
-
|
|
1145
|
-
selectedSkuIds: {
|
|
1146
|
-
s1: true
|
|
1147
|
-
},
|
|
1148
|
-
collectionStyleSkus: {
|
|
1149
|
-
'family-sku': {
|
|
1150
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1151
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1152
|
-
childrenSkuIds: [],
|
|
1153
|
-
name: 'Family'
|
|
1154
|
-
}
|
|
1155
|
-
},
|
|
1156
|
-
skuPrices: {
|
|
1157
|
-
'family-sku': 200,
|
|
1158
|
-
s1: 40,
|
|
1159
|
-
s2: 40,
|
|
1160
|
-
s3: 40,
|
|
1161
|
-
s4: 40,
|
|
1162
|
-
s5: 40
|
|
1163
|
-
}
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
// Family is $200, 1 selected = $40, diff = $160. Style s2 costs $40.
|
|
1167
|
-
// $160 > $40 → not worth upgrading
|
|
1168
|
-
const newState = reducer(state, {
|
|
1169
|
-
type: 'SELECT_SKU_ID',
|
|
1170
|
-
skuId: 's2',
|
|
1171
|
-
selected: true
|
|
1172
|
-
});
|
|
1173
|
-
expect(newState.selectedSkuIds['family-sku']).toBeUndefined();
|
|
1174
|
-
expect(newState.selectedSkuIds['s1']).toBe(true);
|
|
1175
|
-
expect(newState.selectedSkuIds['s2']).toBe(true);
|
|
382
|
+
expect(selected(select(makeState(FAMILY), 's1', 's2'))).toEqual(['s1', 's2']);
|
|
1176
383
|
});
|
|
1177
384
|
});
|
|
1178
385
|
|
|
@@ -1180,81 +387,32 @@ describe('reducer individual style upgrade', () => {
|
|
|
1180
387
|
// reducer SELECT_SKU_ID — edge cases with missing data
|
|
1181
388
|
// =========================================================================
|
|
1182
389
|
describe('reducer edge cases', () => {
|
|
1183
|
-
it('
|
|
1184
|
-
const state = makeState({
|
|
390
|
+
it('selects a SKU whose styles aren’t known as it is', () => {
|
|
391
|
+
const state = makeState({}, {
|
|
1185
392
|
skuPrices: {
|
|
1186
393
|
'some-sku': 100
|
|
1187
394
|
}
|
|
1188
395
|
});
|
|
1189
|
-
|
|
1190
|
-
type: 'SELECT_SKU_ID',
|
|
1191
|
-
skuId: 'some-sku',
|
|
1192
|
-
selected: true
|
|
1193
|
-
});
|
|
1194
|
-
expect(newState.selectedSkuIds['some-sku']).toBe(true);
|
|
396
|
+
expect(selected(select(state, 'some-sku'))).toEqual(['some-sku']);
|
|
1195
397
|
});
|
|
1196
|
-
it('
|
|
398
|
+
it('selects a SKU whose price isn’t known as it is', () => {
|
|
1197
399
|
const state = makeState({
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
fontStyleSkuIds: ['s1'],
|
|
1201
|
-
fontStyleIds: ['f1'],
|
|
1202
|
-
childrenSkuIds: [],
|
|
1203
|
-
name: 'Family'
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
400
|
+
'family-sku': [undefined, ['f1']],
|
|
401
|
+
s1: [undefined, ['f1']]
|
|
1206
402
|
});
|
|
1207
|
-
|
|
1208
|
-
// No prices → no collection upgrade possible, just select directly
|
|
1209
|
-
const newState = reducer(state, {
|
|
1210
|
-
type: 'SELECT_SKU_ID',
|
|
1211
|
-
skuId: 's1',
|
|
1212
|
-
selected: true
|
|
1213
|
-
});
|
|
1214
|
-
expect(newState.selectedSkuIds['s1']).toBe(true);
|
|
1215
|
-
expect(newState.selectedSkuIds['family-sku']).toBeUndefined();
|
|
403
|
+
expect(selected(select(state, 's1'))).toEqual(['s1']);
|
|
1216
404
|
});
|
|
1217
|
-
it('re-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
// at $500. So it upgrades to the superfamily.
|
|
1222
|
-
//
|
|
1223
|
-
// This is a documented consequence of the greedy price-optimization: every
|
|
1224
|
-
// SELECT_SKU_ID re-evaluates the cheapest collection, even for members
|
|
1225
|
-
// that are already effectively selected.
|
|
1226
|
-
const state = makeThunderState({
|
|
1227
|
-
selectedSkuIds: {
|
|
1228
|
-
'text-bundle-sku': true,
|
|
1229
|
-
'text-sku': false,
|
|
1230
|
-
'text-var-sku': false,
|
|
1231
|
-
'text-var-italic-sku': false,
|
|
1232
|
-
s1: false,
|
|
1233
|
-
s2: false,
|
|
1234
|
-
vs1: false,
|
|
1235
|
-
vis1: false
|
|
1236
|
-
}
|
|
1237
|
-
});
|
|
1238
|
-
const newState = reducer(state, {
|
|
1239
|
-
type: 'SELECT_SKU_ID',
|
|
1240
|
-
skuId: 'text-sku',
|
|
1241
|
-
selected: true
|
|
1242
|
-
});
|
|
1243
|
-
expect(newState.selectedSkuIds['super-sku']).toBe(true);
|
|
1244
|
-
expect(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
|
|
405
|
+
it('re-picking something already held changes nothing', () => {
|
|
406
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
407
|
+
expect(select(state, 'text-sku').selectedSkuIds).toBe(state.selectedSkuIds);
|
|
408
|
+
expect(selected(select(state, 's1'))).toEqual(['text-bundle-sku']);
|
|
1245
409
|
});
|
|
1246
410
|
it('UNSELECT_SKUS clears all selections cleanly', () => {
|
|
1247
|
-
const state = makeThunderState({
|
|
1248
|
-
selectedSkuIds: {
|
|
1249
|
-
'text-bundle-sku': true,
|
|
1250
|
-
'text-sku': false,
|
|
1251
|
-
'text-var-sku': false
|
|
1252
|
-
}
|
|
1253
|
-
});
|
|
1254
|
-
const newState = reducer(state, {
|
|
411
|
+
const state = reducer(select(makeThunderState(), 'text-sku'), {
|
|
1255
412
|
type: 'UNSELECT_SKUS'
|
|
1256
413
|
});
|
|
1257
|
-
expect(
|
|
414
|
+
expect(selected(state)).toEqual([]);
|
|
415
|
+
expect(state.requestedSkuIds).toEqual([]);
|
|
1258
416
|
});
|
|
1259
417
|
});
|
|
1260
418
|
|
|
@@ -1262,371 +420,208 @@ describe('reducer edge cases', () => {
|
|
|
1262
420
|
// isSelected — family bundles (style packages within a family)
|
|
1263
421
|
// =========================================================================
|
|
1264
422
|
describe('isSelected with family bundles (style packages)', () => {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1272
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1273
|
-
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1274
|
-
name: 'Thunder Text Family'
|
|
1275
|
-
},
|
|
1276
|
-
'pkg-upright-sku': {
|
|
1277
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1278
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1279
|
-
childrenSkuIds: [],
|
|
1280
|
-
name: 'Package Text'
|
|
1281
|
-
},
|
|
1282
|
-
'pkg-italic-sku': {
|
|
1283
|
-
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1284
|
-
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1285
|
-
childrenSkuIds: [],
|
|
1286
|
-
name: 'Package Text Italic'
|
|
1287
|
-
}
|
|
1288
|
-
},
|
|
1289
|
-
skuPrices: {
|
|
1290
|
-
'family-sku': 500,
|
|
1291
|
-
'pkg-upright-sku': 250,
|
|
1292
|
-
'pkg-italic-sku': 250,
|
|
1293
|
-
s1: 100,
|
|
1294
|
-
s2: 100,
|
|
1295
|
-
s3: 100,
|
|
1296
|
-
s4: 100,
|
|
1297
|
-
s5: 100,
|
|
1298
|
-
si1: 100,
|
|
1299
|
-
si2: 100,
|
|
1300
|
-
si3: 100,
|
|
1301
|
-
si4: 100,
|
|
1302
|
-
si5: 100
|
|
1303
|
-
},
|
|
1304
|
-
...overrides
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
423
|
+
const WITH_COLLECTION_BUNDLE = {
|
|
424
|
+
...FAMILY_WITH_PACKAGES,
|
|
425
|
+
'coll-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1']],
|
|
426
|
+
'var-sku': [300, ['vf1']],
|
|
427
|
+
vs1: [100, ['vf1']]
|
|
428
|
+
};
|
|
1307
429
|
it('bundles show selected when family is directly selected', () => {
|
|
1308
|
-
const state =
|
|
1309
|
-
|
|
1310
|
-
'family-sku': true,
|
|
1311
|
-
'pkg-upright-sku': false,
|
|
1312
|
-
'pkg-italic-sku': false,
|
|
1313
|
-
s1: false,
|
|
1314
|
-
s2: false,
|
|
1315
|
-
s3: false,
|
|
1316
|
-
s4: false,
|
|
1317
|
-
s5: false,
|
|
1318
|
-
si1: false,
|
|
1319
|
-
si2: false,
|
|
1320
|
-
si3: false,
|
|
1321
|
-
si4: false,
|
|
1322
|
-
si5: false
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
430
|
+
const state = select(makeState(FAMILY_WITH_PACKAGES), 'family-sku');
|
|
431
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
1325
432
|
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
1326
433
|
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1327
434
|
expect(isSelected('s1')(state)).toBe(true);
|
|
1328
435
|
expect(isSelected('si1')(state)).toBe(true);
|
|
1329
436
|
});
|
|
1330
437
|
it('bundles show selected when collection bundle is selected (transitive)', () => {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const state = makeFamilyWithBundlesState({
|
|
1334
|
-
collectionStyleSkus: {
|
|
1335
|
-
// Collection bundle: groups family + variable family
|
|
1336
|
-
'coll-bundle-sku': {
|
|
1337
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1'],
|
|
1338
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1'],
|
|
1339
|
-
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1340
|
-
name: 'Thunder Text + Variable'
|
|
1341
|
-
},
|
|
1342
|
-
'family-sku': {
|
|
1343
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1344
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1345
|
-
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1346
|
-
name: 'Thunder Text Family'
|
|
1347
|
-
},
|
|
1348
|
-
'var-sku': {
|
|
1349
|
-
fontStyleSkuIds: ['vs1'],
|
|
1350
|
-
fontStyleIds: ['vf1'],
|
|
1351
|
-
childrenSkuIds: [],
|
|
1352
|
-
name: 'Thunder Text Variable'
|
|
1353
|
-
},
|
|
1354
|
-
'pkg-upright-sku': {
|
|
1355
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1356
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1357
|
-
childrenSkuIds: [],
|
|
1358
|
-
name: 'Package Text'
|
|
1359
|
-
},
|
|
1360
|
-
'pkg-italic-sku': {
|
|
1361
|
-
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1362
|
-
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1363
|
-
childrenSkuIds: [],
|
|
1364
|
-
name: 'Package Text Italic'
|
|
1365
|
-
}
|
|
1366
|
-
},
|
|
1367
|
-
selectedSkuIds: {
|
|
1368
|
-
'coll-bundle-sku': true,
|
|
1369
|
-
'family-sku': false,
|
|
1370
|
-
'var-sku': false,
|
|
1371
|
-
'pkg-upright-sku': false,
|
|
1372
|
-
'pkg-italic-sku': false,
|
|
1373
|
-
s1: false,
|
|
1374
|
-
s2: false,
|
|
1375
|
-
s3: false,
|
|
1376
|
-
s4: false,
|
|
1377
|
-
s5: false,
|
|
1378
|
-
si1: false,
|
|
1379
|
-
si2: false,
|
|
1380
|
-
si3: false,
|
|
1381
|
-
si4: false,
|
|
1382
|
-
si5: false,
|
|
1383
|
-
vs1: false
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
// Family shows selected (direct child of collection bundle)
|
|
438
|
+
const state = select(makeState(WITH_COLLECTION_BUNDLE), 'family-sku');
|
|
439
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
1388
440
|
expect(isSelected('family-sku')(state)).toBe(true);
|
|
1389
|
-
// Bundles should show selected (grandchildren — family is child of
|
|
1390
|
-
// collection bundle, bundles are children of family)
|
|
1391
441
|
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
1392
442
|
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1393
|
-
// Individual styles should also show selected
|
|
1394
443
|
expect(isSelected('s1')(state)).toBe(true);
|
|
1395
444
|
});
|
|
1396
445
|
it('selecting one bundle then another upgrades to family via reducer', () => {
|
|
1397
|
-
|
|
446
|
+
let state = select(makeState(FAMILY_WITH_PACKAGES), 'pkg-upright-sku');
|
|
447
|
+
expect(selected(state)).toEqual(['pkg-upright-sku']);
|
|
1398
448
|
|
|
1399
|
-
//
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
});
|
|
1405
|
-
expect(newState.selectedSkuIds['pkg-upright-sku']).toBe(true);
|
|
1406
|
-
|
|
1407
|
-
// Select second bundle — should upgrade to family
|
|
1408
|
-
newState = reducer(newState, {
|
|
1409
|
-
type: 'SELECT_SKU_ID',
|
|
1410
|
-
skuId: 'pkg-italic-sku',
|
|
1411
|
-
selected: true
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
// Family should be selected since family ($500) = two bundles ($250+$250)
|
|
1415
|
-
expect(newState.selectedSkuIds['family-sku']).toBe(true);
|
|
1416
|
-
// Both bundles should report as selected
|
|
1417
|
-
expect(isSelected('pkg-upright-sku')(newState)).toBe(true);
|
|
1418
|
-
expect(isSelected('pkg-italic-sku')(newState)).toBe(true);
|
|
449
|
+
// Family ($500) = two packages ($250 + $250): one SKU wins the tie
|
|
450
|
+
state = select(state, 'pkg-italic-sku');
|
|
451
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
452
|
+
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
453
|
+
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1419
454
|
});
|
|
1420
455
|
it('auto-upgrades transitively: bundle → family → collection bundle', () => {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1'],
|
|
1428
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1'],
|
|
1429
|
-
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1430
|
-
name: 'Thunder Text + Variable'
|
|
1431
|
-
},
|
|
1432
|
-
'family-sku': {
|
|
1433
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1434
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1435
|
-
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1436
|
-
name: 'Thunder Text Family'
|
|
1437
|
-
},
|
|
1438
|
-
'var-sku': {
|
|
1439
|
-
fontStyleSkuIds: ['vs1'],
|
|
1440
|
-
fontStyleIds: ['vf1'],
|
|
1441
|
-
childrenSkuIds: [],
|
|
1442
|
-
name: 'Thunder Text Variable'
|
|
1443
|
-
},
|
|
1444
|
-
'pkg-upright-sku': {
|
|
1445
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1446
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1447
|
-
childrenSkuIds: [],
|
|
1448
|
-
name: 'Package Text'
|
|
1449
|
-
},
|
|
1450
|
-
'pkg-italic-sku': {
|
|
1451
|
-
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1452
|
-
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1453
|
-
childrenSkuIds: [],
|
|
1454
|
-
name: 'Package Text Italic'
|
|
1455
|
-
}
|
|
1456
|
-
},
|
|
1457
|
-
skuPrices: {
|
|
1458
|
-
'coll-bundle-sku': 500,
|
|
1459
|
-
'family-sku': 500,
|
|
1460
|
-
'var-sku': 300,
|
|
1461
|
-
'pkg-upright-sku': 250,
|
|
1462
|
-
'pkg-italic-sku': 250,
|
|
1463
|
-
s1: 100,
|
|
1464
|
-
s2: 100,
|
|
1465
|
-
s3: 100,
|
|
1466
|
-
s4: 100,
|
|
1467
|
-
s5: 100,
|
|
1468
|
-
si1: 100,
|
|
1469
|
-
si2: 100,
|
|
1470
|
-
si3: 100,
|
|
1471
|
-
si4: 100,
|
|
1472
|
-
si5: 100,
|
|
1473
|
-
vs1: 100
|
|
1474
|
-
},
|
|
1475
|
-
selectedSkuIds: {
|
|
1476
|
-
'pkg-upright-sku': true
|
|
1477
|
-
}
|
|
1478
|
-
});
|
|
1479
|
-
const newState = reducer(state, {
|
|
1480
|
-
type: 'SELECT_SKU_ID',
|
|
1481
|
-
skuId: 'pkg-italic-sku',
|
|
1482
|
-
selected: true
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
// Should transitively upgrade: bundle → family → collection bundle
|
|
1486
|
-
expect(newState.selectedSkuIds['coll-bundle-sku']).toBe(true);
|
|
1487
|
-
expect(newState.selectedSkuIds['family-sku']).toBe(false);
|
|
1488
|
-
expect(newState.selectedSkuIds['pkg-upright-sku']).toBe(false);
|
|
1489
|
-
expect(newState.selectedSkuIds['pkg-italic-sku']).toBe(false);
|
|
1490
|
-
|
|
1491
|
-
// All items should report as selected via isSelected
|
|
1492
|
-
expect(isSelected('family-sku')(newState)).toBe(true);
|
|
1493
|
-
expect(isSelected('pkg-upright-sku')(newState)).toBe(true);
|
|
1494
|
-
expect(isSelected('pkg-italic-sku')(newState)).toBe(true);
|
|
1495
|
-
expect(isSelected('s1')(newState)).toBe(true);
|
|
1496
|
-
expect(isSelected('vs1')(newState)).toBe(true);
|
|
456
|
+
const state = select(makeState(WITH_COLLECTION_BUNDLE), 'pkg-upright-sku', 'pkg-italic-sku');
|
|
457
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
458
|
+
expect(isSelected('family-sku')(state)).toBe(true);
|
|
459
|
+
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
460
|
+
expect(isSelected('s1')(state)).toBe(true);
|
|
461
|
+
expect(isSelected('vs1')(state)).toBe(true);
|
|
1497
462
|
});
|
|
1498
|
-
it('
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 's1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1509
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1510
|
-
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1511
|
-
name: 'Thunder Caption Family'
|
|
1512
|
-
},
|
|
1513
|
-
'coll-bundle-sku': {
|
|
1514
|
-
// 12 unique styles (family 10 + variable 2)
|
|
1515
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1', 'vs2'],
|
|
1516
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1', 'vf2'],
|
|
1517
|
-
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1518
|
-
name: 'Thunder Caption Family + Variable'
|
|
1519
|
-
},
|
|
1520
|
-
'var-sku': {
|
|
1521
|
-
fontStyleSkuIds: ['vs1', 'vs2'],
|
|
1522
|
-
fontStyleIds: ['vf1', 'vf2'],
|
|
1523
|
-
childrenSkuIds: [],
|
|
1524
|
-
name: 'Thunder Caption Variable'
|
|
1525
|
-
},
|
|
1526
|
-
'pkg-upright-sku': {
|
|
1527
|
-
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1528
|
-
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1529
|
-
childrenSkuIds: [],
|
|
1530
|
-
name: 'Package Caption'
|
|
1531
|
-
},
|
|
1532
|
-
'pkg-italic-sku': {
|
|
1533
|
-
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1534
|
-
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1535
|
-
childrenSkuIds: [],
|
|
1536
|
-
name: 'Package Caption Italic'
|
|
1537
|
-
}
|
|
1538
|
-
},
|
|
1539
|
-
skuPrices: {
|
|
1540
|
-
'family-sku': 500,
|
|
1541
|
-
'coll-bundle-sku': 500,
|
|
1542
|
-
'var-sku': 500,
|
|
1543
|
-
'pkg-upright-sku': 300,
|
|
1544
|
-
'pkg-italic-sku': 300,
|
|
1545
|
-
s1: 100,
|
|
1546
|
-
s2: 100,
|
|
1547
|
-
s3: 100,
|
|
1548
|
-
s4: 100,
|
|
1549
|
-
s5: 100,
|
|
1550
|
-
si1: 100,
|
|
1551
|
-
si2: 100,
|
|
1552
|
-
si3: 100,
|
|
1553
|
-
si4: 100,
|
|
1554
|
-
si5: 100,
|
|
1555
|
-
vs1: 100,
|
|
1556
|
-
vs2: 100
|
|
1557
|
-
}
|
|
1558
|
-
});
|
|
463
|
+
it('transitive upgrade prefers the collection with the most font styles', () => {
|
|
464
|
+
const state = select(makeState({
|
|
465
|
+
...FAMILY_WITH_PACKAGES,
|
|
466
|
+
'small-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
467
|
+
'big-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1']],
|
|
468
|
+
'var-sku': [300, ['vf1']]
|
|
469
|
+
}), 'family-sku');
|
|
470
|
+
expect(selected(state)).toEqual(['big-bundle-sku']);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
1559
473
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
474
|
+
// =========================================================================
|
|
475
|
+
// POPULATE_PRODUCT_STATE
|
|
476
|
+
// =========================================================================
|
|
477
|
+
describe('POPULATE_PRODUCT_STATE', () => {
|
|
478
|
+
const sku = (id, amount) => ({
|
|
479
|
+
id,
|
|
480
|
+
price: {
|
|
481
|
+
amount
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const style = (id, skuId) => ({
|
|
485
|
+
id,
|
|
486
|
+
sku: skuId ? sku(skuId, 40) : null
|
|
487
|
+
});
|
|
488
|
+
const textFamily = {
|
|
489
|
+
id: 'text',
|
|
490
|
+
name: 'Text',
|
|
491
|
+
sku: sku('text-sku', 500),
|
|
492
|
+
fontStyles: [style('f1', 's1'), style('f2', 's2')],
|
|
493
|
+
bundles: [{
|
|
494
|
+
id: 'upright',
|
|
495
|
+
name: 'Upright',
|
|
496
|
+
sku: sku('upright-sku', 60),
|
|
497
|
+
fontStyles: [style('f1', 's1')]
|
|
498
|
+
}]
|
|
499
|
+
};
|
|
500
|
+
const textVariable = {
|
|
501
|
+
id: 'text-var',
|
|
502
|
+
name: 'Text Variable',
|
|
503
|
+
sku: sku('text-var-sku', 300),
|
|
504
|
+
fontStyles: [style('vf1', null)],
|
|
505
|
+
bundles: []
|
|
506
|
+
};
|
|
507
|
+
const superfamily = {
|
|
508
|
+
node: {
|
|
509
|
+
__typename: 'FontCollection',
|
|
510
|
+
id: 'super',
|
|
511
|
+
name: 'Thunder',
|
|
512
|
+
sku: sku('super-sku', 750),
|
|
513
|
+
fontStyles: [],
|
|
514
|
+
bundles: [],
|
|
515
|
+
collectionBundles: [{
|
|
516
|
+
id: 'text-bundle',
|
|
517
|
+
name: 'Text + Variable',
|
|
518
|
+
sku: sku('text-bundle-sku', 500),
|
|
519
|
+
memberCollections: [textFamily, textVariable]
|
|
520
|
+
}],
|
|
521
|
+
children: [textFamily, textVariable]
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
it('records the styles every SKU grants', () => {
|
|
525
|
+
const state = reducer(makeState(), {
|
|
526
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
527
|
+
data: superfamily
|
|
528
|
+
});
|
|
529
|
+
expect(state.skuStyleIds).toEqual({
|
|
530
|
+
'super-sku': ['f1', 'f2', 'vf1'],
|
|
531
|
+
'text-bundle-sku': ['f1', 'f2', 'vf1'],
|
|
532
|
+
'text-sku': ['f1', 'f2'],
|
|
533
|
+
'text-var-sku': ['vf1'],
|
|
534
|
+
'upright-sku': ['f1'],
|
|
535
|
+
s1: ['f1'],
|
|
536
|
+
s2: ['f2']
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
it('prices a SKU picked before its product loaded once it has', () => {
|
|
540
|
+
const picked = select(makeState(), 'text-sku');
|
|
541
|
+
expect(selected(picked)).toEqual(['text-sku']);
|
|
542
|
+
const loaded = reducer(picked, {
|
|
543
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
544
|
+
data: superfamily
|
|
545
|
+
});
|
|
546
|
+
expect(selected(loaded)).toEqual(['text-bundle-sku']);
|
|
1565
547
|
});
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// =========================================================================
|
|
551
|
+
// Random catalogs – the selection is the cheapest possible, whatever the order
|
|
552
|
+
// =========================================================================
|
|
553
|
+
describe('selection on random catalogs', () => {
|
|
554
|
+
// mulberry32: a small seeded PRNG so failures reproduce
|
|
555
|
+
const random = seed => () => {
|
|
556
|
+
seed = seed + 0x6d2b79f5 | 0;
|
|
557
|
+
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
|
558
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
559
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Two families of three styles, their family SKUs and a superfamily, plus a
|
|
563
|
+
// few bundles – some within a family, some across both.
|
|
564
|
+
const randomCatalog = rand => {
|
|
565
|
+
const pick = items => items[Math.floor(rand() * items.length)];
|
|
566
|
+
const styles = ['a1', 'a2', 'a3', 'b1', 'b2', 'b3'];
|
|
567
|
+
const catalog = {};
|
|
568
|
+
for (const styleId of styles) catalog[`s:${styleId}`] = [10 + Math.floor(rand() * 10), [styleId]];
|
|
569
|
+
const priceFor = styleIds => Math.round(styleIds.length * (6 + rand() * 12));
|
|
570
|
+
catalog['family:a'] = [priceFor(styles.slice(0, 3)), styles.slice(0, 3)];
|
|
571
|
+
catalog['family:b'] = [priceFor(styles.slice(3)), styles.slice(3)];
|
|
572
|
+
catalog['super'] = [priceFor(styles), styles];
|
|
573
|
+
for (let i = 0; i < 4; i++) {
|
|
574
|
+
const pool = pick([styles.slice(0, 3), styles.slice(3), styles]);
|
|
575
|
+
const styleIds = [...new Set([pick(pool), pick(pool), pick(pool)])];
|
|
576
|
+
if (styleIds.length > 1) catalog[`bundle:${i}`] = [priceFor(styleIds), styleIds];
|
|
577
|
+
}
|
|
578
|
+
return catalog;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// Every subset of SKUs, keeping the cheapest in which each pick sits inside
|
|
582
|
+
// one SKU granting all its styles.
|
|
583
|
+
const bruteForceCost = (catalog, picks) => {
|
|
584
|
+
const skuIds = Object.keys(catalog);
|
|
585
|
+
let best = Infinity;
|
|
586
|
+
for (let subset = 0; subset < 1 << skuIds.length; subset++) {
|
|
587
|
+
const chosen = skuIds.filter((_, i) => subset & 1 << i);
|
|
588
|
+
const holds = pick => chosen.some(skuId => catalog[pick][1].every(s => catalog[skuId][1].includes(s)));
|
|
589
|
+
if (!picks.every(holds)) continue;
|
|
590
|
+
best = Math.min(best, chosen.reduce((sum, skuId) => sum + catalog[skuId][0], 0));
|
|
591
|
+
}
|
|
592
|
+
return best;
|
|
593
|
+
};
|
|
594
|
+
const shuffled = (items, rand) => items.map(item => [rand(), item]).sort((a, b) => a[0] - b[0]).map(_ref2 => {
|
|
595
|
+
let [, item] = _ref2;
|
|
596
|
+
return item;
|
|
597
|
+
});
|
|
598
|
+
it('matches brute force and ignores click order', () => {
|
|
599
|
+
const rand = random(1238);
|
|
600
|
+
for (let run = 0; run < 150; run++) {
|
|
601
|
+
const catalog = randomCatalog(rand);
|
|
602
|
+
const state = makeState(catalog);
|
|
603
|
+
const picks = shuffled(Object.keys(catalog), rand).slice(0, 1 + Math.floor(rand() * 4));
|
|
604
|
+
const cost = cheapestSkuIds(picks, state).reduce((sum, skuId) => sum + catalog[skuId][0], 0);
|
|
605
|
+
expect(cost, JSON.stringify({
|
|
606
|
+
catalog,
|
|
607
|
+
picks
|
|
608
|
+
})).toBe(bruteForceCost(catalog, picks));
|
|
609
|
+
const orders = [picks, shuffled(picks, rand), [...picks].reverse()];
|
|
610
|
+
const results = orders.map(order => selected(select(state, ...order)));
|
|
611
|
+
expect(results[1], JSON.stringify({
|
|
612
|
+
catalog,
|
|
613
|
+
orders
|
|
614
|
+
})).toEqual(results[0]);
|
|
615
|
+
expect(results[2], JSON.stringify({
|
|
616
|
+
catalog,
|
|
617
|
+
orders
|
|
618
|
+
})).toEqual(results[0]);
|
|
619
|
+
const chosen = select(state, ...picks);
|
|
620
|
+
for (const skuId of Object.keys(catalog)) {
|
|
621
|
+
const added = additionalCost(chosen, skuId);
|
|
622
|
+
expect(added).toBeGreaterThanOrEqual(0);
|
|
623
|
+
expect(added).toBeLessThanOrEqual(catalog[skuId][0]);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
1631
626
|
});
|
|
1632
627
|
});
|