main
js 806 lines 24.2 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 {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
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 serverExports;
21 let webpackServerMap;
22 let React;
23 let ReactServerDOMServer;
24 let ReactServerDOMClient;
25 let ReactServerScheduler;
26 let serverAct;
27
28 describe('ReactFlightDOMReply', () => {
29 beforeEach(() => {
30 jest.resetModules();
31
32 ReactServerScheduler = require('scheduler');
33 patchMessageChannel(ReactServerScheduler);
34 serverAct = require('internal-test-utils').serverAct;
35
36 // Simulate the condition resolution
37 jest.mock('react', () => require('react/react.react-server'));
38 jest.mock('react-server-dom-webpack/server', () =>
39 require('react-server-dom-webpack/server.browser'),
40 );
41 const WebpackMock = require('./utils/WebpackMock');
42 // serverExports = WebpackMock.serverExports;
43 webpackServerMap = WebpackMock.webpackServerMap;
44 React = require('react');
45 ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
46 jest.resetModules();
47 __unmockReact();
48 jest.mock('react-server-dom-webpack/client', () =>
49 require('react-server-dom-webpack/client.browser'),
50 );
51 ReactServerDOMClient = require('react-server-dom-webpack/client');
52 });
53
54 // This method should exist on File but is not implemented in JSDOM
55 async function arrayBuffer(file) {
56 return new Promise((resolve, reject) => {
57 const reader = new FileReader();
58 reader.onload = function () {
59 return resolve(reader.result);
60 };
61 reader.onerror = function () {
62 return reject(reader.error);
63 };
64 reader.readAsArrayBuffer(file);
65 });
66 }
67
68 it('can pass undefined as a reply', async () => {
69 const body = await ReactServerDOMClient.encodeReply(undefined);
70 const missing = await ReactServerDOMServer.decodeReply(
71 body,
72 webpackServerMap,
73 );
74 expect(missing).toBe(undefined);
75
76 const body2 = await ReactServerDOMClient.encodeReply({
77 array: [undefined, null, undefined],
78 prop: undefined,
79 });
80 const object = await ReactServerDOMServer.decodeReply(
81 body2,
82 webpackServerMap,
83 );
84 expect(object.array.length).toBe(3);
85 expect(object.array[0]).toBe(undefined);
86 expect(object.array[1]).toBe(null);
87 expect(object.array[3]).toBe(undefined);
88 expect(object.prop).toBe(undefined);
89 // These should really be true but our deserialization doesn't currently deal with it.
90 expect('3' in object.array).toBe(false);
91 expect('prop' in object).toBe(false);
92 });
93
94 it('can pass an iterable as a reply', async () => {
95 const body = await ReactServerDOMClient.encodeReply({
96 [Symbol.iterator]: function* () {
97 yield 'A';
98 yield 'B';
99 yield 'C';
100 },
101 });
102 const iterable = await ReactServerDOMServer.decodeReply(
103 body,
104 webpackServerMap,
105 );
106 const items = [];
107 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
108 for (const item of iterable) {
109 items.push(item);
110 }
111 expect(items).toEqual(['A', 'B', 'C']);
112
113 // Multipass
114 const items2 = [];
115 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
116 for (const item of iterable) {
117 items2.push(item);
118 }
119 expect(items2).toEqual(['A', 'B', 'C']);
120 });
121
122 it('can pass an iterator as a reply', async () => {
123 const iterator = (function* () {
124 yield 'A';
125 yield 'B';
126 yield 'C';
127 })();
128
129 const body = await ReactServerDOMClient.encodeReply(iterator);
130 const result = await ReactServerDOMServer.decodeReply(
131 body,
132 webpackServerMap,
133 );
134
135 // The iterator should be the same as itself.
136 expect(result[Symbol.iterator]()).toBe(result);
137
138 expect(Array.from(result)).toEqual(['A', 'B', 'C']);
139 // We've already consumed this iterator.
140 expect(Array.from(result)).toEqual([]);
141 });
142
143 it('can pass weird numbers as a reply', async () => {
144 const nums = [0, -0, Infinity, -Infinity, NaN];
145 const body = await ReactServerDOMClient.encodeReply(nums);
146 const nums2 = await ReactServerDOMServer.decodeReply(
147 body,
148 webpackServerMap,
149 );
150
151 expect(nums).toEqual(nums2);
152 expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
153 });
154
155 it('can pass a BigInt as a reply', async () => {
156 const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
157 const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
158
159 expect(n).toEqual(90071992547409910000n);
160 });
161
162 it('can pass FormData as a reply', async () => {
163 const formData = new FormData();
164 formData.set('hello', 'world');
165 formData.append('list', '1');
166 formData.append('list', '2');
167 formData.append('list', '3');
168 const typedArray = new Uint8Array([0, 1, 2, 3]);
169 const blob = new Blob([typedArray]);
170 formData.append('blob', blob, 'filename.blob');
171
172 const body = await ReactServerDOMClient.encodeReply(formData);
173 const formData2 = await ReactServerDOMServer.decodeReply(
174 body,
175 webpackServerMap,
176 );
177
178 expect(formData2).not.toBe(formData);
179 expect(Array.from(formData2).length).toBe(5);
180 expect(formData2.get('hello')).toBe('world');
181 expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
182 const blob2 = formData.get('blob');
183 expect(blob2.size).toBe(4);
184 expect(blob2.name).toBe('filename.blob');
185 expect(blob2.type).toBe('');
186 const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
187 expect(typedArray2).toEqual(typedArray);
188 });
189
190 it('can pass multiple Files in FormData', async () => {
191 const typedArrayA = new Uint8Array([0, 1, 2, 3]);
192 const typedArrayB = new Uint8Array([4, 5]);
193 const blobA = new Blob([typedArrayA]);
194 const blobB = new Blob([typedArrayB]);
195 const formData = new FormData();
196 formData.append('filelist', 'string');
197 formData.append('filelist', blobA);
198 formData.append('filelist', blobB);
199
200 const body = await ReactServerDOMClient.encodeReply(formData);
201 const formData2 = await ReactServerDOMServer.decodeReply(
202 body,
203 webpackServerMap,
204 );
205
206 const filelist2 = formData2.getAll('filelist');
207 expect(filelist2.length).toBe(3);
208 expect(filelist2[0]).toBe('string');
209 const blobA2 = filelist2[1];
210 expect(blobA2.size).toBe(4);
211 expect(blobA2.name).toBe('blob');
212 expect(blobA2.type).toBe('');
213 const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
214 expect(typedArrayA2).toEqual(typedArrayA);
215 const blobB2 = filelist2[2];
216 expect(blobB2.size).toBe(2);
217 expect(blobB2.name).toBe('blob');
218 expect(blobB2.type).toBe('');
219 const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
220 expect(typedArrayB2).toEqual(typedArrayB);
221 });
222
223 it('can pass two independent FormData with same keys', async () => {
224 const formDataA = new FormData();
225 formDataA.set('greeting', 'hello');
226 const formDataB = new FormData();
227 formDataB.set('greeting', 'hi');
228
229 const body = await ReactServerDOMClient.encodeReply({
230 a: formDataA,
231 b: formDataB,
232 });
233 const {a: formDataA2, b: formDataB2} =
234 await ReactServerDOMServer.decodeReply(body, webpackServerMap);
235
236 expect(Array.from(formDataA2).length).toBe(1);
237 expect(Array.from(formDataB2).length).toBe(1);
238 expect(formDataA2.get('greeting')).toBe('hello');
239 expect(formDataB2.get('greeting')).toBe('hi');
240 });
241
242 it('can pass a Date as a reply', async () => {
243 const d = new Date(1234567890123);
244 const body = await ReactServerDOMClient.encodeReply(d);
245 const d2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
246
247 expect(d).toEqual(d2);
248 expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
249 });
250
251 it('can pass a Map as a reply', async () => {
252 const objKey = {obj: 'key'};
253 const m = new Map([
254 ['hi', {greet: 'world'}],
255 [objKey, 123],
256 ]);
257 const body = await ReactServerDOMClient.encodeReply(m);
258 const m2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
259
260 expect(m2 instanceof Map).toBe(true);
261 expect(m2.size).toBe(2);
262 expect(m2.get('hi').greet).toBe('world');
263 expect(m2).toEqual(m);
264 });
265
266 it('can pass a Set as a reply', async () => {
267 const objKey = {obj: 'key'};
268 const s = new Set(['hi', objKey]);
269
270 const body = await ReactServerDOMClient.encodeReply(s);
271 const s2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
272
273 expect(s2 instanceof Set).toBe(true);
274 expect(s2.size).toBe(2);
275 expect(s2.has('hi')).toBe(true);
276 expect(s2).toEqual(s);
277 });
278
279 it('does not hang indefinitely when calling decodeReply with FormData', async () => {
280 let error;
281 try {
282 await ReactServerDOMServer.decodeReply(new FormData(), webpackServerMap);
283 } catch (e) {
284 error = e;
285 }
286 expect(error.message).toBe('Connection closed.');
287 });
288
289 it('resolves a promise and includes its value', async () => {
290 let resolve;
291 const promise = new Promise(r => (resolve = r));
292 const bodyPromise = ReactServerDOMClient.encodeReply({promise: promise});
293 resolve('Hi');
294 const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
295 expect(await result.promise).toBe('Hi');
296 });
297
298 it('resolves a React.lazy and includes its value', async () => {
299 let resolve;
300 const lazy = React.lazy(() => new Promise(r => (resolve = r)));
301 const bodyPromise = ReactServerDOMClient.encodeReply({lazy: lazy});
302 resolve({default: 'Hi'});
303 const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
304 expect(result.lazy).toBe('Hi');
305 });
306
307 it('resolves a proxy throwing a promise inside React.lazy', async () => {
308 let resolve1;
309 let resolve2;
310 const lazy = React.lazy(() => new Promise(r => (resolve1 = r)));
311 const promise = new Promise(r => (resolve2 = r));
312 const bodyPromise1 = ReactServerDOMClient.encodeReply({lazy: lazy});
313 const target = {value: ''};
314 let loaded = false;
315 const proxy = new Proxy(target, {
316 get(targetObj, prop, receiver) {
317 if (prop === 'value') {
318 if (!loaded) {
319 throw promise;
320 }
321 return 'Hello';
322 }
323 return targetObj[prop];
324 },
325 });
326 await resolve1({default: proxy});
327
328 // Encode it again so that we have an already initialized lazy
329 // This is now already resolved but the proxy inside isn't. This ensures
330 // we trigger the retry code path.
331 const bodyPromise2 = ReactServerDOMClient.encodeReply({lazy: lazy});
332
333 // Then resolve the inner thrown promise.
334 loaded = true;
335 await resolve2('Hello');
336
337 const result1 = await ReactServerDOMServer.decodeReply(await bodyPromise1);
338 expect(await result1.lazy.value).toBe('Hello');
339 const result2 = await ReactServerDOMServer.decodeReply(await bodyPromise2);
340 expect(await result2.lazy.value).toBe('Hello');
341 });
342
343 it('errors when called with JSX by default', async () => {
344 let error;
345 try {
346 await ReactServerDOMClient.encodeReply(<div />);
347 } catch (x) {
348 error = x;
349 }
350 expect(error).toEqual(
351 expect.objectContaining({
352 message: __DEV__
353 ? expect.stringContaining(
354 'React Element cannot be passed to Server Functions from the Client without a temporary reference set.',
355 )
356 : expect.stringContaining(''),
357 }),
358 );
359 });
360
361 it('can pass JSX through a round trip using temporary references', async () => {
362 function Component() {
363 return <div />;
364 }
365
366 const children = <Component />;
367
368 const temporaryReferences =
369 ReactServerDOMClient.createTemporaryReferenceSet();
370 const body = await ReactServerDOMClient.encodeReply(
371 {children},
372 {
373 temporaryReferences,
374 },
375 );
376
377 const temporaryReferencesServer =
378 ReactServerDOMServer.createTemporaryReferenceSet();
379 const serverPayload = await ReactServerDOMServer.decodeReply(
380 body,
381 webpackServerMap,
382 {temporaryReferences: temporaryReferencesServer},
383 );
384 const stream = await serverAct(() =>
385 ReactServerDOMServer.renderToReadableStream(serverPayload, null, {
386 temporaryReferences: temporaryReferencesServer,
387 }),
388 );
389 const response = await ReactServerDOMClient.createFromReadableStream(
390 stream,
391 {
392 temporaryReferences,
393 },
394 );
395
396 // This should've been the same reference that we already saw.
397 expect(response.children).toBe(children);
398 });
399
400 it('can pass JSX as root model through a round trip using temporary references', async () => {
401 const jsx = <div />;
402
403 const temporaryReferences =
404 ReactServerDOMClient.createTemporaryReferenceSet();
405 const body = await ReactServerDOMClient.encodeReply(jsx, {
406 temporaryReferences,
407 });
408
409 const temporaryReferencesServer =
410 ReactServerDOMServer.createTemporaryReferenceSet();
411 const serverPayload = await ReactServerDOMServer.decodeReply(
412 body,
413 webpackServerMap,
414 {temporaryReferences: temporaryReferencesServer},
415 );
416 const stream = await serverAct(() =>
417 ReactServerDOMServer.renderToReadableStream(serverPayload, null, {
418 temporaryReferences: temporaryReferencesServer,
419 }),
420 );
421 const response = await ReactServerDOMClient.createFromReadableStream(
422 stream,
423 {
424 temporaryReferences,
425 },
426 );
427
428 // This should be the same reference that we already saw.
429 await expect(response).toBe(jsx);
430 });
431
432 it('can pass a promise that resolves to JSX through a round trip using temporary references', async () => {
433 const jsx = <div />;
434 const promise = Promise.resolve(jsx);
435
436 const temporaryReferences =
437 ReactServerDOMClient.createTemporaryReferenceSet();
438 const body = await ReactServerDOMClient.encodeReply(
439 {promise},
440 {
441 temporaryReferences,
442 },
443 );
444
445 const temporaryReferencesServer =
446 ReactServerDOMServer.createTemporaryReferenceSet();
447 const serverPayload = await ReactServerDOMServer.decodeReply(
448 body,
449 webpackServerMap,
450 {temporaryReferences: temporaryReferencesServer},
451 );
452 const stream = await serverAct(() =>
453 ReactServerDOMServer.renderToReadableStream(serverPayload, null, {
454 temporaryReferences: temporaryReferencesServer,
455 }),
456 );
457 const response = await ReactServerDOMClient.createFromReadableStream(
458 stream,
459 {
460 temporaryReferences,
461 },
462 );
463
464 // This should resolve to the same reference that we already saw.
465 await expect(response.promise).resolves.toBe(jsx);
466 });
467
468 it('can return the same object using temporary references', async () => {
469 const obj = {
470 this: {is: 'a large object'},
471 with: {many: 'properties in it'},
472 };
473
474 const root = {obj};
475
476 const temporaryReferences =
477 ReactServerDOMClient.createTemporaryReferenceSet();
478 const body = await ReactServerDOMClient.encodeReply(root, {
479 temporaryReferences,
480 });
481
482 const temporaryReferencesServer =
483 ReactServerDOMServer.createTemporaryReferenceSet();
484 const serverPayload = await ReactServerDOMServer.decodeReply(
485 body,
486 webpackServerMap,
487 {temporaryReferences: temporaryReferencesServer},
488 );
489 const stream = await serverAct(() =>
490 ReactServerDOMServer.renderToReadableStream(
491 {
492 root: serverPayload,
493 obj: serverPayload.obj,
494 },
495 null,
496 {temporaryReferences: temporaryReferencesServer},
497 ),
498 );
499 const response = await ReactServerDOMClient.createFromReadableStream(
500 stream,
501 {
502 temporaryReferences,
503 },
504 );
505
506 // This should've been the same reference that we already saw because
507 // we returned it by reference.
508 expect(response.root).toBe(root);
509 expect(response.obj).toBe(obj);
510 });
511
512 it('can return an opaque object through an async function', async () => {
513 function fn() {
514 return 'this is a client function';
515 }
516
517 const args = [fn];
518
519 const temporaryReferences =
520 ReactServerDOMClient.createTemporaryReferenceSet();
521 const body = await ReactServerDOMClient.encodeReply(args, {
522 temporaryReferences,
523 });
524
525 const temporaryReferencesServer =
526 ReactServerDOMServer.createTemporaryReferenceSet();
527 const serverPayload = await ReactServerDOMServer.decodeReply(
528 body,
529 webpackServerMap,
530 {temporaryReferences: temporaryReferencesServer},
531 );
532
533 async function action(arg) {
534 return arg;
535 }
536
537 const stream = await serverAct(() =>
538 ReactServerDOMServer.renderToReadableStream(
539 {
540 result: action.apply(null, serverPayload),
541 },
542 null,
543 {temporaryReferences: temporaryReferencesServer},
544 ),
545 );
546 const response = await ReactServerDOMClient.createFromReadableStream(
547 stream,
548 {
549 temporaryReferences,
550 },
551 );
552
553 expect(await response.result).toBe(fn);
554 });
555
556 it('should supports streaming ReadableStream with objects', async () => {
557 let controller1;
558 let controller2;
559 const s1 = new ReadableStream({
560 start(c) {
561 controller1 = c;
562 },
563 });
564 const s2 = new ReadableStream({
565 start(c) {
566 controller2 = c;
567 },
568 });
569
570 const promise = ReactServerDOMClient.encodeReply({s1, s2});
571
572 controller1.enqueue({hello: 'world'});
573 controller2.enqueue({hi: 'there'});
574
575 controller1.enqueue('text1');
576 controller2.enqueue('text2');
577
578 controller1.close();
579 controller2.close();
580
581 const body = await promise;
582
583 const result = await ReactServerDOMServer.decodeReply(
584 body,
585 webpackServerMap,
586 );
587 const reader1 = result.s1.getReader();
588 const reader2 = result.s2.getReader();
589
590 expect(await reader1.read()).toEqual({
591 value: {hello: 'world'},
592 done: false,
593 });
594 expect(await reader2.read()).toEqual({
595 value: {hi: 'there'},
596 done: false,
597 });
598
599 expect(await reader1.read()).toEqual({
600 value: 'text1',
601 done: false,
602 });
603 expect(await reader1.read()).toEqual({
604 value: undefined,
605 done: true,
606 });
607 expect(await reader2.read()).toEqual({
608 value: 'text2',
609 done: false,
610 });
611 expect(await reader2.read()).toEqual({
612 value: undefined,
613 done: true,
614 });
615 });
616
617 it('should supports streaming AsyncIterables with objects', async () => {
618 let resolve;
619 const wait = new Promise(r => (resolve = r));
620 const multiShotIterable = {
621 async *[Symbol.asyncIterator]() {
622 const next = yield {hello: 'A'};
623 expect(next).toBe(undefined);
624 await wait;
625 yield {hi: 'B'};
626 return 'C';
627 },
628 };
629 const singleShotIterator = (async function* () {
630 const next = yield {hello: 'D'};
631 expect(next).toBe(undefined);
632 await wait;
633 yield {hi: 'E'};
634 return 'F';
635 })();
636
637 await resolve();
638
639 const body = await ReactServerDOMClient.encodeReply({
640 multiShotIterable,
641 singleShotIterator,
642 });
643 const result = await ReactServerDOMServer.decodeReply(
644 body,
645 webpackServerMap,
646 );
647
648 const iterator1 = result.multiShotIterable[Symbol.asyncIterator]();
649 const iterator2 = result.singleShotIterator[Symbol.asyncIterator]();
650
651 expect(iterator1).not.toBe(result.multiShotIterable);
652 expect(iterator2).toBe(result.singleShotIterator);
653
654 expect(await iterator1.next()).toEqual({
655 value: {hello: 'A'},
656 done: false,
657 });
658 expect(await iterator2.next()).toEqual({
659 value: {hello: 'D'},
660 done: false,
661 });
662
663 expect(await iterator1.next()).toEqual({
664 value: {hi: 'B'},
665 done: false,
666 });
667 expect(await iterator2.next()).toEqual({
668 value: {hi: 'E'},
669 done: false,
670 });
671 expect(await iterator1.next()).toEqual({
672 value: 'C', // Return value
673 done: true,
674 });
675 expect(await iterator1.next()).toEqual({
676 value: undefined,
677 done: true,
678 });
679
680 expect(await iterator2.next()).toEqual({
681 value: 'F', // Return value
682 done: true,
683 });
684
685 // Multi-shot iterables should be able to do the same thing again
686 const iterator3 = result.multiShotIterable[Symbol.asyncIterator]();
687
688 expect(iterator3).not.toBe(iterator1);
689
690 // We should be able to iterate over the iterable again and it should be
691 // synchronously available using instrumented promises so that React can
692 // rerender it synchronously.
693 expect(iterator3.next().value).toEqual({
694 value: {hello: 'A'},
695 done: false,
696 });
697 expect(iterator3.next().value).toEqual({
698 value: {hi: 'B'},
699 done: false,
700 });
701 expect(iterator3.next().value).toEqual({
702 value: 'C', // Return value
703 done: true,
704 });
705 expect(iterator3.next().value).toEqual({
706 value: undefined,
707 done: true,
708 });
709
710 expect(() => iterator3.next('this is not allowed')).toThrow(
711 'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
712 );
713 });
714
715 it('can transport cyclic objects', async () => {
716 const cyclic = {obj: null};
717 cyclic.obj = cyclic;
718
719 const body = await ReactServerDOMClient.encodeReply({prop: cyclic});
720 const root = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
721 expect(root.prop.obj).toBe(root.prop);
722 });
723
724 it('can transport cyclic arrays', async () => {
725 const obj = {};
726 const cyclic = [obj];
727 cyclic[1] = cyclic;
728
729 const body = await ReactServerDOMClient.encodeReply({prop: cyclic, obj});
730 const root = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
731 expect(root.prop[1]).toBe(root.prop);
732 expect(root.prop[0]).toBe(root.obj);
733 });
734
735 it('can abort an unresolved model and get the partial result', async () => {
736 const promise = new Promise(r => {});
737 const controller = new AbortController();
738 const bodyPromise = ReactServerDOMClient.encodeReply(
739 {promise: promise, hello: 'world'},
740 {signal: controller.signal},
741 );
742 controller.abort();
743
744 const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
745 expect(result.hello).toBe('world');
746 await expect(result.promise).rejects.toThrow('Connection closed.');
747 });
748
749 it('cannot deserialize a Blob reference backed by a string', async () => {
750 const formData = new FormData();
751 formData.set('1', '-'.repeat(50000));
752 formData.set('0', JSON.stringify(['$B1']));
753 let error;
754 try {
755 await ReactServerDOMServer.decodeReply(formData, webpackServerMap);
756 } catch (x) {
757 error = x;
758 }
759 expect(error.message).toContain('Referenced Blob is not a Blob.');
760 });
761
762 it('detaches the abort listener once the reply is encoded', async () => {
763 const controller = new AbortController();
764 const signal = controller.signal;
765
766 // Collects the lifetime signal that React bounds each abort listener with.
767 // React passes that signal to addEventListener instead of calling
768 // removeEventListener, so the runtime performs the removal and nothing here
769 // observes it directly. An aborted lifetime is what shows the listener is
770 // gone.
771 const lifetimes = [];
772 const add = signal.addEventListener.bind(signal);
773 signal.addEventListener = (type, listener, options) => {
774 if (type === 'abort') {
775 lifetimes.push(options.signal);
776 }
777 return add(type, listener, options);
778 };
779
780 let resolvePart;
781 const part = new Promise(r => (resolvePart = r));
782 const bodyPromise = ReactServerDOMClient.encodeReply(
783 {part, hello: 'world'},
784 {signal},
785 );
786 expect(lifetimes).toHaveLength(1);
787 expect(lifetimes[0].aborted).toBe(false);
788
789 resolvePart('done');
790 await bodyPromise;
791
792 expect(lifetimes[0].aborted).toBe(true);
793 });
794
795 it('resolves with the partial result when the signal is already aborted', async () => {
796 const controller = new AbortController();
797 controller.abort();
798 const neverResolves = new Promise(() => {});
799 const body = await ReactServerDOMClient.encodeReply(
800 {promise: neverResolves, hello: 'world'},
801 {signal: controller.signal},
802 );
803 const result = await ReactServerDOMServer.decodeReply(body);
804 expect(result.hello).toBe('world');
805 });
806 });