@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.9 → 3.9.3-ultramodern.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 (112) hide show
  1. package/dist/cjs/cli/artifacts.js +15 -1
  2. package/dist/cjs/cli/index.js +13 -2
  3. package/dist/cjs/cli/routeMetadata.js +131 -0
  4. package/dist/cjs/cli/rscRouteLayer.js +86 -0
  5. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  6. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  7. package/dist/cjs/cli/tanstackTypes/source.js +28 -63
  8. package/dist/cjs/runtime/lifecycle.js +0 -3
  9. package/dist/cjs/runtime/loaderBridge.js +10 -4
  10. package/dist/cjs/runtime/navigation.js +102 -0
  11. package/dist/cjs/runtime/plugin.js +10 -1
  12. package/dist/cjs/runtime/plugin.node.js +89 -64
  13. package/dist/cjs/runtime/routeTree/construction.js +19 -61
  14. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  15. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  16. package/dist/cjs/runtime/ssrManagedTags.js +2 -12
  17. package/dist/cjs/runtime/utils.js +1 -0
  18. package/dist/cjs/shared/routeDescriptor.js +60 -0
  19. package/dist/esm/cli/artifacts.mjs +13 -2
  20. package/dist/esm/cli/index.mjs +13 -11
  21. package/dist/esm/cli/routeMetadata.mjs +71 -0
  22. package/dist/esm/cli/rscRouteLayer.mjs +35 -0
  23. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  24. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  25. package/dist/esm/cli/tanstackTypes/source.mjs +29 -64
  26. package/dist/esm/runtime/lifecycle.mjs +1 -1
  27. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  28. package/dist/esm/runtime/navigation.mjs +64 -0
  29. package/dist/esm/runtime/plugin.mjs +10 -1
  30. package/dist/esm/runtime/plugin.node.mjs +91 -66
  31. package/dist/esm/runtime/routeTree/construction.mjs +19 -61
  32. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  33. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  34. package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
  35. package/dist/esm/runtime/utils.mjs +1 -0
  36. package/dist/esm/shared/routeDescriptor.mjs +22 -0
  37. package/dist/esm-node/cli/artifacts.mjs +13 -2
  38. package/dist/esm-node/cli/index.mjs +13 -11
  39. package/dist/esm-node/cli/routeMetadata.mjs +72 -0
  40. package/dist/esm-node/cli/rscRouteLayer.mjs +36 -0
  41. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  42. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  43. package/dist/esm-node/cli/tanstackTypes/source.mjs +29 -64
  44. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  45. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  46. package/dist/esm-node/runtime/navigation.mjs +65 -0
  47. package/dist/esm-node/runtime/plugin.mjs +10 -1
  48. package/dist/esm-node/runtime/plugin.node.mjs +91 -66
  49. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
  50. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  51. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  52. package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
  53. package/dist/esm-node/runtime/utils.mjs +1 -0
  54. package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
  55. package/dist/types/cli/artifacts.d.ts +16 -7
  56. package/dist/types/cli/index.d.ts +1 -1
  57. package/dist/types/cli/routeMetadata.d.ts +18 -0
  58. package/dist/types/cli/rscRouteLayer.d.ts +11 -0
  59. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  60. package/dist/types/cli/tanstackTypes/source.d.ts +3 -2
  61. package/dist/types/runtime/lifecycle.d.ts +1 -1
  62. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  63. package/dist/types/runtime/navigation.d.ts +4 -0
  64. package/dist/types/runtime/pluginShared.d.ts +5 -0
  65. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  66. package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
  67. package/dist/types/runtime/ssrTypes.d.ts +0 -7
  68. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  69. package/package.json +32 -26
  70. package/src/cli/artifacts.ts +59 -8
  71. package/src/cli/index.ts +17 -4
  72. package/src/cli/routeMetadata.ts +121 -0
  73. package/src/cli/rscRouteLayer.ts +93 -0
  74. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  75. package/src/cli/tanstackTypes/shared.ts +0 -14
  76. package/src/cli/tanstackTypes/source.ts +48 -120
  77. package/src/runtime/lifecycle.ts +0 -1
  78. package/src/runtime/loaderBridge.ts +9 -9
  79. package/src/runtime/navigation.ts +78 -0
  80. package/src/runtime/plugin.node.tsx +121 -91
  81. package/src/runtime/plugin.tsx +10 -0
  82. package/src/runtime/pluginShared.ts +12 -0
  83. package/src/runtime/routeTree/construction.ts +32 -112
  84. package/src/runtime/routeTree/loaders.ts +2 -4
  85. package/src/runtime/routeTree/paths.ts +0 -6
  86. package/src/runtime/ssrManagedTags.ts +0 -17
  87. package/src/runtime/ssrTypes.ts +0 -7
  88. package/src/runtime/utils.tsx +1 -0
  89. package/src/shared/routeDescriptor.ts +46 -0
  90. package/dist/esm/rslib-runtime~1.mjs +0 -18
  91. package/dist/esm-node/rslib-runtime~0.mjs +0 -19
  92. package/rslib.config.mts +0 -4
  93. package/rstest.config.mts +0 -53
  94. package/tests/router/cli.test.ts +0 -420
  95. package/tests/router/clientHydration.test.tsx +0 -200
  96. package/tests/router/dataMutation.test.tsx +0 -428
  97. package/tests/router/fixtures/router-gen.oxlint.json +0 -5
  98. package/tests/router/flightSerialization.roundtrip.test.tsx +0 -140
  99. package/tests/router/generateRouteArtifacts.test.ts +0 -146
  100. package/tests/router/loaderBridge.test.ts +0 -207
  101. package/tests/router/localisedIdentity.test.tsx +0 -95
  102. package/tests/router/prefetchLink.test.tsx +0 -122
  103. package/tests/router/prefetchLinkPreload.test.tsx +0 -65
  104. package/tests/router/routeHooks.test.ts +0 -49
  105. package/tests/router/routeTree.test.ts +0 -240
  106. package/tests/router/rsc.test.tsx +0 -480
  107. package/tests/router/serverPlugin.test.ts +0 -157
  108. package/tests/router/slotUsageSanitizer.test.ts +0 -23
  109. package/tests/router/ssrPreload.test.ts +0 -30
  110. package/tests/router/tanstackTypes.test.ts +0 -384
  111. package/tsconfig.json +0 -12
  112. package/tsconfig.tsgo.json +0 -6
@@ -1,200 +0,0 @@
1
- import type { AnyRouter } from '@tanstack/react-router';
2
- import { act, render, screen, waitFor } from '@testing-library/react';
3
- import React, { Suspense } from 'react';
4
- import { ModernRouterClient } from '../../src/runtime/clientHydration';
5
- import { createTreeGetterFromFlightStream } from '../../src/runtime/rsc/treeGetter';
6
-
7
- let hydrateImpl: () => Promise<unknown>;
8
- let createFromReadableStreamImpl: () => Promise<unknown>;
9
- let registeredCallServer: unknown;
10
- let decoderCallServer: unknown;
11
-
12
- rstest.mock('@tanstack/react-router', () => ({
13
- RouterProvider: ({ router }: { router: { label: string } }) => (
14
- <div data-testid="router-provider">{router.label}</div>
15
- ),
16
- }));
17
-
18
- rstest.mock('@tanstack/react-router/ssr/client', () => ({
19
- hydrate: () => hydrateImpl(),
20
- }));
21
-
22
- rstest.mock('@modern-js/runtime/rsc/client', () => {
23
- const callServer = () => undefined;
24
-
25
- return {
26
- callServer,
27
- createFromReadableStream: (
28
- _stream: ReadableStream<Uint8Array>,
29
- options: { callServer: unknown },
30
- ) => {
31
- decoderCallServer = options.callServer;
32
- return createFromReadableStreamImpl();
33
- },
34
- setServerCallback: (callback: unknown) => {
35
- registeredCallServer = callback;
36
- },
37
- };
38
- });
39
-
40
- function createDeferred<T>() {
41
- let resolve!: (value: T | PromiseLike<T>) => void;
42
- let reject!: (reason?: unknown) => void;
43
- const promise = new Promise<T>((resolvePromise, rejectPromise) => {
44
- resolve = resolvePromise;
45
- reject = rejectPromise;
46
- });
47
- return { promise, reject, resolve };
48
- }
49
-
50
- function createRouter(label: string): AnyRouter {
51
- return {
52
- label,
53
- routesById: {},
54
- stores: {
55
- matches: {
56
- get: () => [],
57
- },
58
- },
59
- } as unknown as AnyRouter;
60
- }
61
-
62
- function FlightTree({ getTree }: { getTree: () => unknown }) {
63
- const tree = getTree();
64
- if (
65
- typeof tree !== 'object' ||
66
- tree === null ||
67
- !('label' in tree) ||
68
- typeof tree.label !== 'string'
69
- ) {
70
- throw new Error('Expected the decoded Flight tree to contain a label.');
71
- }
72
- return <div data-testid="flight-tree">{tree.label}</div>;
73
- }
74
-
75
- class FlightErrorBoundary extends React.Component<
76
- { children: React.ReactNode; onError: (error: unknown) => void },
77
- { error: unknown }
78
- > {
79
- state = { error: undefined };
80
-
81
- static getDerivedStateFromError(error: unknown) {
82
- return { error };
83
- }
84
-
85
- componentDidCatch(error: unknown) {
86
- this.props.onError(error);
87
- }
88
-
89
- render() {
90
- if (this.state.error !== undefined) {
91
- return <div data-testid="flight-error">failed</div>;
92
- }
93
- return this.props.children;
94
- }
95
- }
96
-
97
- describe('TanStack SSR client hydration', () => {
98
- it('keeps the router behind Suspense until late SSR hydration completes', async () => {
99
- const hydration = createDeferred<void>();
100
- hydrateImpl = () => hydration.promise;
101
-
102
- render(
103
- <Suspense fallback={<div data-testid="hydrating">hydrating</div>}>
104
- <ModernRouterClient router={createRouter('inventory')} />
105
- </Suspense>,
106
- );
107
-
108
- expect(screen.getByTestId('hydrating')).toBeTruthy();
109
- expect(screen.queryByTestId('router-provider')).toBeNull();
110
-
111
- hydration.resolve();
112
-
113
- expect(
114
- (await screen.findByTestId('router-provider')).textContent,
115
- ).toContain('inventory');
116
- expect(screen.queryByTestId('hydrating')).toBeNull();
117
- });
118
-
119
- it('reuses one Flight decode while suspended and returns the resolved tree synchronously', async () => {
120
- const flight = createDeferred<unknown>();
121
- const decodeFlight = rstest.fn(() => flight.promise);
122
- createFromReadableStreamImpl = decodeFlight;
123
- registeredCallServer = undefined;
124
- decoderCallServer = undefined;
125
-
126
- const getTree = createTreeGetterFromFlightStream(
127
- new ReadableStream<Uint8Array>(),
128
- );
129
- const view = (
130
- <Suspense fallback={<div data-testid="flight-loading">loading</div>}>
131
- <FlightTree getTree={getTree} />
132
- </Suspense>
133
- );
134
- const { rerender } = render(view);
135
-
136
- expect(screen.getByTestId('flight-loading')).toBeTruthy();
137
- await waitFor(() => expect(decodeFlight).toHaveBeenCalledTimes(1));
138
- expect(registeredCallServer).toBe(decoderCallServer);
139
-
140
- await act(() => rerender(view));
141
- expect(screen.getByTestId('flight-loading')).toBeTruthy();
142
- expect(decodeFlight).toHaveBeenCalledTimes(1);
143
-
144
- const tree = { label: 'decoded inventory' };
145
- await act(async () => {
146
- flight.resolve(tree);
147
- await flight.promise;
148
- });
149
-
150
- expect((await screen.findByTestId('flight-tree')).textContent).toBe(
151
- tree.label,
152
- );
153
- expect(decodeFlight).toHaveBeenCalledTimes(1);
154
- expect(getTree()).toBe(tree);
155
- });
156
-
157
- it('preserves the original Flight decode rejection for the error boundary', async () => {
158
- const flight = createDeferred<unknown>();
159
- const failure = new Error('Flight decode failed');
160
- const decodeFlight = rstest.fn(() => flight.promise);
161
- createFromReadableStreamImpl = decodeFlight;
162
- let caughtError: unknown;
163
- const errorLog = rstest
164
- .spyOn(console, 'error')
165
- .mockImplementation(() => undefined);
166
-
167
- try {
168
- const getTree = createTreeGetterFromFlightStream(
169
- new ReadableStream<Uint8Array>(),
170
- );
171
- await act(() =>
172
- render(
173
- <FlightErrorBoundary
174
- onError={error => {
175
- caughtError = error;
176
- }}
177
- >
178
- <Suspense
179
- fallback={<div data-testid="flight-loading">loading</div>}
180
- >
181
- <FlightTree getTree={getTree} />
182
- </Suspense>
183
- </FlightErrorBoundary>,
184
- ),
185
- );
186
-
187
- expect(screen.getByTestId('flight-loading')).toBeTruthy();
188
- await waitFor(() => expect(decodeFlight).toHaveBeenCalledTimes(1));
189
- await act(async () => {
190
- flight.reject(failure);
191
- await flight.promise.catch(() => undefined);
192
- });
193
-
194
- expect(await screen.findByTestId('flight-error')).toBeTruthy();
195
- expect(caughtError).toBe(failure);
196
- } finally {
197
- errorLog.mockRestore();
198
- }
199
- });
200
- });
@@ -1,428 +0,0 @@
1
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
- import React, { act } from 'react';
3
- import type { Fetcher } from '../../src/runtime/dataMutation';
4
- import { Form, useFetcher } from '../../src/runtime/dataMutation';
5
- import { createFormDataFromSubmit } from '../../src/runtime/formData';
6
-
7
- type RouteHandler = (args: {
8
- request: Request;
9
- params: Record<string, string>;
10
- context?: unknown;
11
- }) => Promise<unknown> | unknown;
12
-
13
- let currentRouter: any;
14
-
15
- rstest.mock('@tanstack/react-router', () => ({
16
- useRouter: () => currentRouter,
17
- }));
18
-
19
- function createDeferred<T>() {
20
- let resolve!: (value: T | PromiseLike<T>) => void;
21
- let reject!: (reason?: unknown) => void;
22
- const promise = new Promise<T>((resolvePromise, rejectPromise) => {
23
- resolve = resolvePromise;
24
- reject = rejectPromise;
25
- });
26
- return {
27
- promise,
28
- resolve,
29
- reject,
30
- };
31
- }
32
-
33
- function createRouter(handler: {
34
- action?: RouteHandler;
35
- loader?: RouteHandler;
36
- invalidate?: () => Promise<void>;
37
- params?: Record<string, string>;
38
- }) {
39
- return {
40
- buildLocation: ({ to }: { to?: string }) => {
41
- const href = typeof to === 'string' ? to : '/';
42
- const url = new URL(href, 'http://localhost');
43
- return {
44
- href: `${url.pathname}${url.search}${url.hash}`,
45
- pathname: url.pathname,
46
- };
47
- },
48
- getMatchedRoutes: () => [
49
- [],
50
- handler.params || {},
51
- {
52
- options: {
53
- staticData: {
54
- modernRouteAction: handler.action,
55
- modernRouteLoader: handler.loader,
56
- },
57
- },
58
- },
59
- ],
60
- navigate: rstest.fn(async () => undefined),
61
- invalidate: rstest.fn(handler.invalidate || (async () => undefined)),
62
- };
63
- }
64
-
65
- function formatFetcherError(error: unknown) {
66
- if (error instanceof Error) {
67
- return error.message;
68
- }
69
- if (typeof error === 'string') {
70
- return error;
71
- }
72
- if (error == null) {
73
- return '';
74
- }
75
- return JSON.stringify(error);
76
- }
77
-
78
- describe('tanstack data mutation fetcher', () => {
79
- let latestFetcher: Fetcher | undefined;
80
- const states: string[] = [];
81
-
82
- function FetcherHarness() {
83
- const fetcher = useFetcher();
84
- latestFetcher = fetcher;
85
-
86
- React.useEffect(() => {
87
- states.push(fetcher.state);
88
- }, [fetcher.state]);
89
-
90
- return (
91
- <div>
92
- <div data-testid="state">{fetcher.state}</div>
93
- <div data-testid="data">
94
- {fetcher.data === undefined
95
- ? 'undefined'
96
- : JSON.stringify(fetcher.data)}
97
- </div>
98
- <div data-testid="error">{formatFetcherError(fetcher.error)}</div>
99
- </div>
100
- );
101
- }
102
-
103
- beforeEach(() => {
104
- latestFetcher = undefined;
105
- states.length = 0;
106
- });
107
-
108
- test.each([
109
- { amount: 2, count: 2, method: 'post' as const, name: 'explicit method' },
110
- { amount: 1, count: 1, method: undefined, name: 'default method' },
111
- ])('tracks submitting/loading phases with the $name', async ({
112
- amount,
113
- count,
114
- method,
115
- }) => {
116
- const actionResult = createDeferred<Response>();
117
- const invalidateResult = createDeferred<void>();
118
- const action = rstest.fn(async () => actionResult.promise);
119
- const loader = rstest.fn(async () => ({ count: 0 }));
120
-
121
- currentRouter = createRouter({
122
- action,
123
- loader,
124
- invalidate: async () => invalidateResult.promise,
125
- });
126
-
127
- render(<FetcherHarness />);
128
- expect(screen.getByTestId('state').textContent).toBe('idle');
129
-
130
- let submitPromise: Promise<void> | undefined;
131
- act(() => {
132
- submitPromise = latestFetcher!.submit(
133
- { amount },
134
- {
135
- ...(method === undefined ? {} : { method }),
136
- action: '/mutation',
137
- },
138
- );
139
- });
140
-
141
- await waitFor(() => {
142
- expect(screen.getByTestId('state').textContent).toBe('submitting');
143
- });
144
- expect(action).toHaveBeenCalledTimes(1);
145
- expect(loader).not.toHaveBeenCalled();
146
-
147
- actionResult.resolve(
148
- new Response(JSON.stringify({ count }), {
149
- headers: {
150
- 'Content-Type': 'application/json',
151
- },
152
- }),
153
- );
154
-
155
- await waitFor(() => {
156
- expect(screen.getByTestId('state').textContent).toBe('loading');
157
- });
158
-
159
- invalidateResult.resolve();
160
-
161
- await act(async () => {
162
- await submitPromise;
163
- });
164
-
165
- expect(screen.getByTestId('state').textContent).toBe('idle');
166
- expect(screen.getByTestId('data').textContent).toBe(
167
- JSON.stringify({ count }),
168
- );
169
- expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
170
- });
171
-
172
- test('passes built href and matched route params to mutation actions', async () => {
173
- const action = rstest.fn(
174
- async ({ request, params }: Parameters<RouteHandler>[0]) => ({
175
- params,
176
- requestUrl: request.url,
177
- }),
178
- );
179
- currentRouter = createRouter({
180
- action,
181
- params: { userId: '42' },
182
- });
183
-
184
- render(<FetcherHarness />);
185
-
186
- await act(async () => {
187
- await latestFetcher!.submit(
188
- { amount: 2 },
189
- {
190
- method: 'post',
191
- action: '/users/42?tab=edit#details',
192
- },
193
- );
194
- });
195
-
196
- expect(action).toHaveBeenCalledTimes(1);
197
- const actionArgs = action.mock.calls[0][0];
198
- const expectedRequestUrl = `${window.location.origin}/users/42?tab=edit#details`;
199
- expect(actionArgs.params).toEqual({ userId: '42' });
200
- expect(actionArgs.request.url).toBe(expectedRequestUrl);
201
- expect(latestFetcher?.data).toEqual({
202
- params: { userId: '42' },
203
- requestUrl: expectedRequestUrl,
204
- });
205
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
206
- });
207
-
208
- test('surfaces non-2xx action responses as fetcher errors', async () => {
209
- const actionResult = createDeferred<Response>();
210
- currentRouter = createRouter({
211
- action: async () => actionResult.promise,
212
- });
213
-
214
- render(<FetcherHarness />);
215
-
216
- let thrownError: unknown;
217
- let submitPromise: Promise<void> | undefined;
218
- act(() => {
219
- submitPromise = latestFetcher!.submit(
220
- { amount: 'not-a-number' },
221
- { method: 'post', action: '/mutation' },
222
- );
223
- });
224
-
225
- await waitFor(() => {
226
- expect(screen.getByTestId('state').textContent).toBe('submitting');
227
- });
228
-
229
- actionResult.resolve(
230
- new Response(JSON.stringify({ message: 'invalid amount' }), {
231
- status: 422,
232
- headers: {
233
- 'Content-Type': 'application/json',
234
- },
235
- }),
236
- );
237
-
238
- await act(async () => {
239
- try {
240
- await submitPromise;
241
- } catch (error) {
242
- thrownError = error;
243
- }
244
- });
245
-
246
- expect(thrownError).toBeInstanceOf(Error);
247
- expect(screen.getByTestId('state').textContent).toBe('idle');
248
- expect(screen.getByTestId('error').textContent).toContain('422');
249
- expect(currentRouter.invalidate).not.toHaveBeenCalled();
250
- expect(states).toEqual(['idle', 'submitting', 'idle']);
251
- });
252
-
253
- test('uses loading state for loader fetches', async () => {
254
- const loaderResult = createDeferred<{ count: number }>();
255
-
256
- currentRouter = createRouter({
257
- loader: async () => loaderResult.promise,
258
- });
259
-
260
- render(<FetcherHarness />);
261
-
262
- let submitPromise: Promise<void> | undefined;
263
- act(() => {
264
- submitPromise = latestFetcher!.submit(
265
- {},
266
- { method: 'get', action: '/mutation' },
267
- );
268
- });
269
-
270
- await waitFor(() => {
271
- expect(screen.getByTestId('state').textContent).toBe('loading');
272
- });
273
-
274
- loaderResult.resolve({ count: 7 });
275
-
276
- await act(async () => {
277
- await submitPromise;
278
- });
279
-
280
- expect(screen.getByTestId('state').textContent).toBe('idle');
281
- expect(screen.getByTestId('data').textContent).toBe('{"count":7}');
282
- expect(currentRouter.invalidate).not.toHaveBeenCalled();
283
- expect(states).toEqual(['idle', 'loading', 'idle']);
284
- });
285
-
286
- test('GET Form navigation replaces action query before hash', async () => {
287
- currentRouter = createRouter({});
288
-
289
- render(
290
- <Form method="get" action="/products?sort=old#results">
291
- <input name="q" defaultValue="boots" />
292
- <button type="submit">Search</button>
293
- </Form>,
294
- );
295
-
296
- const form = document.querySelector('form');
297
- expect(form).toBeTruthy();
298
-
299
- await act(async () => {
300
- fireEvent.submit(form!);
301
- });
302
-
303
- expect(currentRouter.navigate).toHaveBeenCalledWith({
304
- href: '/products?q=boots#results',
305
- });
306
- });
307
-
308
- test('keeps non-idle state while overlapping mutation submits are still active', async () => {
309
- const actionResults = [
310
- createDeferred<Response>(),
311
- createDeferred<Response>(),
312
- ];
313
- const invalidateResults = [createDeferred<void>(), createDeferred<void>()];
314
- let actionCallIndex = 0;
315
- let invalidateCallIndex = 0;
316
-
317
- currentRouter = createRouter({
318
- action: async () => {
319
- const current = actionResults[actionCallIndex];
320
- actionCallIndex += 1;
321
- return current.promise;
322
- },
323
- invalidate: async () => {
324
- const current = invalidateResults[invalidateCallIndex];
325
- invalidateCallIndex += 1;
326
- return current.promise;
327
- },
328
- });
329
-
330
- render(<FetcherHarness />);
331
-
332
- let firstSubmit: Promise<void> | undefined;
333
- let secondSubmit: Promise<void> | undefined;
334
-
335
- act(() => {
336
- firstSubmit = latestFetcher!.submit(
337
- { amount: 1 },
338
- { method: 'post', action: '/mutation' },
339
- );
340
- });
341
- await waitFor(() => {
342
- expect(screen.getByTestId('state').textContent).toBe('submitting');
343
- });
344
-
345
- act(() => {
346
- secondSubmit = latestFetcher!.submit(
347
- { amount: 2 },
348
- { method: 'post', action: '/mutation' },
349
- );
350
- });
351
- await waitFor(() => {
352
- expect(screen.getByTestId('state').textContent).toBe('submitting');
353
- });
354
-
355
- actionResults[0].resolve(
356
- new Response(JSON.stringify({ count: 1 }), {
357
- headers: {
358
- 'Content-Type': 'application/json',
359
- },
360
- }),
361
- );
362
- await waitFor(() => {
363
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
364
- expect(screen.getByTestId('state').textContent).toBe('submitting');
365
- });
366
-
367
- invalidateResults[0].resolve();
368
- await waitFor(() => {
369
- expect(screen.getByTestId('state').textContent).toBe('submitting');
370
- });
371
-
372
- actionResults[1].resolve(
373
- new Response(JSON.stringify({ count: 3 }), {
374
- headers: {
375
- 'Content-Type': 'application/json',
376
- },
377
- }),
378
- );
379
- await waitFor(() => {
380
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(2);
381
- expect(screen.getByTestId('state').textContent).toBe('loading');
382
- });
383
-
384
- invalidateResults[1].resolve();
385
- await act(async () => {
386
- await Promise.all([firstSubmit, secondSubmit]);
387
- });
388
-
389
- expect(screen.getByTestId('state').textContent).toBe('idle');
390
- expect(screen.getByTestId('data').textContent).toBe('{"count":3}');
391
- });
392
-
393
- test('preserves submitter name value when FormData submitter overload is unavailable', () => {
394
- const NativeFormData = globalThis.FormData;
395
-
396
- class SingleArgumentFormData extends NativeFormData {
397
- constructor(form?: HTMLFormElement, submitter?: HTMLElement) {
398
- if (submitter) {
399
- throw new TypeError('FormData submitter overload unavailable');
400
- }
401
- super(form);
402
- }
403
- }
404
-
405
- globalThis.FormData = SingleArgumentFormData as typeof FormData;
406
-
407
- try {
408
- const form = document.createElement('form');
409
- form.innerHTML = `
410
- <input name="amount" value="2" />
411
- <button type="submit" name="intent" value="save">Save</button>
412
- `;
413
-
414
- const submitter = form.querySelector('button');
415
- expect(submitter).toBeInstanceOf(HTMLButtonElement);
416
-
417
- const formData = createFormDataFromSubmit({
418
- form,
419
- submitter,
420
- });
421
-
422
- expect(formData.get('amount')).toBe('2');
423
- expect(formData.get('intent')).toBe('save');
424
- } finally {
425
- globalThis.FormData = NativeFormData;
426
- }
427
- });
428
- });
@@ -1,5 +0,0 @@
1
- {
2
- "rules": {
3
- "no-unused-vars": "error"
4
- }
5
- }