fontdue-js 3.6.0 → 3.8.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 +14 -0
- package/README.md +5 -4
- package/dist/__generated__/AddToCartBannerQuery.graphql.d.ts +1 -1
- package/dist/__generated__/AddToCartBannerQuery.graphql.js +16 -10
- package/dist/__generated__/AddToCartBanner_item.graphql.d.ts +4 -1
- package/dist/__generated__/AddToCartBanner_item.graphql.js +7 -2
- package/dist/__generated__/AddToCartBanner_order.graphql.d.ts +4 -1
- package/dist/__generated__/AddToCartBanner_order.graphql.js +6 -1
- package/dist/__generated__/BuyButtonIDQuery.graphql.d.ts +3 -3
- package/dist/__generated__/BuyButtonIDQuery.graphql.js +31 -16
- package/dist/__generated__/BuyButtonPackIDQuery.graphql.d.ts +21 -0
- package/dist/__generated__/BuyButtonPackIDQuery.graphql.js +129 -0
- package/dist/__generated__/BuyButtonPackSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/BuyButtonPackSlugQuery.graphql.js +167 -0
- package/dist/__generated__/BuyButtonSlugQuery.graphql.d.ts +3 -3
- package/dist/__generated__/BuyButtonSlugQuery.graphql.js +45 -17
- package/dist/__generated__/BuyButton_product.graphql.d.ts +27 -0
- package/dist/__generated__/BuyButton_product.graphql.js +72 -0
- package/dist/__generated__/CartItemProduct_product.graphql.d.ts +1 -1
- package/dist/__generated__/CartItemProduct_product.graphql.js +7 -2
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +26 -20
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +26 -20
- package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderUpdateMutation.graphql.js +26 -20
- package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CartQuery.graphql.js +26 -20
- package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartStateUpdateMutation.graphql.js +26 -20
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +26 -20
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +26 -20
- package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
- package/dist/__generated__/CollectionAa_Query.graphql.js +20 -14
- package/dist/__generated__/CollectionAa_product.graphql.d.ts +7 -1
- package/dist/__generated__/CollectionAa_product.graphql.js +22 -16
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.d.ts +3 -3
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.js +9 -9
- package/dist/__generated__/NodePasswordFormElementPackSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/NodePasswordFormElementPackSlugQuery.graphql.js +108 -0
- package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
- package/dist/__generated__/PrecartAddToCartMutation.graphql.js +32 -26
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalCartQuery.graphql.js +37 -31
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPackItem_fontPack.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPackItem_fontPack.graphql.js +58 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +6 -3
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +218 -117
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalPackButton_fontPack.graphql.d.ts +33 -0
- package/dist/__generated__/StoreModalPackButton_fontPack.graphql.js +119 -0
- package/dist/__generated__/StoreModalPack_fontPack.graphql.d.ts +45 -0
- package/dist/__generated__/StoreModalPack_fontPack.graphql.js +150 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +6 -2
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +43 -19
- package/dist/__generated__/{StoreModalProductLicenseSelection_collection.graphql.d.ts → StoreModalProductLicenseSelection_licenseable.graphql.d.ts} +7 -6
- package/dist/__generated__/StoreModalProductLicenseSelection_licenseable.graphql.js +43 -0
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +199 -142
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +190 -133
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +5 -1
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +254 -176
- package/dist/__generated__/StoreModalProductSummaryRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductSummaryRefetchQuery.graphql.js +9 -4
- package/dist/__generated__/StoreModalProductSummary_viewer.graphql.d.ts +7 -1
- package/dist/__generated__/StoreModalProductSummary_viewer.graphql.js +7 -2
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +26 -20
- 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__/productState_Query.graphql.d.ts +99 -1
- package/dist/__generated__/productState_Query.graphql.js +43 -4
- package/dist/__generated__/productState_fontPack.graphql.d.ts +111 -0
- package/dist/__generated__/productState_fontPack.graphql.js +117 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__tests__/bundleSelectionOrder.test.js +94 -0
- package/dist/__tests__/collectionBundleSelection.test.js +443 -1446
- package/dist/__tests__/fontPackSelection.test.js +435 -0
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +4 -1
- package/dist/__tests__/storeModalPackButton.test.js +102 -0
- package/dist/__tests__/storeModalPackUnlisted.test.js +112 -0
- package/dist/__tests__/storeModalRoutes.test.js +9 -0
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/components/AddToCartBanner/index.js +1 -1
- package/dist/components/BuyButton/index.d.ts +29 -9
- package/dist/components/BuyButton/index.js +123 -26
- package/dist/components/BuyButton/index.server.d.ts +4 -2
- package/dist/components/BuyButton/index.server.js +24 -3
- package/dist/components/Cart/CartItem/CartItemProduct.js +3 -3
- 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/CollectionAa/index.js +2 -2
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/NodePasswordForm/NodePasswordFormElement.d.ts +1 -1
- package/dist/components/NodePasswordForm/NodePasswordFormElement.js +45 -21
- package/dist/components/NodePasswordForm/index.d.ts +7 -4
- package/dist/components/NodePasswordForm/index.js +6 -6
- 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 +100 -61
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.d.ts +7 -0
- package/dist/components/StoreModal/StoreModalFamily.js +11 -7
- package/dist/components/StoreModal/StoreModalIndex.js +58 -7
- package/dist/components/StoreModal/StoreModalIndexEntry.d.ts +13 -0
- package/dist/components/StoreModal/StoreModalIndexEntry.js +43 -0
- package/dist/components/StoreModal/StoreModalIndexItem.js +7 -30
- package/dist/components/StoreModal/StoreModalIndexPackItem.d.ts +7 -0
- package/dist/components/StoreModal/StoreModalIndexPackItem.js +21 -0
- package/dist/components/StoreModal/StoreModalPack.d.ts +9 -0
- package/dist/components/StoreModal/StoreModalPack.js +79 -0
- package/dist/components/StoreModal/StoreModalPackButton.d.ts +8 -0
- package/dist/components/StoreModal/StoreModalPackButton.js +53 -0
- package/dist/components/StoreModal/StoreModalProduct.js +49 -19
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.d.ts +3 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +7 -6
- package/dist/components/StoreModalProductSummary/index.js +1 -1
- 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/StoreModalFamily/index.d.ts +1 -1
- package/dist/components/elements/StoreModalFamily/index.js +1 -1
- 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 +39 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/reducer.d.ts +12 -5
- package/dist/reducer.js +18 -87
- package/dist/selection.d.ts +44 -0
- package/dist/selection.js +539 -0
- package/dist/storeModalRoutes.js +5 -4
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/BuyButton_collection.graphql.d.ts +0 -25
- package/dist/__generated__/BuyButton_collection.graphql.js +0 -49
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +0 -40
- package/fontdue.css +0 -2733
- package/fontdue.css.map +0 -1
|
@@ -5,21 +5,32 @@ 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
|
-
|
|
31
|
+
productSkuIds: {},
|
|
32
|
+
viewedProductId: null,
|
|
33
|
+
skuPrices,
|
|
23
34
|
fetchedCollectionIds: [],
|
|
24
35
|
storeModalRoute: {
|
|
25
36
|
name: 'index'
|
|
@@ -30,739 +41,162 @@ function makeState() {
|
|
|
30
41
|
...overrides
|
|
31
42
|
};
|
|
32
43
|
}
|
|
44
|
+
const select = function (state) {
|
|
45
|
+
for (var _len = arguments.length, skuIds = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
46
|
+
skuIds[_key - 1] = arguments[_key];
|
|
47
|
+
}
|
|
48
|
+
return skuIds.reduce((current, skuId) => reducer(current, {
|
|
49
|
+
type: 'SELECT_SKU_ID',
|
|
50
|
+
skuId,
|
|
51
|
+
selected: true
|
|
52
|
+
}), state);
|
|
53
|
+
};
|
|
54
|
+
const deselect = (state, skuId) => reducer(state, {
|
|
55
|
+
type: 'SELECT_SKU_ID',
|
|
56
|
+
skuId,
|
|
57
|
+
selected: false
|
|
58
|
+
});
|
|
59
|
+
const selected = state => Object.keys(state.selectedSkuIds).filter(skuId => state.selectedSkuIds[skuId] === true).sort();
|
|
33
60
|
|
|
34
61
|
// ---------------------------------------------------------------------------
|
|
35
|
-
// Shared
|
|
36
|
-
//
|
|
62
|
+
// Shared fixture – a superfamily of three families, each with a variable
|
|
63
|
+
// upright and italic, and a collection bundle of the family and its variables
|
|
64
|
+
// priced like the family alone.
|
|
37
65
|
// ---------------------------------------------------------------------------
|
|
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
|
-
});
|
|
66
|
+
const THUNDER = {
|
|
67
|
+
'super-sku': [750, ['f1', 'f2', 'df1', 'df2', 'cf1', 'cf2', 'vf1', 'vif1', 'dvf1', 'dvif1', 'cvf1', 'cvif1']],
|
|
68
|
+
'text-sku': [500, ['f1', 'f2']],
|
|
69
|
+
'display-sku': [500, ['df1', 'df2']],
|
|
70
|
+
'caption-sku': [500, ['cf1', 'cf2']],
|
|
71
|
+
'text-var-sku': [300, ['vf1']],
|
|
72
|
+
'text-var-italic-sku': [300, ['vif1']],
|
|
73
|
+
'display-var-sku': [300, ['dvf1']],
|
|
74
|
+
'display-var-italic-sku': [300, ['dvif1']],
|
|
75
|
+
'caption-var-sku': [300, ['cvf1']],
|
|
76
|
+
'caption-var-italic-sku': [300, ['cvif1']],
|
|
77
|
+
'text-bundle-sku': [500, ['f1', 'f2', 'vf1', 'vif1']],
|
|
78
|
+
'display-bundle-sku': [500, ['df1', 'df2', 'dvf1', 'dvif1']],
|
|
79
|
+
'caption-bundle-sku': [500, ['cf1', 'cf2', 'cvf1', 'cvif1']],
|
|
80
|
+
s1: [40, ['f1']],
|
|
81
|
+
s2: [40, ['f2']],
|
|
82
|
+
ds1: [40, ['df1']],
|
|
83
|
+
ds2: [40, ['df2']],
|
|
84
|
+
cs1: [40, ['cf1']],
|
|
85
|
+
cs2: [40, ['cf2']],
|
|
86
|
+
vs1: [40, ['vf1']],
|
|
87
|
+
vis1: [40, ['vif1']],
|
|
88
|
+
dvs1: [40, ['dvf1']],
|
|
89
|
+
dvis1: [40, ['dvif1']],
|
|
90
|
+
cvs1: [40, ['cvf1']],
|
|
91
|
+
cvis1: [40, ['cvif1']]
|
|
92
|
+
};
|
|
93
|
+
const makeThunderState = function () {
|
|
94
|
+
let prices = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
95
|
+
return makeState(Object.fromEntries(Object.entries(THUNDER).map(_ref => {
|
|
96
|
+
let [skuId, [price, styleIds]] = _ref;
|
|
97
|
+
return [skuId, [prices[skuId] ?? price, styleIds]];
|
|
98
|
+
})));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// A family of five uprights and five italics, sold whole, as upright and
|
|
102
|
+
// italic packages, or by the style.
|
|
103
|
+
const FAMILY_WITH_PACKAGES = {
|
|
104
|
+
'family-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
105
|
+
'pkg-upright-sku': [250, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
106
|
+
'pkg-italic-sku': [250, ['fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
107
|
+
s1: [100, ['f1']],
|
|
108
|
+
s2: [100, ['f2']],
|
|
109
|
+
s3: [100, ['f3']],
|
|
110
|
+
s4: [100, ['f4']],
|
|
111
|
+
s5: [100, ['f5']],
|
|
112
|
+
si1: [100, ['fi1']],
|
|
113
|
+
si2: [100, ['fi2']],
|
|
114
|
+
si3: [100, ['fi3']],
|
|
115
|
+
si4: [100, ['fi4']],
|
|
116
|
+
si5: [100, ['fi5']]
|
|
117
|
+
};
|
|
200
118
|
|
|
201
119
|
// =========================================================================
|
|
202
120
|
// isSelected
|
|
203
121
|
// =========================================================================
|
|
204
122
|
describe('isSelected', () => {
|
|
205
123
|
it('returns true when the SKU is directly selected', () => {
|
|
206
|
-
const state = makeState({
|
|
124
|
+
const state = makeState({}, {
|
|
207
125
|
selectedSkuIds: {
|
|
208
126
|
'sku-1': true
|
|
209
127
|
}
|
|
210
128
|
});
|
|
211
129
|
expect(isSelected('sku-1')(state)).toBe(true);
|
|
212
130
|
});
|
|
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);
|
|
131
|
+
it('returns false when the SKU is not selected', () => {
|
|
132
|
+
expect(isSelected('sku-1')(makeState())).toBe(false);
|
|
220
133
|
});
|
|
221
|
-
it('returns true when
|
|
134
|
+
it('returns true when every style it grants is granted by the selection', () => {
|
|
222
135
|
const state = makeState({
|
|
136
|
+
'bundle-sku': [300, ['f1', 'f2']],
|
|
137
|
+
'family-sku': [500, ['f1']],
|
|
138
|
+
s1: [100, ['f1']]
|
|
139
|
+
}, {
|
|
223
140
|
selectedSkuIds: {
|
|
224
141
|
'bundle-sku': true
|
|
225
|
-
},
|
|
226
|
-
collectionStyleSkus: {
|
|
227
|
-
'bundle-sku': {
|
|
228
|
-
fontStyleSkuIds: ['s1'],
|
|
229
|
-
fontStyleIds: ['f1'],
|
|
230
|
-
childrenSkuIds: ['family-sku'],
|
|
231
|
-
name: 'Bundle'
|
|
232
|
-
}
|
|
233
142
|
}
|
|
234
143
|
});
|
|
235
|
-
// Style SKU contained via fontStyleSkuIds
|
|
236
144
|
expect(isSelected('s1')(state)).toBe(true);
|
|
237
|
-
// Family SKU contained via childrenSkuIds
|
|
238
145
|
expect(isSelected('family-sku')(state)).toBe(true);
|
|
239
|
-
//
|
|
146
|
+
// A style with no SKU of its own, by its style id
|
|
240
147
|
expect(isSelected('f1')(state)).toBe(true);
|
|
241
148
|
});
|
|
242
|
-
it('returns false when
|
|
149
|
+
it('returns false when only some of its styles are selected', () => {
|
|
243
150
|
const state = makeState({
|
|
151
|
+
'family-sku': [500, ['f1', 'f2']],
|
|
152
|
+
s1: [100, ['f1']]
|
|
153
|
+
}, {
|
|
244
154
|
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
|
-
}
|
|
155
|
+
s1: true
|
|
255
156
|
}
|
|
256
157
|
});
|
|
257
|
-
expect(isSelected('s1')(state)).toBe(false);
|
|
258
158
|
expect(isSelected('family-sku')(state)).toBe(false);
|
|
259
159
|
});
|
|
260
160
|
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);
|
|
161
|
+
expect(isSelected(null)(makeState())).toBe(false);
|
|
436
162
|
});
|
|
437
163
|
});
|
|
438
164
|
|
|
439
165
|
// =========================================================================
|
|
440
|
-
//
|
|
166
|
+
// additionalCost – the “+$X” next to what isn't selected
|
|
441
167
|
// =========================================================================
|
|
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);
|
|
168
|
+
describe('additionalCost', () => {
|
|
169
|
+
const FIVE_STYLES = {
|
|
170
|
+
'family-sku': [150, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
171
|
+
s1: [40, ['f1']],
|
|
172
|
+
s2: [40, ['f2']],
|
|
173
|
+
s3: [40, ['f3']],
|
|
174
|
+
s4: [40, ['f4']],
|
|
175
|
+
s5: [40, ['f5']]
|
|
176
|
+
};
|
|
177
|
+
it('is the SKU’s own price when nothing cheaper holds it', () => {
|
|
178
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 's1'), 's2')).toBe(40);
|
|
631
179
|
});
|
|
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');
|
|
180
|
+
it('is what completing a cheaper bundle adds', () => {
|
|
181
|
+
// Three styles selected (120); the family (150) holds a fourth for 30 more
|
|
182
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 's1', 's2', 's3'), 's4')).toBe(30);
|
|
655
183
|
});
|
|
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);
|
|
184
|
+
it('is zero for what the selection already holds', () => {
|
|
185
|
+
expect(additionalCost(select(makeState(FIVE_STYLES), 'family-sku'), 's1')).toBe(0);
|
|
675
186
|
});
|
|
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.
|
|
187
|
+
it('is null when the SKU’s price or styles are unknown', () => {
|
|
679
188
|
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
|
|
189
|
+
'unpriced-sku': [undefined, ['f1']]
|
|
699
190
|
});
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// 0 <= 500 ✓ → ghost-bundle wins
|
|
703
|
-
expect(collId).toBe('ghost-bundle');
|
|
704
|
-
expect(amount).toBe(0);
|
|
191
|
+
expect(additionalCost(state, 'unpriced-sku')).toBeNull();
|
|
192
|
+
expect(additionalCost(state, 'unknown-sku')).toBeNull();
|
|
705
193
|
});
|
|
706
|
-
it('handles zero-price SKUs
|
|
194
|
+
it('handles zero-price SKUs', () => {
|
|
707
195
|
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
|
-
}
|
|
196
|
+
'free-sku': [0, ['f1']],
|
|
197
|
+
s2: [40, ['f2']]
|
|
729
198
|
});
|
|
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);
|
|
199
|
+
expect(additionalCost(select(state, 's2'), 'free-sku')).toBe(0);
|
|
766
200
|
});
|
|
767
201
|
});
|
|
768
202
|
|
|
@@ -771,46 +205,25 @@ describe('collectionSkuIdWithDiscount', () => {
|
|
|
771
205
|
// =========================================================================
|
|
772
206
|
describe('reducer SELECT_SKU_ID with collection bundles', () => {
|
|
773
207
|
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);
|
|
208
|
+
expect(selected(select(makeThunderState(), 'text-sku'))).toEqual(['text-bundle-sku']);
|
|
784
209
|
});
|
|
785
210
|
it('does not auto-select bundle when bundle is more expensive', () => {
|
|
786
211
|
const state = makeThunderState({
|
|
787
|
-
|
|
788
|
-
...makeThunderState().skuPrices,
|
|
789
|
-
'text-bundle-sku': 600 // more expensive than text-sku ($500)
|
|
790
|
-
}
|
|
212
|
+
'text-bundle-sku': 600
|
|
791
213
|
});
|
|
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();
|
|
214
|
+
expect(selected(select(state, 'text-sku'))).toEqual(['text-sku']);
|
|
799
215
|
});
|
|
800
216
|
it('auto-selects bundle when clicking variable family with static already selected', () => {
|
|
217
|
+
expect(selected(select(makeThunderState(), 'text-sku', 'text-var-sku'))).toEqual(['text-bundle-sku']);
|
|
218
|
+
});
|
|
219
|
+
it('never splits a pick into cheaper parts', () => {
|
|
220
|
+
// The styles cost 80 together, but a family bought is not the same
|
|
221
|
+
// purchase as today's styles bought one by one
|
|
801
222
|
const state = makeThunderState({
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
223
|
+
'text-bundle-sku': 600,
|
|
224
|
+
'super-sku': 2000
|
|
805
225
|
});
|
|
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);
|
|
226
|
+
expect(selected(select(state, 'text-sku'))).toEqual(['text-sku']);
|
|
814
227
|
});
|
|
815
228
|
});
|
|
816
229
|
|
|
@@ -819,41 +232,45 @@ describe('reducer SELECT_SKU_ID with collection bundles', () => {
|
|
|
819
232
|
// =========================================================================
|
|
820
233
|
describe('reducer SELECT_SKU_ID with multiple bundles', () => {
|
|
821
234
|
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();
|
|
235
|
+
expect(selected(select(makeThunderState(), 'display-sku'))).toEqual(['display-bundle-sku']);
|
|
835
236
|
});
|
|
836
237
|
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
|
-
});
|
|
238
|
+
// Two families ($500 + $500) cost more than the superfamily ($750)
|
|
239
|
+
expect(selected(select(makeThunderState(), 'text-sku', 'display-sku'))).toEqual(['super-sku']);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
851
242
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
243
|
+
// =========================================================================
|
|
244
|
+
// reducer SELECT_SKU_ID — order independence (FD-1238)
|
|
245
|
+
// =========================================================================
|
|
246
|
+
describe('reducer selection does not depend on click order', () => {
|
|
247
|
+
// Overlapping style bundles: every pair of the three uprights, each upright
|
|
248
|
+
// with its italic, and the three uprights together
|
|
249
|
+
const OVERLAPPING = {
|
|
250
|
+
light: [90, ['light']],
|
|
251
|
+
regular: [90, ['regular']],
|
|
252
|
+
bold: [90, ['bold']],
|
|
253
|
+
italic: [90, ['italic']],
|
|
254
|
+
'bold-italic': [90, ['bold-italic']],
|
|
255
|
+
'light+regular': [153, ['light', 'regular']],
|
|
256
|
+
'light+bold': [153, ['light', 'bold']],
|
|
257
|
+
'regular+bold': [153, ['regular', 'bold']],
|
|
258
|
+
'regular+italic': [144, ['regular', 'italic']],
|
|
259
|
+
'bold+bold-italic': [144, ['bold', 'bold-italic']],
|
|
260
|
+
'light+regular+bold': [202.5, ['light', 'regular', 'bold']]
|
|
261
|
+
};
|
|
262
|
+
it('reaches a triple that every pair inside it would otherwise snap to first', () => {
|
|
263
|
+
const state = makeState(OVERLAPPING);
|
|
264
|
+
expect(selected(select(state, 'bold', 'regular', 'light'))).toEqual(['light+regular+bold']);
|
|
265
|
+
expect(selected(select(state, 'light', 'regular', 'bold'))).toEqual(['light+regular+bold']);
|
|
266
|
+
});
|
|
267
|
+
it('re-partitions a pair when a cheaper split appears', () => {
|
|
268
|
+
const state = makeState(OVERLAPPING);
|
|
269
|
+
expect(selected(select(state, 'bold', 'regular', 'bold-italic', 'italic'))).toEqual(['bold+bold-italic', 'regular+italic']);
|
|
270
|
+
});
|
|
271
|
+
it('deselecting a style keeps the rest of its bundle', () => {
|
|
272
|
+
const state = select(makeState(OVERLAPPING), 'bold', 'regular', 'light');
|
|
273
|
+
expect(selected(deselect(state, 'regular'))).toEqual(['light+bold']);
|
|
857
274
|
});
|
|
858
275
|
});
|
|
859
276
|
|
|
@@ -862,161 +279,61 @@ describe('reducer SELECT_SKU_ID with multiple bundles', () => {
|
|
|
862
279
|
// =========================================================================
|
|
863
280
|
describe('reducer deselection', () => {
|
|
864
281
|
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
|
-
});
|
|
282
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
283
|
+
expect(selected(state)).toEqual(['text-bundle-sku']);
|
|
885
284
|
|
|
886
|
-
// The bundle
|
|
887
|
-
|
|
285
|
+
// The variable comes free with the bundle, so taking it out alone would
|
|
286
|
+
// change nothing: the bundle goes
|
|
287
|
+
expect(selected(deselect(state, 'text-var-sku'))).toEqual([]);
|
|
888
288
|
});
|
|
889
289
|
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);
|
|
290
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
291
|
+
const after = deselect(state, 's1');
|
|
292
|
+
expect(selected(after)).toEqual([]);
|
|
293
|
+
expect(isSelected('s1')(after)).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
it('deselecting a style a cheaper family still holds deselects the family', () => {
|
|
296
|
+
const catalog = {
|
|
297
|
+
'family-sku': [200, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
298
|
+
s1: [50, ['f1']],
|
|
299
|
+
s2: [50, ['f2']],
|
|
300
|
+
s3: [50, ['f3']],
|
|
301
|
+
s4: [50, ['f4']],
|
|
302
|
+
s5: [50, ['f5']]
|
|
303
|
+
};
|
|
304
|
+
const state = select(makeState(catalog), 's1', 's2', 's3', 's4', 's5');
|
|
305
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
306
|
+
// Without s5 the family still costs no more than four styles, so it would
|
|
307
|
+
// stay selected – the click takes the family out instead
|
|
308
|
+
expect(selected(deselect(state, 's5'))).toEqual([]);
|
|
910
309
|
});
|
|
911
310
|
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
|
|
311
|
+
const state = select(makeState({
|
|
312
|
+
'coll-bundle-sku': [500, ['f1', 'f2', 'fi1', 'fi2', 'vf1']],
|
|
313
|
+
'family-sku': [500, ['f1', 'f2', 'fi1', 'fi2']],
|
|
314
|
+
'var-sku': [300, ['vf1']],
|
|
315
|
+
'pkg-upright-sku': [250, ['f1', 'f2']],
|
|
316
|
+
'pkg-italic-sku': [250, ['fi1', 'fi2']],
|
|
317
|
+
s1: [100, ['f1']],
|
|
318
|
+
s2: [100, ['f2']],
|
|
319
|
+
si1: [100, ['fi1']],
|
|
320
|
+
si2: [100, ['fi2']],
|
|
321
|
+
vs1: [100, ['vf1']]
|
|
322
|
+
}), 'family-sku');
|
|
323
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
975
324
|
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);
|
|
325
|
+
const after = deselect(state, 'pkg-upright-sku');
|
|
326
|
+
expect(selected(after)).toEqual([]);
|
|
327
|
+
expect(isSelected('pkg-upright-sku')(after)).toBe(false);
|
|
328
|
+
expect(isSelected('family-sku')(after)).toBe(false);
|
|
329
|
+
expect(isSelected('s1')(after)).toBe(false);
|
|
990
330
|
});
|
|
991
331
|
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);
|
|
332
|
+
const state = select(makeThunderState({
|
|
333
|
+
'super-sku': 2000
|
|
334
|
+
}), 'text-sku', 'display-sku');
|
|
335
|
+
expect(selected(state)).toEqual(['display-bundle-sku', 'text-bundle-sku']);
|
|
336
|
+
expect(selected(deselect(state, 'text-var-sku'))).toEqual(['display-bundle-sku']);
|
|
1020
337
|
});
|
|
1021
338
|
});
|
|
1022
339
|
|
|
@@ -1025,37 +342,10 @@ describe('reducer deselection', () => {
|
|
|
1025
342
|
// =========================================================================
|
|
1026
343
|
describe('reducer re-selection cycle', () => {
|
|
1027
344
|
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);
|
|
345
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
346
|
+
const afterDeselect = deselect(state, 'text-var-sku');
|
|
347
|
+
expect(selected(afterDeselect)).toEqual([]);
|
|
348
|
+
expect(selected(select(afterDeselect, 'text-sku'))).toEqual(['text-bundle-sku']);
|
|
1059
349
|
});
|
|
1060
350
|
});
|
|
1061
351
|
|
|
@@ -1064,37 +354,12 @@ describe('reducer re-selection cycle', () => {
|
|
|
1064
354
|
// =========================================================================
|
|
1065
355
|
describe('reducer superfamily upgrade', () => {
|
|
1066
356
|
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);
|
|
357
|
+
const state = select(makeThunderState({
|
|
358
|
+
'super-sku': 1200
|
|
359
|
+
}), 'text-sku', 'display-sku');
|
|
360
|
+
expect(selected(state)).toEqual(['display-bundle-sku', 'text-bundle-sku']);
|
|
361
|
+
// A third $500 bundle would make $1,500
|
|
362
|
+
expect(selected(select(state, 'caption-sku'))).toEqual(['super-sku']);
|
|
1098
363
|
});
|
|
1099
364
|
});
|
|
1100
365
|
|
|
@@ -1102,77 +367,21 @@ describe('reducer superfamily upgrade', () => {
|
|
|
1102
367
|
// reducer SELECT_SKU_ID — individual style to family upgrade
|
|
1103
368
|
// =========================================================================
|
|
1104
369
|
describe('reducer individual style upgrade', () => {
|
|
370
|
+
const FAMILY = {
|
|
371
|
+
'family-sku': [200, ['f1', 'f2', 'f3', 'f4', 'f5']],
|
|
372
|
+
s1: [40, ['f1']],
|
|
373
|
+
s2: [40, ['f2']],
|
|
374
|
+
s3: [40, ['f3']],
|
|
375
|
+
s4: [40, ['f4']],
|
|
376
|
+
s5: [40, ['f5']]
|
|
377
|
+
};
|
|
1105
378
|
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);
|
|
379
|
+
const state = select(makeState(FAMILY), 's1', 's2', 's3', 's4', 's5');
|
|
380
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
381
|
+
expect(isSelected('s1')(state)).toBe(true);
|
|
1142
382
|
});
|
|
1143
383
|
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);
|
|
384
|
+
expect(selected(select(makeState(FAMILY), 's1', 's2'))).toEqual(['s1', 's2']);
|
|
1176
385
|
});
|
|
1177
386
|
});
|
|
1178
387
|
|
|
@@ -1180,81 +389,32 @@ describe('reducer individual style upgrade', () => {
|
|
|
1180
389
|
// reducer SELECT_SKU_ID — edge cases with missing data
|
|
1181
390
|
// =========================================================================
|
|
1182
391
|
describe('reducer edge cases', () => {
|
|
1183
|
-
it('
|
|
1184
|
-
const state = makeState({
|
|
392
|
+
it('selects a SKU whose styles aren’t known as it is', () => {
|
|
393
|
+
const state = makeState({}, {
|
|
1185
394
|
skuPrices: {
|
|
1186
395
|
'some-sku': 100
|
|
1187
396
|
}
|
|
1188
397
|
});
|
|
1189
|
-
|
|
1190
|
-
type: 'SELECT_SKU_ID',
|
|
1191
|
-
skuId: 'some-sku',
|
|
1192
|
-
selected: true
|
|
1193
|
-
});
|
|
1194
|
-
expect(newState.selectedSkuIds['some-sku']).toBe(true);
|
|
398
|
+
expect(selected(select(state, 'some-sku'))).toEqual(['some-sku']);
|
|
1195
399
|
});
|
|
1196
|
-
it('
|
|
400
|
+
it('selects a SKU whose price isn’t known as it is', () => {
|
|
1197
401
|
const state = makeState({
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
fontStyleSkuIds: ['s1'],
|
|
1201
|
-
fontStyleIds: ['f1'],
|
|
1202
|
-
childrenSkuIds: [],
|
|
1203
|
-
name: 'Family'
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
402
|
+
'family-sku': [undefined, ['f1']],
|
|
403
|
+
s1: [undefined, ['f1']]
|
|
1206
404
|
});
|
|
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();
|
|
405
|
+
expect(selected(select(state, 's1'))).toEqual(['s1']);
|
|
1216
406
|
});
|
|
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);
|
|
407
|
+
it('re-picking something already held changes nothing', () => {
|
|
408
|
+
const state = select(makeThunderState(), 'text-sku');
|
|
409
|
+
expect(select(state, 'text-sku').selectedSkuIds).toBe(state.selectedSkuIds);
|
|
410
|
+
expect(selected(select(state, 's1'))).toEqual(['text-bundle-sku']);
|
|
1245
411
|
});
|
|
1246
412
|
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, {
|
|
413
|
+
const state = reducer(select(makeThunderState(), 'text-sku'), {
|
|
1255
414
|
type: 'UNSELECT_SKUS'
|
|
1256
415
|
});
|
|
1257
|
-
expect(
|
|
416
|
+
expect(selected(state)).toEqual([]);
|
|
417
|
+
expect(state.requestedSkuIds).toEqual([]);
|
|
1258
418
|
});
|
|
1259
419
|
});
|
|
1260
420
|
|
|
@@ -1262,371 +422,208 @@ describe('reducer edge cases', () => {
|
|
|
1262
422
|
// isSelected — family bundles (style packages within a family)
|
|
1263
423
|
// =========================================================================
|
|
1264
424
|
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
|
-
}
|
|
425
|
+
const WITH_COLLECTION_BUNDLE = {
|
|
426
|
+
...FAMILY_WITH_PACKAGES,
|
|
427
|
+
'coll-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1']],
|
|
428
|
+
'var-sku': [300, ['vf1']],
|
|
429
|
+
vs1: [100, ['vf1']]
|
|
430
|
+
};
|
|
1307
431
|
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
|
-
});
|
|
432
|
+
const state = select(makeState(FAMILY_WITH_PACKAGES), 'family-sku');
|
|
433
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
1325
434
|
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
1326
435
|
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1327
436
|
expect(isSelected('s1')(state)).toBe(true);
|
|
1328
437
|
expect(isSelected('si1')(state)).toBe(true);
|
|
1329
438
|
});
|
|
1330
439
|
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)
|
|
440
|
+
const state = select(makeState(WITH_COLLECTION_BUNDLE), 'family-sku');
|
|
441
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
1388
442
|
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
443
|
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
1392
444
|
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1393
|
-
// Individual styles should also show selected
|
|
1394
445
|
expect(isSelected('s1')(state)).toBe(true);
|
|
1395
446
|
});
|
|
1396
447
|
it('selecting one bundle then another upgrades to family via reducer', () => {
|
|
1397
|
-
|
|
448
|
+
let state = select(makeState(FAMILY_WITH_PACKAGES), 'pkg-upright-sku');
|
|
449
|
+
expect(selected(state)).toEqual(['pkg-upright-sku']);
|
|
1398
450
|
|
|
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);
|
|
451
|
+
// Family ($500) = two packages ($250 + $250): one SKU wins the tie
|
|
452
|
+
state = select(state, 'pkg-italic-sku');
|
|
453
|
+
expect(selected(state)).toEqual(['family-sku']);
|
|
454
|
+
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
455
|
+
expect(isSelected('pkg-italic-sku')(state)).toBe(true);
|
|
1419
456
|
});
|
|
1420
457
|
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);
|
|
458
|
+
const state = select(makeState(WITH_COLLECTION_BUNDLE), 'pkg-upright-sku', 'pkg-italic-sku');
|
|
459
|
+
expect(selected(state)).toEqual(['coll-bundle-sku']);
|
|
460
|
+
expect(isSelected('family-sku')(state)).toBe(true);
|
|
461
|
+
expect(isSelected('pkg-upright-sku')(state)).toBe(true);
|
|
462
|
+
expect(isSelected('s1')(state)).toBe(true);
|
|
463
|
+
expect(isSelected('vs1')(state)).toBe(true);
|
|
1497
464
|
});
|
|
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
|
-
});
|
|
465
|
+
it('transitive upgrade prefers the collection with the most font styles', () => {
|
|
466
|
+
const state = select(makeState({
|
|
467
|
+
...FAMILY_WITH_PACKAGES,
|
|
468
|
+
'small-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5']],
|
|
469
|
+
'big-bundle-sku': [500, ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1']],
|
|
470
|
+
'var-sku': [300, ['vf1']]
|
|
471
|
+
}), 'family-sku');
|
|
472
|
+
expect(selected(state)).toEqual(['big-bundle-sku']);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
1559
475
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
476
|
+
// =========================================================================
|
|
477
|
+
// POPULATE_PRODUCT_STATE
|
|
478
|
+
// =========================================================================
|
|
479
|
+
describe('POPULATE_PRODUCT_STATE', () => {
|
|
480
|
+
const sku = (id, amount) => ({
|
|
481
|
+
id,
|
|
482
|
+
price: {
|
|
483
|
+
amount
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
const style = (id, skuId) => ({
|
|
487
|
+
id,
|
|
488
|
+
sku: skuId ? sku(skuId, 40) : null
|
|
489
|
+
});
|
|
490
|
+
const textFamily = {
|
|
491
|
+
id: 'text',
|
|
492
|
+
name: 'Text',
|
|
493
|
+
sku: sku('text-sku', 500),
|
|
494
|
+
fontStyles: [style('f1', 's1'), style('f2', 's2')],
|
|
495
|
+
bundles: [{
|
|
496
|
+
id: 'upright',
|
|
497
|
+
name: 'Upright',
|
|
498
|
+
sku: sku('upright-sku', 60),
|
|
499
|
+
fontStyles: [style('f1', 's1')]
|
|
500
|
+
}]
|
|
501
|
+
};
|
|
502
|
+
const textVariable = {
|
|
503
|
+
id: 'text-var',
|
|
504
|
+
name: 'Text Variable',
|
|
505
|
+
sku: sku('text-var-sku', 300),
|
|
506
|
+
fontStyles: [style('vf1', null)],
|
|
507
|
+
bundles: []
|
|
508
|
+
};
|
|
509
|
+
const superfamily = {
|
|
510
|
+
node: {
|
|
511
|
+
__typename: 'FontCollection',
|
|
512
|
+
id: 'super',
|
|
513
|
+
name: 'Thunder',
|
|
514
|
+
sku: sku('super-sku', 750),
|
|
515
|
+
fontStyles: [],
|
|
516
|
+
bundles: [],
|
|
517
|
+
collectionBundles: [{
|
|
518
|
+
id: 'text-bundle',
|
|
519
|
+
name: 'Text + Variable',
|
|
520
|
+
sku: sku('text-bundle-sku', 500),
|
|
521
|
+
memberCollections: [textFamily, textVariable]
|
|
522
|
+
}],
|
|
523
|
+
children: [textFamily, textVariable]
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
it('records the styles every SKU grants', () => {
|
|
527
|
+
const state = reducer(makeState(), {
|
|
528
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
529
|
+
data: superfamily
|
|
530
|
+
});
|
|
531
|
+
expect(state.skuStyleIds).toEqual({
|
|
532
|
+
'super-sku': ['f1', 'f2', 'vf1'],
|
|
533
|
+
'text-bundle-sku': ['f1', 'f2', 'vf1'],
|
|
534
|
+
'text-sku': ['f1', 'f2'],
|
|
535
|
+
'text-var-sku': ['vf1'],
|
|
536
|
+
'upright-sku': ['f1'],
|
|
537
|
+
s1: ['f1'],
|
|
538
|
+
s2: ['f2']
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
it('prices a SKU picked before its product loaded once it has', () => {
|
|
542
|
+
const picked = select(makeState(), 'text-sku');
|
|
543
|
+
expect(selected(picked)).toEqual(['text-sku']);
|
|
544
|
+
const loaded = reducer(picked, {
|
|
545
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
546
|
+
data: superfamily
|
|
547
|
+
});
|
|
548
|
+
expect(selected(loaded)).toEqual(['text-bundle-sku']);
|
|
1565
549
|
});
|
|
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
|
-
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// =========================================================================
|
|
553
|
+
// Random catalogs – the selection is the cheapest possible, whatever the order
|
|
554
|
+
// =========================================================================
|
|
555
|
+
describe('selection on random catalogs', () => {
|
|
556
|
+
// mulberry32: a small seeded PRNG so failures reproduce
|
|
557
|
+
const random = seed => () => {
|
|
558
|
+
seed = seed + 0x6d2b79f5 | 0;
|
|
559
|
+
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
|
560
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
561
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// Two families of three styles, their family SKUs and a superfamily, plus a
|
|
565
|
+
// few bundles – some within a family, some across both.
|
|
566
|
+
const randomCatalog = rand => {
|
|
567
|
+
const pick = items => items[Math.floor(rand() * items.length)];
|
|
568
|
+
const styles = ['a1', 'a2', 'a3', 'b1', 'b2', 'b3'];
|
|
569
|
+
const catalog = {};
|
|
570
|
+
for (const styleId of styles) catalog[`s:${styleId}`] = [10 + Math.floor(rand() * 10), [styleId]];
|
|
571
|
+
const priceFor = styleIds => Math.round(styleIds.length * (6 + rand() * 12));
|
|
572
|
+
catalog['family:a'] = [priceFor(styles.slice(0, 3)), styles.slice(0, 3)];
|
|
573
|
+
catalog['family:b'] = [priceFor(styles.slice(3)), styles.slice(3)];
|
|
574
|
+
catalog['super'] = [priceFor(styles), styles];
|
|
575
|
+
for (let i = 0; i < 4; i++) {
|
|
576
|
+
const pool = pick([styles.slice(0, 3), styles.slice(3), styles]);
|
|
577
|
+
const styleIds = [...new Set([pick(pool), pick(pool), pick(pool)])];
|
|
578
|
+
if (styleIds.length > 1) catalog[`bundle:${i}`] = [priceFor(styleIds), styleIds];
|
|
579
|
+
}
|
|
580
|
+
return catalog;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Every subset of SKUs, keeping the cheapest in which each pick sits inside
|
|
584
|
+
// one SKU granting all its styles.
|
|
585
|
+
const bruteForceCost = (catalog, picks) => {
|
|
586
|
+
const skuIds = Object.keys(catalog);
|
|
587
|
+
let best = Infinity;
|
|
588
|
+
for (let subset = 0; subset < 1 << skuIds.length; subset++) {
|
|
589
|
+
const chosen = skuIds.filter((_, i) => subset & 1 << i);
|
|
590
|
+
const holds = pick => chosen.some(skuId => catalog[pick][1].every(s => catalog[skuId][1].includes(s)));
|
|
591
|
+
if (!picks.every(holds)) continue;
|
|
592
|
+
best = Math.min(best, chosen.reduce((sum, skuId) => sum + catalog[skuId][0], 0));
|
|
593
|
+
}
|
|
594
|
+
return best;
|
|
595
|
+
};
|
|
596
|
+
const shuffled = (items, rand) => items.map(item => [rand(), item]).sort((a, b) => a[0] - b[0]).map(_ref2 => {
|
|
597
|
+
let [, item] = _ref2;
|
|
598
|
+
return item;
|
|
599
|
+
});
|
|
600
|
+
it('matches brute force and ignores click order', () => {
|
|
601
|
+
const rand = random(1238);
|
|
602
|
+
for (let run = 0; run < 150; run++) {
|
|
603
|
+
const catalog = randomCatalog(rand);
|
|
604
|
+
const state = makeState(catalog);
|
|
605
|
+
const picks = shuffled(Object.keys(catalog), rand).slice(0, 1 + Math.floor(rand() * 4));
|
|
606
|
+
const cost = cheapestSkuIds(picks, state).reduce((sum, skuId) => sum + catalog[skuId][0], 0);
|
|
607
|
+
expect(cost, JSON.stringify({
|
|
608
|
+
catalog,
|
|
609
|
+
picks
|
|
610
|
+
})).toBe(bruteForceCost(catalog, picks));
|
|
611
|
+
const orders = [picks, shuffled(picks, rand), [...picks].reverse()];
|
|
612
|
+
const results = orders.map(order => selected(select(state, ...order)));
|
|
613
|
+
expect(results[1], JSON.stringify({
|
|
614
|
+
catalog,
|
|
615
|
+
orders
|
|
616
|
+
})).toEqual(results[0]);
|
|
617
|
+
expect(results[2], JSON.stringify({
|
|
618
|
+
catalog,
|
|
619
|
+
orders
|
|
620
|
+
})).toEqual(results[0]);
|
|
621
|
+
const chosen = select(state, ...picks);
|
|
622
|
+
for (const skuId of Object.keys(catalog)) {
|
|
623
|
+
const added = additionalCost(chosen, skuId);
|
|
624
|
+
expect(added).toBeGreaterThanOrEqual(0);
|
|
625
|
+
expect(added).toBeLessThanOrEqual(catalog[skuId][0]);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
1631
628
|
});
|
|
1632
629
|
});
|