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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock react-relay's graphql tag since productState.ts uses it at import time
|
|
4
|
+
vi.mock('react-relay', () => ({
|
|
5
|
+
graphql: () => null,
|
|
6
|
+
fetchQuery: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
import reducer, { isSelected } from '../reducer.js';
|
|
9
|
+
import { additionalCost } from '../selection.js';
|
|
10
|
+
function makeState() {
|
|
11
|
+
let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
12
|
+
return {
|
|
13
|
+
cartOpen: false,
|
|
14
|
+
precartOpen: false,
|
|
15
|
+
selectedSkuIds: {},
|
|
16
|
+
requestedSkuIds: [],
|
|
17
|
+
licenseOptions: [],
|
|
18
|
+
skuStyleIds: {},
|
|
19
|
+
collectionSkus: {},
|
|
20
|
+
productSkuIds: {},
|
|
21
|
+
viewedProductId: null,
|
|
22
|
+
skuPrices: {},
|
|
23
|
+
fetchedCollectionIds: [],
|
|
24
|
+
storeModalRoute: {
|
|
25
|
+
name: 'index'
|
|
26
|
+
},
|
|
27
|
+
storeModalHistory: [],
|
|
28
|
+
orderVariableSelections: [],
|
|
29
|
+
licenseeIsBillingIdentity: null,
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const sku = (id, amount) => ({
|
|
34
|
+
id,
|
|
35
|
+
price: {
|
|
36
|
+
amount
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const style = (id, skuId, amount) => ({
|
|
40
|
+
id,
|
|
41
|
+
sku: sku(skuId, amount)
|
|
42
|
+
});
|
|
43
|
+
const selected = state => Object.keys(state.selectedSkuIds).filter(skuId => state.selectedSkuIds[skuId]).sort();
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Fixture: "Starter Pack" = two styles picked out of Thunder Text (a family
|
|
47
|
+
// that is NOT a member as a whole) + all of Thunder Display (a whole-family
|
|
48
|
+
// member). Pack $400; styles $100 each; Display family $250.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
const packNode = {
|
|
51
|
+
__typename: 'FontPack',
|
|
52
|
+
id: 'pack-id',
|
|
53
|
+
name: 'Starter Pack',
|
|
54
|
+
sku: sku('pack-sku', 400),
|
|
55
|
+
fontStyles: [style('f1', 's1', 100), style('f2', 's2', 100), style('df1', 'ds1', 100), style('df2', 'ds2', 100)],
|
|
56
|
+
members: [{
|
|
57
|
+
__typename: 'FontStyle'
|
|
58
|
+
}, {
|
|
59
|
+
__typename: 'FontStyle'
|
|
60
|
+
}, {
|
|
61
|
+
__typename: 'FontCollection',
|
|
62
|
+
id: 'display-id',
|
|
63
|
+
name: 'Thunder Display',
|
|
64
|
+
sku: sku('display-sku', 250),
|
|
65
|
+
fontStyles: [style('df1', 'ds1', 100), style('df2', 'ds2', 100)],
|
|
66
|
+
bundles: null,
|
|
67
|
+
children: []
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
const populate = function (node) {
|
|
71
|
+
let state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : makeState();
|
|
72
|
+
return reducer(state, {
|
|
73
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
74
|
+
data: {
|
|
75
|
+
node
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const select = function (state, skuId) {
|
|
80
|
+
let isSelectedNow = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
81
|
+
return reducer(state, {
|
|
82
|
+
type: 'SELECT_SKU_ID',
|
|
83
|
+
skuId,
|
|
84
|
+
selected: isSelectedNow
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
describe('populateProductStateReducer with a font pack', () => {
|
|
88
|
+
const state = populate(packNode);
|
|
89
|
+
it('registers the pack SKU as granting every style the pack lists', () => {
|
|
90
|
+
expect(state.skuStyleIds['pack-sku']).toEqual(['f1', 'f2', 'df1', 'df2']);
|
|
91
|
+
});
|
|
92
|
+
it('registers a whole-family member like its own product page would', () => {
|
|
93
|
+
expect(state.skuStyleIds['display-sku']).toEqual(['df1', 'df2']);
|
|
94
|
+
expect(state.skuStyleIds['ds1']).toEqual(['df1']);
|
|
95
|
+
expect(state.skuStyleIds['ds2']).toEqual(['df2']);
|
|
96
|
+
});
|
|
97
|
+
it('registers each style member by its own SKU', () => {
|
|
98
|
+
expect(state.skuStyleIds['s1']).toEqual(['f1']);
|
|
99
|
+
expect(state.skuStyleIds['s2']).toEqual(['f2']);
|
|
100
|
+
});
|
|
101
|
+
it('records a price for every SKU seen', () => {
|
|
102
|
+
expect(state.skuPrices).toEqual({
|
|
103
|
+
'pack-sku': 400,
|
|
104
|
+
'display-sku': 250,
|
|
105
|
+
s1: 100,
|
|
106
|
+
s2: 100,
|
|
107
|
+
ds1: 100,
|
|
108
|
+
ds2: 100
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
it('maps the pack id to its SKU and marks it fetched', () => {
|
|
112
|
+
expect(state.collectionSkus['pack-id']).toBe('pack-sku');
|
|
113
|
+
expect(state.fetchedCollectionIds).toContain('pack-id');
|
|
114
|
+
});
|
|
115
|
+
it('ignores nodes that are neither a collection nor a pack', () => {
|
|
116
|
+
const before = makeState();
|
|
117
|
+
expect(populate({
|
|
118
|
+
__typename: '%other'
|
|
119
|
+
}, before)).toBe(before);
|
|
120
|
+
expect(populate(null, before)).toBe(before);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('selecting the pack SKU', () => {
|
|
124
|
+
const state = select(populate(packNode), 'pack-sku');
|
|
125
|
+
it('marks every member style selected', () => {
|
|
126
|
+
expect(selected(state)).toEqual(['pack-sku']);
|
|
127
|
+
for (const skuId of ['s1', 's2', 'ds1', 'ds2']) {
|
|
128
|
+
expect(isSelected(skuId)(state)).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
// by style id too, for buttons whose style has no SKU
|
|
131
|
+
expect(isSelected('f1')(state)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it('marks the whole-family member selected, as covered by the pack', () => {
|
|
134
|
+
expect(isSelected('display-sku')(state)).toBe(true);
|
|
135
|
+
expect(state.selectedSkuIds['display-sku']).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
it('deselecting a member style deselects the pack', () => {
|
|
138
|
+
const after = select(state, 'ds1', false);
|
|
139
|
+
expect(selected(after)).toEqual([]);
|
|
140
|
+
expect(isSelected('s1')(after)).toBe(false);
|
|
141
|
+
expect(isSelected('display-sku')(after)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('selecting member styles one by one', () => {
|
|
145
|
+
it('buys the pack once it is the cheaper buy', () => {
|
|
146
|
+
let state = populate(packNode);
|
|
147
|
+
state = select(state, 's1');
|
|
148
|
+
state = select(state, 's2');
|
|
149
|
+
state = select(state, 'ds1');
|
|
150
|
+
// $300 of styles; the pack at $400 is still dearer
|
|
151
|
+
expect(selected(state)).toEqual(['ds1', 's1', 's2']);
|
|
152
|
+
expect(additionalCost(state, 'ds2')).toBe(100);
|
|
153
|
+
state = select(state, 'ds2');
|
|
154
|
+
// every style: $400 = the pack, which wins the tie as one SKU
|
|
155
|
+
expect(selected(state)).toEqual(['pack-sku']);
|
|
156
|
+
for (const skuId of ['s1', 's2', 'ds1', 'ds2']) {
|
|
157
|
+
expect(isSelected(skuId)(state)).toBe(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
it('buys the pack when the whole-family member completes it', () => {
|
|
161
|
+
let state = populate(packNode);
|
|
162
|
+
state = select(state, 's1');
|
|
163
|
+
state = select(state, 's2');
|
|
164
|
+
|
|
165
|
+
// Display family is $250; the pack costs $200 more than what's selected
|
|
166
|
+
expect(additionalCost(state, 'display-sku')).toBe(200);
|
|
167
|
+
state = select(state, 'display-sku');
|
|
168
|
+
expect(selected(state)).toEqual(['pack-sku']);
|
|
169
|
+
expect(isSelected('display-sku')(state)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
it('does not buy the pack while it is dearer than the styles', () => {
|
|
172
|
+
let state = populate({
|
|
173
|
+
...packNode,
|
|
174
|
+
sku: sku('pack-sku', 1000)
|
|
175
|
+
});
|
|
176
|
+
for (const skuId of ['s1', 's2', 'ds1', 'ds2']) state = select(state, skuId);
|
|
177
|
+
// nor Display: $250 is more than its two $100 styles
|
|
178
|
+
expect(selected(state)).toEqual(['ds1', 'ds2', 's1', 's2']);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('a superfamily member', () => {
|
|
182
|
+
const superPackNode = {
|
|
183
|
+
...packNode,
|
|
184
|
+
fontStyles: [style('f1', 's1', 100), style('f2', 's2', 100), style('cf1', 'cs1', 100)],
|
|
185
|
+
members: [{
|
|
186
|
+
__typename: 'FontCollection',
|
|
187
|
+
id: 'super-id',
|
|
188
|
+
name: 'Thunder Collection',
|
|
189
|
+
sku: sku('super-sku', 350),
|
|
190
|
+
fontStyles: [],
|
|
191
|
+
bundles: null,
|
|
192
|
+
children: [{
|
|
193
|
+
id: 'text-id',
|
|
194
|
+
name: 'Thunder Text',
|
|
195
|
+
sku: sku('text-sku', 180),
|
|
196
|
+
fontStyles: [style('f1', 's1', 100), style('f2', 's2', 100)],
|
|
197
|
+
bundles: [{
|
|
198
|
+
id: 'bundle-id',
|
|
199
|
+
name: 'Thunder Text Bundle',
|
|
200
|
+
sku: sku('bundle-sku', 150),
|
|
201
|
+
fontStyles: [style('f1', 's1', 100), style('f2', 's2', 100)]
|
|
202
|
+
}]
|
|
203
|
+
}, {
|
|
204
|
+
id: 'caption-id',
|
|
205
|
+
name: 'Thunder Caption',
|
|
206
|
+
sku: sku('caption-sku', 90),
|
|
207
|
+
fontStyles: [style('cf1', 'cs1', 100)],
|
|
208
|
+
bundles: null
|
|
209
|
+
}]
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
const state = populate(superPackNode);
|
|
213
|
+
it('registers the superfamily, its families and their bundles under the pack', () => {
|
|
214
|
+
expect(state.skuStyleIds['pack-sku']).toEqual(['f1', 'f2', 'cf1']);
|
|
215
|
+
expect(state.skuStyleIds['super-sku']).toEqual(['f1', 'f2', 'cf1']);
|
|
216
|
+
expect(state.skuStyleIds['text-sku']).toEqual(['f1', 'f2']);
|
|
217
|
+
expect(state.skuStyleIds['bundle-sku']).toEqual(['f1', 'f2']);
|
|
218
|
+
expect(state.skuStyleIds['caption-sku']).toEqual(['cf1']);
|
|
219
|
+
});
|
|
220
|
+
it('selecting the pack marks the families and bundles selected', () => {
|
|
221
|
+
const after = select(state, 'pack-sku');
|
|
222
|
+
for (const skuId of ['super-sku', 'text-sku', 'caption-sku', 'bundle-sku', 's1', 'cs1']) {
|
|
223
|
+
expect(isSelected(skuId)(after)).toBe(true);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Every product a buyer has seen – or has a BuyButton for on the host page –
|
|
230
|
+
// stays registered, so on a product page a pick the page shows only sits
|
|
231
|
+
// inside SKUs the page shows. The page reports the product it shows.
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
const viewing = (state, productId) => reducer(state, {
|
|
234
|
+
type: 'STORE_MODAL_VIEW_PRODUCT',
|
|
235
|
+
productId
|
|
236
|
+
});
|
|
237
|
+
const family = (id, name, skuId, amount, styles) => ({
|
|
238
|
+
__typename: 'FontCollection',
|
|
239
|
+
id,
|
|
240
|
+
name,
|
|
241
|
+
sku: sku(skuId, amount),
|
|
242
|
+
fontStyles: styles,
|
|
243
|
+
bundles: null,
|
|
244
|
+
children: []
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Display at $180 is bought once both its $100 styles are picked; a $150 pack
|
|
248
|
+
// over it would be cheaper still.
|
|
249
|
+
const displayAt180 = {
|
|
250
|
+
...packNode.members[2],
|
|
251
|
+
sku: sku('display-sku', 180)
|
|
252
|
+
};
|
|
253
|
+
const cheapPack = {
|
|
254
|
+
...packNode,
|
|
255
|
+
sku: sku('pack-sku', 150),
|
|
256
|
+
members: [displayAt180]
|
|
257
|
+
};
|
|
258
|
+
describe('on a product page', () => {
|
|
259
|
+
it("buys a family's styles through the family, not a pack seen earlier", () => {
|
|
260
|
+
// the pack page registered Display as a member; its own page then
|
|
261
|
+
// registers it as the product being viewed
|
|
262
|
+
const display = family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]);
|
|
263
|
+
let state = viewing(populate(display, populate(cheapPack)), 'display-id');
|
|
264
|
+
state = select(state, 'ds1');
|
|
265
|
+
expect(selected(state)).toEqual(['ds1']);
|
|
266
|
+
state = select(state, 'ds2');
|
|
267
|
+
expect(selected(state)).toEqual(['display-sku']);
|
|
268
|
+
expect(isSelected('ds1')(state)).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
it("does not buy the pack page's partial block through a family seen earlier", () => {
|
|
271
|
+
const text = family('text-id', 'Thunder Text', 'text-sku', 180, [style('f1', 's1', 100), style('f2', 's2', 100)]);
|
|
272
|
+
let state = viewing(populate(packNode, populate(text)), 'pack-id');
|
|
273
|
+
state = select(state, 's1');
|
|
274
|
+
state = select(state, 's2');
|
|
275
|
+
expect(selected(state)).toEqual(['s1', 's2']);
|
|
276
|
+
});
|
|
277
|
+
it('still buys a whole-family member through its own SKU on the pack page', () => {
|
|
278
|
+
let state = viewing(populate({
|
|
279
|
+
...packNode,
|
|
280
|
+
members: [displayAt180]
|
|
281
|
+
}), 'pack-id');
|
|
282
|
+
state = select(state, 'ds1');
|
|
283
|
+
state = select(state, 'ds2');
|
|
284
|
+
expect(selected(state)).toEqual(['display-sku']);
|
|
285
|
+
});
|
|
286
|
+
it("keeps a family that has no SKU of its own to its page, so a pack seen earlier can't claim it", () => {
|
|
287
|
+
// Display sells à la carte only: there is no family SKU on its page, and
|
|
288
|
+
// the pack another page registered must not stand in for one
|
|
289
|
+
const display = {
|
|
290
|
+
...family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]),
|
|
291
|
+
sku: null
|
|
292
|
+
};
|
|
293
|
+
let state = viewing(populate(display, populate(cheapPack)), 'display-id');
|
|
294
|
+
state = select(state, 'ds1');
|
|
295
|
+
expect(additionalCost(state, 'ds2')).toBe(100);
|
|
296
|
+
state = select(state, 'ds2');
|
|
297
|
+
expect(selected(state)).toEqual(['ds1', 'ds2']);
|
|
298
|
+
});
|
|
299
|
+
it('scopes a page opened by slug, once it reports the product it shows', () => {
|
|
300
|
+
const display = {
|
|
301
|
+
...family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]),
|
|
302
|
+
sku: null
|
|
303
|
+
};
|
|
304
|
+
let state = reducer(populate(display, populate(cheapPack)), {
|
|
305
|
+
type: 'STORE_MODAL_NAVIGATE',
|
|
306
|
+
route: 'product',
|
|
307
|
+
variables: {
|
|
308
|
+
productSlug: 'thunder-display'
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
state = viewing(state, 'display-id');
|
|
312
|
+
state = select(select(state, 'ds1'), 'ds2');
|
|
313
|
+
expect(selected(state)).toEqual(['ds1', 'ds2']);
|
|
314
|
+
});
|
|
315
|
+
it('keeps every registered SKU once the page is left', () => {
|
|
316
|
+
const display = {
|
|
317
|
+
...family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]),
|
|
318
|
+
sku: null
|
|
319
|
+
};
|
|
320
|
+
let state = viewing(populate(display, populate(cheapPack)), 'display-id');
|
|
321
|
+
state = select(select(state, 'ds1'), 'ds2');
|
|
322
|
+
expect(selected(viewing(state, null))).toEqual(['pack-sku']);
|
|
323
|
+
});
|
|
324
|
+
it('re-prices when the page changes', () => {
|
|
325
|
+
const display = {
|
|
326
|
+
...family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]),
|
|
327
|
+
sku: null
|
|
328
|
+
};
|
|
329
|
+
let state = viewing(populate(display, populate(cheapPack)), 'display-id');
|
|
330
|
+
state = select(select(state, 'ds1'), 'ds2');
|
|
331
|
+
expect(selected(state)).toEqual(['ds1', 'ds2']);
|
|
332
|
+
// on the pack page the pack is shown, and cheaper
|
|
333
|
+
expect(selected(viewing(state, 'pack-id'))).toEqual(['pack-sku']);
|
|
334
|
+
});
|
|
335
|
+
it('keeps a pick in a SKU identical to it that the page does not show', () => {
|
|
336
|
+
// a one-style pack at a style's price is the same purchase as the style;
|
|
337
|
+
// only one of the two is offered, and it may be the one off this page
|
|
338
|
+
const oneStylePack = {
|
|
339
|
+
...packNode,
|
|
340
|
+
id: 'a-pack-id',
|
|
341
|
+
sku: sku('a-pack-sku', 100),
|
|
342
|
+
fontStyles: [style('df1', 'ds1', 100)],
|
|
343
|
+
members: [{
|
|
344
|
+
__typename: 'FontStyle'
|
|
345
|
+
}]
|
|
346
|
+
};
|
|
347
|
+
const display = family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100), style('df2', 'ds2', 100)]);
|
|
348
|
+
let state = viewing(populate(display, populate(oneStylePack)), 'display-id');
|
|
349
|
+
state = select(state, 'ds1');
|
|
350
|
+
// not pushed into the $180 family for want of a place to sit
|
|
351
|
+
expect(selected(state)).toHaveLength(1);
|
|
352
|
+
expect(isSelected('ds1')(state)).toBe(true);
|
|
353
|
+
expect(additionalCost(state, 'ds2')).toBe(80);
|
|
354
|
+
});
|
|
355
|
+
it('remembers every SKU each product page registered', () => {
|
|
356
|
+
const display = family('display-id', 'Thunder Display', 'display-sku', 180, [style('df1', 'ds1', 100)]);
|
|
357
|
+
const state = populate(display, populate(cheapPack));
|
|
358
|
+
// the pack, its two Thunder Text styles, and the whole-family member with its styles
|
|
359
|
+
expect([...state.productSkuIds['pack-id']].sort()).toEqual(['display-sku', 'ds1', 'ds2', 'pack-sku', 's1', 's2']);
|
|
360
|
+
expect([...state.productSkuIds['display-id']].sort()).toEqual(['display-sku', 'ds1']);
|
|
361
|
+
expect(populate({
|
|
362
|
+
...display,
|
|
363
|
+
sku: null
|
|
364
|
+
}).productSkuIds['display-id']).toEqual(['ds1']);
|
|
365
|
+
});
|
|
366
|
+
it('keeps every registered SKU for a pick the page does not show', () => {
|
|
367
|
+
// a price shown on the host page while the modal sits on another product
|
|
368
|
+
const other = family('other-id', 'Other', 'other-sku', 150, [style('o1', 'os1', 100), style('o2', 'os2', 100)]);
|
|
369
|
+
let state = viewing(populate(packNode, populate(other)), 'pack-id');
|
|
370
|
+
state = select(state, 'os1');
|
|
371
|
+
expect(additionalCost(state, 'os2')).toBe(50);
|
|
372
|
+
});
|
|
373
|
+
it('keeps every registered SKU while the viewed product is not yet registered', () => {
|
|
374
|
+
let state = viewing(populate(cheapPack), 'not-fetched-id');
|
|
375
|
+
state = select(state, 'ds1');
|
|
376
|
+
state = select(state, 'ds2');
|
|
377
|
+
expect(selected(state)).toEqual(['pack-sku']);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
describe('a pack without a SKU', () => {
|
|
381
|
+
const state = populate({
|
|
382
|
+
...packNode,
|
|
383
|
+
sku: null
|
|
384
|
+
});
|
|
385
|
+
it('registers only its members', () => {
|
|
386
|
+
expect(state.skuStyleIds['pack-sku']).toBeUndefined();
|
|
387
|
+
expect(state.skuPrices['pack-sku']).toBeUndefined();
|
|
388
|
+
expect(state.collectionSkus['pack-id']).toBeNull();
|
|
389
|
+
expect(state.fetchedCollectionIds).toContain('pack-id');
|
|
390
|
+
});
|
|
391
|
+
it('never puts the pack in the selection', () => {
|
|
392
|
+
let after = viewing(state, 'pack-id');
|
|
393
|
+
for (const skuId of ['s1', 's2', 'ds1', 'ds2']) after = select(after, skuId);
|
|
394
|
+
expect(selected(after)).toEqual(['ds1', 'ds2', 's1', 's2']);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
describe('a style member without a SKU', () => {
|
|
398
|
+
const state = populate({
|
|
399
|
+
...packNode,
|
|
400
|
+
fontStyles: [...packNode.fontStyles, {
|
|
401
|
+
id: 'f3',
|
|
402
|
+
sku: null
|
|
403
|
+
}]
|
|
404
|
+
});
|
|
405
|
+
it('is granted by the pack SKU, by style id', () => {
|
|
406
|
+
expect(state.skuStyleIds['pack-sku']).toContain('f3');
|
|
407
|
+
});
|
|
408
|
+
it('is selected and deselected with the pack', () => {
|
|
409
|
+
const withPack = select(state, 'pack-sku');
|
|
410
|
+
expect(isSelected('f3')(withPack)).toBe(true);
|
|
411
|
+
expect(isSelected('f3')(select(withPack, 'pack-sku', false))).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
describe('deselecting via the pack button', () => {
|
|
415
|
+
const state = select(select(populate(packNode), 'pack-sku'), 'pack-sku', false);
|
|
416
|
+
it('clears the whole pack', () => {
|
|
417
|
+
expect(selected(state)).toEqual([]);
|
|
418
|
+
for (const skuId of ['s1', 's2', 'ds1', 'ds2', 'display-sku', 'f1']) {
|
|
419
|
+
expect(isSelected(skuId)(state)).toBe(false);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
describe("a '%other' member", () => {
|
|
424
|
+
it('is ignored', () => {
|
|
425
|
+
const withOther = populate({
|
|
426
|
+
...packNode,
|
|
427
|
+
members: [...packNode.members, {
|
|
428
|
+
__typename: '%other'
|
|
429
|
+
}]
|
|
430
|
+
});
|
|
431
|
+
const without = populate(packNode);
|
|
432
|
+
expect(withOther.skuStyleIds).toEqual(without.skuStyleIds);
|
|
433
|
+
expect(withOther.skuPrices).toEqual(without.skuPrices);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act, useRef } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import useInfiniteScroll from '../components/useInfiniteScroll.js';
|
|
6
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
class FakeIntersectionObserver {
|
|
8
|
+
static instances = [];
|
|
9
|
+
observed = [];
|
|
10
|
+
disconnected = false;
|
|
11
|
+
constructor(callback, options) {
|
|
12
|
+
this.callback = callback;
|
|
13
|
+
this.options = options;
|
|
14
|
+
FakeIntersectionObserver.instances.push(this);
|
|
15
|
+
}
|
|
16
|
+
observe(element) {
|
|
17
|
+
this.observed.push(element);
|
|
18
|
+
}
|
|
19
|
+
unobserve() {}
|
|
20
|
+
disconnect() {
|
|
21
|
+
this.disconnected = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Report every observed element as having come into view.
|
|
25
|
+
enter() {
|
|
26
|
+
this.callback(this.observed.map(target => ({
|
|
27
|
+
target,
|
|
28
|
+
isIntersecting: true
|
|
29
|
+
})), this);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const observers = () => FakeIntersectionObserver.instances;
|
|
33
|
+
const live = () => observers().filter(observer => !observer.disconnected);
|
|
34
|
+
|
|
35
|
+
// The shape the store modal index renders: the sentinel stays mounted while a
|
|
36
|
+
// page is in flight (`enabled` false) and only goes away once the list is
|
|
37
|
+
// exhausted, and the scroller is an ancestor whose ref is still null while the
|
|
38
|
+
// sentinel renders.
|
|
39
|
+
function Harness(_ref) {
|
|
40
|
+
let {
|
|
41
|
+
enabled,
|
|
42
|
+
hasMore = true,
|
|
43
|
+
onLoadMore
|
|
44
|
+
} = _ref;
|
|
45
|
+
const scrollContainer = useRef(null);
|
|
46
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
47
|
+
ref: scrollContainer,
|
|
48
|
+
"data-testid": "scroller"
|
|
49
|
+
}, /*#__PURE__*/React.createElement(Sentinel, {
|
|
50
|
+
enabled: enabled,
|
|
51
|
+
hasMore: hasMore,
|
|
52
|
+
onLoadMore: onLoadMore,
|
|
53
|
+
rootRef: scrollContainer
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function Sentinel(_ref2) {
|
|
57
|
+
let {
|
|
58
|
+
enabled,
|
|
59
|
+
hasMore,
|
|
60
|
+
onLoadMore,
|
|
61
|
+
rootRef
|
|
62
|
+
} = _ref2;
|
|
63
|
+
const sentinelRef = useInfiniteScroll({
|
|
64
|
+
enabled,
|
|
65
|
+
onLoadMore,
|
|
66
|
+
rootRef,
|
|
67
|
+
rootMargin: '0px 0px 600px 0px'
|
|
68
|
+
});
|
|
69
|
+
return hasMore ? /*#__PURE__*/React.createElement("div", {
|
|
70
|
+
ref: sentinelRef,
|
|
71
|
+
"data-testid": "sentinel"
|
|
72
|
+
}) : null;
|
|
73
|
+
}
|
|
74
|
+
let container;
|
|
75
|
+
let root;
|
|
76
|
+
const render = element => {
|
|
77
|
+
act(() => {
|
|
78
|
+
root.render(element);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
FakeIntersectionObserver.instances = [];
|
|
83
|
+
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver);
|
|
84
|
+
container = document.createElement('div');
|
|
85
|
+
document.body.appendChild(container);
|
|
86
|
+
root = createRoot(container);
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
act(() => root.unmount());
|
|
90
|
+
container.remove();
|
|
91
|
+
vi.unstubAllGlobals();
|
|
92
|
+
});
|
|
93
|
+
describe('useInfiniteScroll', () => {
|
|
94
|
+
it('loads more when the sentinel comes into view', () => {
|
|
95
|
+
const onLoadMore = vi.fn();
|
|
96
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
97
|
+
enabled: true,
|
|
98
|
+
onLoadMore: onLoadMore
|
|
99
|
+
}));
|
|
100
|
+
expect(live()).toHaveLength(1);
|
|
101
|
+
act(() => live()[0].enter());
|
|
102
|
+
expect(onLoadMore).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
it('watches the scrolling ancestor, whose ref is only attached after render', () => {
|
|
105
|
+
var _options, _options2;
|
|
106
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
107
|
+
enabled: true,
|
|
108
|
+
onLoadMore: vi.fn()
|
|
109
|
+
}));
|
|
110
|
+
const [observer] = live();
|
|
111
|
+
expect((_options = observer.options) === null || _options === void 0 ? void 0 : _options.root).toBe(container.querySelector('[data-testid="scroller"]'));
|
|
112
|
+
expect((_options2 = observer.options) === null || _options2 === void 0 ? void 0 : _options2.rootMargin).toBe('0px 0px 600px 0px');
|
|
113
|
+
expect(observer.observed).toEqual([container.querySelector('[data-testid="sentinel"]')]);
|
|
114
|
+
});
|
|
115
|
+
it('stops watching while a page is in flight, and picks it up again after', () => {
|
|
116
|
+
const onLoadMore = vi.fn();
|
|
117
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
118
|
+
enabled: true,
|
|
119
|
+
onLoadMore: onLoadMore
|
|
120
|
+
}));
|
|
121
|
+
act(() => live()[0].enter());
|
|
122
|
+
expect(onLoadMore).toHaveBeenCalledTimes(1);
|
|
123
|
+
|
|
124
|
+
// The sentinel is still on screen, but `loadNext` is running: nothing
|
|
125
|
+
// should be watching it, so the load isn't asked for again on every frame.
|
|
126
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
127
|
+
enabled: false,
|
|
128
|
+
onLoadMore: onLoadMore
|
|
129
|
+
}));
|
|
130
|
+
expect(live()).toHaveLength(0);
|
|
131
|
+
|
|
132
|
+
// Page arrived, more to come: back to watching, and still in view.
|
|
133
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
134
|
+
enabled: true,
|
|
135
|
+
onLoadMore: onLoadMore
|
|
136
|
+
}));
|
|
137
|
+
expect(live()).toHaveLength(1);
|
|
138
|
+
act(() => live()[0].enter());
|
|
139
|
+
expect(onLoadMore).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
it('never observes when there is nothing left to load', () => {
|
|
142
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
143
|
+
enabled: false,
|
|
144
|
+
hasMore: false,
|
|
145
|
+
onLoadMore: vi.fn()
|
|
146
|
+
}));
|
|
147
|
+
expect(observers()).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
it('disconnects when the sentinel is removed', () => {
|
|
150
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
151
|
+
enabled: true,
|
|
152
|
+
onLoadMore: vi.fn()
|
|
153
|
+
}));
|
|
154
|
+
const [observer] = observers();
|
|
155
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
156
|
+
enabled: true,
|
|
157
|
+
hasMore: false,
|
|
158
|
+
onLoadMore: vi.fn()
|
|
159
|
+
}));
|
|
160
|
+
expect(observer.disconnected).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it('keeps watching when only the callback identity changes', () => {
|
|
163
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
164
|
+
enabled: true,
|
|
165
|
+
onLoadMore: () => {}
|
|
166
|
+
}));
|
|
167
|
+
const [observer] = observers();
|
|
168
|
+
const onLoadMore = vi.fn();
|
|
169
|
+
render( /*#__PURE__*/React.createElement(Harness, {
|
|
170
|
+
enabled: true,
|
|
171
|
+
onLoadMore: onLoadMore
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
// Same observer, and it calls the callback the latest render passed.
|
|
175
|
+
expect(observers()).toHaveLength(1);
|
|
176
|
+
expect(observer.disconnected).toBe(false);
|
|
177
|
+
act(() => observer.enter());
|
|
178
|
+
expect(onLoadMore).toHaveBeenCalledTimes(1);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -10,9 +10,12 @@ function makeState() {
|
|
|
10
10
|
cartOpen: false,
|
|
11
11
|
precartOpen: false,
|
|
12
12
|
selectedSkuIds: {},
|
|
13
|
+
requestedSkuIds: [],
|
|
13
14
|
licenseOptions: [],
|
|
14
|
-
|
|
15
|
+
skuStyleIds: {},
|
|
15
16
|
collectionSkus: {},
|
|
17
|
+
productSkuIds: {},
|
|
18
|
+
viewedProductId: null,
|
|
16
19
|
skuPrices: {},
|
|
17
20
|
fetchedCollectionIds: [],
|
|
18
21
|
storeModalRoute: {
|