fontdue-js 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +78 -77
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +78 -77
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/components/Cart/Checkout.js +11 -1
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.js +16 -9
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TypeTester/useTypeTesterStyler.js +1 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.js +12 -0
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +33 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -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,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
|
+
});
|
|
@@ -4,9 +4,10 @@ import _CheckoutUpdateOrderMutation from "../../__generated__/CheckoutUpdateOrde
|
|
|
4
4
|
import _Checkout_UpdateOrderErrors from "../../__generated__/Checkout_UpdateOrderErrors.graphql.js";
|
|
5
5
|
import _CheckoutUpdateCustomerMutation from "../../__generated__/CheckoutUpdateCustomerMutation.graphql.js";
|
|
6
6
|
import _Checkout_identity from "../../__generated__/Checkout_identity.graphql.js";
|
|
7
|
-
import React, { Fragment, useContext, useEffect, useState } from 'react';
|
|
7
|
+
import React, { Fragment, useContext, useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
9
9
|
import { CardElement } from '@stripe/react-stripe-js';
|
|
10
|
+
import { trackPaymentInfoEntered } from './storefrontEvents.js';
|
|
10
11
|
import { CreditCard, User, Check, License, Receipt, X } from '../Icons/index.js';
|
|
11
12
|
import AddressFields from './AddressFields.js';
|
|
12
13
|
import CustomerFields from './CustomerFields.js';
|
|
@@ -99,6 +100,7 @@ const Checkout = _ref => {
|
|
|
99
100
|
onScrollToTop();
|
|
100
101
|
}, [checkoutStep]);
|
|
101
102
|
const environment = useRelayEnvironment();
|
|
103
|
+
const paymentInfoTracked = useRef(false);
|
|
102
104
|
const [errorsObject, setErrorsObject] = useState(null);
|
|
103
105
|
const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
|
|
104
106
|
const onCompleted = (res, errors, onSuccess) => {
|
|
@@ -454,6 +456,14 @@ const Checkout = _ref => {
|
|
|
454
456
|
style: inputStyles,
|
|
455
457
|
hidePostalCode: true
|
|
456
458
|
}
|
|
459
|
+
// The first time the card details are complete. The server
|
|
460
|
+
// keeps it to one per order; the ref only saves requests.
|
|
461
|
+
,
|
|
462
|
+
onChange: event => {
|
|
463
|
+
if (!event.complete || paymentInfoTracked.current) return;
|
|
464
|
+
paymentInfoTracked.current = true;
|
|
465
|
+
trackPaymentInfoEntered(environment);
|
|
466
|
+
}
|
|
457
467
|
})), errorsObject && errorsObject.stripePaymentMethodId && errorsObject.stripePaymentMethodId.length > 0 && /*#__PURE__*/React.createElement("p", {
|
|
458
468
|
className: "checkout-modal__section__errors"
|
|
459
469
|
}, errorsObject.stripePaymentMethodId[0]));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Environment } from 'relay-runtime';
|
|
2
|
+
import type { FontdueState } from '../../reducer.js';
|
|
3
|
+
import type { LicenseSelectionInput } from '__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js';
|
|
4
|
+
/** Reports the store modal opening on a collection. */
|
|
5
|
+
export declare function trackProductViewed(environment: Environment, collectionId: string): void;
|
|
6
|
+
type LicenseOptions = FontdueState['licenseOptions'];
|
|
7
|
+
/**
|
|
8
|
+
* The licence choices in the store's state, in the shape `createOrderItems`
|
|
9
|
+
* takes them – one selection per licence variable, or one bare selection for a
|
|
10
|
+
* licence without variables. Sorted, so the same configuration reached in a
|
|
11
|
+
* different order reads the same.
|
|
12
|
+
*/
|
|
13
|
+
export declare function licenseSelectionsFromOptions(licenseOptions: LicenseOptions): LicenseSelectionInput[];
|
|
14
|
+
/**
|
|
15
|
+
* Reports the buyer changing their licence choices for a collection, once the
|
|
16
|
+
* choices settle. Call it from the handler of the buyer's own click, never from
|
|
17
|
+
* an effect: licences a foundry marks as selected by default are applied on
|
|
18
|
+
* mount, and that isn't the buyer customizing anything.
|
|
19
|
+
*
|
|
20
|
+
* `currentLicenseOptions` is read when the report fires, so it sees the
|
|
21
|
+
* configuration the buyer settled on.
|
|
22
|
+
*/
|
|
23
|
+
export declare function trackProductCustomized(environment: Environment, collectionId: string, currentLicenseOptions: () => LicenseOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* `trackProductCustomized` bound to the store and Relay environment in
|
|
26
|
+
* context, for a licence control's click handlers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useTrackProductCustomized(collectionId: string | null | undefined): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Reports the checkout's payment details as complete. The server records it
|
|
31
|
+
* once per order, however often this is called.
|
|
32
|
+
*/
|
|
33
|
+
export declare function trackPaymentInfoEntered(environment: Environment): void;
|
|
34
|
+
export {};
|