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.
Files changed (31) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.d.ts +2 -1
  3. package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.js +1 -1
  4. package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.d.ts +21 -0
  5. package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.js +86 -0
  6. package/dist/__generated__/CustomerSignInElementQuery.graphql.d.ts +22 -0
  7. package/dist/__generated__/CustomerSignInElementQuery.graphql.js +87 -0
  8. package/dist/__tests__/customerLoginRecaptcha.test.js +113 -0
  9. package/dist/__tests__/customerSignInElement.test.js +155 -0
  10. package/dist/__tests__/customerSignInRequest.test.js +85 -0
  11. package/dist/__tests__/stripeCheckoutError.test.js +110 -0
  12. package/dist/__tests__/typeTesterHeightAnimation.test.js +124 -0
  13. package/dist/__tests__/typeTesterPrefetchStyles.test.js +108 -0
  14. package/dist/__tests__/typeTesterShyFeaturesClose.test.js +133 -0
  15. package/dist/components/CustomerLoginForm/index.js +106 -11
  16. package/dist/components/CustomerSignIn/CustomerSignInElement.d.ts +22 -0
  17. package/dist/components/CustomerSignIn/CustomerSignInElement.js +132 -0
  18. package/dist/components/CustomerSignIn/index.js +4 -2
  19. package/dist/components/Root/index.js +2 -0
  20. package/dist/components/StripeProvider/index.js +19 -2
  21. package/dist/components/TypeTester/TypeTesterFeatures.js +27 -44
  22. package/dist/components/TypeTester/TypeTesterStyleSelect.js +6 -1
  23. package/dist/components/TypeTester/useCloseWhenToolbarHides.d.ts +17 -0
  24. package/dist/components/TypeTester/useCloseWhenToolbarHides.js +79 -0
  25. package/dist/components/TypeTester/useHeightAnimation.d.ts +27 -0
  26. package/dist/components/TypeTester/useHeightAnimation.js +85 -0
  27. package/dist/components/TypeTester/usePrefetchWhenNear.d.ts +14 -0
  28. package/dist/components/TypeTester/usePrefetchWhenNear.js +69 -0
  29. package/dist/fontdue.css +2 -1
  30. package/dist/version.js +1 -1
  31. 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<<fd35a09343fd0b9af65f7c50420747b6>>
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 = {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<fd35a09343fd0b9af65f7c50420747b6>>
2
+ * @generated SignedSource<<e3f8ae800d45eb22248382f5c156624a>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -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
+ });