}
-const RedisItemWithWrapper = ({ getValue, redisKey }) => (
- Waiting for the key "{redisKey}"}>
-
-
+const RedisItemWithWrapper = ({ getValue, itemIndex }) => (
+
+ Waiting for the key "Item{itemIndex + 1}"}>
+
+
+
)
-const RedisReceiver = ({ requestId }, railsContext) => {
+// Convert it to async component and make tests control when it's rendered
+// To test the page behavior when a client component is rendered asynchronously at the page
+const AsyncToggleContainer = async ({ children, childrenTitle, getValue }) => {
+ await getValue('ToggleContainer');
+ return
+}
+
+const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => {
const { getValue, close } = listenToRequestData(requestId);
if ('addPostSSRHook' in railsContext) {
railsContext.addPostSSRHook(close);
}
+ const UsedToggleContainer = asyncToggleContainer ? AsyncToggleContainer : ToggleContainer;
+
return (
-
A list of items received from Redis:
-
- {
- [1,2,3,4,5].map(index => )
- }
-
+
+
A list of items received from Redis:
+
+
+ {
+ [0,1,2,3,4].map(index => )
+ }
+
+
+
)
}
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts
new file mode 100644
index 000000000..647e9c5d6
--- /dev/null
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts
@@ -0,0 +1,12 @@
+import { test, expect } from '@playwright/test';
+import { redisControlledTest } from './fixture';
+
+redisControlledTest('test1', ({ redisRequestId, redisClient }) => {
+ console.log('Test1 request id', redisRequestId);
+ console.log(redisClient);
+});
+
+redisControlledTest('test2', ({ redisRequestId, redisClient }) => {
+ console.log('Test2 request id', redisRequestId);
+ console.log(redisClient);
+});
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
new file mode 100644
index 000000000..7adab7128
--- /dev/null
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
@@ -0,0 +1,92 @@
+import { randomUUID } from 'crypto';
+import { test as base, Response, expect } from '@playwright/test';
+import { createClient, RedisClientType } from 'redis';
+
+type RedisClientFixture = {
+ redisClient: RedisClientType;
+};
+
+type RedisRequestIdFixture = {
+ redisRequestId: string;
+ nonBlockingNavigateWithRequestId: (path: string) => Promise
+}
+
+type RedisReceiverPageFixture = {
+ pagePath: string;
+}
+
+export type RedisReceiverControllerFixture = {
+ sendRedisValue: (key: string, value: unknown) => Promise;
+ sendRedisItemValue: (itemIndex: Number, value: unknown) => Promise;
+ matchPageSnapshot: (snapshotPath: string) => Promise;
+}
+
+const redisControlledTest = base.extend({
+ redisClient: [async ({}, use, workerInfo) => {
+ console.log(`Creating Redis Client at Worker ${workerInfo.workerIndex}`)
+ const url = process.env.REDIS_URL || 'redis://localhost:6379';
+ const client = createClient({ url });
+ await client.connect();
+ await use(client as RedisClientType);
+ await client.close();
+ }, { scope: 'worker' }],
+
+ redisRequestId: async ({}, use) => {
+ await use(randomUUID());
+ },
+
+ nonBlockingNavigateWithRequestId: async ({ redisRequestId, page }, use) => {
+ await use((path) => page.goto(`${path}?request_id=${redisRequestId}`, { waitUntil: "commit" }))
+ },
+});
+
+const redisReceiverPageController = redisControlledTest.extend({
+ sendRedisValue: async({ redisClient, redisRequestId }, use) => {
+ await use(async(key, value) => {
+ await redisClient.xAdd(`stream:${redisRequestId}`, '*', { [`:${key}`]: JSON.stringify(value) });
+ })
+ },
+ sendRedisItemValue: async({ sendRedisValue }, use) => {
+ await use(async(itemIndex, value) => {
+ await sendRedisValue(`Item${itemIndex}`, value);
+ })
+ },
+ matchPageSnapshot: async({ page }, use) => {
+ await use(async(snapshotPath) => {
+ await expect(page.locator('.redis-receiver-container:visible')).toBeVisible();
+ await expect(page.locator('.redis-receiver-container:visible').first()).toMatchAriaSnapshot({ name: `${snapshotPath}.aria.yml` });
+ })
+ },
+})
+
+const redisReceiverPageTest = redisReceiverPageController.extend({
+ pagePath: [async({ nonBlockingNavigateWithRequestId }, use) => {
+ const pagePath = '/redis_receiver_for_testing';
+ await nonBlockingNavigateWithRequestId(pagePath);
+ await use(pagePath);
+ }, { auto: true }]
+})
+
+const redisReceiverInsideRouterPageTest = redisReceiverPageController.extend({
+ pagePath: [async({ nonBlockingNavigateWithRequestId }, use) => {
+ const pagePath = '/server_router/redis-receiver-for-testing';
+ await nonBlockingNavigateWithRequestId(pagePath);
+ await use(pagePath);
+ }, { auto: true }]
+})
+
+const redisReceiverPageAfterNavigationTest = redisReceiverPageController.extend({
+ pagePath: [async({ nonBlockingNavigateWithRequestId, page }, use) => {
+ await nonBlockingNavigateWithRequestId('/server_router/simple-server-component');
+ await expect(page.getByText("Post 1")).toBeVisible({ timeout: 3000 });
+ await page.getByText("Redis Receiver For Testing").click();
+ await use('/server_router/redis-receiver-for-testing');
+ }, { auto: true }]
+})
+
+export {
+ redisControlledTest,
+ redisReceiverPageTest,
+ redisReceiverInsideRouterPageTest,
+ redisReceiverPageAfterNavigationTest,
+ };
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
index 9e423a56d..9641af16c 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
@@ -1,51 +1,39 @@
-import { randomUUID } from 'crypto';
-import { test, expect, Page } from '@playwright/test';
-import { createClient } from 'redis';
-
-const createRedisClient = async () => {
- const url = process.env.REDIS_URL || 'redis://localhost:6379';
- const client = createClient({ url });
- await client.connect();
- return client;
-}
-
-const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
-
-const assertPageState = async(page: Page, sentValues: Number[]) => {
- const nonSentValues = [1,2,3,4,5].filter(v => !sentValues.includes(v));
-
- await Promise.all(sentValues.map(async (v) => {
- await expect(page.getByText(`Value of "Item${v}": Value${v}`)).toBeVisible();
- await expect(page.getByText(`Waiting for the key "Item${v}"`)).not.toBeVisible();
- }));
-
- await Promise.all(nonSentValues.map(async (v) => {
- await expect(page.getByText(`Value of "Item${v}": Value${v}`)).not.toBeVisible()
- await expect(page.getByText(`Waiting for the key "Item${v}"`)).toBeVisible()
- }));
-}
-
-test('incrementally render RedisReciever page', async ({ page }) => {
- const requestId = randomUUID();
- await page.goto(`http://localhost:3000/redis_receiver_for_testing?request_id=${requestId}`, { waitUntil: "commit" });
-
- const sentValues: Number[] = [];
- await assertPageState(page, sentValues);
-
- const redisClient = await createRedisClient();
- redisClient.xAdd(`stream:${requestId}`, '*', { ':Item1': JSON.stringify('Value1') });
- sentValues.push(1);
- await assertPageState(page, sentValues);
-
- redisClient.xAdd(`stream:${requestId}`, '*', { ':Item4': JSON.stringify('Value4') });
- sentValues.push(4);
- await assertPageState(page, sentValues);
-
- redisClient.xAdd(`stream:${requestId}`, '*', { ':Item2': JSON.stringify('Value2') });
- sentValues.push(2);
- await assertPageState(page, sentValues);
-
- redisClient.xAdd(`stream:${requestId}`, '*', { ':Item3': JSON.stringify('Value3') });
- sentValues.push(3);
- await assertPageState(page, sentValues);
-});
+import {
+ redisReceiverPageTest,
+ redisReceiverInsideRouterPageTest,
+ redisReceiverPageAfterNavigationTest,
+} from './fixture';
+
+// Can be used to delay the execution
+const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+// Snapshot testing the best testing strategy for our use case
+// Because we need to ensure that any transformation done on the HTML or RSC payload stream won't affect
+// - Order of fallback or components at the page
+// - Any update chunk won't affect previously rendered parts of the page
+// - Rendered component won't get back to its fallback component at any stage of the page
+// - Snapshot testing saves huge number of complex assertions
+([
+ ['RedisReceiver', redisReceiverPageTest],
+ ['RedisReceiver inside router page', redisReceiverInsideRouterPageTest],
+ ['RedisReceiver inside router after navigation', redisReceiverPageAfterNavigationTest],
+] as const).forEach(([pageName, test]) => {
+ test(`snapshot for page ${pageName}`, async ({ matchPageSnapshot, sendRedisItemValue }) => {
+ await matchPageSnapshot('stage0');
+
+ sendRedisItemValue(0, 'Incremental Value1');
+ await matchPageSnapshot('stage1');
+
+ sendRedisItemValue(3, 'Incremental Value4');
+ await matchPageSnapshot('stage2');
+
+ sendRedisItemValue(1, 'Incremental Value2');
+ await matchPageSnapshot('stage3');
+
+ sendRedisItemValue(2, 'Incremental Value3');
+ await matchPageSnapshot('stage4');
+
+ sendRedisItemValue(4, 'Incremental Value5');
+ await matchPageSnapshot('stage5');
+ })
+})
diff --git a/react_on_rails_pro/spec/dummy/playwright.config.ts b/react_on_rails_pro/spec/dummy/playwright.config.ts
index c97fae8bc..37a9239ba 100644
--- a/react_on_rails_pro/spec/dummy/playwright.config.ts
+++ b/react_on_rails_pro/spec/dummy/playwright.config.ts
@@ -12,6 +12,11 @@ import { defineConfig, devices } from '@playwright/test';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
+ expect: {
+ toMatchAriaSnapshot: {
+ pathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
+ },
+ },
testDir: './e2e-tests',
/* Run tests in files in parallel */
fullyParallel: true,
@@ -29,7 +34,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
- // baseURL: 'http://localhost:3000',
+ baseURL: 'http://localhost:3000/',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
From 3668ed4b35014d7beebb1807deff289869a3cb2a Mon Sep 17 00:00:00 2001
From: Abanoub Ghadban
Date: Mon, 20 Oct 2025 16:39:04 +0300
Subject: [PATCH 08/24] add Toggle button to the snapshots
---
.../spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml | 1 +
.../spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml | 1 +
.../spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml | 1 +
.../spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml | 1 +
.../spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml | 1 +
.../spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml | 1 +
6 files changed, 6 insertions(+)
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml
index 26c751575..6f3cf23cb 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- paragraph: Waiting for the key "Item1"
- paragraph: Waiting for the key "Item2"
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml
index 773d8465f..abca25ed2 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- listitem: "Value of \"Item1\": Incremental Value1"
- paragraph: Waiting for the key "Item2"
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml
index 92af35045..93e320d66 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- listitem: "Value of \"Item1\": Incremental Value1"
- paragraph: Waiting for the key "Item2"
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml
index 5b91a7ed5..f9925ea3b 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- listitem: "Value of \"Item1\": Incremental Value1"
- listitem: "Value of \"Item2\": Incremental Value2"
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml
index 98e11cf5c..521d0e661 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- listitem: "Value of \"Item1\": Incremental Value1"
- listitem: "Value of \"Item2\": Incremental Value2"
diff --git a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml
index e0257e200..5d1544f60 100644
--- a/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml
+++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml
@@ -1,5 +1,6 @@
- main:
- heading "A list of items received from Redis:" [level=1]
+ - button "Hide Redis Items"
- list:
- listitem: "Value of \"Item1\": Incremental Value1"
- listitem: "Value of \"Item2\": Incremental Value2"
From 68d10fcf2c3a35d3ab68878f35dd5218fd380766 Mon Sep 17 00:00:00 2001
From: Abanoub Ghadban
Date: Mon, 20 Oct 2025 18:30:51 +0300
Subject: [PATCH 09/24] testing behavior when there is an async client
component
---
.../app/views/pages/redis_receiver.html.erb | 2 +-
.../RSCPostsPage/ToggleContainer.jsx | 2 +-
.../RedisReceiver.jsx | 19 ++++++----
.../spec/dummy/e2e-tests/dummt-fixture.ts | 33 ++++++++++++++++
.../spec/dummy/e2e-tests/dummy.spec.ts | 34 +++++++++++------
.../spec/dummy/e2e-tests/fixture.ts | 34 ++++++++++++++++-
.../spec/dummy/e2e-tests/streaming.spec.ts | 38 +++++++++++++++++--
7 files changed, 134 insertions(+), 28 deletions(-)
create mode 100644 react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts
diff --git a/react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb b/react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb
index af9f75c0e..eb4dc4196 100644
--- a/react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb
+++ b/react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb
@@ -1,5 +1,5 @@
<%= stream_react_component("RedisReceiver",
- props: { requestId: @request_id },
+ props: { requestId: @request_id, asyncToggleContainer: params[:async_toggle_container] },
prerender: true,
trace: true,
id: "RedisReceiver-react-component-0") %>
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/RSCPostsPage/ToggleContainer.jsx b/react_on_rails_pro/spec/dummy/client/app/components/RSCPostsPage/ToggleContainer.jsx
index ce68fb919..7ff7d9798 100644
--- a/react_on_rails_pro/spec/dummy/client/app/components/RSCPostsPage/ToggleContainer.jsx
+++ b/react_on_rails_pro/spec/dummy/client/app/components/RSCPostsPage/ToggleContainer.jsx
@@ -10,7 +10,7 @@ const ToggleContainer = ({ children, childrenTitle }) => {
return (
-
}>
+
+
+ {
+ [0,1,2,3,4].map(index => )
+ }
+
+
+
)
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts b/react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts
new file mode 100644
index 000000000..16521bf3f
--- /dev/null
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts
@@ -0,0 +1,33 @@
+import { test as base } from '@playwright/test';
+
+type F = {
+ h: string;
+}
+
+type F2 = {
+ h2: string;
+}
+
+export const test1 = base.extend({
+ h: [async({}, use) => {
+ console.log('F1')
+ await use('F1');
+ console.log('F1 end');
+ }, { auto: true }]
+})
+
+export const testmid = base.extend({
+ h: [async({}, use) => {
+ console.log('Fm')
+ await use('Fm');
+ console.log('Fm end');
+ }, { auto: true }]
+})
+
+export const test2 = testmid.extend({
+ h2: [async({ h }, use) => {
+ console.log('F2')
+ await use(h + 'F2');
+ console.log('F2 end');
+ }, { auto: true }]
+})
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts
index 647e9c5d6..d50fabcf3 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts
@@ -1,12 +1,22 @@
-import { test, expect } from '@playwright/test';
-import { redisControlledTest } from './fixture';
-
-redisControlledTest('test1', ({ redisRequestId, redisClient }) => {
- console.log('Test1 request id', redisRequestId);
- console.log(redisClient);
-});
-
-redisControlledTest('test2', ({ redisRequestId, redisClient }) => {
- console.log('Test2 request id', redisRequestId);
- console.log(redisClient);
-});
+// import { test, expect } from '@playwright/test';
+// import { redisControlledTest } from './fixture';
+
+// redisControlledTest('test1', ({ redisRequestId, redisClient }) => {
+// console.log('Test1 request id', redisRequestId);
+// console.log(redisClient);
+// });
+
+// redisControlledTest('test2', ({ redisRequestId, redisClient }) => {
+// console.log('Test2 request id', redisRequestId);
+// console.log(redisClient);
+// });
+
+import { mergeTests } from '@playwright/test';
+import { test1, test2 } from './dummt-fixture';
+
+const test = mergeTests(test2);
+
+test('eee', ({ h2 }) => {
+ console.log('TEst', h2);
+})
+
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
index 7adab7128..e0f71f582 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
@@ -19,6 +19,7 @@ export type RedisReceiverControllerFixture = {
sendRedisValue: (key: string, value: unknown) => Promise;
sendRedisItemValue: (itemIndex: Number, value: unknown) => Promise;
matchPageSnapshot: (snapshotPath: string) => Promise;
+ waitForConsoleMessage: (msg: string) => Promise;
}
const redisControlledTest = base.extend({
@@ -36,7 +37,11 @@ const redisControlledTest = base.extend {
- await use((path) => page.goto(`${path}?request_id=${redisRequestId}`, { waitUntil: "commit" }))
+ await use((path) => {
+ const requestIdParam = `request_id=${redisRequestId}`;
+ const fullPath = path.includes('?') ? `${path}&${requestIdParam}` : `${path}?${requestIdParam}`;
+ return page.goto(fullPath, { waitUntil: "commit" })
+ })
},
});
@@ -57,6 +62,17 @@ const redisReceiverPageController = redisControlledTest.extend{
+ await use(async(msg) => {
+ if ((await page.consoleMessages()).find(consoleMsg => consoleMsg.text().includes(msg))) {
+ return;
+ }
+
+ await page.waitForEvent('console', {
+ predicate: (consoleMsg) => consoleMsg.text().includes(msg),
+ })
+ })
+ }
})
const redisReceiverPageTest = redisReceiverPageController.extend({
@@ -67,6 +83,20 @@ const redisReceiverPageTest = redisReceiverPageController.extend({
+ pagePath: [async({ page, nonBlockingNavigateWithRequestId, sendRedisValue }, use) => {
+ const pagePath = '/redis_receiver_for_testing?async_toggle_container=true';
+ await nonBlockingNavigateWithRequestId(pagePath);
+
+ await expect(page.getByText("Loading ToggleContainer")).toBeVisible();
+ await expect(page.locator('.toggle-button')).not.toBeVisible();
+
+ await sendRedisValue('ToggleContainer', 'anything');
+ await expect(page.locator('.toggle-button')).toBeVisible();
+ await use(pagePath);
+ }, { auto: true }]
+})
+
const redisReceiverInsideRouterPageTest = redisReceiverPageController.extend({
pagePath: [async({ nonBlockingNavigateWithRequestId }, use) => {
const pagePath = '/server_router/redis-receiver-for-testing';
@@ -85,8 +115,8 @@ const redisReceiverPageAfterNavigationTest = redisReceiverPageController.extend<
})
export {
- redisControlledTest,
redisReceiverPageTest,
redisReceiverInsideRouterPageTest,
redisReceiverPageAfterNavigationTest,
+ redisReceiverPageWithAsyncClientComponentTest,
};
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
index 9641af16c..87eaa105f 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
@@ -1,12 +1,11 @@
+import { expect } from '@playwright/test';
import {
redisReceiverPageTest,
redisReceiverInsideRouterPageTest,
redisReceiverPageAfterNavigationTest,
+ redisReceiverPageWithAsyncClientComponentTest,
} from './fixture';
-// Can be used to delay the execution
-const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
// Snapshot testing the best testing strategy for our use case
// Because we need to ensure that any transformation done on the HTML or RSC payload stream won't affect
// - Order of fallback or components at the page
@@ -17,8 +16,39 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
['RedisReceiver', redisReceiverPageTest],
['RedisReceiver inside router page', redisReceiverInsideRouterPageTest],
['RedisReceiver inside router after navigation', redisReceiverPageAfterNavigationTest],
+ ['RedisReceiver with Async Toggle Container Client Component', redisReceiverPageWithAsyncClientComponentTest],
] as const).forEach(([pageName, test]) => {
- test(`snapshot for page ${pageName}`, async ({ matchPageSnapshot, sendRedisItemValue }) => {
+ test(`incremental rendering of page: ${pageName}`, async ({ matchPageSnapshot, sendRedisItemValue }) => {
+ await matchPageSnapshot('stage0');
+
+ sendRedisItemValue(0, 'Incremental Value1');
+ await matchPageSnapshot('stage1');
+
+ sendRedisItemValue(3, 'Incremental Value4');
+ await matchPageSnapshot('stage2');
+
+ sendRedisItemValue(1, 'Incremental Value2');
+ await matchPageSnapshot('stage3');
+
+ sendRedisItemValue(2, 'Incremental Value3');
+ await matchPageSnapshot('stage4');
+
+ sendRedisItemValue(4, 'Incremental Value5');
+ await matchPageSnapshot('stage5');
+ });
+
+ test(`early hydration of page: ${pageName}`, async ({ page, waitForConsoleMessage, matchPageSnapshot, sendRedisItemValue }) => {
+ waitForConsoleMessage('ToggleContainer with title');
+
+ await page.click('.toggle-button');
+ await expect(page.getByText(/Waiting for the key "Item\d"/)).not.toBeVisible();
+
+ await page.click('.toggle-button');
+ const fallbackElements = page.getByText(/Waiting for the key "Item\d"/);
+ await expect(fallbackElements).toHaveCount(5);
+ for (const el of await fallbackElements.all()) {
+ await expect(el).toBeVisible();
+ }
await matchPageSnapshot('stage0');
sendRedisItemValue(0, 'Incremental Value1');
From 5ec1071c8076299166ef2e9722c4734e1885ba10 Mon Sep 17 00:00:00 2001
From: Abanoub Ghadban
Date: Mon, 20 Oct 2025 18:53:08 +0300
Subject: [PATCH 10/24] add tests for rsc payload fetching
---
.../spec/dummy/e2e-tests/fixture.ts | 11 +++++++++--
.../spec/dummy/e2e-tests/streaming.spec.ts | 19 ++++++++++++++++++-
2 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
index e0f71f582..f6fcc110b 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts
@@ -1,5 +1,5 @@
import { randomUUID } from 'crypto';
-import { test as base, Response, expect } from '@playwright/test';
+import { test as base, Response, expect, Request } from '@playwright/test';
import { createClient, RedisClientType } from 'redis';
type RedisClientFixture = {
@@ -20,6 +20,7 @@ export type RedisReceiverControllerFixture = {
sendRedisItemValue: (itemIndex: Number, value: unknown) => Promise;
matchPageSnapshot: (snapshotPath: string) => Promise;
waitForConsoleMessage: (msg: string) => Promise;
+ getNetworkRequests: (requestUrlPattern: RegExp) => Promise;
}
const redisControlledTest = base.extend({
@@ -72,6 +73,11 @@ const redisReceiverPageController = redisControlledTest.extend consoleMsg.text().includes(msg),
})
})
+ },
+ getNetworkRequests: async({ page }, use) => {
+ await use(async(requestUrlPattern) => {
+ return (await page.requests()).filter(request => request.url().match(requestUrlPattern))
+ })
}
})
@@ -114,7 +120,8 @@ const redisReceiverPageAfterNavigationTest = redisReceiverPageController.extend<
}, { auto: true }]
})
-export {
+export {
+ redisReceiverPageController,
redisReceiverPageTest,
redisReceiverInsideRouterPageTest,
redisReceiverPageAfterNavigationTest,
diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
index 87eaa105f..a7d146ae7 100644
--- a/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
+++ b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts
@@ -1,5 +1,6 @@
-import { expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
import {
+ redisReceiverPageController,
redisReceiverPageTest,
redisReceiverInsideRouterPageTest,
redisReceiverPageAfterNavigationTest,
@@ -67,3 +68,19 @@ import {
await matchPageSnapshot('stage5');
})
})
+
+redisReceiverInsideRouterPageTest('no RSC payload request is made when the page is server side rendered', async ({ getNetworkRequests }) => {
+ await expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(0);
+})
+
+redisReceiverPageAfterNavigationTest('RSC payload request is made on navigation', async ({ getNetworkRequests }) => {
+ await expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(1);
+})
+
+redisReceiverPageController('client side rendered router fetches RSC payload', async ({ page, getNetworkRequests }) => {
+ await page.goto('/server_router_client_render/simple-server-component');
+
+ await expect(page.getByText('Post 1')).toBeVisible();
+ await expect(page.getByText('Toggle')).toBeVisible();
+ await expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(1);
+})
From 1840336c91501de3cc3f8d8c7ab4a15cb073cf20 Mon Sep 17 00:00:00 2001
From: Abanoub Ghadban
Date: Mon, 20 Oct 2025 18:54:53 +0300
Subject: [PATCH 11/24] add dummy-app-node-renderer-e2-tests to the ci workflow
---
.circleci/config.yml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 94e739379..a491fe1d4 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -347,6 +347,7 @@ jobs:
dummy-app-node-renderer-e2-tests:
docker:
- image: *docker_image
+ - image: cimg/redis:6.2.6
steps:
- checkout
- run: *print-system-info
@@ -419,3 +420,7 @@ workflows:
requires:
- install-package-ruby-gems
- build-dummy-app-webpack-test-bundles
+ - dummy-app-node-renderer-e2-tests:
+ requires:
+ - install-package-ruby-gems
+ - build-dummy-app-webpack-test-bundles
From c5851aa3493773d89b4b6bf20272483ed821383a Mon Sep 17 00:00:00 2001
From: Abanoub Ghadban
Date: Mon, 20 Oct 2025 18:57:38 +0300
Subject: [PATCH 12/24] remove replaced integration specs at rails
---
.../dummy/spec/system/integration_spec.rb | 421 ------------------
1 file changed, 421 deletions(-)
diff --git a/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb b/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb
index 839c8b9a1..3df3178f7 100644
--- a/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb
+++ b/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb
@@ -413,79 +413,6 @@ def change_text_expect_dom_selector(dom_selector, expect_no_change: false)
expect(page).to have_text(/branch1 \(level \d+\)/, count: 5)
end
- shared_examples "shows loading fallback while rendering async components" do |skip_js_packs|
- it "shows the loading fallback while rendering async components" \
- "#{skip_js_packs ? ' when the page is not hydrated' : ''}" do
- path = "#{path}#{skip_js_packs ? '?skip_js_packs=true' : ''}"
- chunks_count = 0
- chunks_count_having_branch1_loading_fallback = 0
- chunks_count_having_branch2_loading_fallback = 0
- navigate_with_streaming(path) do |_content|
- chunks_count += 1
- chunks_count_having_branch1_loading_fallback += 1 if page.has_text?(/Loading branch1 at level \d+/)
- chunks_count_having_branch2_loading_fallback += 1 if page.has_text?(/Loading branch2 at level \d+/)
- end
-
- expect(chunks_count_having_branch1_loading_fallback).to be_between(3, 6)
- expect(chunks_count_having_branch2_loading_fallback).to be_between(1, 3)
- expect(page).not_to have_text(/Loading branch1 at level \d+/)
- expect(page).not_to have_text(/Loading branch2 at level \d+/)
- expect(chunks_count).to be_between(5, 7)
-
- # Check if the page is hydrated or not
- change_text_expect_dom_selector(selector, expect_no_change: skip_js_packs)
- end
- end
-
- it_behaves_like "shows loading fallback while rendering async components", false
- it_behaves_like "shows loading fallback while rendering async components", true
-
- it "replays console logs" do
- visit path
- logs = page.driver.browser.logs.get(:browser)
- info = logs.select { |log| log.level == "INFO" }
- info_messages = info.map(&:message)
- errors = logs.select { |log| log.level == "SEVERE" }
- errors_messages = errors.map(&:message)
-
- expect(info_messages).to include(/\[SERVER\] Sync console log from AsyncComponentsTreeForTesting/)
- 5.times do |i|
- expect(info_messages).to include(/\[SERVER\] branch1 \(level #{i}\)/)
- expect(errors_messages).to include(
- /"\[SERVER\] Error message" "{\\"branchName\\":\\"branch1\\",\\"level\\":#{i}}"/
- )
- end
- 2.times do |i|
- expect(info_messages).to include(/\[SERVER\] branch2 \(level #{i}\)/)
- expect(errors_messages).to include(
- /"\[SERVER\] Error message" "{\\"branchName\\":\\"branch2\\",\\"level\\":#{i}}"/
- )
- end
- end
-
- it "replays console logs with each chunk" do
- chunks_count = 0
- chunks_count_containing_server_logs = 0
- navigate_with_streaming(path) do |content|
- chunks_count += 1
- logs = page.driver.browser.logs.get(:browser)
- info = logs.select { |log| log.level == "INFO" }
- info_messages = info.map(&:message)
- errors = logs.select { |log| log.level == "SEVERE" }
- errors_messages = errors.map(&:message)
-
- next if content.empty? || chunks_count == 1
-
- if info_messages.any?(/\[SERVER\] branch1 \(level \d+\)/) && errors_messages.any?(
- /"\[SERVER\] Error message" "{\\"branchName\\":\\"branch1\\",\\"level\\":\d+}/
- )
- chunks_count_containing_server_logs += 1
- end
- end
- expect(chunks_count).to be >= 5
- expect(chunks_count_containing_server_logs).to be > 2
- end
-
it "doesn't hydrate status component if packs are not loaded" do
# visit waits for the page to load, so we ensure that the page is loaded before checking the hydration status
visit "#{path}?skip_js_packs=true"
@@ -493,35 +420,6 @@ def change_text_expect_dom_selector(dom_selector, expect_no_change: false)
expect(page).not_to have_text "HydrationStatus: Hydrated"
expect(page).not_to have_text "HydrationStatus: Page loaded"
end
-
- it "hydrates loaded components early before the full page is loaded" do
- chunks_count = 0
- status_component_hydrated_on_chunk = nil
- input_component_hydrated_on_chunk = nil
- navigate_with_streaming(path) do |_content|
- chunks_count += 1
-
- # The code that updates the states to Hydrated is executed on `useEffect` which is called only on hydration
- if status_component_hydrated_on_chunk.nil? && page.has_text?("HydrationStatus: Hydrated")
- status_component_hydrated_on_chunk = chunks_count
- end
-
- if input_component_hydrated_on_chunk.nil?
- begin
- # Checks that the input field is hydrated
- change_text_expect_dom_selector(selector)
- input_component_hydrated_on_chunk = chunks_count
- rescue RSpec::Expectations::ExpectationNotMetError, Capybara::ElementNotFound
- # Do nothing if the test fails - component not yet hydrated
- end
- end
- end
-
- # The component should be hydrated before the full page is loaded
- expect(status_component_hydrated_on_chunk).to be < chunks_count
- expect(input_component_hydrated_on_chunk).to be < chunks_count
- expect(page).to have_text "HydrationStatus: Page loaded"
- end
end
describe "Pages/stream_async_components_for_testing", :js do
@@ -533,322 +431,3 @@ def change_text_expect_dom_selector(dom_selector, expect_no_change: false)
it_behaves_like "streamed component tests", "/server_router/streaming-server-component",
"#ServerComponentRouter-react-component-0"
end
-
-def rsc_payload_fetch_requests
- fetch_requests_while_streaming.select { |request| request[:url].include?("/rsc_payload/") }
-end
-
-shared_examples "RSC payload only fetched if component is not server-side rendered" do |server_rendered_path,
- client_rendered_path|
- before do
- # Clear the browser logs. so any test reading the logs will only read the logs from the current page navigation
- page.driver.browser.logs.get(:browser)
- end
-
- it "doesn't fetch RSC payload if component is server-side rendered" do
- navigate_with_streaming server_rendered_path
-
- expect(rsc_payload_fetch_requests).to eq([])
- end
-
- it "fetches RSC payload if component is client-side rendered" do
- navigate_with_streaming client_rendered_path
-
- expect(rsc_payload_fetch_requests.size).to be > 0
- end
-end
-
-describe "Pages/server_router/streaming-server-component rsc payload fetching", :js do
- it_behaves_like "RSC payload only fetched if component is not server-side rendered", "/server_router/sixth",
- "/server_router_client_render/streaming-server-component"
-end
-
-describe "Pages/stream_async_components_for_testing rsc payload fetching", :js do
- it_behaves_like "RSC payload only fetched if component is not server-side rendered",
- "/stream_async_components_for_testing", "/stream_async_components_for_testing_client_render"
-end
-
-describe "Pages/server_router", :js do
- subject { page }
-
- it "navigates between pages" do
- navigate_with_streaming("/server_router/simple-server-component")
- expect_client_component_inside_server_component_hydrated(page)
- expect(page).not_to have_text("Server Component Title")
- expect(page).not_to have_text("Server Component Description")
- expect(rsc_payload_fetch_requests).to eq([])
-
- click_link "Another Simple Server Component"
- expect(rsc_payload_fetch_requests).to eq([
- { url: "/rsc_payload/MyServerComponent?props=%7B%7D" }
- ])
-
- expect(page).to have_text("Server Component Title")
- expect(page).to have_text("Server Component Description")
- expect(page).not_to have_text("Post 1")
- expect(page).not_to have_text("Content 1")
- end
-
- it "streams the navigation between pages" do
- navigate_with_streaming("/server_router/simple-server-component")
-
- click_link "Server Component with visible streaming behavior"
- expect(rsc_payload_fetch_requests.first[:url]).to include("/rsc_payload/AsyncComponentsTreeForTesting")
-
- expect(page).not_to have_text("Post 1")
- expect(page).not_to have_text("Content 1")
- expect(page).to have_text("Loading branch1 at level 3...", wait: 5)
-
- # Client component is hydrated before the full page is loaded
- expect(page).to have_text("HydrationStatus: Hydrated")
- change_text_expect_dom_selector("#ServerComponentRouter-react-component-0")
-
- expect(page).to have_text("Loading branch1 at level 1...", wait: 5)
- expect(page).to have_text("branch1 (level 1)")
- expect(page).not_to have_text("Loading branch1 at level 1...")
- expect(page).not_to have_text("Loading branch1 at level 3...")
- end
-end
-
-def async_on_server_sync_on_client_client_render_logs
- logs = page.driver.browser.logs.get(:browser)
- component_logs = logs.select { |log| log.message.include?(component_logs_tag) }
- client_component_logs = component_logs.reject { |log| log.message.include?("[SERVER]") }
- client_component_logs.map do |log|
- # Extract string between double quotes that contains component_logs_tag
- # The string can contain escaped double quotes (\").
- message = log.message.match(/"([^"]*(?:\\"[^"]*)*#{component_logs_tag}[^"]*(?:\\"[^"]*)*)"/)[1]
- JSON.parse("\"#{message}\"").gsub(component_logs_tag, "").strip
- end
-end
-
-def expect_client_component_inside_server_component_hydrated(page)
- expect(page).to have_text("Post 1")
- expect(page).to have_text("Content 1")
- expect(page).to have_button("Toggle")
-
- # Check that the client component is hydrated
- click_button "Toggle"
- expect(page).not_to have_text("Content 1")
-end
-
-# The following two tests ensure that server components can be rendered inside client components
-# and ensure that no race condition happens that make client side refetch the RSC payload
-# that is already embedded in the HTML
-# By ensuring that the client component is only hydrated after the server component is
-# rendered and its HTML is embedded in the page
-describe "Pages/async_on_server_sync_on_client_client_render", :js do
- subject(:async_component) { find_by_id("AsyncOnServerSyncOnClient-react-component-0") }
-
- let(:component_logs_tag) { "[AsyncOnServerSyncOnClient]" }
-
- before do
- # Clear the browser logs. so any test reading the logs will only read the logs from the current page navigation
- page.driver.browser.logs.get(:browser)
- end
-
- it "all components are rendered on client" do
- chunks_count = 0
- # Nothing is rendered on the server
- navigate_with_streaming("/async_on_server_sync_on_client_client_render") do |content|
- next unless content.include?("Understanding Server/Client Component Hydration Patterns")
-
- chunks_count += 1
- # This part is rendered from the rails view
- expect(content).to include("Understanding Server/Client Component Hydration Patterns")
- # remove the rails view content
- rails_view_index = content.index("Understanding Server/Client Component Hydration Patterns")
- content = content[0...rails_view_index]
-
- # This part is rendered from the server component on client
- expect(content).not_to include("Async Component 1 from Suspense Boundary1")
- expect(content).not_to include("Async Component 1 from Suspense Boundary2")
- expect(content).not_to include("Async Component 1 from Suspense Boundary3")
- end
- expect(chunks_count).to be <= 1
-
- # After client side rendering, the component should exist in the DOM
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary1")
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary2")
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary3")
-
- # Should render "Simple Component" server component
- expect(async_component).to have_text("Post 1")
- expect(async_component).to have_button("Toggle")
- end
-
- it "fetches RSC payload of the Simple Component to render it on client" do
- fetch_requests_while_streaming
-
- navigate_with_streaming "/async_on_server_sync_on_client_client_render"
- expect(async_component).to have_text("Post 1")
- expect(async_component).to have_button("Toggle")
- fetch_requests = fetch_requests_while_streaming
- expect(fetch_requests).to eq([
- { url: "/rsc_payload/SimpleComponent?props=%7B%7D" }
- ])
- end
-
- it "renders the client components on the client side in a sync manner" do
- navigate_with_streaming "/async_on_server_sync_on_client_client_render"
-
- component_logs = async_on_server_sync_on_client_client_render_logs
- # The last log happen if the test catched the re-render of the suspensed component on the client
- expect(component_logs.size).to be_between(13, 15)
-
- # To understand how these logs show that components are rendered in a sync manner,
- # check the component page in the dummy app `/async_on_server_sync_on_client_client_render`
- expect(component_logs[0...13]).to eq([
- "AsyncContent rendered",
- async_component_rendered_message(0, 0),
- async_component_rendered_message(0, 1),
- async_component_rendered_message(1, 0),
- async_component_rendered_message(2, 0),
- async_component_rendered_message(3, 0),
- async_loading_component_message(3),
- async_component_hydrated_message(0, 0),
- async_component_hydrated_message(0, 1),
- async_component_hydrated_message(1, 0),
- async_component_hydrated_message(2, 0),
- "AsyncContent has been mounted",
- async_component_rendered_message(3, 0)
- ])
- end
-
- it "hydrates the client component inside server component" do # rubocop:disable RSpec/NoExpectationExample
- navigate_with_streaming "/async_on_server_sync_on_client_client_render"
- expect_client_component_inside_server_component_hydrated(async_component)
- end
-end
-
-describe "Pages/async_on_server_sync_on_client", :js do
- subject(:async_component) { find_by_id("AsyncOnServerSyncOnClient-react-component-0") }
-
- let(:component_logs_tag) { "[AsyncOnServerSyncOnClient]" }
-
- before do
- # Clear the browser logs. so any test reading the logs will only read the logs from the current page navigation
- page.driver.browser.logs.get(:browser)
- end
-
- it "all components are rendered on server" do
- received_server_html = ""
- navigate_with_streaming("/async_on_server_sync_on_client") do |content|
- received_server_html += content
- end
- expect(received_server_html).to include("Async Component 1 from Suspense Boundary1")
- expect(received_server_html).to include("Async Component 1 from Suspense Boundary2")
- expect(received_server_html).to include("Async Component 1 from Suspense Boundary3")
- expect(received_server_html).to include("Post 1")
- expect(received_server_html).to include("Content 1")
- expect(received_server_html).to include("Toggle")
- expect(received_server_html).to include(
- "Understanding Server/Client Component Hydration Patterns"
- )
- end
-
- it "doesn't fetch the RSC payload of the server component in the page" do
- navigate_with_streaming "/async_on_server_sync_on_client"
- expect(fetch_requests_while_streaming).to eq([])
- end
-
- it "hydrates the client component inside server component" do # rubocop:disable RSpec/NoExpectationExample
- navigate_with_streaming "/async_on_server_sync_on_client"
- expect_client_component_inside_server_component_hydrated(page)
- end
-
- it "progressively renders the page content" do
- rendering_stages = []
- navigate_with_streaming "/async_on_server_sync_on_client" do |content|
- # The first stage when all components are still being rendered on the server
- if content.include?("Loading Suspense Boundary3")
- rendering_stages << 1
- expect(async_component).to have_text("Loading Suspense Boundary3")
- expect(async_component).to have_text("Loading Suspense Boundary2")
- expect(async_component).to have_text("Loading Suspense Boundary1")
-
- expect(async_component).not_to have_text("Post 1")
- expect(async_component).not_to have_text("Async Component 1 from Suspense Boundary1")
- expect(async_component).not_to have_text("Async Component 1 from Suspense Boundary2")
- expect(async_component).not_to have_text("Async Component 1 from Suspense Boundary3")
- # The second stage when the Suspense Boundary3 (with 1000ms delay) is rendered on the server
- elsif content.include?("Async Component 1 from Suspense Boundary3")
- rendering_stages << 2
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary3")
- expect(async_component).not_to have_text("Post 1")
- expect(async_component).not_to have_text("Async Component 1 from Suspense Boundary1")
- expect(async_component).not_to have_text("Async Component 1 from Suspense Boundary2")
- expect(async_component).not_to have_text("Loading Suspense Boundary3")
- # The third stage when the Suspense Boundary2 (with 3000ms delay) is rendered on the server
- elsif content.include?("Async Component 1 from Suspense Boundary2")
- rendering_stages << 3
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary3")
- expect(async_component).to have_text("Post 1")
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary1")
- expect(async_component).to have_text("Async Component 1 from Suspense Boundary2")
- expect(async_component).not_to have_text("Loading Suspense Boundary2")
-
- # Expect that client component is hydrated
- expect(async_component).to have_text("Content 1")
- expect(async_component).to have_button("Toggle")
-
- # Expect that the client component is hydrated
- click_button "Toggle"
- expect(page).not_to have_text("Content 1")
- end
- end
- expect(rendering_stages).to eq([1, 2, 3])
- end
-
- it "doesn't hydrate client components until they are rendered on the server" do
- rendering_stages = []
- component_logs = []
-
- navigate_with_streaming "/async_on_server_sync_on_client" do |content|
- component_logs += async_on_server_sync_on_client_client_render_logs
-
- # The first stage when all components are still being rendered on the server
- if content.include?("
Loading Suspense Boundary3
")
- rendering_stages << 1
- expect(component_logs).not_to include(async_component_rendered_message(0, 0))
- expect(component_logs).not_to include(async_component_rendered_message(1, 0))
- expect(component_logs).not_to include(async_component_rendered_message(2, 0))
- # The second stage when the Suspense Boundary3 (with 1000ms delay) is rendered on the server
- elsif content.include?("
Async Component 1 from Suspense Boundary3 (1000ms server side delay)
")
- rendering_stages << 2
- expect(component_logs).to include("AsyncContent rendered")
- expect(component_logs).to include("AsyncContent has been mounted")
- expect(component_logs).not_to include(async_component_rendered_message(1, 0))
- # The third stage when the Suspense Boundary2 (with 3000ms delay) is rendered on the server
- elsif content.include?("
Async Component 1 from Suspense Boundary2 (3000ms server side delay)