main
js 1,046 lines 32 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils';
13
14 // Polyfills for test environment
15 global.ReadableStream =
16 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17 global.TextEncoder = require('util').TextEncoder;
18 global.TextDecoder = require('util').TextDecoder;
19
20 // Polyfill stream methods on JSDOM.
21 global.Blob.prototype.stream = function () {
22 const impl = Object.getOwnPropertySymbols(this)[0];
23 const buffer = this[impl]._buffer;
24 return new ReadableStream({
25 start(c) {
26 c.enqueue(new Uint8Array(buffer));
27 c.close();
28 },
29 });
30 };
31
32 global.Blob.prototype.text = async function () {
33 const impl = Object.getOwnPropertySymbols(this)[0];
34 return this[impl]._buffer.toString('utf8');
35 };
36
37 // Don't wait before processing work on the server.
38 // TODO: we can replace this with FlightServer.act().
39 global.setTimeout = cb => cb();
40
41 let container;
42 let clientExports;
43 let serverExports;
44 let webpackMap;
45 let webpackServerMap;
46 let React;
47 let ReactDOMServer;
48 let ReactServerDOMServer;
49 let ReactServerDOMClient;
50 let ReactDOMClient;
51 let useActionState;
52 let act;
53 let assertConsoleErrorDev;
54
55 describe('ReactFlightDOMForm', () => {
56 beforeEach(() => {
57 jest.resetModules();
58 // Simulate the condition resolution
59 jest.mock('react', () => require('react/react.react-server'));
60 jest.mock('react-server-dom-webpack/server', () =>
61 require('react-server-dom-webpack/server.edge'),
62 );
63 ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
64 const WebpackMock = require('./utils/WebpackMock');
65 clientExports = WebpackMock.clientExports;
66 serverExports = WebpackMock.serverExports;
67 webpackMap = WebpackMock.webpackMap;
68 webpackServerMap = WebpackMock.webpackServerMap;
69 __unmockReact();
70 jest.resetModules();
71 React = require('react');
72 ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
73 ReactDOMServer = require('react-dom/server.edge');
74 ReactDOMClient = require('react-dom/client');
75 act = React.act;
76 assertConsoleErrorDev =
77 require('internal-test-utils').assertConsoleErrorDev;
78
79 // TODO: Test the old api but it warns so needs warnings to be asserted.
80 // if (__VARIANT__) {
81 // Remove after API is deleted.
82 // useActionState = require('react-dom').useFormState;
83 // }
84 useActionState = require('react').useActionState;
85 container = document.createElement('div');
86 document.body.appendChild(container);
87 });
88
89 afterEach(() => {
90 document.body.removeChild(container);
91 });
92
93 async function POST(formData) {
94 const boundAction = await ReactServerDOMServer.decodeAction(
95 formData,
96 webpackServerMap,
97 );
98 const returnValue = boundAction();
99 const formState = await ReactServerDOMServer.decodeFormState(
100 await returnValue,
101 formData,
102 webpackServerMap,
103 );
104 return {returnValue, formState};
105 }
106
107 function submit(submitter) {
108 const form = submitter.form || submitter;
109 if (!submitter.form) {
110 submitter = undefined;
111 }
112 const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
113 submitEvent.submitter = submitter;
114 const returnValue = form.dispatchEvent(submitEvent);
115 if (!returnValue) {
116 return;
117 }
118 const action =
119 (submitter && submitter.getAttribute('formaction')) || form.action;
120 if (!/\s*javascript:/i.test(action)) {
121 const method = (submitter && submitter.formMethod) || form.method;
122 const encType = (submitter && submitter.formEnctype) || form.enctype;
123 if (method === 'post' && encType === 'multipart/form-data') {
124 const formData = new FormData(form, submitter);
125 return POST(formData);
126 }
127 throw new Error('Navigate to: ' + action);
128 }
129 }
130
131 async function readIntoContainer(stream) {
132 const reader = stream.getReader();
133 let result = '';
134 while (true) {
135 const {done, value} = await reader.read();
136 if (done) {
137 break;
138 }
139 result += Buffer.from(value).toString('utf8');
140 }
141 const temp = document.createElement('div');
142 temp.innerHTML = result;
143 insertNodesAndExecuteScripts(temp, container, null);
144 }
145
146 it('can submit a passed server action without hydrating it', async () => {
147 let foo = null;
148
149 const serverAction = serverExports(function action(formData) {
150 foo = formData.get('foo');
151 return 'hello';
152 });
153 function App() {
154 return (
155 <form action={serverAction}>
156 <input type="text" name="foo" defaultValue="bar" />
157 </form>
158 );
159 }
160 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
161 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
162 serverConsumerManifest: {
163 moduleMap: null,
164 moduleLoading: null,
165 },
166 });
167 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
168 await readIntoContainer(ssrStream);
169
170 const form = container.firstChild;
171
172 expect(foo).toBe(null);
173
174 const {returnValue} = await submit(form);
175
176 expect(returnValue).toBe('hello');
177 expect(foo).toBe('bar');
178 });
179
180 it('can submit an imported server action without hydrating it', async () => {
181 let foo = null;
182
183 const ServerModule = serverExports(function action(formData) {
184 foo = formData.get('foo');
185 return 'hi';
186 });
187 const serverAction = ReactServerDOMClient.createServerReference(
188 ServerModule.$$id,
189 );
190 function App() {
191 return (
192 <form action={serverAction}>
193 <input type="text" name="foo" defaultValue="bar" />
194 </form>
195 );
196 }
197
198 const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
199 await readIntoContainer(ssrStream);
200
201 const form = container.firstChild;
202
203 expect(foo).toBe(null);
204
205 const {returnValue} = await submit(form);
206
207 expect(returnValue).toBe('hi');
208
209 expect(foo).toBe('bar');
210 });
211
212 it('can submit a complex closure server action without hydrating it', async () => {
213 let foo = null;
214
215 const serverAction = serverExports(function action(bound, formData) {
216 foo = formData.get('foo') + bound.complex;
217 return 'hello';
218 });
219 function App() {
220 return (
221 <form action={serverAction.bind(null, {complex: 'object'})}>
222 <input type="text" name="foo" defaultValue="bar" />
223 </form>
224 );
225 }
226 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
227 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
228 serverConsumerManifest: {
229 moduleMap: null,
230 moduleLoading: null,
231 },
232 });
233 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
234 await readIntoContainer(ssrStream);
235
236 const form = container.firstChild;
237
238 expect(foo).toBe(null);
239
240 const {returnValue} = await submit(form);
241
242 expect(returnValue).toBe('hello');
243 expect(foo).toBe('barobject');
244 });
245
246 it('can submit a multiple complex closure server action without hydrating it', async () => {
247 let foo = null;
248
249 const serverAction = serverExports(function action(bound, formData) {
250 foo = formData.get('foo') + bound.complex;
251 return 'hello' + bound.complex;
252 });
253 function App() {
254 return (
255 <form action={serverAction.bind(null, {complex: 'a'})}>
256 <input type="text" name="foo" defaultValue="bar" />
257 <button formAction={serverAction.bind(null, {complex: 'b'})} />
258 <button formAction={serverAction.bind(null, {complex: 'c'})} />
259 <input
260 type="submit"
261 formAction={serverAction.bind(null, {complex: 'd'})}
262 />
263 </form>
264 );
265 }
266 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
267 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
268 serverConsumerManifest: {
269 moduleMap: null,
270 moduleLoading: null,
271 },
272 });
273 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
274 await readIntoContainer(ssrStream);
275
276 const form = container.firstChild;
277
278 expect(foo).toBe(null);
279
280 const {returnValue} = await submit(form.getElementsByTagName('button')[1]);
281
282 expect(returnValue).toBe('helloc');
283 expect(foo).toBe('barc');
284 });
285
286 it('can bind an imported server action on the client without hydrating it', async () => {
287 let foo = null;
288
289 const ServerModule = serverExports(function action(bound, formData) {
290 foo = formData.get('foo') + bound.complex;
291 return 'hello';
292 });
293 const serverAction = ReactServerDOMClient.createServerReference(
294 ServerModule.$$id,
295 );
296 function Client() {
297 return (
298 <form action={serverAction.bind(null, {complex: 'object'})}>
299 <input type="text" name="foo" defaultValue="bar" />
300 </form>
301 );
302 }
303
304 const ssrStream = await ReactDOMServer.renderToReadableStream(<Client />);
305 await readIntoContainer(ssrStream);
306
307 const form = container.firstChild;
308
309 expect(foo).toBe(null);
310
311 const {returnValue} = await submit(form);
312
313 expect(returnValue).toBe('hello');
314 expect(foo).toBe('barobject');
315 });
316
317 it('can bind a server action on the client without hydrating it', async () => {
318 let foo = null;
319
320 const serverAction = serverExports(function action(bound, formData) {
321 foo = formData.get('foo') + bound.complex;
322 return 'hello';
323 });
324
325 function Client({action}) {
326 return (
327 <form action={action.bind(null, {complex: 'object'})}>
328 <input type="text" name="foo" defaultValue="bar" />
329 </form>
330 );
331 }
332 const ClientRef = await clientExports(Client);
333
334 const rscStream = ReactServerDOMServer.renderToReadableStream(
335 <ClientRef action={serverAction} />,
336 webpackMap,
337 );
338 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
339 serverConsumerManifest: {
340 moduleMap: null,
341 moduleLoading: null,
342 },
343 });
344 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
345 await readIntoContainer(ssrStream);
346
347 const form = container.firstChild;
348
349 expect(foo).toBe(null);
350
351 const {returnValue} = await submit(form);
352
353 expect(returnValue).toBe('hello');
354 expect(foo).toBe('barobject');
355 });
356
357 it("useActionState's dispatch binds the initial state to the provided action", async () => {
358 const serverAction = serverExports(
359 async function action(prevState, formData) {
360 return {
361 count:
362 prevState.count + parseInt(formData.get('incrementAmount'), 10),
363 };
364 },
365 );
366
367 const initialState = {count: 1};
368 function Client({action}) {
369 const [state, dispatch, isPending] = useActionState(action, initialState);
370 return (
371 <form action={dispatch}>
372 <span>{isPending ? 'Pending...' : ''}</span>
373 <span>Count: {state.count}</span>
374 <input type="text" name="incrementAmount" defaultValue="5" />
375 </form>
376 );
377 }
378
379 const ClientRef = await clientExports(Client);
380
381 const rscStream = ReactServerDOMServer.renderToReadableStream(
382 <ClientRef action={serverAction} />,
383 webpackMap,
384 );
385 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
386 serverConsumerManifest: {
387 moduleMap: null,
388 moduleLoading: null,
389 },
390 });
391 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
392 await readIntoContainer(ssrStream);
393
394 const form = container.getElementsByTagName('form')[0];
395 const pendingSpan = container.getElementsByTagName('span')[0];
396 const stateSpan = container.getElementsByTagName('span')[1];
397 expect(pendingSpan.textContent).toBe('');
398 expect(stateSpan.textContent).toBe('Count: 1');
399
400 const {returnValue} = await submit(form);
401 expect(await returnValue).toEqual({count: 6});
402 });
403
404 it('useActionState can reuse state during MPA form submission', async () => {
405 const serverAction = serverExports(
406 async function action(prevState, formData) {
407 return prevState + 1;
408 },
409 );
410
411 function Form({action}) {
412 const [count, dispatch, isPending] = useActionState(action, 1);
413 return (
414 <form action={dispatch}>
415 {isPending ? 'Pending...' : ''}
416 {count}
417 </form>
418 );
419 }
420
421 function Client({action}) {
422 return (
423 <div>
424 <Form action={action} />
425 <Form action={action} />
426 <Form action={action} />
427 </div>
428 );
429 }
430
431 const ClientRef = await clientExports(Client);
432
433 const rscStream = ReactServerDOMServer.renderToReadableStream(
434 <ClientRef action={serverAction} />,
435 webpackMap,
436 );
437 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
438 serverConsumerManifest: {
439 moduleMap: null,
440 moduleLoading: null,
441 },
442 });
443 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
444 await readIntoContainer(ssrStream);
445
446 expect(container.textContent).toBe('111');
447
448 // There are three identical forms. We're going to submit the second one.
449 const form = container.getElementsByTagName('form')[1];
450 const {formState} = await submit(form);
451
452 // Simulate an MPA form submission by resetting the container and
453 // rendering again.
454 container.innerHTML = '';
455
456 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
457 <ClientRef action={serverAction} />,
458 webpackMap,
459 );
460 const postbackResponse = ReactServerDOMClient.createFromReadableStream(
461 postbackRscStream,
462 {
463 serverConsumerManifest: {
464 moduleMap: null,
465 moduleLoading: null,
466 },
467 },
468 );
469 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
470 postbackResponse,
471 {formState: formState},
472 );
473 await readIntoContainer(postbackSsrStream);
474
475 // Only the second form's state should have been updated.
476 expect(container.textContent).toBe('121');
477
478 // Test that it hydrates correctly
479 if (__DEV__) {
480 // TODO: Can't use our internal act() util that works in production
481 // because it works by overriding the timer APIs, which this test module
482 // also does. Remove dev condition once FlightServer.act() is available.
483 await act(() => {
484 ReactDOMClient.hydrateRoot(container, postbackResponse, {
485 formState: formState,
486 });
487 });
488 expect(container.textContent).toBe('121');
489 }
490 });
491
492 it(
493 'useActionState preserves state if arity is the same, but different ' +
494 'arguments are bound (i.e. inline closure)',
495 async () => {
496 const serverAction = serverExports(
497 async function action(stepSize, prevState, formData) {
498 return prevState + stepSize;
499 },
500 );
501
502 function Form({action}) {
503 const [count, dispatch, isPending] = useActionState(action, 1);
504 return (
505 <form action={dispatch}>
506 {isPending ? 'Pending...' : ''}
507 {count}
508 </form>
509 );
510 }
511
512 function Client({action}) {
513 return (
514 <div>
515 <Form action={action} />
516 <Form action={action} />
517 <Form action={action} />
518 </div>
519 );
520 }
521
522 const ClientRef = await clientExports(Client);
523
524 const rscStream = ReactServerDOMServer.renderToReadableStream(
525 // Note: `.bind` is the same as an inline closure with 'use server'
526 <ClientRef action={serverAction.bind(null, 1)} />,
527 webpackMap,
528 );
529 const response = ReactServerDOMClient.createFromReadableStream(
530 rscStream,
531 {
532 serverConsumerManifest: {
533 moduleMap: null,
534 moduleLoading: null,
535 },
536 },
537 );
538 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
539 await readIntoContainer(ssrStream);
540
541 expect(container.textContent).toBe('111');
542
543 // There are three identical forms. We're going to submit the second one.
544 const form = container.getElementsByTagName('form')[1];
545 const {formState} = await submit(form);
546
547 // Simulate an MPA form submission by resetting the container and
548 // rendering again.
549 container.innerHTML = '';
550
551 // On the next page, the same server action is rendered again, but with
552 // a different bound stepSize argument. We should treat this as the same
553 // action signature.
554 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
555 // Note: `.bind` is the same as an inline closure with 'use server'
556 <ClientRef action={serverAction.bind(null, 5)} />,
557 webpackMap,
558 );
559 const postbackResponse = ReactServerDOMClient.createFromReadableStream(
560 postbackRscStream,
561 {
562 serverConsumerManifest: {
563 moduleMap: null,
564 moduleLoading: null,
565 },
566 },
567 );
568 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
569 postbackResponse,
570 {formState: formState},
571 );
572 await readIntoContainer(postbackSsrStream);
573
574 // The state should have been preserved because the action signatures are
575 // the same. (Note that the amount increased by 1, because that was the
576 // value of stepSize at the time the form was submitted)
577 expect(container.textContent).toBe('121');
578
579 // Now submit the form again. This time, the state should increase by 5
580 // because the stepSize argument has changed.
581 const form2 = container.getElementsByTagName('form')[1];
582 const {formState: formState2} = await submit(form2);
583
584 container.innerHTML = '';
585
586 const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
587 // Note: `.bind` is the same as an inline closure with 'use server'
588 <ClientRef action={serverAction.bind(null, 5)} />,
589 webpackMap,
590 );
591 const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
592 postbackRscStream2,
593 {
594 serverConsumerManifest: {
595 moduleMap: null,
596 moduleLoading: null,
597 },
598 },
599 );
600 const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
601 postbackResponse2,
602 {formState: formState2},
603 );
604 await readIntoContainer(postbackSsrStream2);
605
606 expect(container.textContent).toBe('171');
607 },
608 );
609
610 it('useActionState does not reuse state if action signatures are different', async () => {
611 // This is the same as the previous test, except instead of using bind to
612 // configure the server action (i.e. a closure), it swaps the action.
613 const increaseBy1 = serverExports(
614 async function action(prevState, formData) {
615 return prevState + 1;
616 },
617 );
618
619 const increaseBy5 = serverExports(
620 async function action(prevState, formData) {
621 return prevState + 5;
622 },
623 );
624
625 function Form({action}) {
626 const [count, dispatch, isPending] = useActionState(action, 1);
627 return (
628 <form action={dispatch}>
629 {isPending ? 'Pending...' : ''}
630 {count}
631 </form>
632 );
633 }
634
635 function Client({action}) {
636 return (
637 <div>
638 <Form action={action} />
639 <Form action={action} />
640 <Form action={action} />
641 </div>
642 );
643 }
644
645 const ClientRef = await clientExports(Client);
646
647 const rscStream = ReactServerDOMServer.renderToReadableStream(
648 <ClientRef action={increaseBy1} />,
649 webpackMap,
650 );
651 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
652 serverConsumerManifest: {
653 moduleMap: null,
654 moduleLoading: null,
655 },
656 });
657 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
658 await readIntoContainer(ssrStream);
659
660 expect(container.textContent).toBe('111');
661
662 // There are three identical forms. We're going to submit the second one.
663 const form = container.getElementsByTagName('form')[1];
664 const {formState} = await submit(form);
665
666 // Simulate an MPA form submission by resetting the container and
667 // rendering again.
668 container.innerHTML = '';
669
670 // On the next page, a different server action is rendered. It should not
671 // reuse the state from the previous page.
672 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
673 <ClientRef action={increaseBy5} />,
674 webpackMap,
675 );
676 const postbackResponse = ReactServerDOMClient.createFromReadableStream(
677 postbackRscStream,
678 {
679 serverConsumerManifest: {
680 moduleMap: null,
681 moduleLoading: null,
682 },
683 },
684 );
685 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
686 postbackResponse,
687 {formState: formState},
688 );
689 await readIntoContainer(postbackSsrStream);
690
691 // The state should not have been preserved because the action signatures
692 // are not the same.
693 expect(container.textContent).toBe('111');
694 });
695
696 it('when permalink is provided, useActionState compares that instead of the keypath', async () => {
697 const serverAction = serverExports(
698 async function action(prevState, formData) {
699 return prevState + 1;
700 },
701 );
702
703 function Form({action, permalink}) {
704 const [count, dispatch, isPending] = useActionState(action, 1, permalink);
705 return (
706 <form action={dispatch}>
707 {isPending ? 'Pending...' : ''}
708 {count}
709 </form>
710 );
711 }
712
713 function Page1({action, permalink}) {
714 return <Form action={action} permalink={permalink} />;
715 }
716
717 function Page2({action, permalink}) {
718 return <Form action={action} permalink={permalink} />;
719 }
720
721 const Page1Ref = await clientExports(Page1);
722 const Page2Ref = await clientExports(Page2);
723
724 const rscStream = ReactServerDOMServer.renderToReadableStream(
725 <Page1Ref action={serverAction} permalink="/permalink" />,
726 webpackMap,
727 );
728 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
729 serverConsumerManifest: {
730 moduleMap: null,
731 moduleLoading: null,
732 },
733 });
734 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
735 await readIntoContainer(ssrStream);
736
737 expect(container.textContent).toBe('1');
738
739 // Submit the form
740 const form = container.getElementsByTagName('form')[0];
741 const {formState} = await submit(form);
742
743 // Simulate an MPA form submission by resetting the container and
744 // rendering again.
745 container.innerHTML = '';
746
747 // On the next page, the same server action is rendered again, but in
748 // a different component tree. However, because a permalink option was
749 // passed, the state should be preserved.
750 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
751 <Page2Ref action={serverAction} permalink="/permalink" />,
752 webpackMap,
753 );
754 const postbackResponse = ReactServerDOMClient.createFromReadableStream(
755 postbackRscStream,
756 {
757 serverConsumerManifest: {
758 moduleMap: null,
759 moduleLoading: null,
760 },
761 },
762 );
763 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
764 postbackResponse,
765 {formState: formState},
766 );
767 await readIntoContainer(postbackSsrStream);
768
769 expect(container.textContent).toBe('2');
770
771 // Now submit the form again. This time, the permalink will be different, so
772 // the state is not preserved.
773 const form2 = container.getElementsByTagName('form')[0];
774 const {formState: formState2} = await submit(form2);
775
776 container.innerHTML = '';
777
778 const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
779 <Page1Ref action={serverAction} permalink="/some-other-permalink" />,
780 webpackMap,
781 );
782 const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
783 postbackRscStream2,
784 {
785 serverConsumerManifest: {
786 moduleMap: null,
787 moduleLoading: null,
788 },
789 },
790 );
791 const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
792 postbackResponse2,
793 {formState: formState2},
794 );
795 await readIntoContainer(postbackSsrStream2);
796
797 // The state was reset because the permalink didn't match
798 expect(container.textContent).toBe('1');
799 });
800
801 it('useActionState can change the action URL with the `permalink` argument', async () => {
802 const serverAction = serverExports(function action(prevState) {
803 return {state: prevState.count + 1};
804 });
805
806 const initialState = {count: 1};
807 function Client({action}) {
808 const [state, dispatch, isPending] = useActionState(
809 action,
810 initialState,
811 '/permalink',
812 );
813 return (
814 <form action={dispatch}>
815 <span>{isPending ? 'Pending...' : ''}</span>
816 <span>Count: {state.count}</span>
817 </form>
818 );
819 }
820
821 const ClientRef = await clientExports(Client);
822
823 const rscStream = ReactServerDOMServer.renderToReadableStream(
824 <ClientRef action={serverAction} />,
825 webpackMap,
826 );
827 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
828 serverConsumerManifest: {
829 moduleMap: null,
830 moduleLoading: null,
831 },
832 });
833 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
834 await readIntoContainer(ssrStream);
835
836 const form = container.getElementsByTagName('form')[0];
837 const pendingSpan = container.getElementsByTagName('span')[0];
838 const stateSpan = container.getElementsByTagName('span')[1];
839 expect(pendingSpan.textContent).toBe('');
840 expect(stateSpan.textContent).toBe('Count: 1');
841
842 expect(form.action).toBe('http://localhost/permalink');
843 });
844
845 it('useActionState `permalink` is coerced to string', async () => {
846 const serverAction = serverExports(function action(prevState) {
847 return {state: prevState.count + 1};
848 });
849
850 class Permalink {
851 toString() {
852 return '/permalink';
853 }
854 }
855
856 const permalink = new Permalink();
857
858 const initialState = {count: 1};
859 function Client({action}) {
860 const [state, dispatch, isPending] = useActionState(
861 action,
862 initialState,
863 permalink,
864 );
865 return (
866 <form action={dispatch}>
867 <span>{isPending ? 'Pending...' : ''}</span>
868 <span>Count: {state.count}</span>
869 </form>
870 );
871 }
872
873 const ClientRef = await clientExports(Client);
874
875 const rscStream = ReactServerDOMServer.renderToReadableStream(
876 <ClientRef action={serverAction} />,
877 webpackMap,
878 );
879 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
880 serverConsumerManifest: {
881 moduleMap: null,
882 moduleLoading: null,
883 },
884 });
885 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
886 await readIntoContainer(ssrStream);
887
888 const form = container.getElementsByTagName('form')[0];
889 const pendingSpan = container.getElementsByTagName('span')[0];
890 const stateSpan = container.getElementsByTagName('span')[1];
891 expect(pendingSpan.textContent).toBe('');
892 expect(stateSpan.textContent).toBe('Count: 1');
893
894 expect(form.action).toBe('http://localhost/permalink');
895 });
896
897 it('useActionState can return JSX state during MPA form submission', async () => {
898 const serverAction = serverExports(
899 async function action(prevState, formData) {
900 return <div>error message</div>;
901 },
902 );
903
904 function Form({action}) {
905 const [errorMsg, dispatch] = useActionState(action, null);
906 return <form action={dispatch}>{errorMsg}</form>;
907 }
908
909 const FormRef = await clientExports(Form);
910
911 const rscStream = ReactServerDOMServer.renderToReadableStream(
912 <FormRef action={serverAction} />,
913 webpackMap,
914 );
915 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
916 serverConsumerManifest: {
917 moduleMap: null,
918 moduleLoading: null,
919 },
920 });
921 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
922 await readIntoContainer(ssrStream);
923
924 const form1 = container.getElementsByTagName('form')[0];
925 expect(form1.textContent).toBe('');
926
927 async function submitTheForm() {
928 const form = container.getElementsByTagName('form')[0];
929 const {formState} = await submit(form);
930
931 // Simulate an MPA form submission by resetting the container and
932 // rendering again.
933 container.innerHTML = '';
934
935 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
936 <FormRef action={serverAction} />,
937 webpackMap,
938 );
939 const postbackResponse = ReactServerDOMClient.createFromReadableStream(
940 postbackRscStream,
941 {
942 serverConsumerManifest: {
943 moduleMap: null,
944 moduleLoading: null,
945 },
946 },
947 );
948 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
949 postbackResponse,
950 {formState: formState},
951 );
952 await readIntoContainer(postbackSsrStream);
953 }
954
955 await submitTheForm();
956 assertConsoleErrorDev([
957 'Failed to serialize an action for progressive enhancement:\n' +
958 'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' +
959 ' [<div/>]\n' +
960 ' ^^^^^^' +
961 '\n in <stack>',
962 ]);
963
964 // The error message was returned as JSX.
965 const form2 = container.getElementsByTagName('form')[0];
966 expect(form2.textContent).toBe('error message');
967 expect(form2.firstChild.tagName).toBe('DIV');
968 });
969
970 it('useActionState can return binary state during MPA form submission', async () => {
971 const serverAction = serverExports(
972 async function action(prevState, formData) {
973 return new Blob([new Uint8Array([104, 105])]);
974 },
975 );
976
977 let blob;
978
979 function Form({action}) {
980 const [errorMsg, dispatch] = useActionState(action, null);
981 let text;
982 if (errorMsg) {
983 blob = errorMsg;
984 text = React.use(blob.text());
985 }
986 return <form action={dispatch}>{text}</form>;
987 }
988
989 const FormRef = await clientExports(Form);
990
991 const rscStream = ReactServerDOMServer.renderToReadableStream(
992 <FormRef action={serverAction} />,
993 webpackMap,
994 );
995 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
996 serverConsumerManifest: {
997 moduleMap: null,
998 moduleLoading: null,
999 },
1000 });
1001 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
1002 await readIntoContainer(ssrStream);
1003
1004 const form1 = container.getElementsByTagName('form')[0];
1005 expect(form1.textContent).toBe('');
1006
1007 async function submitTheForm() {
1008 const form = container.getElementsByTagName('form')[0];
1009 const {formState} = await submit(form);
1010
1011 // Simulate an MPA form submission by resetting the container and
1012 // rendering again.
1013 container.innerHTML = '';
1014
1015 const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
1016 {formState, root: <FormRef action={serverAction} />},
1017 webpackMap,
1018 );
1019 const postbackResponse =
1020 await ReactServerDOMClient.createFromReadableStream(postbackRscStream, {
1021 serverConsumerManifest: {
1022 moduleMap: null,
1023 moduleLoading: null,
1024 },
1025 });
1026 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
1027 postbackResponse.root,
1028 {formState: postbackResponse.formState},
1029 );
1030 await readIntoContainer(postbackSsrStream);
1031 }
1032
1033 await submitTheForm();
1034 assertConsoleErrorDev([
1035 'Failed to serialize an action for progressive enhancement:\n' +
1036 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.' +
1037 '\n in <stack>',
1038 ]);
1039
1040 expect(blob instanceof Blob).toBe(true);
1041 expect(blob.size).toBe(2);
1042
1043 const form2 = container.getElementsByTagName('form')[0];
1044 expect(form2.textContent).toBe('hi');
1045 });
1046 });