Skip to content

Commit 0ef1cd4

Browse files
refactor incremental HTML streaming tests
1 parent 9ecf308 commit 0ef1cd4

File tree

2 files changed

+117
-36
lines changed

2 files changed

+117
-36
lines changed

react_on_rails_pro/packages/node-renderer/tests/httpRequestUtils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,66 @@ export const makeRequest = (app: ReturnType<typeof buildApp>, options: Partial<R
198198
getBuffer,
199199
};
200200
};
201+
202+
export const getNextChunkInternal = (
203+
stream: NodeJS.ReadableStream,
204+
{ timeout = 250 }: { timeout?: number } = {},
205+
) => {
206+
return new Promise<string>((resolve, reject) => {
207+
let timeoutId: NodeJS.Timeout;
208+
let cancelDataListener = () => {};
209+
if (timeout) {
210+
timeoutId = setTimeout(() => {
211+
cancelDataListener();
212+
reject(new Error(`Timeout after waiting for ${timeout}ms to get the next stream chunk`));
213+
}, timeout);
214+
}
215+
216+
const onData = (chunk: Buffer) => {
217+
clearTimeout(timeoutId);
218+
cancelDataListener();
219+
resolve(chunk.toString());
220+
};
221+
222+
const onError = (error: Error) => {
223+
clearTimeout(timeoutId);
224+
cancelDataListener();
225+
reject(error);
226+
};
227+
228+
const onClose = () => {
229+
reject(new Error('Stream Closed'));
230+
};
231+
232+
cancelDataListener = () => {
233+
stream.off('data', onData);
234+
stream.off('error', onError);
235+
stream.off('close', onClose);
236+
};
237+
238+
stream.once('data', onData);
239+
stream.once('error', onError);
240+
if (stream.closed) {
241+
onClose();
242+
} else {
243+
stream.once('close', onClose);
244+
}
245+
});
246+
};
247+
248+
export const getNextChunk = async (stream: NodeJS.ReadableStream, options: { timeout?: number } = {}) => {
249+
const receivedChunks: string[] = [];
250+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
251+
while (true) {
252+
try {
253+
// eslint-disable-next-line no-await-in-loop
254+
const chunk = await getNextChunkInternal(stream, options);
255+
receivedChunks.push(chunk);
256+
} catch (err) {
257+
if (receivedChunks.length > 0) {
258+
return receivedChunks.join('');
259+
}
260+
throw err;
261+
}
262+
}
263+
};

react_on_rails_pro/packages/node-renderer/tests/incrementalHtmlStreaming.test.ts

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
createRenderingRequest,
88
createUploadAssetsForm,
99
getAppUrl,
10+
getNextChunk,
1011
RSC_BUNDLE_TIMESTAMP,
1112
SERVER_BUNDLE_TIMESTAMP,
1213
} from './httpRequestUtils';
@@ -50,6 +51,15 @@ const createInitialObject = (bundleTimestamp: string = RSC_BUNDLE_TIMESTAMP, pas
5051
protocolVersion: packageJson.protocolVersion,
5152
password,
5253
renderingRequest: createRenderingRequest({ componentName: 'AsyncPropsComponent' }),
54+
onRequestClosedUpdateChunk: {
55+
bundleTimestamp: RSC_BUNDLE_TIMESTAMP,
56+
updateChunk: `
57+
(function(){
58+
var asyncPropsManager = sharedExecutionContext.get("asyncPropsManager");
59+
asyncPropsManager.endStream();
60+
})()
61+
`,
62+
},
5363
dependencyBundleTimestamps: [bundleTimestamp],
5464
});
5565

@@ -97,14 +107,20 @@ const makeRequest = async (options = {}) => {
97107
};
98108
};
99109

100-
// it('uploads the bundles', async () => {
101-
// const { status, body } = await makeRequest();
102-
// expect(body).toBe('');
103-
// expect(status).toBe(200);
104-
// });
110+
const waitForStatus = (request: http2.ClientHttp2Stream) =>
111+
new Promise<number | undefined>((resolve) => {
112+
request.on('response', (headers) => {
113+
resolve(headers[':status']);
114+
});
115+
});
116+
117+
it('uploads the bundles', async () => {
118+
const { status, body } = await makeRequest();
119+
expect(body).toBe('');
120+
expect(status).toBe(200);
121+
});
105122

106123
it('incremental render html', async () => {
107-
console.log('starting');
108124
const { status, body } = await makeRequest();
109125
expect(body).toBe('');
110126
expect(status).toBe(200);
@@ -113,13 +129,8 @@ it('incremental render html', async () => {
113129
const initialRequestObject = createInitialObject();
114130
request.write(`${JSON.stringify(initialRequestObject)}\n`);
115131

116-
request.on('response', (headers) => {
117-
console.log(headers);
118-
});
119-
120-
request.on('data', (data: Buffer) => {
121-
console.log(data.toString());
122-
});
132+
await expect(waitForStatus(request)).resolves.toBe(200);
133+
await expect(getNextChunk(request)).resolves.toContain('AsyncPropsComponent is a renderFunction');
123134

124135
const updateChunk = {
125136
bundleTimestamp: RSC_BUNDLE_TIMESTAMP,
@@ -130,37 +141,44 @@ it('incremental render html', async () => {
130141
})()
131142
`,
132143
};
133-
setTimeout(() => {
134-
request.write(`${JSON.stringify(updateChunk)}\n`);
135-
}, 1000);
144+
request.write(`${JSON.stringify(updateChunk)}\n`);
145+
await expect(getNextChunk(request)).resolves.toContain('Tale of two towns');
136146

137147
const updateChunk2 = {
138148
bundleTimestamp: RSC_BUNDLE_TIMESTAMP,
139149
updateChunk: `
140150
(function(){
141151
var asyncPropsManager = sharedExecutionContext.get("asyncPropsManager");
142-
asyncPropsManager.setProp("researches", ["Tale of two towns", "Pro Git"]);
152+
asyncPropsManager.setProp("researches", ["AI effect on productivity", "Pro Git"]);
143153
})()
144154
`,
145155
};
146-
setTimeout(() => {
147-
request.write(`${JSON.stringify(updateChunk2)}\n`);
148-
request.end();
149-
}, 1500);
156+
request.write(`${JSON.stringify(updateChunk2)}\n`);
157+
request.end();
158+
await expect(getNextChunk(request)).resolves.toContain('AI effect on productivity');
150159

151-
await new Promise<void>((resolve, reject) => {
152-
request.on('end', () => {
153-
close();
154-
resolve();
155-
});
156-
request.on('error', (err) => {
157-
close();
158-
reject(err as Error);
159-
});
160-
});
161-
await new Promise<void>((resolve) => {
162-
setTimeout(() => {
163-
resolve();
164-
}, 100);
165-
});
160+
await expect(getNextChunk(request)).rejects.toThrow('Stream Closed');
161+
close();
162+
});
163+
164+
// TODO: fix the problem of having a global shared `runOnOtherBundle` function
165+
it.skip('raises an error if a specific async prop is not sent', async () => {
166+
const { status, body } = await makeRequest();
167+
expect(body).toBe('');
168+
expect(status).toBe(200);
169+
170+
const { request, close } = createHttpRequest();
171+
const initialRequestObject = createInitialObject();
172+
request.write(`${JSON.stringify(initialRequestObject)}\n`);
173+
174+
await expect(waitForStatus(request)).resolves.toBe(200);
175+
await expect(getNextChunk(request)).resolves.toContain('AsyncPropsComponent is a renderFunction');
176+
177+
request.end();
178+
await expect(getNextChunk(request)).resolves.toContain(
179+
'The async prop \\"researches\\" is not received. Esnure to send the async prop from ruby side',
180+
);
181+
182+
await expect(getNextChunk(request)).rejects.toThrow('Stream Closed');
183+
close();
166184
});

0 commit comments

Comments
 (0)