1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and its 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
-// Polyfills for test environment
13
-global.ReadableStream =
14
- require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
-global.TextEncoder = require('util').TextEncoder;
16
-global.TextDecoder = require('util').TextDecoder;
17
-global.Headers = require('node-fetch').Headers;
18
-global.Request = require('node-fetch').Request;
19
-global.Response = require('node-fetch').Response;
20
-
21
-let fetchCount = 0;
22
-async function fetchMock(resource, options) {
23
- fetchCount++;
24
- const request = new Request(resource, options);
25
- return new Response(
26
- request.method +
27
- ' ' +
28
- request.url +
29
- ' ' +
30
- JSON.stringify(Array.from(request.headers.entries())),
31
- );
32
-}
33
-
34
-let React;
35
-let ReactServer;
36
-let ReactServerDOMServer;
37
-let ReactServerDOMClient;
38
-let use;
39
-let cache;
40
-
41
-describe('ReactFetch', () => {
42
- beforeEach(() => {
43
- jest.resetModules();
44
- fetchCount = 0;
45
- global.fetch = fetchMock;
46
-
47
- jest.mock('react', () => require('react/react.react-server'));
48
- jest.mock('react-server-dom-webpack/server', () =>
49
- require('react-server-dom-webpack/server.browser'),
50
- );
51
- require('react-server-dom-webpack/src/__tests__/utils/WebpackMock');
52
- ReactServerDOMServer = require('react-server-dom-webpack/server');
53
- ReactServer = require('react');
54
-
55
- jest.resetModules();
56
- __unmockReact();
57
- jest.unmock('react-server-dom-webpack/server');
58
- ReactServerDOMClient = require('react-server-dom-webpack/client');
59
- React = require('react');
60
- use = ReactServer.use;
61
- cache = ReactServer.cache;
62
- });
63
-
64
- function render(Component) {
65
- const stream = ReactServerDOMServer.renderToReadableStream(<Component />);
66
- return ReactServerDOMClient.createFromReadableStream(stream);
67
- }
68
-
69
- it('can fetch duplicates outside of render', async () => {
70
- let response = await fetch('world');
71
- let text = await response.text();
72
- expect(text).toMatchInlineSnapshot(`"GET world []"`);
73
- response = await fetch('world');
74
- text = await response.text();
75
- expect(text).toMatchInlineSnapshot(`"GET world []"`);
76
- expect(fetchCount).toBe(2);
77
- });
78
-
79
- // @gate enableFetchInstrumentation && enableCache
80
- it('can dedupe fetches inside of render', async () => {
81
- function Component() {
82
- const response = use(fetch('world'));
83
- const text = use(response.text());
84
- return text;
85
- }
86
- const promise = render(Component);
87
- expect(await promise).toMatchInlineSnapshot(`"GET world []"`);
88
- expect(promise._debugInfo).toEqual(
89
- __DEV__ ? [{name: 'Component', env: 'Server', owner: null}] : undefined,
90
- );
91
- expect(fetchCount).toBe(1);
92
- });
93
-
94
- // @gate enableFetchInstrumentation && enableCache
95
- it('can dedupe fetches in micro tasks', async () => {
96
- async function getData() {
97
- const r1 = await fetch('hello');
98
- const t1 = await r1.text();
99
- const r2 = await fetch('world');
100
- const t2 = await r2.text();
101
- return t1 + ' ' + t2;
102
- }
103
- function Component() {
104
- return use(getData());
105
- }
106
- expect(await render(Component)).toMatchInlineSnapshot(
107
- `"GET hello [] GET world []"`,
108
- );
109
- expect(fetchCount).toBe(2);
110
- });
111
-
112
- // @gate enableFetchInstrumentation && enableCache
113
- it('can dedupe cache in micro tasks', async () => {
114
- const cached = cache(async () => {
115
- fetchCount++;
116
- return 'world';
117
- });
118
- async function getData() {
119
- const r1 = await fetch('hello');
120
- const t1 = await r1.text();
121
- const t2 = await cached();
122
- return t1 + ' ' + t2;
123
- }
124
- function Component() {
125
- return use(getData());
126
- }
127
- expect(await render(Component)).toMatchInlineSnapshot(
128
- `"GET hello [] world"`,
129
- );
130
- expect(fetchCount).toBe(2);
131
- });
132
-
133
- // @gate enableFetchInstrumentation && enableCache
134
- it('can dedupe fetches using Request and not', async () => {
135
- function Component() {
136
- const response = use(fetch('world'));
137
- const text = use(response.text());
138
- const sameRequest = new Request('world', {method: 'get'});
139
- const response2 = use(fetch(sameRequest));
140
- const text2 = use(response2.text());
141
- return text + ' ' + text2;
142
- }
143
- expect(await render(Component)).toMatchInlineSnapshot(
144
- `"GET world [] GET world []"`,
145
- );
146
- expect(fetchCount).toBe(1);
147
- });
148
-
149
- // @gate enableFetchInstrumentation && enableCache
150
- it('can dedupe fetches using URL and not', async () => {
151
- const url = 'http://example.com/';
152
- function Component() {
153
- const response = use(fetch(url));
154
- const text = use(response.text());
155
- const response2 = use(fetch(new URL(url)));
156
- const text2 = use(response2.text());
157
- return text + ' ' + text2;
158
- }
159
- expect(await render(Component)).toMatchInlineSnapshot(
160
- `"GET ${url} [] GET ${url} []"`,
161
- );
162
- expect(fetchCount).toBe(1);
163
- });
164
-
165
- it('can opt-out of deduping fetches inside of render with custom signal', async () => {
166
- const controller = new AbortController();
167
- function useCustomHook() {
168
- return use(
169
- fetch('world', {signal: controller.signal}).then(response =>
170
- response.text(),
171
- ),
172
- );
173
- }
174
- function Component() {
175
- return useCustomHook() + ' ' + useCustomHook();
176
- }
177
- expect(await render(Component)).toMatchInlineSnapshot(
178
- `"GET world [] GET world []"`,
179
- );
180
- expect(fetchCount).not.toBe(1);
181
- });
182
-
183
- it('opts out of deduping for POST requests', async () => {
184
- function useCustomHook() {
185
- return use(
186
- fetch('world', {method: 'POST'}).then(response => response.text()),
187
- );
188
- }
189
- function Component() {
190
- return useCustomHook() + ' ' + useCustomHook();
191
- }
192
- expect(await render(Component)).toMatchInlineSnapshot(
193
- `"POST world [] POST world []"`,
194
- );
195
- expect(fetchCount).not.toBe(1);
196
- });
197
-
198
- // @gate enableFetchInstrumentation && enableCache
199
- it('can dedupe fetches using same headers but not different', async () => {
200
- function Component() {
201
- const response = use(fetch('world', {headers: {a: 'A'}}));
202
- const text = use(response.text());
203
- const sameRequest = new Request('world', {
204
- headers: new Headers({b: 'B'}),
205
- });
206
- const response2 = use(fetch(sameRequest));
207
- const text2 = use(response2.text());
208
- return text + ' ' + text2;
209
- }
210
- expect(await render(Component)).toMatchInlineSnapshot(
211
- `"GET world [["a","A"]] GET world [["b","B"]]"`,
212
- );
213
- expect(fetchCount).toBe(2);
214
- });
215
-});