fontdue-js 3.12.0 → 3.13.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 +10 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.d.ts +22 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.js +87 -0
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +8 -4
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -5
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -5
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +9 -5
- package/dist/__generated__/productState_Query.graphql.d.ts +1 -29
- package/dist/__generated__/productState_Query.graphql.js +15 -30
- package/dist/__generated__/productState_collection.graphql.d.ts +1 -1
- package/dist/__generated__/productState_collection.graphql.js +8 -4
- package/dist/__generated__/productState_fontPack.graphql.d.ts +1 -1
- package/dist/__generated__/productState_fontPack.graphql.js +7 -3
- package/dist/__generated__/productState_node.graphql.d.ts +1 -29
- package/dist/__generated__/productState_node.graphql.js +8 -22
- package/dist/__tests__/bundleSelectionOrder.test.js +0 -1
- package/dist/__tests__/collectionBundleSelection.test.js +31 -6
- package/dist/__tests__/customerSignInElement.test.js +155 -0
- package/dist/__tests__/customerSignInRequest.test.js +85 -0
- package/dist/__tests__/stripeCheckoutError.test.js +110 -0
- package/dist/__tests__/typeTesterPrefetchStyles.test.js +108 -0
- package/dist/components/CustomerSignIn/CustomerSignInElement.d.ts +22 -0
- package/dist/components/CustomerSignIn/CustomerSignInElement.js +132 -0
- package/dist/components/CustomerSignIn/index.js +4 -2
- package/dist/components/Root/index.js +2 -0
- package/dist/components/Root/productState.js +7 -33
- package/dist/components/StoreModal/StoreModalFamily.js +9 -5
- package/dist/components/StripeProvider/index.js +19 -2
- package/dist/components/TypeTester/TypeTesterStyleSelect.js +6 -1
- package/dist/components/TypeTester/usePrefetchWhenNear.d.ts +14 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.js +69 -0
- package/dist/fontdue.css +10 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/productState_collectionBundle.graphql.d.ts +0 -41
- package/dist/__generated__/productState_collectionBundle.graphql.js +0 -81
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @generated SignedSource<<
|
|
2
|
+
* @generated SignedSource<<225503323456f9f96cbe77f3b83dd674>>
|
|
3
3
|
* @lightSyntaxTransform
|
|
4
4
|
* @nogrep
|
|
5
5
|
*/
|
|
@@ -60,13 +60,17 @@ const node = function () {
|
|
|
60
60
|
},
|
|
61
61
|
v4 = {
|
|
62
62
|
"alias": null,
|
|
63
|
-
"args":
|
|
63
|
+
"args": [{
|
|
64
|
+
"kind": "Literal",
|
|
65
|
+
"name": "acrossFamilies",
|
|
66
|
+
"value": true
|
|
67
|
+
}],
|
|
64
68
|
"concreteType": "Bundle",
|
|
65
69
|
"kind": "LinkedField",
|
|
66
70
|
"name": "bundles",
|
|
67
71
|
"plural": true,
|
|
68
72
|
"selections": [v0 /*: any*/, v1 /*: any*/, v2 /*: any*/, v3 /*: any*/],
|
|
69
|
-
"storageKey":
|
|
73
|
+
"storageKey": "bundles(acrossFamilies:true)"
|
|
70
74
|
};
|
|
71
75
|
return {
|
|
72
76
|
"argumentDefinitions": [],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @generated SignedSource<<
|
|
2
|
+
* @generated SignedSource<<7112b01c24002ecc9175f687500a7e12>>
|
|
3
3
|
* @lightSyntaxTransform
|
|
4
4
|
* @nogrep
|
|
5
5
|
*/
|
|
@@ -63,34 +63,6 @@ export type productState_node$data = {
|
|
|
63
63
|
};
|
|
64
64
|
} | null;
|
|
65
65
|
}> | null;
|
|
66
|
-
readonly collectionBundles: ReadonlyArray<{
|
|
67
|
-
readonly id: string;
|
|
68
|
-
readonly memberCollections: ReadonlyArray<{
|
|
69
|
-
readonly fontStyles: ReadonlyArray<{
|
|
70
|
-
readonly id: string;
|
|
71
|
-
readonly sku: {
|
|
72
|
-
readonly id: string;
|
|
73
|
-
readonly price: {
|
|
74
|
-
readonly amount: number;
|
|
75
|
-
};
|
|
76
|
-
} | null;
|
|
77
|
-
}>;
|
|
78
|
-
readonly id: string;
|
|
79
|
-
readonly sku: {
|
|
80
|
-
readonly id: string;
|
|
81
|
-
readonly price: {
|
|
82
|
-
readonly amount: number;
|
|
83
|
-
};
|
|
84
|
-
} | null;
|
|
85
|
-
}> | null;
|
|
86
|
-
readonly name: string;
|
|
87
|
-
readonly sku: {
|
|
88
|
-
readonly id: string;
|
|
89
|
-
readonly price: {
|
|
90
|
-
readonly amount: number;
|
|
91
|
-
};
|
|
92
|
-
} | null;
|
|
93
|
-
}> | null;
|
|
94
66
|
readonly fontStyles: ReadonlyArray<{
|
|
95
67
|
readonly id: string;
|
|
96
68
|
readonly sku: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @generated SignedSource<<
|
|
2
|
+
* @generated SignedSource<<7112b01c24002ecc9175f687500a7e12>>
|
|
3
3
|
* @lightSyntaxTransform
|
|
4
4
|
* @nogrep
|
|
5
5
|
*/
|
|
@@ -60,13 +60,17 @@ const node = function () {
|
|
|
60
60
|
},
|
|
61
61
|
v4 = {
|
|
62
62
|
"alias": null,
|
|
63
|
-
"args":
|
|
63
|
+
"args": [{
|
|
64
|
+
"kind": "Literal",
|
|
65
|
+
"name": "acrossFamilies",
|
|
66
|
+
"value": true
|
|
67
|
+
}],
|
|
64
68
|
"concreteType": "Bundle",
|
|
65
69
|
"kind": "LinkedField",
|
|
66
70
|
"name": "bundles",
|
|
67
71
|
"plural": true,
|
|
68
72
|
"selections": [v0 /*: any*/, v1 /*: any*/, v2 /*: any*/, v3 /*: any*/],
|
|
69
|
-
"storageKey":
|
|
73
|
+
"storageKey": "bundles(acrossFamilies:true)"
|
|
70
74
|
};
|
|
71
75
|
return {
|
|
72
76
|
"argumentDefinitions": [],
|
|
@@ -76,24 +80,6 @@ const node = function () {
|
|
|
76
80
|
},
|
|
77
81
|
"name": "productState_node",
|
|
78
82
|
"selections": [v0 /*: any*/, v1 /*: any*/, v2 /*: any*/, v3 /*: any*/, v4 /*: any*/, {
|
|
79
|
-
"alias": null,
|
|
80
|
-
"args": null,
|
|
81
|
-
"concreteType": "CollectionBundle",
|
|
82
|
-
"kind": "LinkedField",
|
|
83
|
-
"name": "collectionBundles",
|
|
84
|
-
"plural": true,
|
|
85
|
-
"selections": [v0 /*: any*/, v1 /*: any*/, v2 /*: any*/, {
|
|
86
|
-
"alias": null,
|
|
87
|
-
"args": null,
|
|
88
|
-
"concreteType": "FontCollection",
|
|
89
|
-
"kind": "LinkedField",
|
|
90
|
-
"name": "memberCollections",
|
|
91
|
-
"plural": true,
|
|
92
|
-
"selections": [v0 /*: any*/, v2 /*: any*/, v3 /*: any*/],
|
|
93
|
-
"storageKey": null
|
|
94
|
-
}],
|
|
95
|
-
"storageKey": null
|
|
96
|
-
}, {
|
|
97
83
|
"alias": null,
|
|
98
84
|
"args": [{
|
|
99
85
|
"kind": "Literal",
|
|
@@ -111,5 +97,5 @@ const node = function () {
|
|
|
111
97
|
"abstractKey": null
|
|
112
98
|
};
|
|
113
99
|
}();
|
|
114
|
-
node.hash = "
|
|
100
|
+
node.hash = "aab1ef07e0192a0d22a4c300084d9d05";
|
|
115
101
|
export default node;
|
|
@@ -506,6 +506,16 @@ describe('POPULATE_PRODUCT_STATE', () => {
|
|
|
506
506
|
fontStyles: [style('vf1', null)],
|
|
507
507
|
bundles: []
|
|
508
508
|
};
|
|
509
|
+
const display = {
|
|
510
|
+
id: 'display',
|
|
511
|
+
name: 'Display',
|
|
512
|
+
sku: sku('display-sku', 500),
|
|
513
|
+
fontStyles: [style('d1', 'sd1')],
|
|
514
|
+
bundles: []
|
|
515
|
+
};
|
|
516
|
+
// The superfamily's own bundles, read with `bundles(acrossFamilies: true)`:
|
|
517
|
+
// whole families (what collection bundles were), and styles from two
|
|
518
|
+
// families. The server expands a family member into its styles.
|
|
509
519
|
const superfamily = {
|
|
510
520
|
node: {
|
|
511
521
|
__typename: 'FontCollection',
|
|
@@ -513,14 +523,18 @@ describe('POPULATE_PRODUCT_STATE', () => {
|
|
|
513
523
|
name: 'Thunder',
|
|
514
524
|
sku: sku('super-sku', 750),
|
|
515
525
|
fontStyles: [],
|
|
516
|
-
bundles: [
|
|
517
|
-
collectionBundles: [{
|
|
526
|
+
bundles: [{
|
|
518
527
|
id: 'text-bundle',
|
|
519
528
|
name: 'Text + Variable',
|
|
520
529
|
sku: sku('text-bundle-sku', 500),
|
|
521
|
-
|
|
530
|
+
fontStyles: [...textFamily.fontStyles, ...textVariable.fontStyles]
|
|
531
|
+
}, {
|
|
532
|
+
id: 'regulars',
|
|
533
|
+
name: 'Regulars',
|
|
534
|
+
sku: sku('regulars-sku', 60),
|
|
535
|
+
fontStyles: [style('f1', 's1'), style('d1', 'sd1')]
|
|
522
536
|
}],
|
|
523
|
-
children: [textFamily, textVariable]
|
|
537
|
+
children: [textFamily, textVariable, display]
|
|
524
538
|
}
|
|
525
539
|
};
|
|
526
540
|
it('records the styles every SKU grants', () => {
|
|
@@ -529,14 +543,25 @@ describe('POPULATE_PRODUCT_STATE', () => {
|
|
|
529
543
|
data: superfamily
|
|
530
544
|
});
|
|
531
545
|
expect(state.skuStyleIds).toEqual({
|
|
532
|
-
'super-sku': ['f1', 'f2', 'vf1'],
|
|
546
|
+
'super-sku': ['f1', 'f2', 'vf1', 'd1'],
|
|
533
547
|
'text-bundle-sku': ['f1', 'f2', 'vf1'],
|
|
548
|
+
'regulars-sku': ['f1', 'd1'],
|
|
534
549
|
'text-sku': ['f1', 'f2'],
|
|
535
550
|
'text-var-sku': ['vf1'],
|
|
551
|
+
'display-sku': ['d1'],
|
|
536
552
|
'upright-sku': ['f1'],
|
|
537
553
|
s1: ['f1'],
|
|
538
|
-
s2: ['f2']
|
|
554
|
+
s2: ['f2'],
|
|
555
|
+
sd1: ['d1']
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
it('rolls styles from two families up to a superfamily bundle', () => {
|
|
559
|
+
const loaded = reducer(makeState(), {
|
|
560
|
+
type: 'POPULATE_PRODUCT_STATE',
|
|
561
|
+
data: superfamily
|
|
539
562
|
});
|
|
563
|
+
const state = select(select(loaded, 's1'), 'sd1');
|
|
564
|
+
expect(selected(state)).toEqual(['regulars-sku']);
|
|
540
565
|
});
|
|
541
566
|
it('prices a SKU picked before its product loaded once it has', () => {
|
|
542
567
|
const picked = select(makeState(), 'text-sku');
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// `<fontdue-customer-sign-in>`: a script-tag page has no React of its own, so
|
|
4
|
+
// the element shows who is signed in and tells the page about sign-in and
|
|
5
|
+
// sign-out with window events.
|
|
6
|
+
|
|
7
|
+
import React, { act } from 'react';
|
|
8
|
+
import { createRoot } from 'react-dom/client';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
const {
|
|
11
|
+
relay
|
|
12
|
+
} = vi.hoisted(() => ({
|
|
13
|
+
relay: {
|
|
14
|
+
currentCustomer: null,
|
|
15
|
+
lookups: 0
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('react-relay', () => ({
|
|
19
|
+
graphql: () => null,
|
|
20
|
+
useLazyLoadQuery: () => {
|
|
21
|
+
relay.lookups += 1;
|
|
22
|
+
return {
|
|
23
|
+
viewer: {
|
|
24
|
+
currentCustomer: relay.currentCustomer
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// The form itself is CustomerSignIn's business; stand in a button that signs
|
|
31
|
+
// in the way it does – tokens stored first, then `onSignedIn`.
|
|
32
|
+
vi.mock('../components/CustomerSignIn/index.js', async () => {
|
|
33
|
+
const {
|
|
34
|
+
setTokens
|
|
35
|
+
} = await import('../auth/tokenStore.js');
|
|
36
|
+
return {
|
|
37
|
+
default: _ref => {
|
|
38
|
+
let {
|
|
39
|
+
onSignedIn
|
|
40
|
+
} = _ref;
|
|
41
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
42
|
+
"data-testid": "form",
|
|
43
|
+
onClick: () => {
|
|
44
|
+
setTokens({
|
|
45
|
+
accessToken: 'fresh',
|
|
46
|
+
refreshToken: 'r'
|
|
47
|
+
});
|
|
48
|
+
onSignedIn({
|
|
49
|
+
id: 'C2',
|
|
50
|
+
name: 'Nina',
|
|
51
|
+
email: 'nina@example.com'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
import CustomerSignInElement from '../components/CustomerSignIn/CustomerSignInElement.js';
|
|
59
|
+
import { getTokens, resetTokenStore, setTokens } from '../auth/tokenStore.js';
|
|
60
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
61
|
+
let container;
|
|
62
|
+
let root;
|
|
63
|
+
let events;
|
|
64
|
+
const record = event => events.push({
|
|
65
|
+
type: event.type,
|
|
66
|
+
detail: event.detail
|
|
67
|
+
});
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
window.localStorage.clear();
|
|
70
|
+
resetTokenStore();
|
|
71
|
+
relay.currentCustomer = null;
|
|
72
|
+
relay.lookups = 0;
|
|
73
|
+
events = [];
|
|
74
|
+
window.addEventListener('fontdue:customer-signed-in', record);
|
|
75
|
+
window.addEventListener('fontdue:customer-signed-out', record);
|
|
76
|
+
container = document.createElement('div');
|
|
77
|
+
document.body.append(container);
|
|
78
|
+
root = createRoot(container);
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
act(() => root.unmount());
|
|
82
|
+
container.remove();
|
|
83
|
+
window.removeEventListener('fontdue:customer-signed-in', record);
|
|
84
|
+
window.removeEventListener('fontdue:customer-signed-out', record);
|
|
85
|
+
});
|
|
86
|
+
const render = async () => {
|
|
87
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(CustomerSignInElement, null)));
|
|
88
|
+
};
|
|
89
|
+
const click = async selector => {
|
|
90
|
+
await act(async () => container.querySelector(selector).click());
|
|
91
|
+
};
|
|
92
|
+
describe('<fontdue-customer-sign-in>', () => {
|
|
93
|
+
it('shows the form to a visitor with no session, without a lookup', async () => {
|
|
94
|
+
await render();
|
|
95
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
96
|
+
expect(relay.lookups).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it('shows a returning customer who is signed in, not the form', async () => {
|
|
99
|
+
setTokens({
|
|
100
|
+
accessToken: 'stored',
|
|
101
|
+
refreshToken: 'r'
|
|
102
|
+
});
|
|
103
|
+
relay.currentCustomer = {
|
|
104
|
+
id: 'C1',
|
|
105
|
+
name: 'Ada',
|
|
106
|
+
email: 'ada@example.com'
|
|
107
|
+
};
|
|
108
|
+
await render();
|
|
109
|
+
expect(container.querySelector('[data-testid="form"]')).toBeNull();
|
|
110
|
+
expect(container.textContent).toContain('Signed in as ada@example.com.');
|
|
111
|
+
});
|
|
112
|
+
it('offers the form when the session names nobody', async () => {
|
|
113
|
+
setTokens({
|
|
114
|
+
accessToken: 'stored',
|
|
115
|
+
refreshToken: 'r'
|
|
116
|
+
});
|
|
117
|
+
await render();
|
|
118
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
119
|
+
});
|
|
120
|
+
it('shows the customer it just signed in at once and tells the page', async () => {
|
|
121
|
+
await render();
|
|
122
|
+
await click('[data-testid="form"]');
|
|
123
|
+
expect(container.textContent).toContain('Signed in as nina@example.com.');
|
|
124
|
+
expect(relay.lookups).toBe(0);
|
|
125
|
+
expect(events).toEqual([{
|
|
126
|
+
type: 'fontdue:customer-signed-in',
|
|
127
|
+
detail: {
|
|
128
|
+
customer: {
|
|
129
|
+
id: 'C2',
|
|
130
|
+
name: 'Nina',
|
|
131
|
+
email: 'nina@example.com'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}]);
|
|
135
|
+
});
|
|
136
|
+
it('signs out: forgets the session, shows the form again and tells the page', async () => {
|
|
137
|
+
setTokens({
|
|
138
|
+
accessToken: 'stored',
|
|
139
|
+
refreshToken: 'r'
|
|
140
|
+
});
|
|
141
|
+
relay.currentCustomer = {
|
|
142
|
+
id: 'C1',
|
|
143
|
+
name: 'Ada',
|
|
144
|
+
email: 'ada@example.com'
|
|
145
|
+
};
|
|
146
|
+
await render();
|
|
147
|
+
await click('[data-step="signed-in"] button');
|
|
148
|
+
expect(getTokens()).toBeNull();
|
|
149
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
150
|
+
expect(events).toEqual([{
|
|
151
|
+
type: 'fontdue:customer-signed-out',
|
|
152
|
+
detail: null
|
|
153
|
+
}]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// Asking for a sign-in code: the only unsuccessful *answer* is the rate limit,
|
|
4
|
+
// but a request can also fail outright, and that must not be reported as one.
|
|
5
|
+
|
|
6
|
+
import React, { act } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
const {
|
|
10
|
+
relay
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
relay: {
|
|
13
|
+
respond: null
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('react-relay', () => ({
|
|
17
|
+
graphql: () => null,
|
|
18
|
+
useMutation: () => [config => {
|
|
19
|
+
var _relay$respond;
|
|
20
|
+
return (_relay$respond = relay.respond) === null || _relay$respond === void 0 ? void 0 : _relay$respond.call(relay, config.onCompleted);
|
|
21
|
+
}, false]
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('../components/FontdueContextProvider/index.js', () => ({
|
|
24
|
+
EnsureFontdueContext: _ref => {
|
|
25
|
+
let {
|
|
26
|
+
children
|
|
27
|
+
} = _ref;
|
|
28
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
import CustomerSignIn from '../components/CustomerSignIn/index.js';
|
|
32
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
33
|
+
let container;
|
|
34
|
+
let root;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
container = document.createElement('div');
|
|
37
|
+
document.body.append(container);
|
|
38
|
+
root = createRoot(container);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
act(() => root.unmount());
|
|
42
|
+
container.remove();
|
|
43
|
+
});
|
|
44
|
+
const requestCode = async () => {
|
|
45
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(CustomerSignIn, {
|
|
46
|
+
email: "reader@example.com"
|
|
47
|
+
})));
|
|
48
|
+
await act(async () => {
|
|
49
|
+
container.querySelector('form').dispatchEvent(new Event('submit', {
|
|
50
|
+
bubbles: true,
|
|
51
|
+
cancelable: true
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
describe('CustomerSignIn asking for a code', () => {
|
|
56
|
+
it('moves to the code step when the code is on its way', async () => {
|
|
57
|
+
relay.respond = done => done({
|
|
58
|
+
requestLoginCode: {
|
|
59
|
+
success: true
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
await requestCode();
|
|
63
|
+
expect(container.querySelector('[data-step="code"]')).not.toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('reports the rate limit', async () => {
|
|
66
|
+
relay.respond = done => done({
|
|
67
|
+
requestLoginCode: {
|
|
68
|
+
success: false
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
await requestCode();
|
|
72
|
+
expect(container.textContent).toContain('Too many codes requested');
|
|
73
|
+
});
|
|
74
|
+
it('reports a failed request as itself, not as the rate limit', async () => {
|
|
75
|
+
relay.respond = done => done({
|
|
76
|
+
requestLoginCode: null
|
|
77
|
+
}, [{
|
|
78
|
+
message: 'Something went wrong'
|
|
79
|
+
}]);
|
|
80
|
+
await requestCode();
|
|
81
|
+
expect(container.querySelector('[data-step="email"]')).not.toBeNull();
|
|
82
|
+
expect(container.textContent).toContain('Something went wrong');
|
|
83
|
+
expect(container.textContent).not.toContain('Too many codes requested');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// When Stripe refuses to create the Checkout Session (a total under its $0.50
|
|
4
|
+
// minimum, say), the embedded checkout only says “Something went wrong”. The
|
|
5
|
+
// store modal shows Stripe's reason in its place instead.
|
|
6
|
+
|
|
7
|
+
import React, { act, useEffect } from 'react';
|
|
8
|
+
import { createRoot } from 'react-dom/client';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
const {
|
|
11
|
+
relay
|
|
12
|
+
} = vi.hoisted(() => ({
|
|
13
|
+
relay: {
|
|
14
|
+
response: null,
|
|
15
|
+
errors: undefined,
|
|
16
|
+
networkError: null
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('react-relay', () => ({
|
|
20
|
+
graphql: () => null,
|
|
21
|
+
useRelayEnvironment: () => ({}),
|
|
22
|
+
useFragment: () => ({
|
|
23
|
+
stripeAccountId: 'acct_test',
|
|
24
|
+
stripePublishableKey: 'pk_test'
|
|
25
|
+
}),
|
|
26
|
+
commitMutation: (_env, config) => {
|
|
27
|
+
if (relay.networkError) config.onError(relay.networkError);else config.onCompleted(relay.response, relay.errors);
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
vi.mock('@stripe/stripe-js', () => ({
|
|
31
|
+
loadStripe: () => Promise.resolve({})
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// react-stripe-js asks for the client secret as soon as Stripe.js has loaded.
|
|
35
|
+
vi.mock('@stripe/react-stripe-js', () => ({
|
|
36
|
+
Elements: _ref => {
|
|
37
|
+
let {
|
|
38
|
+
children
|
|
39
|
+
} = _ref;
|
|
40
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
41
|
+
},
|
|
42
|
+
EmbeddedCheckoutProvider: _ref2 => {
|
|
43
|
+
let {
|
|
44
|
+
options,
|
|
45
|
+
children
|
|
46
|
+
} = _ref2;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
options.fetchClientSecret().catch(() => {});
|
|
49
|
+
}, []);
|
|
50
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
import StripeProvider from '../components/StripeProvider/index.js';
|
|
54
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
55
|
+
let container;
|
|
56
|
+
let root;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
relay.response = {
|
|
59
|
+
createOrderCheckoutSession: null
|
|
60
|
+
};
|
|
61
|
+
relay.errors = undefined;
|
|
62
|
+
relay.networkError = null;
|
|
63
|
+
container = document.createElement('div');
|
|
64
|
+
document.body.append(container);
|
|
65
|
+
root = createRoot(container);
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
act(() => root.unmount());
|
|
69
|
+
container.remove();
|
|
70
|
+
});
|
|
71
|
+
const render = async () => {
|
|
72
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(StripeProvider, {
|
|
73
|
+
viewer: {},
|
|
74
|
+
providerType: "checkout"
|
|
75
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
76
|
+
"data-testid": "embedded-checkout"
|
|
77
|
+
}))));
|
|
78
|
+
};
|
|
79
|
+
describe('StripeProvider embedded checkout errors', () => {
|
|
80
|
+
it('shows Stripe’s reason when the Checkout Session is refused', async () => {
|
|
81
|
+
var _container$querySelec;
|
|
82
|
+
const message = "The Checkout Session's total amount due must add up to at least $0.50 usd";
|
|
83
|
+
relay.errors = [{
|
|
84
|
+
message
|
|
85
|
+
}];
|
|
86
|
+
await render();
|
|
87
|
+
expect((_container$querySelec = container.querySelector('[role="alert"]')) === null || _container$querySelec === void 0 ? void 0 : _container$querySelec.textContent).toBe(message);
|
|
88
|
+
expect(container.querySelector('[data-testid="embedded-checkout"]')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it('falls back to a generic message when the request itself fails', async () => {
|
|
91
|
+
var _container$querySelec2;
|
|
92
|
+
relay.networkError = new Error('fontdue-js: request failed with HTTP 502');
|
|
93
|
+
await render();
|
|
94
|
+
expect((_container$querySelec2 = container.querySelector('[role="alert"]')) === null || _container$querySelec2 === void 0 ? void 0 : _container$querySelec2.textContent).toBe('We couldn’t start the checkout – please try again');
|
|
95
|
+
});
|
|
96
|
+
it('mounts the embedded checkout when the session is created', async () => {
|
|
97
|
+
relay.response = {
|
|
98
|
+
createOrderCheckoutSession: {
|
|
99
|
+
order: {
|
|
100
|
+
stripeCheckoutSession: {
|
|
101
|
+
clientSecret: 'cs_test_secret'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
await render();
|
|
107
|
+
expect(container.querySelector('[role="alert"]')).toBeNull();
|
|
108
|
+
expect(container.querySelector('[data-testid="embedded-checkout"]')).not.toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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 usePrefetchWhenNear from '../components/TypeTester/usePrefetchWhenNear.js';
|
|
6
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
|
|
8
|
+
// A controllable IntersectionObserver: tests decide when the element is near.
|
|
9
|
+
let observers = [];
|
|
10
|
+
class FakeIntersectionObserver {
|
|
11
|
+
constructor(cb) {
|
|
12
|
+
this.entry = {
|
|
13
|
+
cb,
|
|
14
|
+
disconnected: false
|
|
15
|
+
};
|
|
16
|
+
observers.push(this.entry);
|
|
17
|
+
}
|
|
18
|
+
observe() {}
|
|
19
|
+
disconnect() {
|
|
20
|
+
this.entry.disconnected = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const intersect = isIntersecting => act(() => {
|
|
24
|
+
observers.filter(o => !o.disconnected).forEach(o => o.cb([{
|
|
25
|
+
isIntersecting
|
|
26
|
+
}], {}));
|
|
27
|
+
});
|
|
28
|
+
function Select(_ref) {
|
|
29
|
+
let {
|
|
30
|
+
prefetch
|
|
31
|
+
} = _ref;
|
|
32
|
+
const ref = useRef(null);
|
|
33
|
+
usePrefetchWhenNear(ref, prefetch);
|
|
34
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
35
|
+
ref: ref
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Native <select> menus snapshot their options on open (Safari never updates
|
|
40
|
+
// an open menu), so the type tester's style list has to be fetched before the
|
|
41
|
+
// first click, without competing with the initial render.
|
|
42
|
+
describe('type tester style list prefetch', () => {
|
|
43
|
+
let root;
|
|
44
|
+
let container;
|
|
45
|
+
let prefetch;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
observers = [];
|
|
49
|
+
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver);
|
|
50
|
+
// Safari: no requestIdleCallback, so the timeout fallback is exercised.
|
|
51
|
+
vi.stubGlobal('requestIdleCallback', undefined);
|
|
52
|
+
container = document.createElement('div');
|
|
53
|
+
document.body.append(container);
|
|
54
|
+
root = createRoot(container);
|
|
55
|
+
prefetch = vi.fn();
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
act(() => root.unmount());
|
|
59
|
+
container.remove();
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
vi.useRealTimers();
|
|
62
|
+
});
|
|
63
|
+
const render = () => act(() => root.render( /*#__PURE__*/React.createElement(Select, {
|
|
64
|
+
prefetch: prefetch
|
|
65
|
+
})));
|
|
66
|
+
it('waits until the tester is near the viewport', async () => {
|
|
67
|
+
await render();
|
|
68
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
69
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
70
|
+
await intersect(false);
|
|
71
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
72
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
it('prefetches at idle once near, not during render', async () => {
|
|
75
|
+
await render();
|
|
76
|
+
await intersect(true);
|
|
77
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
78
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
79
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
it('prefetches only once', async () => {
|
|
82
|
+
await render();
|
|
83
|
+
await intersect(true);
|
|
84
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
85
|
+
await intersect(true);
|
|
86
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
87
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
it('cancels a pending prefetch on unmount', async () => {
|
|
90
|
+
await render();
|
|
91
|
+
await intersect(true);
|
|
92
|
+
act(() => root.render( /*#__PURE__*/React.createElement(React.Fragment, null)));
|
|
93
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
94
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
it('uses requestIdleCallback where the browser has it', async () => {
|
|
97
|
+
const idle = vi.fn(cb => setTimeout(cb, 10));
|
|
98
|
+
vi.stubGlobal('requestIdleCallback', idle);
|
|
99
|
+
vi.stubGlobal('cancelIdleCallback', vi.fn());
|
|
100
|
+
await render();
|
|
101
|
+
await intersect(true);
|
|
102
|
+
expect(idle).toHaveBeenCalledWith(expect.any(Function), {
|
|
103
|
+
timeout: 2000
|
|
104
|
+
});
|
|
105
|
+
await act(() => vi.advanceTimersByTime(10));
|
|
106
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CustomerSignIn_props } from './index.js';
|
|
3
|
+
/** Dispatched on `window` once the customer is signed in; `detail.customer`. */
|
|
4
|
+
export declare const SIGNED_IN_EVENT = "fontdue:customer-signed-in";
|
|
5
|
+
/** Dispatched on `window` when the customer clicks the element's sign-out button. */
|
|
6
|
+
export declare const SIGNED_OUT_EVENT = "fontdue:customer-signed-out";
|
|
7
|
+
interface CustomerSignInElement_props extends Omit<CustomerSignIn_props, 'onSignedIn'> {
|
|
8
|
+
/** Label on the button that signs the customer out. */
|
|
9
|
+
signOutLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `<fontdue-customer-sign-in>`: the script-tag wrapper around `CustomerSignIn`.
|
|
13
|
+
*
|
|
14
|
+
* A page has no React of its own to read `useCurrentCustomer()` with, so the
|
|
15
|
+
* element shows the signed-in state itself – who is signed in and a way out –
|
|
16
|
+
* and tells the page about both changes with window events, as `setConsent`
|
|
17
|
+
* does with `fontdue:consent`.
|
|
18
|
+
*
|
|
19
|
+
* Not in the npm package's `exports`: sign-in is script-tag only for now.
|
|
20
|
+
*/
|
|
21
|
+
export default function CustomerSignInElement({ signOutLabel, ...props }: CustomerSignInElement_props): React.JSX.Element;
|
|
22
|
+
export {};
|