fontdue-js 3.11.0 → 3.12.1
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__/CustomerLoginFormLoginMutation.graphql.d.ts +2 -1
- package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.d.ts +21 -0
- package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.js +86 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.d.ts +22 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.js +87 -0
- package/dist/__tests__/customerLoginRecaptcha.test.js +113 -0
- 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__/typeTesterHeightAnimation.test.js +124 -0
- package/dist/__tests__/typeTesterPrefetchStyles.test.js +108 -0
- package/dist/__tests__/typeTesterShyFeaturesClose.test.js +133 -0
- package/dist/components/CustomerLoginForm/index.js +106 -11
- 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/StripeProvider/index.js +19 -2
- package/dist/components/TypeTester/TypeTesterFeatures.js +27 -44
- package/dist/components/TypeTester/TypeTesterStyleSelect.js +6 -1
- package/dist/components/TypeTester/useCloseWhenToolbarHides.d.ts +17 -0
- package/dist/components/TypeTester/useCloseWhenToolbarHides.js +79 -0
- package/dist/components/TypeTester/useHeightAnimation.d.ts +27 -0
- package/dist/components/TypeTester/useHeightAnimation.js +85 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.d.ts +14 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.js +69 -0
- package/dist/fontdue.css +2 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 3.12.1
|
|
2
|
+
|
|
3
|
+
- Fixed a `TypeTester`’s style menu showing only the current style on the first click, most visibly in Safari. The style list now loads as the type tester nears the viewport.
|
|
4
|
+
- Fixed the `StoreModal` checkout showing Stripe’s generic error when Stripe refuses to start a checkout, such as for a total under its minimum charge. It now shows Stripe’s reason.
|
|
5
|
+
|
|
6
|
+
## 3.12.0
|
|
7
|
+
|
|
8
|
+
- `CustomerLoginForm` runs an invisible reCAPTCHA check when *Enable reCAPTCHA* is on under *Settings → Integration*, giving the sign-in form the same bot protection as `NewsletterSignup` and `TestFontsForm`.
|
|
9
|
+
- Fixed a `TypeTester` with `shy` leaving its OpenType features panel open after the toolbar hides. The panel and the dropdown now close shortly after the toolbar does.
|
|
10
|
+
|
|
1
11
|
## 3.11.0
|
|
2
12
|
|
|
3
13
|
- `TestFontsForm` can group its family checkboxes by superfamily. With *Family list* set to *Grouped by superfamily* under *Settings → Test fonts* – or `testFonts: { familyGrouping: "superfamily" }` in the config, or the `familyGrouping` prop (`family-grouping` on `<fontdue-test-fonts-form>`) – each superfamily gets a checkbox that ticks all of its families, listed beneath it in `.test-fonts__family-group__families`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @generated SignedSource<<
|
|
2
|
+
* @generated SignedSource<<e3f8ae800d45eb22248382f5c156624a>>
|
|
3
3
|
* @lightSyntaxTransform
|
|
4
4
|
* @nogrep
|
|
5
5
|
*/
|
|
@@ -8,6 +8,7 @@ export type LoginInput = {
|
|
|
8
8
|
email?: string | null;
|
|
9
9
|
name?: string | null;
|
|
10
10
|
newsletterOptIn?: boolean | null;
|
|
11
|
+
recaptchaToken?: string | null;
|
|
11
12
|
target?: string | null;
|
|
12
13
|
};
|
|
13
14
|
export type CustomerLoginFormLoginMutation$variables = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<ddf67cfa749ae9a10bfcec2bb2d6b15b>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
import { ConcreteRequest } from 'relay-runtime';
|
|
7
|
+
export type CustomerLoginFormRecaptchaQuery$variables = Record<PropertyKey, never>;
|
|
8
|
+
export type CustomerLoginFormRecaptchaQuery$data = {
|
|
9
|
+
readonly viewer: {
|
|
10
|
+
readonly settings: {
|
|
11
|
+
readonly recaptchaEnabled: boolean;
|
|
12
|
+
readonly recaptchaSiteKey: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type CustomerLoginFormRecaptchaQuery = {
|
|
17
|
+
response: CustomerLoginFormRecaptchaQuery$data;
|
|
18
|
+
variables: CustomerLoginFormRecaptchaQuery$variables;
|
|
19
|
+
};
|
|
20
|
+
declare const node: ConcreteRequest;
|
|
21
|
+
export default node;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<ddf67cfa749ae9a10bfcec2bb2d6b15b>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* tslint:disable */
|
|
8
|
+
/* eslint-disable */
|
|
9
|
+
// @ts-nocheck
|
|
10
|
+
|
|
11
|
+
const node = function () {
|
|
12
|
+
var v0 = {
|
|
13
|
+
"alias": null,
|
|
14
|
+
"args": null,
|
|
15
|
+
"concreteType": "Settings",
|
|
16
|
+
"kind": "LinkedField",
|
|
17
|
+
"name": "settings",
|
|
18
|
+
"plural": false,
|
|
19
|
+
"selections": [{
|
|
20
|
+
"alias": null,
|
|
21
|
+
"args": null,
|
|
22
|
+
"kind": "ScalarField",
|
|
23
|
+
"name": "recaptchaEnabled",
|
|
24
|
+
"storageKey": null
|
|
25
|
+
}, {
|
|
26
|
+
"alias": null,
|
|
27
|
+
"args": null,
|
|
28
|
+
"kind": "ScalarField",
|
|
29
|
+
"name": "recaptchaSiteKey",
|
|
30
|
+
"storageKey": null
|
|
31
|
+
}],
|
|
32
|
+
"storageKey": null
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
"fragment": {
|
|
36
|
+
"argumentDefinitions": [],
|
|
37
|
+
"kind": "Fragment",
|
|
38
|
+
"metadata": null,
|
|
39
|
+
"name": "CustomerLoginFormRecaptchaQuery",
|
|
40
|
+
"selections": [{
|
|
41
|
+
"alias": null,
|
|
42
|
+
"args": null,
|
|
43
|
+
"concreteType": "Viewer",
|
|
44
|
+
"kind": "LinkedField",
|
|
45
|
+
"name": "viewer",
|
|
46
|
+
"plural": false,
|
|
47
|
+
"selections": [v0 /*: any*/],
|
|
48
|
+
"storageKey": null
|
|
49
|
+
}],
|
|
50
|
+
"type": "RootQueryType",
|
|
51
|
+
"abstractKey": null
|
|
52
|
+
},
|
|
53
|
+
"kind": "Request",
|
|
54
|
+
"operation": {
|
|
55
|
+
"argumentDefinitions": [],
|
|
56
|
+
"kind": "Operation",
|
|
57
|
+
"name": "CustomerLoginFormRecaptchaQuery",
|
|
58
|
+
"selections": [{
|
|
59
|
+
"alias": null,
|
|
60
|
+
"args": null,
|
|
61
|
+
"concreteType": "Viewer",
|
|
62
|
+
"kind": "LinkedField",
|
|
63
|
+
"name": "viewer",
|
|
64
|
+
"plural": false,
|
|
65
|
+
"selections": [v0 /*: any*/, {
|
|
66
|
+
"alias": null,
|
|
67
|
+
"args": null,
|
|
68
|
+
"kind": "ScalarField",
|
|
69
|
+
"name": "id",
|
|
70
|
+
"storageKey": null
|
|
71
|
+
}],
|
|
72
|
+
"storageKey": null
|
|
73
|
+
}]
|
|
74
|
+
},
|
|
75
|
+
"params": {
|
|
76
|
+
"cacheID": "a65b8ea63db01df3cbdf602ac7721787",
|
|
77
|
+
"id": null,
|
|
78
|
+
"metadata": {},
|
|
79
|
+
"name": "CustomerLoginFormRecaptchaQuery",
|
|
80
|
+
"operationKind": "query",
|
|
81
|
+
"text": "query CustomerLoginFormRecaptchaQuery {\n viewer {\n settings {\n recaptchaEnabled\n recaptchaSiteKey\n }\n id\n }\n}\n"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}();
|
|
85
|
+
node.hash = "fe7289e19bcc08dbad62e72d67c48c97";
|
|
86
|
+
export default node;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<115e004f4831c3eb09ea98ab9cd0934a>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
import { ConcreteRequest } from 'relay-runtime';
|
|
7
|
+
export type CustomerSignInElementQuery$variables = Record<PropertyKey, never>;
|
|
8
|
+
export type CustomerSignInElementQuery$data = {
|
|
9
|
+
readonly viewer: {
|
|
10
|
+
readonly currentCustomer: {
|
|
11
|
+
readonly email: string | null;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type CustomerSignInElementQuery = {
|
|
18
|
+
response: CustomerSignInElementQuery$data;
|
|
19
|
+
variables: CustomerSignInElementQuery$variables;
|
|
20
|
+
};
|
|
21
|
+
declare const node: ConcreteRequest;
|
|
22
|
+
export default node;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<115e004f4831c3eb09ea98ab9cd0934a>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* tslint:disable */
|
|
8
|
+
/* eslint-disable */
|
|
9
|
+
// @ts-nocheck
|
|
10
|
+
|
|
11
|
+
const node = function () {
|
|
12
|
+
var v0 = {
|
|
13
|
+
"alias": null,
|
|
14
|
+
"args": null,
|
|
15
|
+
"kind": "ScalarField",
|
|
16
|
+
"name": "id",
|
|
17
|
+
"storageKey": null
|
|
18
|
+
},
|
|
19
|
+
v1 = {
|
|
20
|
+
"alias": null,
|
|
21
|
+
"args": null,
|
|
22
|
+
"concreteType": "Customer",
|
|
23
|
+
"kind": "LinkedField",
|
|
24
|
+
"name": "currentCustomer",
|
|
25
|
+
"plural": false,
|
|
26
|
+
"selections": [v0 /*: any*/, {
|
|
27
|
+
"alias": null,
|
|
28
|
+
"args": null,
|
|
29
|
+
"kind": "ScalarField",
|
|
30
|
+
"name": "name",
|
|
31
|
+
"storageKey": null
|
|
32
|
+
}, {
|
|
33
|
+
"alias": null,
|
|
34
|
+
"args": null,
|
|
35
|
+
"kind": "ScalarField",
|
|
36
|
+
"name": "email",
|
|
37
|
+
"storageKey": null
|
|
38
|
+
}],
|
|
39
|
+
"storageKey": null
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
"fragment": {
|
|
43
|
+
"argumentDefinitions": [],
|
|
44
|
+
"kind": "Fragment",
|
|
45
|
+
"metadata": null,
|
|
46
|
+
"name": "CustomerSignInElementQuery",
|
|
47
|
+
"selections": [{
|
|
48
|
+
"alias": null,
|
|
49
|
+
"args": null,
|
|
50
|
+
"concreteType": "Viewer",
|
|
51
|
+
"kind": "LinkedField",
|
|
52
|
+
"name": "viewer",
|
|
53
|
+
"plural": false,
|
|
54
|
+
"selections": [v1 /*: any*/],
|
|
55
|
+
"storageKey": null
|
|
56
|
+
}],
|
|
57
|
+
"type": "RootQueryType",
|
|
58
|
+
"abstractKey": null
|
|
59
|
+
},
|
|
60
|
+
"kind": "Request",
|
|
61
|
+
"operation": {
|
|
62
|
+
"argumentDefinitions": [],
|
|
63
|
+
"kind": "Operation",
|
|
64
|
+
"name": "CustomerSignInElementQuery",
|
|
65
|
+
"selections": [{
|
|
66
|
+
"alias": null,
|
|
67
|
+
"args": null,
|
|
68
|
+
"concreteType": "Viewer",
|
|
69
|
+
"kind": "LinkedField",
|
|
70
|
+
"name": "viewer",
|
|
71
|
+
"plural": false,
|
|
72
|
+
"selections": [v1 /*: any*/, v0 /*: any*/],
|
|
73
|
+
"storageKey": null
|
|
74
|
+
}]
|
|
75
|
+
},
|
|
76
|
+
"params": {
|
|
77
|
+
"cacheID": "3f3148c6d2bf1f2b90edfb3f217f6e50",
|
|
78
|
+
"id": null,
|
|
79
|
+
"metadata": {},
|
|
80
|
+
"name": "CustomerSignInElementQuery",
|
|
81
|
+
"operationKind": "query",
|
|
82
|
+
"text": "query CustomerSignInElementQuery {\n viewer {\n currentCustomer {\n id\n name\n email\n }\n id\n }\n}\n"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}();
|
|
86
|
+
node.hash = "e7d4a8fb8d19581d6bc36f29090230af";
|
|
87
|
+
export default node;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// The customer sign-in form solves the store's invisible reCAPTCHA before it
|
|
4
|
+
// asks for a sign-in link, when the store has reCAPTCHA on: the form emails
|
|
5
|
+
// whatever address is typed, which made it a tool for email-bombing strangers.
|
|
6
|
+
|
|
7
|
+
import React, { act, forwardRef, useImperativeHandle, useRef } 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
|
+
settings: null,
|
|
15
|
+
commits: []
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('react-relay', () => ({
|
|
19
|
+
graphql: () => null,
|
|
20
|
+
useRelayEnvironment: () => ({}),
|
|
21
|
+
useLazyLoadQuery: () => ({
|
|
22
|
+
viewer: {
|
|
23
|
+
settings: relay.settings
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
commitMutation: (_env, config) => {
|
|
27
|
+
relay.commits.push(config.variables);
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Google's widget: `execute()` hands back a token a moment later, the way the
|
|
32
|
+
// real callback arrives after the form has re-rendered – and, like
|
|
33
|
+
// react-google-recaptcha, it calls whichever `onChange` it holds by then.
|
|
34
|
+
vi.mock('../components/Recaptcha.js', () => ({
|
|
35
|
+
ReCAPTCHA: /*#__PURE__*/forwardRef(function FakeRecaptcha(props, ref) {
|
|
36
|
+
const latest = useRef(props);
|
|
37
|
+
latest.current = props;
|
|
38
|
+
useImperativeHandle(ref, () => ({
|
|
39
|
+
execute: () => setTimeout(() => latest.current.onChange('solved-token'), 0),
|
|
40
|
+
reset: () => {}
|
|
41
|
+
}));
|
|
42
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
43
|
+
"data-testid": "recaptcha"
|
|
44
|
+
});
|
|
45
|
+
})
|
|
46
|
+
}));
|
|
47
|
+
import CustomerLoginForm from '../components/CustomerLoginForm/index.js';
|
|
48
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
49
|
+
let container;
|
|
50
|
+
let root;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
relay.commits = [];
|
|
53
|
+
container = document.createElement('div');
|
|
54
|
+
document.body.append(container);
|
|
55
|
+
root = createRoot(container);
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
act(() => root.unmount());
|
|
59
|
+
container.remove();
|
|
60
|
+
});
|
|
61
|
+
const render = async () => {
|
|
62
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(CustomerLoginForm, null)));
|
|
63
|
+
};
|
|
64
|
+
const submit = async email => {
|
|
65
|
+
const input = container.querySelector('input[type="email"]');
|
|
66
|
+
await act(async () => {
|
|
67
|
+
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
68
|
+
setValue.call(input, email);
|
|
69
|
+
input.dispatchEvent(new Event('input', {
|
|
70
|
+
bubbles: true
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
await act(async () => {
|
|
74
|
+
container.querySelector('form').dispatchEvent(new Event('submit', {
|
|
75
|
+
bubbles: true,
|
|
76
|
+
cancelable: true
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
// Let the widget's token callback land.
|
|
80
|
+
await act(async () => new Promise(resolve => setTimeout(resolve, 5)));
|
|
81
|
+
};
|
|
82
|
+
describe('CustomerLoginForm reCAPTCHA', () => {
|
|
83
|
+
it('asks for a link straight away on a store without reCAPTCHA', async () => {
|
|
84
|
+
relay.settings = {
|
|
85
|
+
recaptchaEnabled: false,
|
|
86
|
+
recaptchaSiteKey: null
|
|
87
|
+
};
|
|
88
|
+
await render();
|
|
89
|
+
expect(container.querySelector('[data-testid="recaptcha"]')).toBeNull();
|
|
90
|
+
await submit('reader@example.com');
|
|
91
|
+
expect(relay.commits).toEqual([{
|
|
92
|
+
input: {
|
|
93
|
+
email: 'reader@example.com',
|
|
94
|
+
recaptchaToken: undefined
|
|
95
|
+
}
|
|
96
|
+
}]);
|
|
97
|
+
});
|
|
98
|
+
it('solves the widget first, and sends its token with the request', async () => {
|
|
99
|
+
relay.settings = {
|
|
100
|
+
recaptchaEnabled: true,
|
|
101
|
+
recaptchaSiteKey: 'site-key'
|
|
102
|
+
};
|
|
103
|
+
await render();
|
|
104
|
+
expect(container.querySelector('[data-testid="recaptcha"]')).not.toBeNull();
|
|
105
|
+
await submit('reader@example.com');
|
|
106
|
+
expect(relay.commits).toEqual([{
|
|
107
|
+
input: {
|
|
108
|
+
email: 'reader@example.com',
|
|
109
|
+
recaptchaToken: 'solved-token'
|
|
110
|
+
}
|
|
111
|
+
}]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -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
|
+
});
|