@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/cli/index.js +268 -0
  3. package/dist/cjs/cli/tanstackTypes.js +388 -0
  4. package/dist/cjs/cli.js +65 -0
  5. package/dist/cjs/runtime/DefaultNotFound.js +47 -0
  6. package/dist/cjs/runtime/basepathRewrite.js +62 -0
  7. package/dist/cjs/runtime/dataMutation.js +345 -0
  8. package/dist/cjs/runtime/hooks.js +57 -0
  9. package/dist/cjs/runtime/index.js +114 -0
  10. package/dist/cjs/runtime/lifecycle.js +125 -0
  11. package/dist/cjs/runtime/plugin.js +250 -0
  12. package/dist/cjs/runtime/plugin.node.js +304 -0
  13. package/dist/cjs/runtime/prefetchLink.js +55 -0
  14. package/dist/cjs/runtime/routeTree.js +492 -0
  15. package/dist/cjs/runtime/rsc/ClientSlot.js +53 -0
  16. package/dist/cjs/runtime/rsc/CompositeComponent.js +75 -0
  17. package/dist/cjs/runtime/rsc/ReplayableStream.js +141 -0
  18. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +65 -0
  19. package/dist/cjs/runtime/rsc/SlotContext.js +54 -0
  20. package/dist/cjs/runtime/rsc/client.js +93 -0
  21. package/dist/cjs/runtime/rsc/createRscProxy.js +141 -0
  22. package/dist/cjs/runtime/rsc/index.js +42 -0
  23. package/dist/cjs/runtime/rsc/payloadRouter.js +211 -0
  24. package/dist/cjs/runtime/rsc/server.js +246 -0
  25. package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +65 -0
  26. package/dist/cjs/runtime/rsc/symbols.js +72 -0
  27. package/dist/cjs/runtime/types.js +18 -0
  28. package/dist/cjs/runtime/utils.js +142 -0
  29. package/dist/cjs/runtime.js +58 -0
  30. package/dist/esm/cli/index.mjs +201 -0
  31. package/dist/esm/cli/tanstackTypes.mjs +341 -0
  32. package/dist/esm/cli.mjs +2 -0
  33. package/dist/esm/rslib-runtime.mjs +18 -0
  34. package/dist/esm/runtime/DefaultNotFound.mjs +13 -0
  35. package/dist/esm/runtime/basepathRewrite.mjs +28 -0
  36. package/dist/esm/runtime/dataMutation.mjs +305 -0
  37. package/dist/esm/runtime/hooks.mjs +8 -0
  38. package/dist/esm/runtime/index.mjs +6 -0
  39. package/dist/esm/runtime/lifecycle.mjs +82 -0
  40. package/dist/esm/runtime/plugin.mjs +214 -0
  41. package/dist/esm/runtime/plugin.node.mjs +268 -0
  42. package/dist/esm/runtime/prefetchLink.mjs +18 -0
  43. package/dist/esm/runtime/routeTree.mjs +452 -0
  44. package/dist/esm/runtime/rsc/ClientSlot.mjs +19 -0
  45. package/dist/esm/runtime/rsc/CompositeComponent.mjs +41 -0
  46. package/dist/esm/runtime/rsc/ReplayableStream.mjs +104 -0
  47. package/dist/esm/runtime/rsc/RscNodeRenderer.mjs +31 -0
  48. package/dist/esm/runtime/rsc/SlotContext.mjs +17 -0
  49. package/dist/esm/runtime/rsc/client.mjs +53 -0
  50. package/dist/esm/runtime/rsc/createRscProxy.mjs +107 -0
  51. package/dist/esm/runtime/rsc/index.mjs +1 -0
  52. package/dist/esm/runtime/rsc/payloadRouter.mjs +162 -0
  53. package/dist/esm/runtime/rsc/server.mjs +200 -0
  54. package/dist/esm/runtime/rsc/slotUsageSanitizer.mjs +31 -0
  55. package/dist/esm/runtime/rsc/symbols.mjs +17 -0
  56. package/dist/esm/runtime/types.mjs +0 -0
  57. package/dist/esm/runtime/utils.mjs +89 -0
  58. package/dist/esm/runtime.mjs +1 -0
  59. package/dist/esm-node/cli/index.mjs +205 -0
  60. package/dist/esm-node/cli/tanstackTypes.mjs +342 -0
  61. package/dist/esm-node/cli.mjs +3 -0
  62. package/dist/esm-node/rslib-runtime.mjs +19 -0
  63. package/dist/esm-node/runtime/DefaultNotFound.mjs +14 -0
  64. package/dist/esm-node/runtime/basepathRewrite.mjs +29 -0
  65. package/dist/esm-node/runtime/dataMutation.mjs +306 -0
  66. package/dist/esm-node/runtime/hooks.mjs +9 -0
  67. package/dist/esm-node/runtime/index.mjs +7 -0
  68. package/dist/esm-node/runtime/lifecycle.mjs +83 -0
  69. package/dist/esm-node/runtime/plugin.mjs +215 -0
  70. package/dist/esm-node/runtime/plugin.node.mjs +269 -0
  71. package/dist/esm-node/runtime/prefetchLink.mjs +19 -0
  72. package/dist/esm-node/runtime/routeTree.mjs +453 -0
  73. package/dist/esm-node/runtime/rsc/ClientSlot.mjs +20 -0
  74. package/dist/esm-node/runtime/rsc/CompositeComponent.mjs +42 -0
  75. package/dist/esm-node/runtime/rsc/ReplayableStream.mjs +105 -0
  76. package/dist/esm-node/runtime/rsc/RscNodeRenderer.mjs +32 -0
  77. package/dist/esm-node/runtime/rsc/SlotContext.mjs +18 -0
  78. package/dist/esm-node/runtime/rsc/client.mjs +54 -0
  79. package/dist/esm-node/runtime/rsc/createRscProxy.mjs +108 -0
  80. package/dist/esm-node/runtime/rsc/index.mjs +2 -0
  81. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +163 -0
  82. package/dist/esm-node/runtime/rsc/server.mjs +201 -0
  83. package/dist/esm-node/runtime/rsc/slotUsageSanitizer.mjs +32 -0
  84. package/dist/esm-node/runtime/rsc/symbols.mjs +18 -0
  85. package/dist/esm-node/runtime/types.mjs +1 -0
  86. package/dist/esm-node/runtime/utils.mjs +90 -0
  87. package/dist/esm-node/runtime.mjs +2 -0
  88. package/dist/types/cli/index.d.ts +20 -0
  89. package/dist/types/cli/tanstackTypes.d.ts +11 -0
  90. package/dist/types/cli.d.ts +2 -0
  91. package/dist/types/runtime/DefaultNotFound.d.ts +2 -0
  92. package/dist/types/runtime/basepathRewrite.d.ts +8 -0
  93. package/dist/types/runtime/dataMutation.d.ts +29 -0
  94. package/dist/types/runtime/hooks.d.ts +18 -0
  95. package/dist/types/runtime/index.d.ts +9 -0
  96. package/dist/types/runtime/lifecycle.d.ts +22 -0
  97. package/dist/types/runtime/plugin.d.ts +17 -0
  98. package/dist/types/runtime/plugin.node.d.ts +17 -0
  99. package/dist/types/runtime/prefetchLink.d.ts +11 -0
  100. package/dist/types/runtime/routeTree.d.ts +11 -0
  101. package/dist/types/runtime/rsc/ClientSlot.d.ts +5 -0
  102. package/dist/types/runtime/rsc/CompositeComponent.d.ts +3 -0
  103. package/dist/types/runtime/rsc/ReplayableStream.d.ts +24 -0
  104. package/dist/types/runtime/rsc/RscNodeRenderer.d.ts +5 -0
  105. package/dist/types/runtime/rsc/SlotContext.d.ts +11 -0
  106. package/dist/types/runtime/rsc/client.d.ts +11 -0
  107. package/dist/types/runtime/rsc/createRscProxy.d.ts +7 -0
  108. package/dist/types/runtime/rsc/index.d.ts +2 -0
  109. package/dist/types/runtime/rsc/payloadRouter.d.ts +24 -0
  110. package/dist/types/runtime/rsc/server.d.ts +14 -0
  111. package/dist/types/runtime/rsc/slotUsageSanitizer.d.ts +2 -0
  112. package/dist/types/runtime/rsc/symbols.d.ts +46 -0
  113. package/dist/types/runtime/types.d.ts +68 -0
  114. package/dist/types/runtime/utils.d.ts +36 -0
  115. package/dist/types/runtime.d.ts +1 -0
  116. package/dist/types-direct/cli/index.d.ts +20 -0
  117. package/dist/types-direct/cli/tanstackTypes.d.ts +11 -0
  118. package/dist/types-direct/cli.d.ts +2 -0
  119. package/dist/types-direct/runtime/DefaultNotFound.d.ts +2 -0
  120. package/dist/types-direct/runtime/basepathRewrite.d.ts +8 -0
  121. package/dist/types-direct/runtime/dataMutation.d.ts +29 -0
  122. package/dist/types-direct/runtime/hooks.d.ts +18 -0
  123. package/dist/types-direct/runtime/index.d.ts +9 -0
  124. package/dist/types-direct/runtime/lifecycle.d.ts +22 -0
  125. package/dist/types-direct/runtime/plugin.d.ts +17 -0
  126. package/dist/types-direct/runtime/plugin.node.d.ts +17 -0
  127. package/dist/types-direct/runtime/prefetchLink.d.ts +11 -0
  128. package/dist/types-direct/runtime/routeTree.d.ts +11 -0
  129. package/dist/types-direct/runtime/rsc/ClientSlot.d.ts +5 -0
  130. package/dist/types-direct/runtime/rsc/CompositeComponent.d.ts +3 -0
  131. package/dist/types-direct/runtime/rsc/ReplayableStream.d.ts +24 -0
  132. package/dist/types-direct/runtime/rsc/RscNodeRenderer.d.ts +5 -0
  133. package/dist/types-direct/runtime/rsc/SlotContext.d.ts +11 -0
  134. package/dist/types-direct/runtime/rsc/client.d.ts +11 -0
  135. package/dist/types-direct/runtime/rsc/createRscProxy.d.ts +7 -0
  136. package/dist/types-direct/runtime/rsc/index.d.ts +2 -0
  137. package/dist/types-direct/runtime/rsc/payloadRouter.d.ts +24 -0
  138. package/dist/types-direct/runtime/rsc/server.d.ts +14 -0
  139. package/dist/types-direct/runtime/rsc/slotUsageSanitizer.d.ts +2 -0
  140. package/dist/types-direct/runtime/rsc/symbols.d.ts +46 -0
  141. package/dist/types-direct/runtime/types.d.ts +68 -0
  142. package/dist/types-direct/runtime/utils.d.ts +36 -0
  143. package/dist/types-direct/runtime.d.ts +1 -0
  144. package/package.json +126 -0
  145. package/rslib.config.mts +4 -0
  146. package/rstest.config.mts +43 -0
  147. package/src/cli/index.ts +388 -0
  148. package/src/cli/tanstackTypes.ts +503 -0
  149. package/src/cli.ts +2 -0
  150. package/src/runtime/DefaultNotFound.tsx +15 -0
  151. package/src/runtime/basepathRewrite.ts +59 -0
  152. package/src/runtime/dataMutation.tsx +517 -0
  153. package/src/runtime/hooks.ts +34 -0
  154. package/src/runtime/index.tsx +30 -0
  155. package/src/runtime/lifecycle.ts +150 -0
  156. package/src/runtime/plugin.node.tsx +534 -0
  157. package/src/runtime/plugin.tsx +395 -0
  158. package/src/runtime/prefetchLink.tsx +87 -0
  159. package/src/runtime/routeTree.ts +942 -0
  160. package/src/runtime/rsc/ClientSlot.tsx +25 -0
  161. package/src/runtime/rsc/CompositeComponent.tsx +65 -0
  162. package/src/runtime/rsc/ReplayableStream.ts +155 -0
  163. package/src/runtime/rsc/RscNodeRenderer.tsx +45 -0
  164. package/src/runtime/rsc/SlotContext.tsx +31 -0
  165. package/src/runtime/rsc/client.tsx +90 -0
  166. package/src/runtime/rsc/createRscProxy.tsx +189 -0
  167. package/src/runtime/rsc/index.ts +10 -0
  168. package/src/runtime/rsc/payloadRouter.ts +318 -0
  169. package/src/runtime/rsc/server.tsx +303 -0
  170. package/src/runtime/rsc/slotUsageSanitizer.ts +76 -0
  171. package/src/runtime/rsc/symbols.ts +106 -0
  172. package/src/runtime/ssr-shim.d.ts +12 -0
  173. package/src/runtime/types.ts +83 -0
  174. package/src/runtime/utils.tsx +161 -0
  175. package/src/runtime.ts +1 -0
  176. package/tests/router/cli.test.ts +386 -0
  177. package/tests/router/dataMutation.test.tsx +396 -0
  178. package/tests/router/prefetchLink.test.tsx +43 -0
  179. package/tests/router/routeTree.test.ts +502 -0
  180. package/tests/router/rsc.test.tsx +256 -0
  181. package/tests/router/tanstackTypes.test.ts +62 -0
  182. package/tsconfig.json +12 -0
  183. package/tsconfig.tsgo.json +6 -0
@@ -0,0 +1,396 @@
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
+
6
+ type RouteHandler = (args: {
7
+ request: Request;
8
+ params: Record<string, string>;
9
+ context?: unknown;
10
+ }) => Promise<unknown> | unknown;
11
+
12
+ let currentRouter: any;
13
+
14
+ rstest.mock('@tanstack/react-router', () => ({
15
+ useRouter: () => currentRouter,
16
+ }));
17
+
18
+ function createDeferred<T>() {
19
+ let resolve!: (value: T | PromiseLike<T>) => void;
20
+ let reject!: (reason?: unknown) => void;
21
+ const promise = new Promise<T>((resolvePromise, rejectPromise) => {
22
+ resolve = resolvePromise;
23
+ reject = rejectPromise;
24
+ });
25
+ return {
26
+ promise,
27
+ resolve,
28
+ reject,
29
+ };
30
+ }
31
+
32
+ function createRouter(handler: {
33
+ action?: RouteHandler;
34
+ loader?: RouteHandler;
35
+ invalidate?: () => Promise<void>;
36
+ }) {
37
+ return {
38
+ buildLocation: ({ to }: { to?: string }) => ({
39
+ pathname: typeof to === 'string' ? to : '/',
40
+ }),
41
+ getParsedLocationHref: (location: { pathname: string }) =>
42
+ location.pathname,
43
+ getMatchedRoutes: () => ({
44
+ foundRoute: {
45
+ options: {
46
+ staticData: {
47
+ modernRouteAction: handler.action,
48
+ modernRouteLoader: handler.loader,
49
+ },
50
+ },
51
+ },
52
+ routeParams: {},
53
+ }),
54
+ navigate: rstest.fn(async () => undefined),
55
+ invalidate: rstest.fn(handler.invalidate || (async () => undefined)),
56
+ };
57
+ }
58
+
59
+ function formatFetcherError(error: unknown) {
60
+ if (error instanceof Error) {
61
+ return error.message;
62
+ }
63
+ if (typeof error === 'string') {
64
+ return error;
65
+ }
66
+ if (error == null) {
67
+ return '';
68
+ }
69
+ return JSON.stringify(error);
70
+ }
71
+
72
+ describe('tanstack data mutation fetcher', () => {
73
+ let latestFetcher: Fetcher | undefined;
74
+ const states: string[] = [];
75
+
76
+ function FetcherHarness() {
77
+ const fetcher = useFetcher();
78
+ latestFetcher = fetcher;
79
+
80
+ React.useEffect(() => {
81
+ states.push(fetcher.state);
82
+ }, [fetcher.state]);
83
+
84
+ return (
85
+ <div>
86
+ <div data-testid="state">{fetcher.state}</div>
87
+ <div data-testid="data">
88
+ {fetcher.data === undefined
89
+ ? 'undefined'
90
+ : JSON.stringify(fetcher.data)}
91
+ </div>
92
+ <div data-testid="error">{formatFetcherError(fetcher.error)}</div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ beforeEach(() => {
98
+ latestFetcher = undefined;
99
+ states.length = 0;
100
+ });
101
+
102
+ test('tracks submitting and loading phases for mutation submit', async () => {
103
+ const actionResult = createDeferred<Response>();
104
+ const invalidateResult = createDeferred<void>();
105
+
106
+ currentRouter = createRouter({
107
+ action: async () => actionResult.promise,
108
+ invalidate: async () => invalidateResult.promise,
109
+ });
110
+
111
+ render(<FetcherHarness />);
112
+ expect(screen.getByTestId('state').textContent).toBe('idle');
113
+
114
+ let submitPromise: Promise<void> | undefined;
115
+ act(() => {
116
+ submitPromise = latestFetcher!.submit(
117
+ { amount: 2 },
118
+ { method: 'post', action: '/mutation' },
119
+ );
120
+ });
121
+
122
+ await waitFor(() => {
123
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
124
+ });
125
+
126
+ actionResult.resolve(
127
+ new Response(JSON.stringify({ count: 2 }), {
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ },
131
+ }),
132
+ );
133
+
134
+ await waitFor(() => {
135
+ expect(screen.getByTestId('state').textContent).toBe('loading');
136
+ });
137
+
138
+ invalidateResult.resolve();
139
+
140
+ await act(async () => {
141
+ await submitPromise;
142
+ });
143
+
144
+ expect(screen.getByTestId('state').textContent).toBe('idle');
145
+ expect(screen.getByTestId('data').textContent).toBe('{"count":2}');
146
+ expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
147
+ });
148
+
149
+ test('defaults fetcher submit without method to mutation state', async () => {
150
+ const actionResult = createDeferred<Response>();
151
+ const invalidateResult = createDeferred<void>();
152
+ const action = rstest.fn(async () => actionResult.promise);
153
+ const loader = rstest.fn(async () => ({ count: 0 }));
154
+
155
+ currentRouter = createRouter({
156
+ action,
157
+ loader,
158
+ invalidate: async () => invalidateResult.promise,
159
+ });
160
+
161
+ render(<FetcherHarness />);
162
+ expect(screen.getByTestId('state').textContent).toBe('idle');
163
+
164
+ let submitPromise: Promise<void> | undefined;
165
+ act(() => {
166
+ submitPromise = latestFetcher!.submit(
167
+ { amount: 1 },
168
+ { action: '/mutation' },
169
+ );
170
+ });
171
+
172
+ await waitFor(() => {
173
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
174
+ });
175
+ expect(action).toHaveBeenCalledTimes(1);
176
+ expect(loader).not.toHaveBeenCalled();
177
+
178
+ actionResult.resolve(
179
+ new Response(JSON.stringify({ count: 1 }), {
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ },
183
+ }),
184
+ );
185
+
186
+ await waitFor(() => {
187
+ expect(screen.getByTestId('state').textContent).toBe('loading');
188
+ });
189
+
190
+ invalidateResult.resolve();
191
+
192
+ await act(async () => {
193
+ await submitPromise;
194
+ });
195
+
196
+ expect(screen.getByTestId('state').textContent).toBe('idle');
197
+ expect(screen.getByTestId('data').textContent).toBe('{"count":1}');
198
+ expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
199
+ });
200
+
201
+ test('surfaces non-2xx action responses as fetcher errors', async () => {
202
+ const actionResult = createDeferred<Response>();
203
+ currentRouter = createRouter({
204
+ action: async () => actionResult.promise,
205
+ });
206
+
207
+ render(<FetcherHarness />);
208
+
209
+ let thrownError: unknown;
210
+ let submitPromise: Promise<void> | undefined;
211
+ act(() => {
212
+ submitPromise = latestFetcher!.submit(
213
+ { amount: 'not-a-number' },
214
+ { method: 'post', action: '/mutation' },
215
+ );
216
+ });
217
+
218
+ await waitFor(() => {
219
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
220
+ });
221
+
222
+ actionResult.resolve(
223
+ new Response(JSON.stringify({ message: 'invalid amount' }), {
224
+ status: 422,
225
+ headers: {
226
+ 'Content-Type': 'application/json',
227
+ },
228
+ }),
229
+ );
230
+
231
+ await act(async () => {
232
+ try {
233
+ await submitPromise;
234
+ } catch (error) {
235
+ thrownError = error;
236
+ }
237
+ });
238
+
239
+ expect(thrownError).toBeInstanceOf(Error);
240
+ expect(screen.getByTestId('state').textContent).toBe('idle');
241
+ expect(screen.getByTestId('error').textContent).toContain('422');
242
+ expect(currentRouter.invalidate).not.toHaveBeenCalled();
243
+ expect(states).toEqual(['idle', 'submitting', 'idle']);
244
+ });
245
+
246
+ test('uses loading state for loader fetches', async () => {
247
+ const loaderResult = createDeferred<{ count: number }>();
248
+
249
+ currentRouter = createRouter({
250
+ loader: async () => loaderResult.promise,
251
+ });
252
+
253
+ render(<FetcherHarness />);
254
+
255
+ let submitPromise: Promise<void> | undefined;
256
+ act(() => {
257
+ submitPromise = latestFetcher!.submit(
258
+ {},
259
+ { method: 'get', action: '/mutation' },
260
+ );
261
+ });
262
+
263
+ await waitFor(() => {
264
+ expect(screen.getByTestId('state').textContent).toBe('loading');
265
+ });
266
+
267
+ loaderResult.resolve({ count: 7 });
268
+
269
+ await act(async () => {
270
+ await submitPromise;
271
+ });
272
+
273
+ expect(screen.getByTestId('state').textContent).toBe('idle');
274
+ expect(screen.getByTestId('data').textContent).toBe('{"count":7}');
275
+ expect(currentRouter.invalidate).not.toHaveBeenCalled();
276
+ expect(states).toEqual(['idle', 'loading', 'idle']);
277
+ });
278
+
279
+ test('keeps non-idle state while overlapping mutation submits are still active', async () => {
280
+ const actionResults = [
281
+ createDeferred<Response>(),
282
+ createDeferred<Response>(),
283
+ ];
284
+ const invalidateResults = [createDeferred<void>(), createDeferred<void>()];
285
+ let actionCallIndex = 0;
286
+ let invalidateCallIndex = 0;
287
+
288
+ currentRouter = createRouter({
289
+ action: async () => {
290
+ const current = actionResults[actionCallIndex];
291
+ actionCallIndex += 1;
292
+ return current.promise;
293
+ },
294
+ invalidate: async () => {
295
+ const current = invalidateResults[invalidateCallIndex];
296
+ invalidateCallIndex += 1;
297
+ return current.promise;
298
+ },
299
+ });
300
+
301
+ render(<FetcherHarness />);
302
+
303
+ let firstSubmit: Promise<void> | undefined;
304
+ let secondSubmit: Promise<void> | undefined;
305
+
306
+ act(() => {
307
+ firstSubmit = latestFetcher!.submit(
308
+ { amount: 1 },
309
+ { method: 'post', action: '/mutation' },
310
+ );
311
+ });
312
+ await waitFor(() => {
313
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
314
+ });
315
+
316
+ act(() => {
317
+ secondSubmit = latestFetcher!.submit(
318
+ { amount: 2 },
319
+ { method: 'post', action: '/mutation' },
320
+ );
321
+ });
322
+ await waitFor(() => {
323
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
324
+ });
325
+
326
+ actionResults[0].resolve(
327
+ new Response(JSON.stringify({ count: 1 }), {
328
+ headers: {
329
+ 'Content-Type': 'application/json',
330
+ },
331
+ }),
332
+ );
333
+ await waitFor(() => {
334
+ expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
335
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
336
+ });
337
+
338
+ invalidateResults[0].resolve();
339
+ await waitFor(() => {
340
+ expect(screen.getByTestId('state').textContent).toBe('submitting');
341
+ });
342
+
343
+ actionResults[1].resolve(
344
+ new Response(JSON.stringify({ count: 3 }), {
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ },
348
+ }),
349
+ );
350
+ await waitFor(() => {
351
+ expect(currentRouter.invalidate).toHaveBeenCalledTimes(2);
352
+ expect(screen.getByTestId('state').textContent).toBe('loading');
353
+ });
354
+
355
+ invalidateResults[1].resolve();
356
+ await act(async () => {
357
+ await Promise.all([firstSubmit, secondSubmit]);
358
+ });
359
+
360
+ expect(screen.getByTestId('state').textContent).toBe('idle');
361
+ expect(screen.getByTestId('data').textContent).toBe('{"count":3}');
362
+ });
363
+
364
+ test('does not throw for Form submit with non-2xx response and still invalidates', async () => {
365
+ const action = rstest.fn(
366
+ async () =>
367
+ new Response(JSON.stringify({ message: 'invalid amount' }), {
368
+ status: 422,
369
+ headers: {
370
+ 'Content-Type': 'application/json',
371
+ },
372
+ }),
373
+ );
374
+ currentRouter = createRouter({
375
+ action,
376
+ });
377
+
378
+ render(
379
+ <Form method="post" action="/mutation">
380
+ <button type="submit">submit</button>
381
+ </Form>,
382
+ );
383
+
384
+ const form = document.querySelector('form');
385
+ expect(form).toBeTruthy();
386
+
387
+ await act(async () => {
388
+ fireEvent.submit(form!);
389
+ });
390
+
391
+ await waitFor(() => {
392
+ expect(action).toHaveBeenCalledTimes(1);
393
+ expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
394
+ });
395
+ });
396
+ });
@@ -0,0 +1,43 @@
1
+ import { render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { Link } from '../../src/runtime/prefetchLink';
4
+
5
+ type MockLinkProps = {
6
+ children?: React.ReactNode;
7
+ preload?: unknown;
8
+ };
9
+
10
+ let capturedPreloads: unknown[] = [];
11
+
12
+ rstest.mock('@tanstack/react-router', () => ({
13
+ Link: (props: MockLinkProps) => {
14
+ capturedPreloads.push(props.preload);
15
+ return <a href="/settings">{props.children}</a>;
16
+ },
17
+ }));
18
+
19
+ describe('tanstack prefetch link adapter', () => {
20
+ beforeEach(() => {
21
+ capturedPreloads = [];
22
+ });
23
+
24
+ it('maps viewport prefetch to TanStack preload', () => {
25
+ render(
26
+ <Link to="/settings" prefetch="viewport">
27
+ Settings
28
+ </Link>,
29
+ );
30
+
31
+ expect(capturedPreloads).toEqual(['viewport']);
32
+ });
33
+
34
+ it('does not override explicit preload', () => {
35
+ render(
36
+ <Link to="/settings" prefetch="viewport" preload="intent">
37
+ Settings
38
+ </Link>,
39
+ );
40
+
41
+ expect(capturedPreloads).toEqual(['intent']);
42
+ });
43
+ });