fontdue-js 3.5.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
- package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
- package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/customerSession.test.js +456 -0
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storeModalRoutes.test.js +118 -0
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/stripeElements.test.js +173 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
- package/dist/__tests__/typeTesterTruncate.test.js +218 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +139 -0
- package/dist/auth/tokenStore.d.ts +61 -0
- package/dist/auth/tokenStore.js +162 -0
- package/dist/components/Cart/Checkout.js +19 -9
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/ConfigContext.d.ts +2 -2
- package/dist/components/CustomerSignIn/index.d.ts +33 -0
- package/dist/components/CustomerSignIn/index.js +170 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/NewsletterSignup/index.js +5 -1
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/index.js +4 -34
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
- package/dist/components/StoreModal/StoreModalProduct.js +69 -16
- package/dist/components/StoreModal/index.js +14 -14
- package/dist/components/StoreModal/routes.js +9 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TestFontsForm/index.js +5 -1
- package/dist/components/TypeTester/TypeTesterContent.js +9 -2
- package/dist/components/TypeTester/TypeTesterContext.js +17 -4
- package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
- package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
- package/dist/components/TypeTester/index.d.ts +3 -1
- package/dist/components/TypeTester/index.js +2 -1
- package/dist/components/TypeTester/truncate.d.ts +12 -0
- package/dist/components/TypeTester/truncate.js +95 -0
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
- package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
- package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
- package/dist/components/TypeTesters/index.d.ts +2 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
- package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +63 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/hooks/useCurrentCustomer.d.ts +34 -0
- package/dist/hooks/useCurrentCustomer.js +93 -0
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/relay/environment.js +91 -50
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/storeModalRoutes.d.ts +38 -0
- package/dist/storeModalRoutes.js +110 -0
- package/dist/stripeElements.d.ts +18 -0
- package/dist/stripeElements.js +49 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
- package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
- package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
- package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
- package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
- package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
- package/dist/components/CartButton/index.server.d.ts +0 -3
- package/dist/components/CartButton/index.server.js +0 -18
- package/dist/components/CorsErrorModal.d.ts +0 -1
- package/dist/components/CorsErrorModal.js +0 -92
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
- package/dist/components/ThemeConfig/index.server.d.ts +0 -2
- package/dist/components/ThemeConfig/index.server.js +0 -17
- package/dist/components/TypeTester/features.d.ts +0 -5
- package/dist/components/TypeTester/features.js +0 -143
- package/dist/data/unicodeData.d.ts +0 -4
- package/dist/data/unicodeData.js +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { storeModalRouteAction, storeModalUrlActions } from "../storeModalRoutes.js";
|
|
3
|
+
const config = indexExcludeTags => ({
|
|
4
|
+
storeModal: {
|
|
5
|
+
indexExcludeTags
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
const OPEN = {
|
|
9
|
+
type: 'OPEN_CART'
|
|
10
|
+
};
|
|
11
|
+
const PRODUCT_ID = 'Rm9udENvbGxlY3Rpb246NzUzNzU2MDgwNjI1OTk4ODQ5';
|
|
12
|
+
describe('storeModalRouteAction', () => {
|
|
13
|
+
it('carries the configured excludeTags onto the index route', () => {
|
|
14
|
+
expect(storeModalRouteAction('index', config(['unlisted']))).toEqual({
|
|
15
|
+
type: 'STORE_MODAL_REPLACE',
|
|
16
|
+
route: 'index',
|
|
17
|
+
variables: {
|
|
18
|
+
excludeTags: ['unlisted']
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('routes to the cart, with or without a trailing slash', () => {
|
|
23
|
+
const cart = {
|
|
24
|
+
type: 'STORE_MODAL_REPLACE',
|
|
25
|
+
route: 'cart'
|
|
26
|
+
};
|
|
27
|
+
expect(storeModalRouteAction('cart', config())).toEqual(cart);
|
|
28
|
+
expect(storeModalRouteAction('cart/', config())).toEqual(cart);
|
|
29
|
+
});
|
|
30
|
+
it('pulls the collection id out of a product route', () => {
|
|
31
|
+
expect(storeModalRouteAction(`product/${PRODUCT_ID}`, config())).toEqual({
|
|
32
|
+
type: 'STORE_MODAL_REPLACE',
|
|
33
|
+
route: 'product',
|
|
34
|
+
variables: {
|
|
35
|
+
productId: PRODUCT_ID
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// A slug and a global id overlap in their character sets, so the two are
|
|
41
|
+
// told apart by decoding, not by a pattern over the raw string.
|
|
42
|
+
it('treats a product that is not a global id as a slug', () => {
|
|
43
|
+
for (const slug of ['example-sans', 'examplesans', 'test', 'example_mono', 'a']) {
|
|
44
|
+
expect(storeModalRouteAction(`product/${slug}`, config())).toEqual({
|
|
45
|
+
type: 'STORE_MODAL_REPLACE',
|
|
46
|
+
route: 'product',
|
|
47
|
+
variables: {
|
|
48
|
+
productSlug: slug
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('does not mistake base64 of something else for a global id', () => {
|
|
54
|
+
// atob('c29tZXRoaW5n') === 'something' — decodable, but not `Type:id`.
|
|
55
|
+
expect(storeModalRouteAction('product/c29tZXRoaW5n', config())).toEqual({
|
|
56
|
+
type: 'STORE_MODAL_REPLACE',
|
|
57
|
+
route: 'product',
|
|
58
|
+
variables: {
|
|
59
|
+
productSlug: 'c29tZXRoaW5n'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it('returns null for a route it does not recognise', () => {
|
|
64
|
+
expect(storeModalRouteAction('checkout', config())).toBeNull();
|
|
65
|
+
expect(storeModalRouteAction('product/', config())).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('storeModalUrlActions', () => {
|
|
69
|
+
it('opens the modal on its default route for ?store=open', () => {
|
|
70
|
+
expect(storeModalUrlActions('?store=open', config(['unlisted']))).toEqual([OPEN]);
|
|
71
|
+
});
|
|
72
|
+
it('opens the modal on a product for ?store=product/<id>', () => {
|
|
73
|
+
expect(storeModalUrlActions(`?store=product/${PRODUCT_ID}`, config())).toEqual([{
|
|
74
|
+
type: 'STORE_MODAL_REPLACE',
|
|
75
|
+
route: 'product',
|
|
76
|
+
variables: {
|
|
77
|
+
productId: PRODUCT_ID
|
|
78
|
+
}
|
|
79
|
+
}, OPEN]);
|
|
80
|
+
});
|
|
81
|
+
it('opens the modal on a product for ?store=product/<slug>', () => {
|
|
82
|
+
expect(storeModalUrlActions('?store=product/example-sans', config())).toEqual([{
|
|
83
|
+
type: 'STORE_MODAL_REPLACE',
|
|
84
|
+
route: 'product',
|
|
85
|
+
variables: {
|
|
86
|
+
productSlug: 'example-sans'
|
|
87
|
+
}
|
|
88
|
+
}, OPEN]);
|
|
89
|
+
});
|
|
90
|
+
it('opens the modal on the cart for ?store=cart', () => {
|
|
91
|
+
expect(storeModalUrlActions('?store=cart', config())).toEqual([{
|
|
92
|
+
type: 'STORE_MODAL_REPLACE',
|
|
93
|
+
route: 'cart'
|
|
94
|
+
}, OPEN]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Undocumented on purpose — `?store=cart` is the form the docs teach — but
|
|
98
|
+
// shared-cart links in the wild redirect to `?cart=open`, so it has to keep
|
|
99
|
+
// working. Don't remove this without retiring that redirect first.
|
|
100
|
+
it('still honours the older ?cart=open', () => {
|
|
101
|
+
expect(storeModalUrlActions('?cart=open', config())).toEqual([{
|
|
102
|
+
type: 'STORE_MODAL_REPLACE',
|
|
103
|
+
route: 'cart'
|
|
104
|
+
}, OPEN]);
|
|
105
|
+
});
|
|
106
|
+
it('reads its parameter from among others', () => {
|
|
107
|
+
expect(storeModalUrlActions('?utm_source=newsletter&store=open&page=2', config())).toEqual([OPEN]);
|
|
108
|
+
});
|
|
109
|
+
it('leaves a ?store= value of its own alone', () => {
|
|
110
|
+
// `store` is a plain enough parameter name that a site may already use it.
|
|
111
|
+
expect(storeModalUrlActions('?store=london', config())).toEqual([]);
|
|
112
|
+
expect(storeModalUrlActions('?store=42', config())).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
it('does nothing when no parameter asks for the modal', () => {
|
|
115
|
+
expect(storeModalUrlActions('', config())).toEqual([]);
|
|
116
|
+
expect(storeModalUrlActions('?cart=closed', config())).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// @vitest-environment-options { "url": "https://store.example.com/fonts/archivo" }
|
|
3
|
+
|
|
4
|
+
// The browser-only storefront moments reach the server through
|
|
5
|
+
// `trackStorefrontEvent`. A buyer clicks through licences in bursts and React
|
|
6
|
+
// re-runs effects, so what matters here is what *doesn't* get reported: the
|
|
7
|
+
// steps before a configuration settles, a configuration already reported, and
|
|
8
|
+
// a second look at the same collection that is really one mount.
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
const commitMutation = vi.fn();
|
|
12
|
+
vi.mock('react-relay', () => ({
|
|
13
|
+
graphql: () => null,
|
|
14
|
+
commitMutation: function () {
|
|
15
|
+
return commitMutation(...arguments);
|
|
16
|
+
},
|
|
17
|
+
useRelayEnvironment: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
import { licenseSelectionsFromOptions, trackPaymentInfoEntered, trackProductCustomized, trackProductViewed } from '../components/Cart/storefrontEvents.js';
|
|
20
|
+
const environment = {};
|
|
21
|
+
function reportedInputs() {
|
|
22
|
+
return commitMutation.mock.calls.map(_ref => {
|
|
23
|
+
let [, config] = _ref;
|
|
24
|
+
return config.variables.input;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
commitMutation.mockReset();
|
|
29
|
+
vi.useFakeTimers();
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.useRealTimers();
|
|
33
|
+
});
|
|
34
|
+
describe('licenseSelectionsFromOptions', () => {
|
|
35
|
+
it('takes the licence choices in the shape createOrderItems takes them, sorted', () => {
|
|
36
|
+
const options = [{
|
|
37
|
+
licenseId: 'web',
|
|
38
|
+
licenseOptions: {}
|
|
39
|
+
}, {
|
|
40
|
+
licenseId: 'desktop',
|
|
41
|
+
licenseOptions: {
|
|
42
|
+
users: 'five',
|
|
43
|
+
seats: 'one'
|
|
44
|
+
}
|
|
45
|
+
}];
|
|
46
|
+
expect(licenseSelectionsFromOptions(options)).toEqual([{
|
|
47
|
+
licenseId: 'desktop',
|
|
48
|
+
licenseVariableId: 'seats',
|
|
49
|
+
licenseOptionId: 'one'
|
|
50
|
+
}, {
|
|
51
|
+
licenseId: 'desktop',
|
|
52
|
+
licenseVariableId: 'users',
|
|
53
|
+
licenseOptionId: 'five'
|
|
54
|
+
}, {
|
|
55
|
+
licenseId: 'web'
|
|
56
|
+
}]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('trackProductCustomized', () => {
|
|
60
|
+
it('reports the configuration a burst of clicks settles on, once', () => {
|
|
61
|
+
let options = [{
|
|
62
|
+
licenseId: 'desktop',
|
|
63
|
+
licenseOptions: {
|
|
64
|
+
users: 'one'
|
|
65
|
+
}
|
|
66
|
+
}];
|
|
67
|
+
const current = () => options;
|
|
68
|
+
trackProductCustomized(environment, 'collection-burst', current);
|
|
69
|
+
options = [{
|
|
70
|
+
licenseId: 'desktop',
|
|
71
|
+
licenseOptions: {
|
|
72
|
+
users: 'five'
|
|
73
|
+
}
|
|
74
|
+
}];
|
|
75
|
+
trackProductCustomized(environment, 'collection-burst', current);
|
|
76
|
+
options = [{
|
|
77
|
+
licenseId: 'desktop',
|
|
78
|
+
licenseOptions: {
|
|
79
|
+
users: 'ten'
|
|
80
|
+
}
|
|
81
|
+
}];
|
|
82
|
+
trackProductCustomized(environment, 'collection-burst', current);
|
|
83
|
+
expect(commitMutation).not.toHaveBeenCalled();
|
|
84
|
+
vi.advanceTimersByTime(1500);
|
|
85
|
+
expect(reportedInputs()).toEqual([expect.objectContaining({
|
|
86
|
+
event: 'PRODUCT_CUSTOMIZED',
|
|
87
|
+
collectionId: 'collection-burst',
|
|
88
|
+
licenseSelections: [{
|
|
89
|
+
licenseId: 'desktop',
|
|
90
|
+
licenseVariableId: 'users',
|
|
91
|
+
licenseOptionId: 'ten'
|
|
92
|
+
}],
|
|
93
|
+
tracking: expect.objectContaining({
|
|
94
|
+
url: 'https://store.example.com/fonts/archivo'
|
|
95
|
+
})
|
|
96
|
+
})]);
|
|
97
|
+
});
|
|
98
|
+
it('skips a configuration already reported for that collection', () => {
|
|
99
|
+
let options = [{
|
|
100
|
+
licenseId: 'desktop',
|
|
101
|
+
licenseOptions: {}
|
|
102
|
+
}];
|
|
103
|
+
const current = () => options;
|
|
104
|
+
trackProductCustomized(environment, 'collection-repeat', current);
|
|
105
|
+
vi.advanceTimersByTime(1500);
|
|
106
|
+
|
|
107
|
+
// Over to web and back again: the buyer ends where they were.
|
|
108
|
+
options = [{
|
|
109
|
+
licenseId: 'web',
|
|
110
|
+
licenseOptions: {}
|
|
111
|
+
}];
|
|
112
|
+
trackProductCustomized(environment, 'collection-repeat', current);
|
|
113
|
+
options = [{
|
|
114
|
+
licenseId: 'desktop',
|
|
115
|
+
licenseOptions: {}
|
|
116
|
+
}];
|
|
117
|
+
trackProductCustomized(environment, 'collection-repeat', current);
|
|
118
|
+
vi.advanceTimersByTime(1500);
|
|
119
|
+
|
|
120
|
+
// Then a real change.
|
|
121
|
+
options = [{
|
|
122
|
+
licenseId: 'web',
|
|
123
|
+
licenseOptions: {}
|
|
124
|
+
}];
|
|
125
|
+
trackProductCustomized(environment, 'collection-repeat', current);
|
|
126
|
+
vi.advanceTimersByTime(1500);
|
|
127
|
+
expect(reportedInputs().map(input => input.licenseSelections)).toEqual([[{
|
|
128
|
+
licenseId: 'desktop'
|
|
129
|
+
}], [{
|
|
130
|
+
licenseId: 'web'
|
|
131
|
+
}]]);
|
|
132
|
+
});
|
|
133
|
+
it('reports nothing when every licence is unticked', () => {
|
|
134
|
+
trackProductCustomized(environment, 'collection-empty', () => []);
|
|
135
|
+
vi.advanceTimersByTime(1500);
|
|
136
|
+
expect(commitMutation).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('trackProductViewed', () => {
|
|
140
|
+
it('counts an effect run twice for one mount as one view', () => {
|
|
141
|
+
trackProductViewed(environment, 'collection-view');
|
|
142
|
+
trackProductViewed(environment, 'collection-view');
|
|
143
|
+
trackProductViewed(environment, 'collection-other');
|
|
144
|
+
|
|
145
|
+
// Coming back to the first collection later is a new look.
|
|
146
|
+
vi.advanceTimersByTime(2000);
|
|
147
|
+
trackProductViewed(environment, 'collection-view');
|
|
148
|
+
expect(reportedInputs().map(_ref2 => {
|
|
149
|
+
let {
|
|
150
|
+
event,
|
|
151
|
+
collectionId
|
|
152
|
+
} = _ref2;
|
|
153
|
+
return [event, collectionId];
|
|
154
|
+
})).toEqual([['PRODUCT_VIEWED', 'collection-view'], ['PRODUCT_VIEWED', 'collection-other'], ['PRODUCT_VIEWED', 'collection-view']]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('trackPaymentInfoEntered', () => {
|
|
158
|
+
it('reports with the buyer’s tracking context', () => {
|
|
159
|
+
trackPaymentInfoEntered(environment);
|
|
160
|
+
expect(reportedInputs()).toEqual([expect.objectContaining({
|
|
161
|
+
event: 'PAYMENT_INFO_ENTERED',
|
|
162
|
+
tracking: expect.objectContaining({
|
|
163
|
+
analyticsConsent: false
|
|
164
|
+
})
|
|
165
|
+
})]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { validateAddressElements, validateElements } from '../stripeElements.js';
|
|
3
|
+
|
|
4
|
+
// FD-1248: Stripe.js throws from `elements.submit()` when the Elements group
|
|
5
|
+
// was created without a `clientSecret` or `mode` – the group a Stripe Tax
|
|
6
|
+
// order checks out with, and the Cart's billing step before the order has a
|
|
7
|
+
// PaymentIntent – and leaks an unhandled rejection besides. Letting that throw
|
|
8
|
+
// escape left the store modal's Checkout button and the Cart's Next button
|
|
9
|
+
// doing nothing.
|
|
10
|
+
const NO_SESSION = new Error('Unable to select session. Make sure Elements is initialized with clientSecret\n or use the nullableSessionSelector() instead.');
|
|
11
|
+
function fakeElements(submit) {
|
|
12
|
+
let addresses = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
13
|
+
const getValue = vi.fn();
|
|
14
|
+
const elements = {
|
|
15
|
+
submit: vi.fn(submit),
|
|
16
|
+
getElement: vi.fn((type, options) => {
|
|
17
|
+
const address = type === 'address' && options ? addresses[options.mode] : undefined;
|
|
18
|
+
if (!address) return null;
|
|
19
|
+
return {
|
|
20
|
+
getValue: () => {
|
|
21
|
+
getValue(options.mode);
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
complete: address.complete,
|
|
24
|
+
isNewAddress: true,
|
|
25
|
+
value: {}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
elements: elements,
|
|
33
|
+
getValue
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
describe('validateAddressElements', () => {
|
|
37
|
+
// Used where only addresses are mounted, so it must never reach submit():
|
|
38
|
+
// on a group with no session that throws, and leaks an unhandled rejection
|
|
39
|
+
// even when caught.
|
|
40
|
+
const submitThatMustNotRun = () => Promise.reject(NO_SESSION);
|
|
41
|
+
it('goes ahead with no Elements group', async () => {
|
|
42
|
+
expect(await validateAddressElements(null)).toBe(true);
|
|
43
|
+
expect(await validateAddressElements(undefined)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('goes ahead when nothing is mounted (Stripe Tax Checkout button)', async () => {
|
|
46
|
+
const {
|
|
47
|
+
elements
|
|
48
|
+
} = fakeElements(submitThatMustNotRun);
|
|
49
|
+
expect(await validateAddressElements(elements)).toBe(true);
|
|
50
|
+
expect(elements.submit).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
it('stops on an incomplete billing address (Cart billing step)', async () => {
|
|
53
|
+
const {
|
|
54
|
+
elements,
|
|
55
|
+
getValue
|
|
56
|
+
} = fakeElements(submitThatMustNotRun, {
|
|
57
|
+
billing: {
|
|
58
|
+
complete: false
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
expect(await validateAddressElements(elements)).toBe(false);
|
|
62
|
+
expect(getValue).toHaveBeenCalledWith('billing');
|
|
63
|
+
expect(elements.submit).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it('stops on an incomplete licensee address', async () => {
|
|
66
|
+
const {
|
|
67
|
+
elements
|
|
68
|
+
} = fakeElements(submitThatMustNotRun, {
|
|
69
|
+
billing: {
|
|
70
|
+
complete: true
|
|
71
|
+
},
|
|
72
|
+
shipping: {
|
|
73
|
+
complete: false
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
expect(await validateAddressElements(elements)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('goes ahead when every mounted address is complete', async () => {
|
|
79
|
+
const {
|
|
80
|
+
elements,
|
|
81
|
+
getValue
|
|
82
|
+
} = fakeElements(submitThatMustNotRun, {
|
|
83
|
+
billing: {
|
|
84
|
+
complete: true
|
|
85
|
+
},
|
|
86
|
+
shipping: {
|
|
87
|
+
complete: true
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
expect(await validateAddressElements(elements)).toBe(true);
|
|
91
|
+
expect(getValue).toHaveBeenCalledTimes(2);
|
|
92
|
+
expect(elements.submit).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('validateElements', () => {
|
|
96
|
+
it('goes ahead with no Elements group', async () => {
|
|
97
|
+
expect(await validateElements(null)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('goes ahead when Stripe validates the group', async () => {
|
|
100
|
+
const {
|
|
101
|
+
elements,
|
|
102
|
+
getValue
|
|
103
|
+
} = fakeElements(() => Promise.resolve({}));
|
|
104
|
+
expect(await validateElements(elements)).toBe(true);
|
|
105
|
+
expect(getValue).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
it('stops when Stripe reports a validation error', async () => {
|
|
108
|
+
const {
|
|
109
|
+
elements
|
|
110
|
+
} = fakeElements(() => Promise.resolve({
|
|
111
|
+
error: {
|
|
112
|
+
message: 'Your card number is incomplete.'
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
expect(await validateElements(elements)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
describe('when the group has no session', () => {
|
|
118
|
+
const noSession = () => Promise.reject(NO_SESSION);
|
|
119
|
+
it('goes ahead when nothing is mounted (an order a coupon makes free)', async () => {
|
|
120
|
+
const {
|
|
121
|
+
elements
|
|
122
|
+
} = fakeElements(noSession);
|
|
123
|
+
expect(await validateElements(elements)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('also survives submit() throwing synchronously', async () => {
|
|
126
|
+
const {
|
|
127
|
+
elements
|
|
128
|
+
} = fakeElements(() => {
|
|
129
|
+
throw NO_SESSION;
|
|
130
|
+
});
|
|
131
|
+
expect(await validateElements(elements)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it('validates the mounted address elements instead', async () => {
|
|
134
|
+
const {
|
|
135
|
+
elements,
|
|
136
|
+
getValue
|
|
137
|
+
} = fakeElements(noSession, {
|
|
138
|
+
billing: {
|
|
139
|
+
complete: true
|
|
140
|
+
},
|
|
141
|
+
shipping: {
|
|
142
|
+
complete: true
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
expect(await validateElements(elements)).toBe(true);
|
|
146
|
+
expect(getValue).toHaveBeenCalledWith('billing');
|
|
147
|
+
expect(getValue).toHaveBeenCalledWith('shipping');
|
|
148
|
+
});
|
|
149
|
+
it('stops on an incomplete billing address (Cart billing step)', async () => {
|
|
150
|
+
const {
|
|
151
|
+
elements
|
|
152
|
+
} = fakeElements(noSession, {
|
|
153
|
+
billing: {
|
|
154
|
+
complete: false
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
expect(await validateElements(elements)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
it('stops on an incomplete licensee address', async () => {
|
|
160
|
+
const {
|
|
161
|
+
elements
|
|
162
|
+
} = fakeElements(noSession, {
|
|
163
|
+
billing: {
|
|
164
|
+
complete: true
|
|
165
|
+
},
|
|
166
|
+
shipping: {
|
|
167
|
+
complete: false
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
expect(await validateElements(elements)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import "../global-shim.js";
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import React, { act, useState } from 'react';
|
|
4
|
+
import { createRoot } from 'react-dom/client';
|
|
5
|
+
import { ContentState, EditorState } from 'draft-js';
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import TypeTesterFitter from '../components/TypeTester/TypeTesterFitter.js';
|
|
8
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
// happy-dom has no text layout, so the two measuring primitives are stubbed
|
|
11
|
+
// with a toy font: every glyph is 0.5em wide, and the `ss01` alternates are
|
|
12
|
+
// 0.6em. Canvas only ever sees the default glyphs – that is true of real
|
|
13
|
+
// browsers too (`ctx.font` has no feature settings), and is why autofit sized
|
|
14
|
+
// a tester for the default glyphs and then rendered it with the alternates.
|
|
15
|
+
const DEFAULT_ADVANCE = 0.5;
|
|
16
|
+
const SS01_ADVANCE = 0.6;
|
|
17
|
+
const TEXT = 'UNICASED CIRCUMSTANCES';
|
|
18
|
+
const CONTAINER_WIDTH = 1010;
|
|
19
|
+
// useTypeTesterStyler keeps 10px of slack when autofit is on.
|
|
20
|
+
const AVAILABLE_WIDTH = CONTAINER_WIDTH - 10;
|
|
21
|
+
const renderedWidth = (size, features) => TEXT.length * size * (features.includes('ss01') ? SS01_ADVANCE : DEFAULT_ADVANCE);
|
|
22
|
+
let domMeasure;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
25
|
+
constructor(callback) {
|
|
26
|
+
this.callback = callback;
|
|
27
|
+
}
|
|
28
|
+
observe() {
|
|
29
|
+
this.callback([{
|
|
30
|
+
contentRect: {
|
|
31
|
+
width: CONTAINER_WIDTH
|
|
32
|
+
}
|
|
33
|
+
}], this);
|
|
34
|
+
}
|
|
35
|
+
disconnect() {}
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(document, 'fonts', {
|
|
38
|
+
configurable: true,
|
|
39
|
+
value: {
|
|
40
|
+
load: () => Promise.resolve([])
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
|
44
|
+
font: '',
|
|
45
|
+
letterSpacing: '',
|
|
46
|
+
measureText: text => ({
|
|
47
|
+
width: text.length * 100 * DEFAULT_ADVANCE
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
domMeasure = vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function () {
|
|
51
|
+
const {
|
|
52
|
+
fontSize,
|
|
53
|
+
fontFeatureSettings
|
|
54
|
+
} = this.style;
|
|
55
|
+
const advance = fontFeatureSettings.includes('ss01') ? SS01_ADVANCE : DEFAULT_ADVANCE;
|
|
56
|
+
return {
|
|
57
|
+
width: (this.textContent ?? '').length * parseFloat(fontSize) * advance
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
vi.unstubAllGlobals();
|
|
64
|
+
});
|
|
65
|
+
let currentSize = 0;
|
|
66
|
+
function Tester(_ref) {
|
|
67
|
+
let {
|
|
68
|
+
features
|
|
69
|
+
} = _ref;
|
|
70
|
+
const [size, setSize] = useState(72);
|
|
71
|
+
const [content] = useState(() => EditorState.createWithContent(ContentState.createFromText(TEXT)));
|
|
72
|
+
currentSize = size;
|
|
73
|
+
return /*#__PURE__*/React.createElement(TypeTesterFitter, {
|
|
74
|
+
autofit: true,
|
|
75
|
+
autofitOnChange: false,
|
|
76
|
+
min: 10,
|
|
77
|
+
max: 200,
|
|
78
|
+
size: size,
|
|
79
|
+
setSize: setSize,
|
|
80
|
+
lineHeight: 1.2,
|
|
81
|
+
letterSpacing: 0,
|
|
82
|
+
features: features,
|
|
83
|
+
focused: false,
|
|
84
|
+
truncate: false,
|
|
85
|
+
fontStyles: {
|
|
86
|
+
fontFamily: 'Toy Sans',
|
|
87
|
+
fontWeight: 200,
|
|
88
|
+
fontStyle: 'italic'
|
|
89
|
+
},
|
|
90
|
+
content: content,
|
|
91
|
+
contentEdited: false,
|
|
92
|
+
alignment: "left",
|
|
93
|
+
variableSettings: null,
|
|
94
|
+
columns: false
|
|
95
|
+
}, TEXT);
|
|
96
|
+
}
|
|
97
|
+
describe('type tester autofit with OpenType features', () => {
|
|
98
|
+
let root;
|
|
99
|
+
let container;
|
|
100
|
+
const render = features => act(async () => {
|
|
101
|
+
root.render( /*#__PURE__*/React.createElement(Tester, {
|
|
102
|
+
features: features
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
container = document.createElement('div');
|
|
107
|
+
document.body.appendChild(container);
|
|
108
|
+
root = createRoot(container);
|
|
109
|
+
});
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
await act(async () => root.unmount());
|
|
112
|
+
container.remove();
|
|
113
|
+
});
|
|
114
|
+
it('fits text on one line when an enabled feature widens it', async () => {
|
|
115
|
+
await render([]);
|
|
116
|
+
const plainSize = currentSize;
|
|
117
|
+
expect(renderedWidth(plainSize, [])).toBeLessThanOrEqual(AVAILABLE_WIDTH);
|
|
118
|
+
await act(async () => root.unmount());
|
|
119
|
+
root = createRoot(container);
|
|
120
|
+
await render(['ss01']);
|
|
121
|
+
expect(currentSize).toBeLessThan(plainSize);
|
|
122
|
+
expect(renderedWidth(currentSize, ['ss01'])).toBeLessThanOrEqual(AVAILABLE_WIDTH);
|
|
123
|
+
});
|
|
124
|
+
it('refits when a feature is toggled', async () => {
|
|
125
|
+
await render([]);
|
|
126
|
+
const plainSize = currentSize;
|
|
127
|
+
await render(['ss01']);
|
|
128
|
+
const featureSize = currentSize;
|
|
129
|
+
expect(featureSize).toBeLessThan(plainSize);
|
|
130
|
+
expect(renderedWidth(featureSize, ['ss01'])).toBeLessThanOrEqual(AVAILABLE_WIDTH);
|
|
131
|
+
await render([]);
|
|
132
|
+
expect(currentSize).toBe(plainSize);
|
|
133
|
+
});
|
|
134
|
+
it('keeps the canvas fast path when no feature is on', async () => {
|
|
135
|
+
await render([]);
|
|
136
|
+
expect(domMeasure).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import TypeTesterContext from '../components/TypeTester/TypeTesterContext.js';
|
|
5
|
+
import useTypeTesterState from '../components/TypeTester/TypeTesterState.js';
|
|
6
|
+
import ConfigContext, { makeConfig } from '../components/ConfigContext.js';
|
|
7
|
+
function Size(_ref) {
|
|
8
|
+
let {
|
|
9
|
+
id
|
|
10
|
+
} = _ref;
|
|
11
|
+
const {
|
|
12
|
+
size
|
|
13
|
+
} = useTypeTesterState({
|
|
14
|
+
id
|
|
15
|
+
});
|
|
16
|
+
return /*#__PURE__*/React.createElement("span", {
|
|
17
|
+
"data-id": id
|
|
18
|
+
}, size);
|
|
19
|
+
}
|
|
20
|
+
const tester = size => ({
|
|
21
|
+
content: 'Hamburgefonstiv',
|
|
22
|
+
size,
|
|
23
|
+
lineHeight: null,
|
|
24
|
+
letterSpacing: null,
|
|
25
|
+
autofit: false,
|
|
26
|
+
alignment: 'left',
|
|
27
|
+
variableSettings: null,
|
|
28
|
+
features: null,
|
|
29
|
+
direction: 'ltr'
|
|
30
|
+
});
|
|
31
|
+
const renderSizes = (sizes, config) => {
|
|
32
|
+
const testers = Object.fromEntries(Object.entries(sizes).map(_ref2 => {
|
|
33
|
+
let [id, size] = _ref2;
|
|
34
|
+
return [id, tester(size)];
|
|
35
|
+
}));
|
|
36
|
+
const html = renderToString( /*#__PURE__*/React.createElement(ConfigContext.Provider, {
|
|
37
|
+
value: makeConfig(config)
|
|
38
|
+
}, /*#__PURE__*/React.createElement(TypeTesterContext, {
|
|
39
|
+
testers: testers
|
|
40
|
+
}, Object.keys(sizes).map(id => /*#__PURE__*/React.createElement(Size, {
|
|
41
|
+
key: id,
|
|
42
|
+
id: id
|
|
43
|
+
})))));
|
|
44
|
+
return Object.fromEntries([...html.matchAll(/data-id="([^"]+)">([^<]+)</g)].map(_ref3 => {
|
|
45
|
+
let [, id, size] = _ref3;
|
|
46
|
+
return [id, Number(size)];
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// The size slider spans config.size.min..max, so a tester saved with a size
|
|
51
|
+
// outside that range would otherwise start with its handle off the track.
|
|
52
|
+
describe('TypeTesterContext initial size', () => {
|
|
53
|
+
it('clamps a stored size into the configured slider range', () => {
|
|
54
|
+
expect(renderSizes({
|
|
55
|
+
below: 4,
|
|
56
|
+
above: 300,
|
|
57
|
+
inside: 48,
|
|
58
|
+
unset: null
|
|
59
|
+
}, {
|
|
60
|
+
typeTester: {
|
|
61
|
+
size: {
|
|
62
|
+
min: 12,
|
|
63
|
+
max: 120
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})).toEqual({
|
|
67
|
+
below: 12,
|
|
68
|
+
above: 120,
|
|
69
|
+
inside: 48,
|
|
70
|
+
unset: 72
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('clamps against the default range when none is configured', () => {
|
|
74
|
+
expect(renderSizes({
|
|
75
|
+
below: 6,
|
|
76
|
+
above: 500
|
|
77
|
+
})).toEqual({
|
|
78
|
+
below: 10,
|
|
79
|
+
above: 200
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|