toiljs 0.0.10 → 0.0.12

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 (128) hide show
  1. package/README.md +315 -1
  2. package/assets/logo.svg +37 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/configure.js +10 -4
  5. package/build/cli/create.js +60 -32
  6. package/build/cli/diagnostics.d.ts +55 -0
  7. package/build/cli/diagnostics.js +333 -0
  8. package/build/cli/doctor.d.ts +6 -0
  9. package/build/cli/doctor.js +249 -0
  10. package/build/cli/index.js +26 -0
  11. package/build/cli/proc.d.ts +5 -0
  12. package/build/cli/proc.js +20 -0
  13. package/build/cli/ui.d.ts +1 -0
  14. package/build/cli/ui.js +1 -0
  15. package/build/cli/update.d.ts +7 -0
  16. package/build/cli/update.js +117 -0
  17. package/build/cli/updates.d.ts +10 -0
  18. package/build/cli/updates.js +45 -0
  19. package/build/client/.tsbuildinfo +1 -1
  20. package/build/client/dev/error-overlay.js +1 -1
  21. package/build/client/head/metadata.js +3 -1
  22. package/build/client/index.d.ts +5 -1
  23. package/build/client/index.js +2 -0
  24. package/build/client/navigation/navigation.js +1 -1
  25. package/build/client/routing/Router.js +2 -2
  26. package/build/client/search/search.d.ts +26 -0
  27. package/build/client/search/search.js +101 -0
  28. package/build/client/search/use-page-search.d.ts +8 -0
  29. package/build/client/search/use-page-search.js +21 -0
  30. package/build/compiler/.tsbuildinfo +1 -1
  31. package/build/compiler/generate.js +35 -26
  32. package/build/compiler/index.d.ts +2 -0
  33. package/build/compiler/index.js +1 -0
  34. package/build/compiler/pages.d.ts +8 -0
  35. package/build/compiler/pages.js +37 -0
  36. package/build/compiler/plugin.js +3 -1
  37. package/build/compiler/prerender.d.ts +1 -0
  38. package/build/compiler/prerender.js +11 -5
  39. package/build/compiler/seo.js +10 -3
  40. package/build/compiler/vite.js +7 -0
  41. package/build/io/.tsbuildinfo +1 -1
  42. package/examples/basic/client/components/Header.tsx +43 -38
  43. package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
  44. package/examples/basic/client/layout.tsx +4 -1
  45. package/examples/basic/client/public/index.html +18 -16
  46. package/examples/basic/client/routes/(legal)/privacy.tsx +18 -0
  47. package/examples/basic/client/routes/(legal)/terms.tsx +15 -0
  48. package/examples/basic/client/routes/about.tsx +21 -19
  49. package/examples/basic/client/routes/blog/[id].tsx +26 -12
  50. package/examples/basic/client/routes/features/actions.tsx +67 -0
  51. package/examples/basic/client/routes/features/error/error.tsx +16 -0
  52. package/examples/basic/client/routes/features/error/index.tsx +27 -0
  53. package/examples/basic/client/routes/features/head.tsx +38 -0
  54. package/examples/basic/client/routes/features/index.tsx +83 -0
  55. package/examples/basic/client/routes/features/realtime.tsx +34 -0
  56. package/examples/basic/client/routes/features/script.tsx +31 -0
  57. package/examples/basic/client/routes/features/seo.tsx +39 -0
  58. package/examples/basic/client/routes/features/template/b.tsx +14 -0
  59. package/examples/basic/client/routes/features/template/index.tsx +20 -0
  60. package/examples/basic/client/routes/features/template/template.tsx +16 -0
  61. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
  62. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
  63. package/examples/basic/client/routes/gallery/index.tsx +42 -0
  64. package/examples/basic/client/routes/gallery/layout.tsx +13 -0
  65. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
  66. package/examples/basic/client/routes/get-started.tsx +157 -84
  67. package/examples/basic/client/routes/index.tsx +137 -87
  68. package/examples/basic/client/routes/loader-demo/index.tsx +59 -50
  69. package/examples/basic/client/routes/search.tsx +61 -0
  70. package/examples/basic/client/routes/test.tsx +7 -8
  71. package/examples/basic/client/styles/main.css +624 -552
  72. package/examples/basic/client/toil.tsx +2 -4
  73. package/package.json +3 -2
  74. package/presets/eslint.js +10 -3
  75. package/src/cli/configure.ts +363 -353
  76. package/src/cli/create.ts +563 -530
  77. package/src/cli/diagnostics.ts +421 -0
  78. package/src/cli/doctor.ts +318 -0
  79. package/src/cli/features.ts +166 -160
  80. package/src/cli/index.ts +242 -211
  81. package/src/cli/proc.ts +30 -0
  82. package/src/cli/ui.ts +111 -103
  83. package/src/cli/update.ts +150 -0
  84. package/src/cli/updates.ts +69 -0
  85. package/src/client/components/Image.tsx +91 -89
  86. package/src/client/dev/error-overlay.tsx +193 -197
  87. package/src/client/head/metadata.ts +94 -92
  88. package/src/client/index.ts +79 -64
  89. package/src/client/navigation/Link.tsx +94 -100
  90. package/src/client/navigation/navigation.ts +215 -218
  91. package/src/client/routing/Router.tsx +210 -193
  92. package/src/client/routing/hooks.ts +110 -114
  93. package/src/client/routing/lazy.ts +77 -81
  94. package/src/client/search/search.ts +189 -0
  95. package/src/client/search/use-page-search.ts +73 -0
  96. package/src/compiler/config.ts +173 -171
  97. package/src/compiler/fonts.ts +89 -87
  98. package/src/compiler/generate.ts +378 -364
  99. package/src/compiler/image-report.ts +88 -85
  100. package/src/compiler/index.ts +2 -0
  101. package/src/compiler/pages.ts +70 -0
  102. package/src/compiler/plugin.ts +51 -47
  103. package/src/compiler/prerender.ts +152 -130
  104. package/src/compiler/routes.ts +132 -131
  105. package/src/compiler/seo.ts +381 -356
  106. package/src/compiler/vite.ts +155 -130
  107. package/src/io/FastSet.ts +99 -96
  108. package/test/configure.test.ts +94 -90
  109. package/test/doctor.test.ts +140 -0
  110. package/test/dom/Image.test.tsx +73 -46
  111. package/test/dom/Script.test.tsx +48 -45
  112. package/test/dom/action.test.tsx +146 -129
  113. package/test/dom/error-overlay.test.tsx +44 -44
  114. package/test/dom/loader.test.tsx +2 -2
  115. package/test/dom/revalidate.test.tsx +1 -1
  116. package/test/dom/route-head.test.tsx +35 -2
  117. package/test/dom/slot.test.tsx +131 -109
  118. package/test/dom/view-transitions.test.tsx +53 -51
  119. package/test/features.test.ts +149 -142
  120. package/test/fonts.test.ts +28 -26
  121. package/test/head.test.ts +45 -35
  122. package/test/metadata.test.ts +42 -41
  123. package/test/pages.test.ts +105 -0
  124. package/test/prerender.test.ts +54 -46
  125. package/test/search.test.ts +114 -0
  126. package/test/seo.test.ts +164 -142
  127. package/test/slot-layouts.test.ts +69 -0
  128. package/test/update.test.ts +44 -0
@@ -1,129 +1,146 @@
1
- // @vitest-environment jsdom
2
- import { act, cleanup, fireEvent, render, renderHook, waitFor } from '@testing-library/react';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import { useAction } from '../../src/client/routing/action';
6
- import { Form } from '../../src/client/components/Form';
7
- import { clearLoaderData, loaderKey, readRouteData } from '../../src/client/routing/loader';
8
- import type { RouteDef } from '../../src/client/types';
9
-
10
- afterEach(cleanup);
11
- beforeEach(() => {
12
- clearLoaderData();
13
- });
14
-
15
- describe('useAction', () => {
16
- it('goes idle → pending → success and returns the result', async () => {
17
- const onSuccess = vi.fn();
18
- const { result } = renderHook(() =>
19
- useAction((n: number) => Promise.resolve(n * 2), { revalidate: false, onSuccess }),
20
- );
21
- expect(result.current.pending).toBe(false);
22
-
23
- let returned: number | undefined;
24
- await act(async () => {
25
- returned = await result.current.run(3);
26
- });
27
- expect(returned).toBe(6);
28
- expect(result.current.data).toBe(6);
29
- expect(result.current.error).toBeUndefined();
30
- expect(onSuccess).toHaveBeenCalledWith(6);
31
- });
32
-
33
- it('captures errors instead of rejecting, and calls onError', async () => {
34
- const onError = vi.fn();
35
- const { result } = renderHook(() =>
36
- useAction(
37
- () => {
38
- throw new Error('boom');
39
- },
40
- { revalidate: false, onError },
41
- ),
42
- );
43
-
44
- let returned: unknown = 'sentinel';
45
- await act(async () => {
46
- returned = await result.current.run();
47
- });
48
- expect(returned).toBeUndefined();
49
- expect((result.current.error as Error).message).toBe('boom');
50
- expect(onError).toHaveBeenCalledOnce();
51
- });
52
-
53
- it('reset() returns to idle', async () => {
54
- const { result } = renderHook(() => useAction(() => 'x', { revalidate: false }));
55
- await act(async () => {
56
- await result.current.run();
57
- });
58
- expect(result.current.data).toBe('x');
59
- act(() => {
60
- result.current.reset();
61
- });
62
- expect(result.current.data).toBeUndefined();
63
- expect(result.current.pending).toBe(false);
64
- });
65
-
66
- it('revalidate: true invalidates cached loader data', async () => {
67
- const route: RouteDef = {
68
- pattern: '/x',
69
- load: () => Promise.resolve({ default: () => null, loader: () => ({ n: 1 }), revalidate: false }),
70
- };
71
- const key = loaderKey('/x', '');
72
- // Seed the cache (suspends once, then resolves).
73
- try {
74
- readRouteData(route, {}, key, 1);
75
- } catch (thrown) {
76
- await (thrown as Promise<void>);
77
- }
78
- // Cached now: a re-read returns synchronously (no throw).
79
- expect(() => readRouteData(route, {}, key, 1)).not.toThrow();
80
-
81
- const { result } = renderHook(() => useAction(() => 'done', { revalidate: true }));
82
- await act(async () => {
83
- await result.current.run();
84
- });
85
- // Cache was cleared → reading again suspends (throws a promise).
86
- expect(() => readRouteData(route, {}, key, 1)).toThrow();
87
- });
88
- });
89
-
90
- describe('Form', () => {
91
- it('submits FormData (no reload) and exposes pending state', async () => {
92
- let received: FormDataEntryValue | null = null;
93
- const action = vi.fn((data: FormData) => {
94
- received = data.get('title');
95
- });
96
- const { getByText, getByPlaceholderText } = render(
97
- <Form action={action} revalidate={false}>
98
- {({ pending }) => (
99
- <>
100
- <input name="title" placeholder="t" />
101
- <button type="submit">{pending ? 'Saving…' : 'Save'}</button>
102
- </>
103
- )}
104
- </Form>,
105
- );
106
- fireEvent.change(getByPlaceholderText('t'), { target: { value: 'Hello' } });
107
- fireEvent.click(getByText('Save'));
108
- await waitFor(() => {
109
- expect(action).toHaveBeenCalledOnce();
110
- });
111
- expect(received).toBe('Hello');
112
- });
113
-
114
- it('resets fields after success when resetOnSuccess is set', async () => {
115
- const { getByText, getByPlaceholderText } = render(
116
- <Form action={() => undefined} revalidate={false} resetOnSuccess>
117
- <input name="title" placeholder="t" defaultValue="" />
118
- <button type="submit">Save</button>
119
- </Form>,
120
- );
121
- const input = getByPlaceholderText('t') as HTMLInputElement;
122
- fireEvent.change(input, { target: { value: 'typed' } });
123
- expect(input.value).toBe('typed');
124
- fireEvent.click(getByText('Save'));
125
- await waitFor(() => {
126
- expect(input.value).toBe('');
127
- });
128
- });
129
- });
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, fireEvent, render, renderHook, waitFor } from '@testing-library/react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useAction } from '../../src/client/routing/action';
6
+ import { Form } from '../../src/client/components/Form';
7
+ import { clearLoaderData, loaderKey, readRouteData } from '../../src/client/routing/loader';
8
+ import type { RouteDef } from '../../src/client/types';
9
+
10
+ afterEach(cleanup);
11
+ beforeEach(() => {
12
+ clearLoaderData();
13
+ });
14
+
15
+ describe('useAction', () => {
16
+ it('goes idle → pending → success and returns the result', async () => {
17
+ const onSuccess = vi.fn();
18
+ const { result } = renderHook(() =>
19
+ useAction((n: number) => Promise.resolve(n * 2), { revalidate: false, onSuccess }),
20
+ );
21
+ expect(result.current.pending).toBe(false);
22
+
23
+ let returned: number | undefined;
24
+ await act(async () => {
25
+ returned = await result.current.run(3);
26
+ });
27
+ expect(returned).toBe(6);
28
+ expect(result.current.data).toBe(6);
29
+ expect(result.current.error).toBeUndefined();
30
+ expect(onSuccess).toHaveBeenCalledWith(6);
31
+ });
32
+
33
+ it('captures errors instead of rejecting, and calls onError', async () => {
34
+ const onError = vi.fn();
35
+ const { result } = renderHook(() =>
36
+ useAction(
37
+ () => {
38
+ throw new Error('boom');
39
+ },
40
+ { revalidate: false, onError },
41
+ ),
42
+ );
43
+
44
+ let returned: unknown = 'sentinel';
45
+ await act(async () => {
46
+ returned = await result.current.run();
47
+ });
48
+ expect(returned).toBeUndefined();
49
+ expect((result.current.error as Error).message).toBe('boom');
50
+ expect(onError).toHaveBeenCalledOnce();
51
+ });
52
+
53
+ it('reset() returns to idle', async () => {
54
+ const { result } = renderHook(() => useAction(() => 'x', { revalidate: false }));
55
+ await act(async () => {
56
+ await result.current.run();
57
+ });
58
+ expect(result.current.data).toBe('x');
59
+ act(() => {
60
+ result.current.reset();
61
+ });
62
+ expect(result.current.data).toBeUndefined();
63
+ expect(result.current.pending).toBe(false);
64
+ });
65
+
66
+ it('revalidate: true invalidates cached loader data', async () => {
67
+ const route: RouteDef = {
68
+ pattern: '/x',
69
+ load: () =>
70
+ Promise.resolve({
71
+ default: () => null,
72
+ loader: () => ({ n: 1 }),
73
+ revalidate: false,
74
+ }),
75
+ };
76
+ const key = loaderKey('/x', '');
77
+ // Seed the cache (suspends once, then resolves).
78
+ try {
79
+ readRouteData(route, {}, key, 1);
80
+ } catch (thrown) {
81
+ await (thrown as Promise<void>);
82
+ }
83
+ // Cached now: a re-read returns synchronously (no throw).
84
+ expect(() => readRouteData(route, {}, key, 1)).not.toThrow();
85
+
86
+ const { result } = renderHook(() => useAction(() => 'done', { revalidate: true }));
87
+ await act(async () => {
88
+ await result.current.run();
89
+ });
90
+ // Cache was cleared → reading again suspends (throws a promise).
91
+ expect(() => readRouteData(route, {}, key, 1)).toThrow();
92
+ });
93
+ });
94
+
95
+ describe('Form', () => {
96
+ it('submits FormData (no reload) and exposes pending state', async () => {
97
+ let received: FormDataEntryValue | null = null;
98
+ const action = vi.fn((data: FormData) => {
99
+ received = data.get('title');
100
+ });
101
+ const { getByText, getByPlaceholderText } = render(
102
+ <Form
103
+ action={action}
104
+ revalidate={false}>
105
+ {({ pending }) => (
106
+ <>
107
+ <input
108
+ name="title"
109
+ placeholder="t"
110
+ />
111
+ <button type="submit">{pending ? 'Saving…' : 'Save'}</button>
112
+ </>
113
+ )}
114
+ </Form>,
115
+ );
116
+ fireEvent.change(getByPlaceholderText('t'), { target: { value: 'Hello' } });
117
+ fireEvent.click(getByText('Save'));
118
+ await waitFor(() => {
119
+ expect(action).toHaveBeenCalledOnce();
120
+ });
121
+ expect(received).toBe('Hello');
122
+ });
123
+
124
+ it('resets fields after success when resetOnSuccess is set', async () => {
125
+ const { getByText, getByPlaceholderText } = render(
126
+ <Form
127
+ action={() => undefined}
128
+ revalidate={false}
129
+ resetOnSuccess>
130
+ <input
131
+ name="title"
132
+ placeholder="t"
133
+ defaultValue=""
134
+ />
135
+ <button type="submit">Save</button>
136
+ </Form>,
137
+ );
138
+ const input = getByPlaceholderText('t') as HTMLInputElement;
139
+ fireEvent.change(input, { target: { value: 'typed' } });
140
+ expect(input.value).toBe('typed');
141
+ fireEvent.click(getByText('Save'));
142
+ await waitFor(() => {
143
+ expect(input.value).toBe('');
144
+ });
145
+ });
146
+ });
@@ -1,44 +1,44 @@
1
- // @vitest-environment jsdom
2
- import { act, cleanup, fireEvent, render } from '@testing-library/react';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import {
6
- DevErrorBoundary,
7
- DevErrorOverlay,
8
- initDevErrorOverlay,
9
- } from '../../src/client/dev/error-overlay';
10
-
11
- afterEach(cleanup);
12
-
13
- function Boom(): never {
14
- throw new Error('render boom');
15
- }
16
-
17
- describe('dev error overlay', () => {
18
- it('surfaces an uncaught render error', () => {
19
- // React logs caught boundary errors to console.error — silence it for a clean test run.
20
- const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
21
- const { getByRole } = render(
22
- <>
23
- <DevErrorBoundary>
24
- <Boom />
25
- </DevErrorBoundary>
26
- <DevErrorOverlay />
27
- </>,
28
- );
29
- expect(getByRole('alert').textContent).toContain('render boom');
30
- spy.mockRestore();
31
- });
32
-
33
- it('surfaces an unhandled window error and dismisses it', async () => {
34
- initDevErrorOverlay();
35
- const { findByRole, queryByRole, getByText } = render(<DevErrorOverlay />);
36
- act(() => {
37
- window.dispatchEvent(new ErrorEvent('error', { error: new Error('async boom') }));
38
- });
39
- const alert = await findByRole('alert');
40
- expect(alert.textContent).toContain('async boom');
41
- fireEvent.click(getByText('Dismiss'));
42
- expect(queryByRole('alert')).toBeNull();
43
- });
44
- });
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, fireEvent, render } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import {
6
+ DevErrorBoundary,
7
+ DevErrorOverlay,
8
+ initDevErrorOverlay,
9
+ } from '../../src/client/dev/error-overlay';
10
+
11
+ afterEach(cleanup);
12
+
13
+ function Boom(): never {
14
+ throw new Error('render boom');
15
+ }
16
+
17
+ describe('dev error overlay', () => {
18
+ it('surfaces an uncaught render error', () => {
19
+ // React logs caught boundary errors to console.error — silence it for a clean test run.
20
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
21
+ const { getByRole } = render(
22
+ <>
23
+ <DevErrorBoundary>
24
+ <Boom />
25
+ </DevErrorBoundary>
26
+ <DevErrorOverlay />
27
+ </>,
28
+ );
29
+ expect(getByRole('alert').textContent).toContain('render boom');
30
+ spy.mockRestore();
31
+ });
32
+
33
+ it('surfaces an unhandled window error and dismisses it', async () => {
34
+ initDevErrorOverlay();
35
+ const { findByRole, queryByRole, getByText } = render(<DevErrorOverlay />);
36
+ act(() => {
37
+ window.dispatchEvent(new ErrorEvent('error', { error: new Error('async boom') }));
38
+ });
39
+ const alert = await findByRole('alert');
40
+ expect(alert.textContent).toContain('async boom');
41
+ fireEvent.click(getByText('Dismiss'));
42
+ expect(queryByRole('alert')).toBeNull();
43
+ });
44
+ });
@@ -1,14 +1,14 @@
1
1
  // @vitest-environment jsdom
2
2
  import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
3
3
 
4
+ import type { Revalidate } from '../../src/client/routing/loader';
4
5
  import {
5
6
  clearLoaderData,
6
7
  invalidateLoaderData,
8
+ type LoaderData,
7
9
  loaderKey,
8
10
  readRouteData,
9
- type LoaderData,
10
11
  } from '../../src/client/routing/loader';
11
- import type { Revalidate } from '../../src/client/routing/loader';
12
12
  import type { RouteDef } from '../../src/client/types';
13
13
 
14
14
  /** Reads route data, awaiting the suspending promise once if it's pending. */
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
  import { act, cleanup, render } from '@testing-library/react';
3
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+ import { afterEach, beforeEach, describe, it } from 'vitest';
4
4
 
5
5
  import { Router } from '../../src/client/routing/Router';
6
6
  import { clearLoaderData, revalidate, useLoaderData } from '../../src/client/routing/loader';
@@ -1,10 +1,9 @@
1
1
  // @vitest-environment jsdom
2
- import { describe, expect, it } from 'vitest';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
3
 
4
4
  import { setRouteHead, useHead } from '../../src/client/head/head';
5
5
  import { resolveMetadata } from '../../src/client/head/metadata';
6
6
  import { cleanup, render } from '@testing-library/react';
7
- import { afterEach } from 'vitest';
8
7
 
9
8
  afterEach(() => {
10
9
  cleanup();
@@ -42,4 +41,38 @@ describe('route head (metadata baseline)', () => {
42
41
  setRouteHead(resolveMetadata({ title: 'About' }));
43
42
  expect(document.title).toBe('About · toiljs');
44
43
  });
44
+
45
+ // Regression for the "metadata title doesn't update" report: a real layout (title + template)
46
+ // plus a route's full `metadata` (the exact shape users write) must land on the route's title,
47
+ // wrapped by the layout template, with the route's og:title applied too.
48
+ it('applies a full route metadata over a layout title + template', () => {
49
+ function LayoutDefaults() {
50
+ useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
51
+ return null;
52
+ }
53
+ render(<LayoutDefaults />);
54
+ setRouteHead(
55
+ resolveMetadata({
56
+ title: 'useReducer | React Hooks',
57
+ description: 'Manage complex state with a reducer.',
58
+ openGraph: { title: 'useReducer | React Hooks', type: 'website' },
59
+ }),
60
+ );
61
+ expect(document.title).toBe('useReducer | React Hooks | ToilJS');
62
+ expect(
63
+ document.head.querySelector('meta[property="og:title"]')?.getAttribute('content'),
64
+ ).toBe('useReducer | React Hooks');
65
+ });
66
+
67
+ // A route can opt out of the layout's template by setting its own `titleTemplate: '%s'`, so the
68
+ // tab reads exactly the route title with no site suffix.
69
+ it("lets a route override the layout template with its own '%s'", () => {
70
+ function LayoutDefaults() {
71
+ useHead({ titleTemplate: '%s | ToilJS', title: 'ToilJS' });
72
+ return null;
73
+ }
74
+ render(<LayoutDefaults />);
75
+ setRouteHead(resolveMetadata({ title: 'useReducer | React Hooks', titleTemplate: '%s' }));
76
+ expect(document.title).toBe('useReducer | React Hooks');
77
+ });
45
78
  });