diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js deleted file mode 100644 index 0a3773fa6f3e4..0000000000000 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js +++ /dev/null @@ -1,1572 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -// Polyfills for test environment -global.ReadableStream = - require('web-streams-polyfill/ponyfill/es6').ReadableStream; -global.TextEncoder = require('util').TextEncoder; -global.TextDecoder = require('util').TextDecoder; - -// Don't wait before processing work on the server. -// TODO: we can replace this with FlightServer.act(). -global.setImmediate = cb => cb(); - -let act; -let use; -let clientExports; -let clientModuleError; -let turbopackMap; -let Stream; -let FlightReact; -let React; -let FlightReactDOM; -let ReactDOMClient; -let ReactServerDOMServer; -let ReactServerDOMClient; -let ReactDOMFizzServer; -let Suspense; -let ErrorBoundary; -let JSDOM; - -describe('ReactFlightDOM', () => { - beforeEach(() => { - // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server - // This can be thought of as essentially being the React Server Components scope with react-server - // condition - jest.resetModules(); - - JSDOM = require('jsdom').JSDOM; - - // Simulate the condition resolution - jest.mock('react-server-dom-turbopack/server', () => - require('react-server-dom-turbopack/server.node.unbundled'), - ); - jest.mock('react', () => require('react/react.shared-subset')); - - const TurbopackMock = require('./utils/TurbopackMock'); - clientExports = TurbopackMock.clientExports; - clientModuleError = TurbopackMock.clientModuleError; - turbopackMap = TurbopackMock.turbopackMap; - - ReactServerDOMServer = require('react-server-dom-turbopack/server'); - FlightReact = require('react'); - FlightReactDOM = require('react-dom'); - - // This reset is to load modules for the SSR/Browser scope. - jest.resetModules(); - __unmockReact(); - act = require('internal-test-utils').act; - Stream = require('stream'); - React = require('react'); - use = React.use; - Suspense = React.Suspense; - ReactDOMClient = require('react-dom/client'); - ReactDOMFizzServer = require('react-dom/server.node'); - ReactServerDOMClient = require('react-server-dom-turbopack/client'); - - ErrorBoundary = class extends React.Component { - state = {hasError: false, error: null}; - static getDerivedStateFromError(error) { - return { - hasError: true, - error, - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback(this.state.error); - } - return this.props.children; - } - }; - }); - - function getTestStream() { - const writable = new Stream.PassThrough(); - const readable = new ReadableStream({ - start(controller) { - writable.on('data', chunk => { - controller.enqueue(chunk); - }); - writable.on('end', () => { - controller.close(); - }); - }, - }); - return { - readable, - writable, - }; - } - - const theInfinitePromise = new Promise(() => {}); - function InfiniteSuspend() { - throw theInfinitePromise; - } - - function getMeaningfulChildren(element) { - const children = []; - let node = element.firstChild; - while (node) { - if (node.nodeType === 1) { - if ( - // some tags are ambiguous and might be hidden because they look like non-meaningful children - // so we have a global override where if this data attribute is included we also include the node - node.hasAttribute('data-meaningful') || - (node.tagName === 'SCRIPT' && - node.hasAttribute('src') && - node.hasAttribute('async')) || - (node.tagName !== 'SCRIPT' && - node.tagName !== 'TEMPLATE' && - node.tagName !== 'template' && - !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden')) - ) { - const props = {}; - const attributes = node.attributes; - for (let i = 0; i < attributes.length; i++) { - if ( - attributes[i].name === 'id' && - attributes[i].value.includes(':') - ) { - // We assume this is a React added ID that's a non-visual implementation detail. - continue; - } - props[attributes[i].name] = attributes[i].value; - } - props.children = getMeaningfulChildren(node); - children.push(React.createElement(node.tagName.toLowerCase(), props)); - } - } else if (node.nodeType === 3) { - children.push(node.data); - } - node = node.nextSibling; - } - return children.length === 0 - ? undefined - : children.length === 1 - ? children[0] - : children; - } - - it('should resolve HTML using Node streams', async () => { - function Text({children}) { - return {children}; - } - function HTML() { - return ( -
- hello - world -
- ); - } - - function App() { - const model = { - html: , - }; - return model; - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - const model = await response; - expect(model).toEqual({ - html: ( -
- hello - world -
- ), - }); - }); - - it('should resolve the root', async () => { - // Model - function Text({children}) { - return {children}; - } - function HTML() { - return ( -
- hello - world -
- ); - } - function RootModel() { - return { - html: , - }; - } - - // View - function Message({response}) { - return
{use(response).html}
; - } - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe( - '
helloworld
', - ); - }); - - it('should not get confused by $', async () => { - // Model - function RootModel() { - return {text: '$1'}; - } - - // View - function Message({response}) { - return

{use(response).text}

; - } - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

$1

'); - }); - - it('should not get confused by @', async () => { - // Model - function RootModel() { - return {text: '@div'}; - } - - // View - function Message({response}) { - return

{use(response).text}

; - } - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

@div

'); - }); - - it('should be able to esm compat test module references', async () => { - const ESMCompatModule = { - __esModule: true, - default: function ({greeting}) { - return greeting + ' World'; - }, - hi: 'Hello', - }; - - function Print({response}) { - return

{use(response)}

; - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - function interopWebpack(obj) { - // Basically what Webpack's ESM interop feature testing does. - if (typeof obj === 'object' && obj.__esModule) { - return obj; - } - return Object.assign({default: obj}, obj); - } - - const {default: Component, hi} = interopWebpack( - clientExports(ESMCompatModule), - ); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

Hello World

'); - }); - - it('should be able to render a named component export', async () => { - const Module = { - Component: function ({greeting}) { - return greeting + ' World'; - }, - }; - - function Print({response}) { - return

{use(response)}

; - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {Component} = clientExports(Module); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

Hello World

'); - }); - - it('should be able to render a module split named component export', async () => { - const Module = { - // This gets split into a separate module from the original one. - split: function ({greeting}) { - return greeting + ' World'; - }, - }; - - function Print({response}) { - return

{use(response)}

; - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {split: Component} = clientExports(Module); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

Hello World

'); - }); - - it('should unwrap async module references', async () => { - const AsyncModule = Promise.resolve(function AsyncModule({text}) { - return 'Async: ' + text; - }); - - const AsyncModule2 = Promise.resolve({ - exportName: 'Module', - }); - - function Print({response}) { - return

{use(response)}

; - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const AsyncModuleRef = await clientExports(AsyncModule); - const AsyncModuleRef2 = await clientExports(AsyncModule2); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

Async: Module

'); - }); - - it('should unwrap async module references using use', async () => { - const AsyncModule = Promise.resolve('Async Text'); - - function Print({response}) { - return use(response); - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const AsyncModuleRef = clientExports(AsyncModule); - - function ServerComponent() { - const text = FlightReact.use(AsyncModuleRef); - return

{text}

; - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

Async Text

'); - }); - - it('should be able to import a name called "then"', async () => { - const thenExports = { - then: function then() { - return 'and then'; - }, - }; - - function Print({response}) { - return

{use(response)}

; - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const ThenRef = clientExports(thenExports).then; - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

and then

'); - }); - - it('throws when accessing a member below the client exports', () => { - const ClientModule = clientExports({ - Component: {deep: 'thing'}, - }); - function dotting() { - return ClientModule.Component.deep; - } - expect(dotting).toThrowError( - 'Cannot access Component.deep on the server. ' + - 'You cannot dot into a client module from a server component. ' + - 'You can only pass the imported name through.', - ); - }); - - it('does not throw when React inspects any deep props', () => { - const ClientModule = clientExports({ - Component: function () {}, - }); - ; - }); - - it('throws when accessing a Context.Provider below the client exports', () => { - const Context = React.createContext(); - const ClientModule = clientExports({ - Context, - }); - function dotting() { - return ClientModule.Context.Provider; - } - expect(dotting).toThrowError( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, - ); - }); - - it('should progressively reveal server components', async () => { - let reportedErrors = []; - - // Client Components - - function MyErrorBoundary({children}) { - return ( - ( -

- {__DEV__ ? e.message + ' + ' : null} - {e.digest} -

- )}> - {children} -
- ); - } - - // Model - function Text({children}) { - return children; - } - - function makeDelayedText() { - let _resolve, _reject; - let promise = new Promise((resolve, reject) => { - _resolve = () => { - promise = null; - resolve(); - }; - _reject = e => { - promise = null; - reject(e); - }; - }); - async function DelayedText({children}) { - await promise; - return {children}; - } - return [DelayedText, _resolve, _reject]; - } - - const [Friends, resolveFriends] = makeDelayedText(); - const [Name, resolveName] = makeDelayedText(); - const [Posts, resolvePosts] = makeDelayedText(); - const [Photos, resolvePhotos] = makeDelayedText(); - const [Games, , rejectGames] = makeDelayedText(); - - // View - function ProfileDetails({avatar}) { - return ( -
- :name: - {avatar} -
- ); - } - function ProfileSidebar({friends}) { - return ( -
- :photos: - {friends} -
- ); - } - function ProfilePosts({posts}) { - return
{posts}
; - } - function ProfileGames({games}) { - return
{games}
; - } - - const MyErrorBoundaryClient = clientExports(MyErrorBoundary); - - function ProfileContent() { - return ( - <> - :avatar:} /> - (loading sidebar)

}> - :friends:} /> -
- (loading posts)

}> - :posts:} /> -
- - (loading games)

}> - :games:} /> -
-
- - ); - } - - const model = { - rootContent: , - }; - - function ProfilePage({response}) { - return use(response).rootContent; - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - model, - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; - }, - }, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - (loading)

}> - -
, - ); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - // This isn't enough to show anything. - await act(() => { - resolveFriends(); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - // We can now show the details. Sidebar and posts are still loading. - await act(() => { - resolveName(); - }); - // Advance time enough to trigger a nested fallback. - await act(() => { - jest.advanceTimersByTime(500); - }); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '

(loading sidebar)

' + - '

(loading posts)

' + - '

(loading games)

', - ); - - expect(reportedErrors).toEqual([]); - - const theError = new Error('Game over'); - // Let's *fail* loading games. - await act(async () => { - await rejectGames(theError); - await 'the inner async function'; - }); - const expectedGamesValue = __DEV__ - ? '

Game over + a dev digest

' - : '

digest("Game over")

'; - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '

(loading sidebar)

' + - '

(loading posts)

' + - expectedGamesValue, - ); - - expect(reportedErrors).toEqual([theError]); - reportedErrors = []; - - // We can now show the sidebar. - await act(async () => { - await resolvePhotos(); - await 'the inner async function'; - }); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '
:photos::friends:
' + - '

(loading posts)

' + - expectedGamesValue, - ); - - // Show everything. - await act(async () => { - await resolvePosts(); - await 'the inner async function'; - }); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '
:photos::friends:
' + - '
:posts:
' + - expectedGamesValue, - ); - - expect(reportedErrors).toEqual([]); - }); - - it('should preserve state of client components on refetch', async () => { - // Client - - function Page({response}) { - return use(response); - } - - function Input() { - return ; - } - - const InputClient = clientExports(Input); - - // Server - - function App({color}) { - // Verify both DOM and Client children. - return ( -
- - -
- ); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - const stream1 = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(stream1.writable); - const response1 = ReactServerDOMClient.createFromReadableStream( - stream1.readable, - ); - await act(() => { - root.render( - (loading)

}> - -
, - ); - }); - expect(container.children.length).toBe(1); - expect(container.children[0].tagName).toBe('DIV'); - expect(container.children[0].style.color).toBe('red'); - - // Change the DOM state for both inputs. - const inputA = container.children[0].children[0]; - expect(inputA.tagName).toBe('INPUT'); - inputA.value = 'hello'; - const inputB = container.children[0].children[1]; - expect(inputB.tagName).toBe('INPUT'); - inputB.value = 'goodbye'; - - const stream2 = getTestStream(); - const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe2(stream2.writable); - const response2 = ReactServerDOMClient.createFromReadableStream( - stream2.readable, - ); - await act(() => { - root.render( - (loading)

}> - -
, - ); - }); - expect(container.children.length).toBe(1); - expect(container.children[0].tagName).toBe('DIV'); - expect(container.children[0].style.color).toBe('blue'); - - // Verify we didn't destroy the DOM for either input. - expect(inputA === container.children[0].children[0]).toBe(true); - expect(inputA.tagName).toBe('INPUT'); - expect(inputA.value).toBe('hello'); - expect(inputB === container.children[0].children[1]).toBe(true); - expect(inputB.tagName).toBe('INPUT'); - expect(inputB.value).toBe('goodbye'); - }); - - it('should be able to complete after aborting and throw the reason client-side', async () => { - const reportedErrors = []; - - const {writable, readable} = getTestStream(); - const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream( -
- -
, - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - const message = typeof x === 'string' ? x : x.message; - return __DEV__ ? 'a dev digest' : `digest("${message}")`; - }, - }, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App({res}) { - return use(res); - } - - await act(() => { - root.render( - ( -

- {__DEV__ ? e.message + ' + ' : null} - {e.digest} -

- )}> - (loading)

}> - -
-
, - ); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - await act(() => { - abort('for reasons'); - }); - if (__DEV__) { - expect(container.innerHTML).toBe( - '

Error: for reasons + a dev digest

', - ); - } else { - expect(container.innerHTML).toBe('

digest("for reasons")

'); - } - - expect(reportedErrors).toEqual(['for reasons']); - }); - - it('should be able to recover from a direct reference erroring client-side', async () => { - const reportedErrors = []; - - const ClientComponent = clientExports(function ({prop}) { - return 'This should never render'; - }); - - const ClientReference = clientModuleError(new Error('module init error')); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( -
- -
, - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - }, - }, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App({res}) { - return use(res); - } - - await act(() => { - root.render( -

{e.message}

}> - (loading)

}> - -
-
, - ); - }); - expect(container.innerHTML).toBe('

module init error

'); - - expect(reportedErrors).toEqual([]); - }); - - it('should be able to recover from a direct reference erroring client-side async', async () => { - const reportedErrors = []; - - const ClientComponent = clientExports(function ({prop}) { - return 'This should never render'; - }); - - let rejectPromise; - const ClientReference = await clientExports( - new Promise((resolve, reject) => { - rejectPromise = reject; - }), - ); - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( -
- -
, - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - }, - }, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App({res}) { - return use(res); - } - - await act(() => { - root.render( -

{e.message}

}> - (loading)

}> - -
-
, - ); - }); - - expect(container.innerHTML).toBe('

(loading)

'); - - await act(() => { - rejectPromise(new Error('async module init error')); - }); - - expect(container.innerHTML).toBe('

async module init error

'); - - expect(reportedErrors).toEqual([]); - }); - - it('should be able to recover from a direct reference erroring server-side', async () => { - const reportedErrors = []; - - const ClientComponent = clientExports(function ({prop}) { - return 'This should never render'; - }); - - // We simulate a bug in the Webpack bundler which causes an error on the server. - for (const id in turbopackMap) { - Object.defineProperty(turbopackMap, id, { - get: () => { - throw new Error('bug in the bundler'); - }, - }); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( -
- -
, - turbopackMap, - { - onError(x) { - reportedErrors.push(x.message); - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; - }, - }, - ); - pipe(writable); - - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App({res}) { - return use(res); - } - - await act(() => { - root.render( - ( -

- {__DEV__ ? e.message + ' + ' : null} - {e.digest} -

- )}> - (loading)

}> - -
-
, - ); - }); - if (__DEV__) { - expect(container.innerHTML).toBe( - '

bug in the bundler + a dev digest

', - ); - } else { - expect(container.innerHTML).toBe('

digest("bug in the bundler")

'); - } - - expect(reportedErrors).toEqual(['bug in the bundler']); - }); - - it('should pass a Promise through props and be able use() it on the client', async () => { - async function getData() { - return 'async hello'; - } - - function Component({data}) { - const text = use(data); - return

{text}

; - } - - const ClientComponent = clientExports(Component); - - function ServerComponent() { - const data = getData(); // no await here - return ; - } - - function Print({response}) { - return use(response); - } - - function App({response}) { - return ( - Loading...}> - - - ); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('

async hello

'); - }); - - it('should throw on the client if a passed promise eventually rejects', async () => { - const reportedErrors = []; - const theError = new Error('Server throw'); - - async function getData() { - throw theError; - } - - function Component({data}) { - const text = use(data); - return

{text}

; - } - - const ClientComponent = clientExports(Component); - - function ServerComponent() { - const data = getData(); // no await here - return ; - } - - function Await({response}) { - return use(response); - } - - function App({response}) { - return ( - Loading...}> - ( -

- {__DEV__ ? e.message + ' + ' : null} - {e.digest} -

- )}> - -
-
- ); - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; - }, - }, - ); - pipe(writable); - const response = ReactServerDOMClient.createFromReadableStream(readable); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe( - __DEV__ - ? '

Server throw + a dev digest

' - : '

digest("Server throw")

', - ); - expect(reportedErrors).toEqual([theError]); - }); - - it('should support float methods when rendering in Fiber', async () => { - function Component() { - return

hello world

; - } - - const ClientComponent = clientExports(Component); - - async function ServerComponent() { - FlightReactDOM.prefetchDNS('d before'); - FlightReactDOM.preconnect('c before'); - FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); - FlightReactDOM.preload('l before', {as: 'style'}); - FlightReactDOM.preloadModule('lm before'); - FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); - FlightReactDOM.preinit('i before', {as: 'script'}); - FlightReactDOM.preinitModule('m before'); - FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); - await 1; - FlightReactDOM.prefetchDNS('d after'); - FlightReactDOM.preconnect('c after'); - FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); - FlightReactDOM.preload('l after', {as: 'style'}); - FlightReactDOM.preloadModule('lm after'); - FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); - FlightReactDOM.preinit('i after', {as: 'script'}); - FlightReactDOM.preinitModule('m after'); - FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); - return ; - } - - const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - pipe(writable); - - let response = null; - function getResponse() { - if (response === null) { - response = ReactServerDOMClient.createFromReadableStream(readable); - } - return response; - } - - function App() { - return getResponse(); - } - - // We pause to allow the float call after the await point to process before the - // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing - // because the module graphs are not different and the HostDispatcher is shared. - // In a real environment the Fiber and Flight code would each have their own independent - // dispatcher. - // @TODO consider what happens when Server-Components-On-The-Client exist. we probably - // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime - // but we need to make sure that actually makes sense - await 1; - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - - expect(getMeaningfulChildren(document)).toEqual( - - - - - - - - - Client Component', - ); - }); - - it('should encode long string in a compact format', async () => { - const testString = '"\n\t'.repeat(500) + '🙃'; - - const stream = ReactServerDOMServer.renderToPipeableStream({ - text: testString, - }); - - const readable = new Stream.PassThrough(); - - const stringResult = readResult(readable); - const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, { - moduleMap: turbopackMap, - moduleLoading: turbopackModuleLoading, - }); - - stream.pipe(readable); - - const serializedContent = await stringResult; - // The content should be compact an unescaped - expect(serializedContent.length).toBeLessThan(2000); - expect(serializedContent).not.toContain('\\n'); - expect(serializedContent).not.toContain('\\t'); - expect(serializedContent).not.toContain('\\"'); - expect(serializedContent).toContain('\t'); - - const result = await parsedResult; - // Should still match the result when parsed - expect(result.text).toBe(testString); - }); - - // @gate enableBinaryFlight - it('should be able to serialize any kind of typed array', async () => { - const buffer = new Uint8Array([ - 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, - ]).buffer; - const buffers = [ - buffer, - new Int8Array(buffer, 1), - new Uint8Array(buffer, 2), - new Uint8ClampedArray(buffer, 2), - new Int16Array(buffer, 2), - new Uint16Array(buffer, 2), - new Int32Array(buffer, 4), - new Uint32Array(buffer, 4), - new Float32Array(buffer, 4), - new Float64Array(buffer, 0), - new BigInt64Array(buffer, 0), - new BigUint64Array(buffer, 0), - new DataView(buffer, 3), - ]; - const stream = ReactServerDOMServer.renderToPipeableStream(buffers); - const readable = new Stream.PassThrough(); - const promise = ReactServerDOMClient.createFromNodeStream(readable, { - moduleMap: turbopackMap, - moduleLoading: turbopackModuleLoading, - }); - stream.pipe(readable); - const result = await promise; - expect(result).toEqual(buffers); - }); - - it('should allow accept a nonce option for Flight preinitialized scripts', async () => { - function ClientComponent() { - return Client Component; - } - // The Client build may not have the same IDs as the Server bundles for the same - // component. - const ClientComponentOnTheClient = clientExports( - ClientComponent, - 'path/to/chunk.js', - ); - const ClientComponentOnTheServer = clientExports(ClientComponent); - - // In the SSR bundle this module won't exist. We simulate this by deleting it. - const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id; - delete turbopackModules[clientId]; - - // Instead, we have to provide a translation from the client meta data to the SSR - // meta data. - const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id]; - const translationMap = { - [clientId]: { - '*': ssrMetadata, - }, - }; - const ssrManifest = { - moduleMap: translationMap, - moduleLoading: turbopackModuleLoading, - }; - - function App() { - return ; - } - - const stream = ReactServerDOMServer.renderToPipeableStream( - , - turbopackMap, - ); - const readable = new Stream.PassThrough(); - let response; - - stream.pipe(readable); - - function ClientRoot() { - if (response) return use(response); - response = ReactServerDOMClient.createFromNodeStream( - readable, - ssrManifest, - { - nonce: 'r4nd0m', - }, - ); - return use(response); - } - - const ssrStream = await ReactDOMServer.renderToPipeableStream( - , - ); - const result = await readResult(ssrStream); - expect(result).toEqual( - 'Client Component', - ); - }); -}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js deleted file mode 100644 index d8475a8762f23..0000000000000 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -// Polyfills for test environment -global.ReadableStream = - require('web-streams-polyfill/ponyfill/es6').ReadableStream; -global.TextEncoder = require('util').TextEncoder; -global.TextDecoder = require('util').TextDecoder; - -// let serverExports; -let turbopackServerMap; -let ReactServerDOMServer; -let ReactServerDOMClient; - -describe('ReactFlightDOMReply', () => { - beforeEach(() => { - jest.resetModules(); - // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); - jest.mock('react-server-dom-turbopack/server', () => - require('react-server-dom-turbopack/server.browser'), - ); - const TurbopackMock = require('./utils/TurbopackMock'); - // serverExports = TurbopackMock.serverExports; - turbopackServerMap = TurbopackMock.turbopackServerMap; - ReactServerDOMServer = require('react-server-dom-turbopack/server.browser'); - jest.resetModules(); - ReactServerDOMClient = require('react-server-dom-turbopack/client'); - }); - - // This method should exist on File but is not implemented in JSDOM - async function arrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function () { - return resolve(reader.result); - }; - reader.onerror = function () { - return reject(reader.error); - }; - reader.readAsArrayBuffer(file); - }); - } - - it('can pass undefined as a reply', async () => { - const body = await ReactServerDOMClient.encodeReply(undefined); - const missing = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - expect(missing).toBe(undefined); - - const body2 = await ReactServerDOMClient.encodeReply({ - array: [undefined, null, undefined], - prop: undefined, - }); - const object = await ReactServerDOMServer.decodeReply( - body2, - turbopackServerMap, - ); - expect(object.array.length).toBe(3); - expect(object.array[0]).toBe(undefined); - expect(object.array[1]).toBe(null); - expect(object.array[3]).toBe(undefined); - expect(object.prop).toBe(undefined); - // These should really be true but our deserialization doesn't currently deal with it. - expect('3' in object.array).toBe(false); - expect('prop' in object).toBe(false); - }); - - it('can pass an iterable as a reply', async () => { - const body = await ReactServerDOMClient.encodeReply({ - [Symbol.iterator]: function* () { - yield 'A'; - yield 'B'; - yield 'C'; - }, - }); - const iterable = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - const items = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const item of iterable) { - items.push(item); - } - expect(items).toEqual(['A', 'B', 'C']); - }); - - it('can pass weird numbers as a reply', async () => { - const nums = [0, -0, Infinity, -Infinity, NaN]; - const body = await ReactServerDOMClient.encodeReply(nums); - const nums2 = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - - expect(nums).toEqual(nums2); - expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true); - }); - - it('can pass a BigInt as a reply', async () => { - const body = await ReactServerDOMClient.encodeReply(90071992547409910000n); - const n = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); - - expect(n).toEqual(90071992547409910000n); - }); - - it('can pass FormData as a reply', async () => { - const formData = new FormData(); - formData.set('hello', 'world'); - formData.append('list', '1'); - formData.append('list', '2'); - formData.append('list', '3'); - const typedArray = new Uint8Array([0, 1, 2, 3]); - const blob = new Blob([typedArray]); - formData.append('blob', blob, 'filename.blob'); - - const body = await ReactServerDOMClient.encodeReply(formData); - const formData2 = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - - expect(formData2).not.toBe(formData); - expect(Array.from(formData2).length).toBe(5); - expect(formData2.get('hello')).toBe('world'); - expect(formData2.getAll('list')).toEqual(['1', '2', '3']); - const blob2 = formData.get('blob'); - expect(blob2.size).toBe(4); - expect(blob2.name).toBe('filename.blob'); - expect(blob2.type).toBe(''); - const typedArray2 = new Uint8Array(await arrayBuffer(blob2)); - expect(typedArray2).toEqual(typedArray); - }); - - it('can pass multiple Files in FormData', async () => { - const typedArrayA = new Uint8Array([0, 1, 2, 3]); - const typedArrayB = new Uint8Array([4, 5]); - const blobA = new Blob([typedArrayA]); - const blobB = new Blob([typedArrayB]); - const formData = new FormData(); - formData.append('filelist', 'string'); - formData.append('filelist', blobA); - formData.append('filelist', blobB); - - const body = await ReactServerDOMClient.encodeReply(formData); - const formData2 = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - - const filelist2 = formData2.getAll('filelist'); - expect(filelist2.length).toBe(3); - expect(filelist2[0]).toBe('string'); - const blobA2 = filelist2[1]; - expect(blobA2.size).toBe(4); - expect(blobA2.name).toBe('blob'); - expect(blobA2.type).toBe(''); - const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2)); - expect(typedArrayA2).toEqual(typedArrayA); - const blobB2 = filelist2[2]; - expect(blobB2.size).toBe(2); - expect(blobB2.name).toBe('blob'); - expect(blobB2.type).toBe(''); - const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2)); - expect(typedArrayB2).toEqual(typedArrayB); - }); - - it('can pass two independent FormData with same keys', async () => { - const formDataA = new FormData(); - formDataA.set('greeting', 'hello'); - const formDataB = new FormData(); - formDataB.set('greeting', 'hi'); - - const body = await ReactServerDOMClient.encodeReply({ - a: formDataA, - b: formDataB, - }); - const {a: formDataA2, b: formDataB2} = - await ReactServerDOMServer.decodeReply(body, turbopackServerMap); - - expect(Array.from(formDataA2).length).toBe(1); - expect(Array.from(formDataB2).length).toBe(1); - expect(formDataA2.get('greeting')).toBe('hello'); - expect(formDataB2.get('greeting')).toBe('hi'); - }); - - it('can pass a Date as a reply', async () => { - const d = new Date(1234567890123); - const body = await ReactServerDOMClient.encodeReply(d); - const d2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); - - expect(d).toEqual(d2); - expect(d % 1000).toEqual(123); // double-check the milliseconds made it through - }); - - it('can pass a Map as a reply', async () => { - const objKey = {obj: 'key'}; - const m = new Map([ - ['hi', {greet: 'world'}], - [objKey, 123], - ]); - const body = await ReactServerDOMClient.encodeReply(m); - const m2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); - - expect(m2 instanceof Map).toBe(true); - expect(m2.size).toBe(2); - expect(m2.get('hi').greet).toBe('world'); - expect(m2).toEqual(m); - }); - - it('can pass a Set as a reply', async () => { - const objKey = {obj: 'key'}; - const s = new Set(['hi', objKey]); - - const body = await ReactServerDOMClient.encodeReply(s); - const s2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); - - expect(s2 instanceof Set).toBe(true); - expect(s2.size).toBe(2); - expect(s2.has('hi')).toBe(true); - expect(s2).toEqual(s); - }); -}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js new file mode 100644 index 0000000000000..81cd49b6cf19a --- /dev/null +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -0,0 +1,208 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setImmediate = cb => cb(); + +let act; +let use; +let clientExports; +let turbopackMap; +let Stream; +let React; +let ReactDOMClient; +let ReactServerDOMServer; +let ReactServerDOMClient; +let Suspense; + +describe('ReactFlightDOM', () => { + beforeEach(() => { + // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server + // This can be thought of as essentially being the React Server Components scope with react-server + // condition + jest.resetModules(); + + // Simulate the condition resolution + jest.mock('react-server-dom-turbopack/server', () => + require('react-server-dom-turbopack/server.node.unbundled'), + ); + jest.mock('react', () => require('react/react.shared-subset')); + + const TurbopackMock = require('./utils/TurbopackMock'); + clientExports = TurbopackMock.clientExports; + turbopackMap = TurbopackMock.turbopackMap; + + ReactServerDOMServer = require('react-server-dom-turbopack/server'); + + // This reset is to load modules for the SSR/Browser scope. + jest.resetModules(); + __unmockReact(); + act = require('internal-test-utils').act; + Stream = require('stream'); + React = require('react'); + use = React.use; + Suspense = React.Suspense; + ReactDOMClient = require('react-dom/client'); + ReactServerDOMClient = require('react-server-dom-turbopack/client'); + }); + + function getTestStream() { + const writable = new Stream.PassThrough(); + const readable = new ReadableStream({ + start(controller) { + writable.on('data', chunk => { + controller.enqueue(chunk); + }); + writable.on('end', () => { + controller.close(); + }); + }, + }); + return { + readable, + writable, + }; + } + + it('should resolve HTML using Node streams', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + function App() { + const model = { + html: , + }; + return model; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), + }); + }); + + it('should resolve the root', async () => { + // Model + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + function RootModel() { + return { + html: , + }; + } + + // View + function Message({response}) { + return
{use(response).html}
; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + '
helloworld
', + ); + }); + + it('should unwrap async module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = await clientExports(AsyncModule); + const AsyncModuleRef2 = await clientExports(AsyncModule2); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async: Module

'); + }); +}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js similarity index 100% rename from packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js rename to packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js similarity index 52% rename from packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js rename to packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js index 3538b4f7a60d9..d0d14915bcda8 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js @@ -54,37 +54,6 @@ describe('ReactFlightDOMEdge', () => { use = React.use; }); - function passThrough(stream) { - // Simulate more realistic network by splitting up and rejoining some chunks. - // This lets us test that we don't accidentally rely on particular bounds of the chunks. - return new ReadableStream({ - async start(controller) { - const reader = stream.getReader(); - let prevChunk = new Uint8Array(0); - function push() { - reader.read().then(({done, value}) => { - if (done) { - controller.enqueue(prevChunk); - controller.close(); - return; - } - const chunk = new Uint8Array(prevChunk.length + value.length); - chunk.set(prevChunk, 0); - chunk.set(value, prevChunk.length); - if (chunk.length > 50) { - controller.enqueue(chunk.subarray(0, chunk.length - 50)); - prevChunk = chunk.subarray(chunk.length - 50); - } else { - prevChunk = chunk; - } - push(); - }); - } - push(); - }, - }); - } - async function readResult(stream) { const reader = stream.getReader(); let result = ''; @@ -144,68 +113,4 @@ describe('ReactFlightDOMEdge', () => { const result = await readResult(ssrStream); expect(result).toEqual('Client Component'); }); - - it('should encode long string in a compact format', async () => { - const testString = '"\n\t'.repeat(500) + '🙃'; - const testString2 = 'hello'.repeat(400); - - const stream = ReactServerDOMServer.renderToReadableStream({ - text: testString, - text2: testString2, - }); - const [stream1, stream2] = passThrough(stream).tee(); - - const serializedContent = await readResult(stream1); - // The content should be compact an unescaped - expect(serializedContent.length).toBeLessThan(4000); - expect(serializedContent).not.toContain('\\n'); - expect(serializedContent).not.toContain('\\t'); - expect(serializedContent).not.toContain('\\"'); - expect(serializedContent).toContain('\t'); - - const result = await ReactServerDOMClient.createFromReadableStream( - stream2, - { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }, - ); - // Should still match the result when parsed - expect(result.text).toBe(testString); - expect(result.text2).toBe(testString2); - }); - - // @gate enableBinaryFlight - it('should be able to serialize any kind of typed array', async () => { - const buffer = new Uint8Array([ - 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, - ]).buffer; - const buffers = [ - buffer, - new Int8Array(buffer, 1), - new Uint8Array(buffer, 2), - new Uint8ClampedArray(buffer, 2), - new Int16Array(buffer, 2), - new Uint16Array(buffer, 2), - new Int32Array(buffer, 4), - new Uint32Array(buffer, 4), - new Float32Array(buffer, 4), - new Float64Array(buffer, 0), - new BigInt64Array(buffer, 0), - new BigUint64Array(buffer, 0), - new DataView(buffer, 3), - ]; - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(buffers), - ); - const result = await ReactServerDOMClient.createFromReadableStream(stream, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); - expect(result).toEqual(buffers); - }); }); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js similarity index 58% rename from packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js rename to packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js index 176003eb4c822..ad7e0d0feed76 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js @@ -145,113 +145,4 @@ describe('ReactFlightDOMForm', () => { expect(result).toBe('hello'); expect(foo).toBe('bar'); }); - - // @gate enableFormActions - it('can submit an imported server action without hydrating it', async () => { - let foo = null; - - const ServerModule = serverExports(function action(formData) { - foo = formData.get('foo'); - return 'hi'; - }); - const serverAction = ReactServerDOMClient.createServerReference( - ServerModule.$$id, - ); - function App() { - return ( -
- -
- ); - } - - const ssrStream = await ReactDOMServer.renderToReadableStream(); - await readIntoContainer(ssrStream); - - const form = container.firstChild; - - expect(foo).toBe(null); - - const result = await submit(form); - - expect(result).toBe('hi'); - - expect(foo).toBe('bar'); - }); - - // @gate enableFormActions - it('can submit a complex closure server action without hydrating it', async () => { - let foo = null; - - const serverAction = serverExports(function action(bound, formData) { - foo = formData.get('foo') + bound.complex; - return 'hello'; - }); - function App() { - return ( -
- -
- ); - } - const rscStream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(rscStream, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); - const ssrStream = await ReactDOMServer.renderToReadableStream(response); - await readIntoContainer(ssrStream); - - const form = container.firstChild; - - expect(foo).toBe(null); - - const result = await submit(form); - - expect(result).toBe('hello'); - expect(foo).toBe('barobject'); - }); - - // @gate enableFormActions - it('can submit a multiple complex closure server action without hydrating it', async () => { - let foo = null; - - const serverAction = serverExports(function action(bound, formData) { - foo = formData.get('foo') + bound.complex; - return 'hello' + bound.complex; - }); - function App() { - return ( -
- -