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
+let container;
21
+let serverExports;
22
+let webpackServerMap;
23
+let React;
24
+let ReactDOMServer;
25
+let ReactServerDOMServer;
26
+let ReactServerDOMClient;
27
+
28
+describe('ReactFlightDOMReply', () => {
29
+ beforeEach(() => {
30
+ jest.resetModules();
31
+ const WebpackMock = require('./utils/WebpackMock');
32
+ serverExports = WebpackMock.serverExports;
33
+ webpackServerMap = WebpackMock.webpackServerMap;
34
+ React = require('react');
35
+ ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
36
+ ReactServerDOMClient = require('react-server-dom-webpack/client');
37
+ ReactDOMServer = require('react-dom/server.browser');
38
+ container = document.createElement('div');
39
+ document.body.appendChild(container);
40
+ });
41
+
42
+ afterEach(() => {
43
+ document.body.removeChild(container);
44
+ });
45
+
46
+ async function POST(formData) {
47
+ const boundAction = await ReactServerDOMServer.decodeAction(
48
+ formData,
49
+ webpackServerMap,
50
+ );
51
+ return boundAction();
52
+ }
53
+
54
+ function submit(submitter) {
55
+ const form = submitter.form || submitter;
56
+ if (!submitter.form) {
57
+ submitter = undefined;
58
+ }
59
+ const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
60
+ submitEvent.submitter = submitter;
61
+ const returnValue = form.dispatchEvent(submitEvent);
62
+ if (!returnValue) {
63
+ return;
64
+ }
65
+ const action =
66
+ (submitter && submitter.getAttribute('formaction')) || form.action;
67
+ if (!/\s*javascript:/i.test(action)) {
68
+ const method = (submitter && submitter.formMethod) || form.method;
69
+ const encType = (submitter && submitter.formEnctype) || form.enctype;
70
+ if (method === 'post' && encType === 'multipart/form-data') {
71
+ let formData;
72
+ if (submitter) {
73
+ const temp = document.createElement('input');
74
+ temp.name = submitter.name;
75
+ temp.value = submitter.value;
76
+ submitter.parentNode.insertBefore(temp, submitter);
77
+ formData = new FormData(form);
78
+ temp.parentNode.removeChild(temp);
79
+ } else {
80
+ formData = new FormData(form);
81
+ }
82
+ return POST(formData);
83
+ }
84
+ throw new Error('Navigate to: ' + action);
85
+ }
86
+ }
87
+
88
+ async function readIntoContainer(stream) {
89
+ const reader = stream.getReader();
90
+ let result = '';
91
+ while (true) {
92
+ const {done, value} = await reader.read();
93
+ if (done) {
94
+ break;
95
+ }
96
+ result += Buffer.from(value).toString('utf8');
97
+ }
98
+ const temp = document.createElement('div');
99
+ temp.innerHTML = result;
100
+ insertNodesAndExecuteScripts(temp, container, null);
101
+ }
102
+
103
+ // @gate enableFormActions
104
+ it('can submit a passed server action without hydrating it', async () => {
105
+ let foo = null;
106
+
107
+ const serverAction = serverExports(function action(formData) {
108
+ foo = formData.get('foo');
109
+ return 'hello';
110
+ });
111
+ function App() {
112
+ return (
113
+ <form action={serverAction}>
114
+ <input type="text" name="foo" defaultValue="bar" />
115
+ </form>
116
+ );
117
+ }
118
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
119
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream);
120
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
121
+ await readIntoContainer(ssrStream);
122
+
123
+ const form = container.firstChild;
124
+
125
+ expect(foo).toBe(null);
126
+
127
+ const result = await submit(form);
128
+
129
+ expect(result).toBe('hello');
130
+ expect(foo).toBe('bar');
131
+ });
132
+
133
+ // @gate enableFormActions
134
+ it('can submit an imported server action without hydrating it', async () => {
135
+ let foo = null;
136
+
137
+ const ServerModule = serverExports(function action(formData) {
138
+ foo = formData.get('foo');
139
+ return 'hi';
140
+ });
141
+ const serverAction = ReactServerDOMClient.createServerReference(
142
+ ServerModule.$$id,
143
+ );
144
+ function App() {
145
+ return (
146
+ <form action={serverAction}>
147
+ <input type="text" name="foo" defaultValue="bar" />
148
+ </form>
149
+ );
150
+ }
151
+
152
+ const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
153
+ await readIntoContainer(ssrStream);
154
+
155
+ const form = container.firstChild;
156
+
157
+ expect(foo).toBe(null);
158
+
159
+ const result = await submit(form);
160
+
161
+ expect(result).toBe('hi');
162
+
163
+ expect(foo).toBe('bar');
164
+ });
165
+
166
+ // @gate enableFormActions
167
+ it('can submit a complex closure server action without hydrating it', async () => {
168
+ let foo = null;
169
+
170
+ const serverAction = serverExports(function action(bound, formData) {
171
+ foo = formData.get('foo') + bound.complex;
172
+ return 'hello';
173
+ });
174
+ function App() {
175
+ return (
176
+ <form action={serverAction.bind(null, {complex: 'object'})}>
177
+ <input type="text" name="foo" defaultValue="bar" />
178
+ </form>
179
+ );
180
+ }
181
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
182
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream);
183
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
184
+ await readIntoContainer(ssrStream);
185
+
186
+ const form = container.firstChild;
187
+
188
+ expect(foo).toBe(null);
189
+
190
+ const result = await submit(form);
191
+
192
+ expect(result).toBe('hello');
193
+ expect(foo).toBe('barobject');
194
+ });
195
+
196
+ // @gate enableFormActions
197
+ it('can submit a multiple complex closure server action without hydrating it', async () => {
198
+ let foo = null;
199
+
200
+ const serverAction = serverExports(function action(bound, formData) {
201
+ foo = formData.get('foo') + bound.complex;
202
+ return 'hello' + bound.complex;
203
+ });
204
+ function App() {
205
+ return (
206
+ <form action={serverAction.bind(null, {complex: 'a'})}>
207
+ <input type="text" name="foo" defaultValue="bar" />
208
+ <button formAction={serverAction.bind(null, {complex: 'b'})} />
209
+ <button formAction={serverAction.bind(null, {complex: 'c'})} />
210
+ <input
211
+ type="submit"
212
+ formAction={serverAction.bind(null, {complex: 'd'})}
213
+ />
214
+ </form>
215
+ );
216
+ }
217
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
218
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream);
219
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
220
+ await readIntoContainer(ssrStream);
221
+
222
+ const form = container.firstChild;
223
+
224
+ expect(foo).toBe(null);
225
+
226
+ const result = await submit(form.getElementsByTagName('button')[1]);
227
+
228
+ expect(result).toBe('helloc');
229
+ expect(foo).toBe('barc');
230
+ });
231
+});