import { concatArrayBuffers, readableStreamToArray, readableStreamToArrayBuffer, readableStreamToBlob, readableStreamToBytes, serve, Server, } from "bun"; import { describe, expect, it } from "bun:test"; import { expectMaxObjectTypeCount, gc } from "harness"; // @ts-ignore import * as React from "react"; import * as ReactDOM from "react-dom/server"; import { renderToReadableStream as renderToReadableStreamBrowser } from "react-dom/server.browser"; Object.defineProperty(renderToReadableStreamBrowser, "name", { value: "server.browser", }); const renderToReadableStreamBun = ReactDOM.renderToReadableStreamBun || {}; if (typeof renderToReadableStreamBun !== "function" && parseInt(ReactDOM.version.split(".")[0], 10) > 18) { if (!import.meta.resolveSync("react-dom/server").includes(".bun.")) { throw new Error( "react-dom/server.bun is not the correct version:\n " + import.meta.resolveSync("react-dom/server"), ); } } Object.defineProperty(renderToReadableStreamBun, "name", { value: "server.bun", }); const fixtures = [ // Needs at least six variations // - < 8 chars, latin1 // - 8+ chars, latin1 // - 16+ chars, latin1 // - < 8 chars, utf16 // - 8+ chars, utf16 // - 16+ chars, utf16 ["b", b], ["Hello World!", Hello World!], ["", ], ["πŸ˜‹", πŸ˜‹], ["πŸ˜‹", πŸ˜‹], ["Hello World! πŸ˜‹", Hello World! πŸ˜‹], [ "Hello World!πŸ˜‹", <> Hello World!πŸ˜‹ , ], [ "πŸ˜‹Hello World!", <> πŸ˜‹Hello World! , ], ["πŸ˜‹", <>πŸ˜‹], ["lπŸ˜‹l", <>lπŸ˜‹l], ["loπŸ˜‹", <>loπŸ˜‹], ["πŸ˜‹lo", <>πŸ˜‹lo], [ "πŸ˜‹Hello World!", <> πŸ˜‹Hello World! , ], [ "πŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹Hello World!", <> πŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹ Hello World! , ], ["HelloπŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹World!", HelloπŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹World!], [ "Hello World!πŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹", <> Hello World! πŸ˜‹πŸ˜‹πŸ˜‹πŸ˜‹ , ], [ "πŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹Alternating latin1 & utf16", <> πŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹Alternating latin1 & utf16 , ], ["HelloπŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹World!", HelloπŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹World!], [ "Hello World!πŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹", <> Hello World! πŸ˜‹LπŸ˜‹lπŸ˜‹LπŸ˜‹ , ], ] as const; describe("React", () => { it("React.createContext works", () => { expect(typeof React.createContext).toBe("function"); const pleaseDontThrow = React.createContext({ foo: true }); expect((pleaseDontThrow as any).$$typeof.description).toBe("react.context"); const pleaseDontThrow2 = (React as any).default.createContext({ foo: true, }); expect(pleaseDontThrow2.$$typeof.description).toBe("react.context"); }); }); describe("ReactDOM", () => { for (let renderToReadableStream of [renderToReadableStreamBun, renderToReadableStreamBrowser]) { for (let [inputString, reactElement] of fixtures) { describe.skipIf(typeof renderToReadableStream !== "function")( `${renderToReadableStream?.name || "renderToReadableStream"}(${inputString})`, () => { it("Response.text()", async () => { const stream = await renderToReadableStream(reactElement); gc(); const response = new Response(stream); gc(); try { const text = await response.text(); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); } catch (e: any) { console.log(e.stack); throw e; } }); it("Response.arrayBuffer()", async () => { const stream = await renderToReadableStream(reactElement); gc(); const response = new Response(stream); gc(); const text = new TextDecoder().decode(await response.arrayBuffer()); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("Response.blob()", async () => { const stream = await renderToReadableStream(reactElement); gc(); const response = new Response(stream); gc(); const text = await (await response.blob()).text(); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("(stream).text()", async () => { const stream = await renderToReadableStream(reactElement); gc(); const text = await stream.text(); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("readableStreamToBlob(stream)", async () => { try { const stream = await renderToReadableStream(reactElement); gc(); const blob = await readableStreamToBlob(stream); const text = await blob.text(); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); } catch (e: any) { console.error(e.message); console.error(e.stack); throw e; } }); it("readableStreamToArray(stream)", async () => { const stream = await renderToReadableStream(reactElement); gc(); const array = await readableStreamToArray(stream); const text = renderToReadableStream === renderToReadableStreamBun ? array.join("") : new TextDecoder().decode(concatArrayBuffers(array as any[])); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("readableStreamToArrayBuffer(stream)", async () => { const stream = await renderToReadableStream(reactElement); gc(); const arrayBuffer = await readableStreamToArrayBuffer(stream); const text = new TextDecoder().decode(arrayBuffer); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("readableStreamToBytes(stream)", async () => { const stream = await renderToReadableStream(reactElement); gc(); const uint8 = await readableStreamToBytes(stream); const text = new TextDecoder().decode(uint8); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("for await (chunk of stream)", async () => { const stream = await renderToReadableStream(reactElement); gc(); const chunks: any = []; for await (let chunk of stream) { chunks.push(chunk); } const text = await new Response(chunks).text(); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); it("for await (chunk of stream) (arrayBuffer)", async () => { const stream = await renderToReadableStream(reactElement); gc(); const chunks: any[] = []; for await (let chunk of stream) { chunks.push(chunk); } const text = new TextDecoder().decode(await new Response(chunks as any).arrayBuffer()); gc(); expect(text.replaceAll("", "")).toBe(inputString); gc(); }); }, ); } } for (let renderToReadableStream of [renderToReadableStreamBun, renderToReadableStreamBrowser]) { // there is an event loop bug that causes deadlocks // the bug is with `fetch`, not with the HTTP server for (let [inputString, reactElement] of fixtures) { describe.skipIf(typeof renderToReadableStream !== "function")( `${renderToReadableStream.name}(${inputString})`, () => { it("http server, 1 request", async () => { await (async () => { var server; try { server = serve({ port: 0, async fetch(req) { return new Response(await renderToReadableStream(reactElement), { headers: { "X-React": "1", }, }); }, }); const response = await fetch("http://localhost:" + server.port + "/"); const result = await response.text(); expect(result.replaceAll("", "")).toBe(inputString); expect(response.headers.get("X-React")).toBe("1"); } finally { server?.stop(true); } })(); await expectMaxObjectTypeCount(expect, "ReadableHTTPResponseSinkController", 2); }); const count = 4; it(`http server, ${count} requests`, async () => { var remain = count; await (async () => { let server!: Server; try { server = serve({ port: 0, async fetch(req) { return new Response(await renderToReadableStream(reactElement)); }, }); while (remain--) { var attempt = remain + 1; const response = await fetch("http://localhost:" + server.port + "/"); const result = await response.text(); try { expect(result.replaceAll("", "")).toBe(inputString); } catch (e: any) { e.message += "\nAttempt: " + attempt; throw e; } } } finally { server?.stop(true); } })(); expect(remain).toBe(-1); await expectMaxObjectTypeCount(expect, "ReadableHTTPResponseSinkController", 3); }); }, ); } } });