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.
Files changed (151) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -0
  3. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
  4. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
  5. package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
  6. package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
  7. package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
  8. package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
  9. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
  10. package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
  11. package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
  12. package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
  13. package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
  14. package/dist/__generated__/PriceBar_node.graphql.js +23 -16
  15. package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
  16. package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
  17. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
  18. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
  19. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
  20. package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
  21. package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
  22. package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
  23. package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
  24. package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
  25. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
  26. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
  27. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
  28. package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
  29. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  30. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
  31. package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
  32. package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
  33. package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
  34. package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
  35. package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
  36. package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
  37. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
  38. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
  39. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
  41. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
  43. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  44. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
  45. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
  46. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
  47. package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
  48. package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
  49. package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
  50. package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
  51. package/dist/__tests__/customerSession.test.js +456 -0
  52. package/dist/__tests__/infiniteScroll.test.js +180 -0
  53. package/dist/__tests__/licenseExclusions.test.js +2 -1
  54. package/dist/__tests__/storeModalRoutes.test.js +118 -0
  55. package/dist/__tests__/storefrontEvents.test.js +167 -0
  56. package/dist/__tests__/stripeElements.test.js +173 -0
  57. package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
  58. package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
  59. package/dist/__tests__/typeTesterTruncate.test.js +218 -0
  60. package/dist/auth/session.d.ts +42 -0
  61. package/dist/auth/session.js +139 -0
  62. package/dist/auth/tokenStore.d.ts +61 -0
  63. package/dist/auth/tokenStore.js +162 -0
  64. package/dist/components/Cart/Checkout.js +19 -9
  65. package/dist/components/Cart/storefrontEvents.d.ts +34 -0
  66. package/dist/components/Cart/storefrontEvents.js +138 -0
  67. package/dist/components/ConfigContext.d.ts +2 -2
  68. package/dist/components/CustomerSignIn/index.d.ts +33 -0
  69. package/dist/components/CustomerSignIn/index.js +170 -0
  70. package/dist/components/FontFamilies/index.js +4 -3
  71. package/dist/components/NewsletterSignup/index.js +5 -1
  72. package/dist/components/Precart/index.js +6 -0
  73. package/dist/components/PriceBar/index.js +5 -3
  74. package/dist/components/Root/index.js +4 -34
  75. package/dist/components/Root/productState.d.ts +2 -8
  76. package/dist/components/Root/productState.js +46 -57
  77. package/dist/components/SKUPrice/index.js +7 -8
  78. package/dist/components/StoreModal/StoreModalFamily.js +4 -2
  79. package/dist/components/StoreModal/StoreModalIndex.js +48 -7
  80. package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
  81. package/dist/components/StoreModal/StoreModalProduct.js +69 -16
  82. package/dist/components/StoreModal/index.js +14 -14
  83. package/dist/components/StoreModal/routes.js +9 -1
  84. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
  85. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
  86. package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
  87. package/dist/components/TestFontsForm/index.js +5 -1
  88. package/dist/components/TypeTester/TypeTesterContent.js +9 -2
  89. package/dist/components/TypeTester/TypeTesterContext.js +17 -4
  90. package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
  91. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
  92. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
  93. package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
  94. package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
  95. package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
  96. package/dist/components/TypeTester/index.d.ts +3 -1
  97. package/dist/components/TypeTester/index.js +2 -1
  98. package/dist/components/TypeTester/truncate.d.ts +12 -0
  99. package/dist/components/TypeTester/truncate.js +95 -0
  100. package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
  101. package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
  102. package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
  103. package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
  104. package/dist/components/TypeTesters/index.d.ts +2 -0
  105. package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
  106. package/dist/components/elements/StoreModalContainer/index.js +9 -4
  107. package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
  108. package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
  109. package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
  110. package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
  111. package/dist/components/useInfiniteScroll.d.ts +28 -0
  112. package/dist/components/useInfiniteScroll.js +50 -0
  113. package/dist/fontdue.css +63 -0
  114. package/dist/hooks/useAutofit.d.ts +2 -1
  115. package/dist/hooks/useAutofit.js +10 -4
  116. package/dist/hooks/useCurrentCustomer.d.ts +34 -0
  117. package/dist/hooks/useCurrentCustomer.js +93 -0
  118. package/dist/reducer.d.ts +5 -5
  119. package/dist/reducer.js +9 -87
  120. package/dist/relay/environment.js +91 -50
  121. package/dist/selection.d.ts +40 -0
  122. package/dist/selection.js +494 -0
  123. package/dist/storeModalRoutes.d.ts +38 -0
  124. package/dist/storeModalRoutes.js +110 -0
  125. package/dist/stripeElements.d.ts +18 -0
  126. package/dist/stripeElements.js +49 -0
  127. package/dist/utils.d.ts +1 -12
  128. package/dist/utils.js +1 -36
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
  131. package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
  132. package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
  133. package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
  134. package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
  135. package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
  136. package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
  137. package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
  138. package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
  139. package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
  140. package/dist/components/CartButton/index.server.d.ts +0 -3
  141. package/dist/components/CartButton/index.server.js +0 -18
  142. package/dist/components/CorsErrorModal.d.ts +0 -1
  143. package/dist/components/CorsErrorModal.js +0 -92
  144. package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
  145. package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
  146. package/dist/components/ThemeConfig/index.server.d.ts +0 -2
  147. package/dist/components/ThemeConfig/index.server.js +0 -17
  148. package/dist/components/TypeTester/features.d.ts +0 -5
  149. package/dist/components/TypeTester/features.js +0 -143
  150. package/dist/data/unicodeData.d.ts +0 -4
  151. 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
+ });