Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add failing end-to-end test for an action that returns client components #30550

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions fixtures/flight/__tests__/__e2e__/action.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {test, expect} from '@playwright/test';

test('action returning client component with deduped references', async ({
page,
}) => {
const errors = [];

page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});

page.on('pageerror', error => {
errors.push(error.stack);
});

await page.goto('/');

const button = await page.getByRole('button', {
name: 'Return element from action',
});

await button.click();

await expect(page.getByTestId('form')).toContainText('Hello');

// Click the button one more time to send the previous result (i.e. the
// returned element) back to the server.
await button.click();

await expect(errors).toEqual([]);

await expect(page.getByTestId('form')).toContainText('HelloHello');
});
23 changes: 16 additions & 7 deletions fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const {readFile} = require('fs').promises;

const React = require('react');

async function renderApp(res, returnValue, formState) {
async function renderApp(res, returnValue, formState, temporaryReferences) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
Expand Down Expand Up @@ -101,7 +101,9 @@ async function renderApp(res, returnValue, formState) {
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap);
const {pipe} = renderToPipeableStream(payload, moduleMap, {
temporaryReferences,
});
pipe(res);
}

Expand All @@ -110,8 +112,13 @@ app.get('/', async function (req, res) {
});

app.post('/', bodyParser.text(), async function (req, res) {
const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} =
await import('react-server-dom-webpack/server');
const {
decodeReply,
decodeReplyFromBusboy,
decodeAction,
decodeFormState,
createTemporaryReferenceSet,
} = await import('react-server-dom-webpack/server');
const serverReference = req.get('rsc-action');
if (serverReference) {
// This is the client-side case
Expand All @@ -124,15 +131,17 @@ app.post('/', bodyParser.text(), async function (req, res) {
throw new Error('Invalid action');
}

const temporaryReferences = createTemporaryReferenceSet();

let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
const reply = decodeReplyFromBusboy(bb, {}, {temporaryReferences});
req.pipe(bb);
args = await reply;
} else {
args = await decodeReply(req.body);
args = await decodeReply(req.body, {}, {temporaryReferences});
}
const result = action.apply(null, args);
try {
Expand All @@ -142,7 +151,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result, null);
renderApp(res, result, null, temporaryReferences);
} else {
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
Expand Down
4 changes: 3 additions & 1 deletion fixtures/flight/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import Button from './Button.js';
import Form from './Form.js';
import {Dynamic} from './Dynamic.js';
import {Client} from './Client.js';
import {TemporaryReferences} from './TemporaryReferences.js';

import {Note} from './cjs/Note.js';

import {like, greet, increment} from './actions.js';
import {like, greet, increment, returnElement} from './actions.js';

import {getServerState} from './ServerState.js';

Expand Down Expand Up @@ -61,6 +62,7 @@ export default async function App() {
</div>
<Client />
<Note />
<TemporaryReferences action={returnElement} />
</Container>
</body>
</html>
Expand Down
8 changes: 8 additions & 0 deletions fixtures/flight/src/Deduped.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import * as React from 'react';

export default function Deduped({children, thing}) {
console.log({thing});
return <div>{children}</div>;
}
14 changes: 14 additions & 0 deletions fixtures/flight/src/TemporaryReferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import * as React from 'react';

export function TemporaryReferences({action}) {
const [result, formAction] = React.useActionState(action, null);

return (
<form action={formAction} data-testid="form">
<button>Return element from action</button>
{result}
</form>
);
}
13 changes: 13 additions & 0 deletions fixtures/flight/src/actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use server';

import * as React from 'react';
import {setServerState} from './ServerState.js';
import Deduped from './Deduped.js';

export async function like() {
setServerState('Liked!');
Expand All @@ -22,3 +24,14 @@ export async function greet(formData) {
export async function increment(n) {
return n + 1;
}

export async function returnElement(prevElement) {
const text = <div>Hello</div>;

return (
<Deduped thing={text}>
{prevElement}
{text}
</Deduped>
);
}
16 changes: 13 additions & 3 deletions fixtures/flight/src/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import * as React from 'react';
import {use, Suspense, useState, startTransition} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
import {
createFromFetch,
createTemporaryReferenceSet,
encodeReply,
} from 'react-server-dom-webpack/client';

// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
import './style.css';

let updateRoot;
const temporaryReferences = createTemporaryReferenceSet();

async function callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
body: await encodeReply(args, {temporaryReferences}),
});
const {returnValue, root} = await createFromFetch(response, {
callServer,
temporaryReferences,
});
const {returnValue, root} = await createFromFetch(response, {callServer});
// Refresh the tree with the new RSC payload.
startTransition(() => {
updateRoot(root);
Expand All @@ -39,6 +48,7 @@ async function hydrateApp() {
}),
{
callServer,
temporaryReferences,
findSourceMapURL(fileName) {
return (
document.location.origin +
Expand Down
Loading