Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
67b2ffc
revert this: making some variations to the streaming tests to check t…
AbanoubGhadban Oct 18, 2025
4545805
Revert "revert this: making some variations to the streaming tests to…
AbanoubGhadban Oct 18, 2025
31bca68
initial installation of playwright
AbanoubGhadban Oct 19, 2025
a3727f8
add redis receiver component and add a e2e test for it
AbanoubGhadban Oct 19, 2025
e8bc6f1
move playwright tests from reat_on_rails_pro to spec/dummy directory
AbanoubGhadban Oct 19, 2025
5a070b6
set proper names and description for the test file
AbanoubGhadban Oct 19, 2025
94aa944
add streaming tests
AbanoubGhadban Oct 20, 2025
3668ed4
add Toggle button to the snapshots
AbanoubGhadban Oct 20, 2025
68d10fc
testing behavior when there is an async client component
AbanoubGhadban Oct 20, 2025
5ec1071
add tests for rsc payload fetching
AbanoubGhadban Oct 20, 2025
1840336
add dummy-app-node-renderer-e2-tests to the ci workflow
AbanoubGhadban Oct 20, 2025
c5851aa
remove replaced integration specs at rails
AbanoubGhadban Oct 20, 2025
6064b02
fix node renderer build
AbanoubGhadban Oct 20, 2025
5d7cfa2
fix CI failure and other tiny improvements
AbanoubGhadban Oct 20, 2025
3954ee2
tiny fixes
AbanoubGhadban Oct 20, 2025
8d4b4c4
improve redis tear down
AbanoubGhadban Oct 20, 2025
182e040
tiny fixes
AbanoubGhadban Oct 20, 2025
916f7c8
linting
AbanoubGhadban Oct 20, 2025
b72d198
increase redis timeout
AbanoubGhadban Oct 21, 2025
57bcf7d
Empty commit
AbanoubGhadban Oct 21, 2025
bf0652a
fix config issue at eslint ts configs
AbanoubGhadban Oct 21, 2025
6454b9e
store capybara screenshots and html for failed tests
AbanoubGhadban Oct 21, 2025
7176d49
adjust eslint config to work with e2e-tests
AbanoubGhadban Oct 21, 2025
f5de26b
update path to tsconfig at the typescript check command
AbanoubGhadban Oct 21, 2025
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
56 changes: 56 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,63 @@ jobs:
path: ~/rspec
- store_artifacts:
path: react_on_rails_pro/spec/dummy/tmp/screenshots
- store_artifacts:
path: react_on_rails_pro/spec/dummy/tmp/capybara
- store_artifacts:
path: react_on_rails_pro/spec/dummy/log/test.log
- store_artifacts:
path: react_on_rails_pro/spec/dummy/yarn-error.log

# TODO: DRY with previous job
dummy-app-node-renderer-e2-tests:
docker:
- image: *docker_image
- image: cimg/redis:6.2.6
steps:
- checkout
- run: *print-system-info
- restore_cache: *restore-package-gem-cache
- restore_cache: *restore-package-node-modules-cache
- restore_cache: *restore-dummy-app-node-modules-cache
- restore_cache: *restore-dummy-app-gem-cache
- run: rm -rf react_on_rails_pro/spec/dummy/public/webpack
- run: rm -rf react_on_rails_pro/spec/dummy/ssr-generated
- restore_cache: *restore-dummy-app-webpack-bundle-cache
- run: *install-dummy-app-ruby-gems
- run: *install-package-node-modules
- run: *install-latest-chrome
- run: *install-dummy-app-node-modules
- run:
name: Generate file-system based entrypoints (Pro)
working_directory: react_on_rails_pro
command: cd spec/dummy && bundle exec rake react_on_rails:generate_packs
- run:
name: Run Pro Node renderer in a background
working_directory: react_on_rails_pro
command: cd spec/dummy && yarn run node-renderer
background: true
- run:
name: run rails server in background (Pro dummy app)
working_directory: react_on_rails_pro
command: cd spec/dummy && RAILS_ENV=test rails server
background: true
- run:
name: wait for rails server to start
command: |
while ! curl -s http://localhost:3000 > /dev/null; do sleep 1; done
- run:
name: install playwright dependencies
working_directory: react_on_rails_pro/spec/dummy
command: yarn playwright install --with-deps
- run:
name: Run playwright tests (Pro dummy app)
working_directory: react_on_rails_pro/spec/dummy
command: yarn e2e-test
- store_test_results:
path: react_on_rails_pro/spec/dummy/test-results/results.xml
- store_artifacts:
path: react_on_rails_pro/spec/dummy/playwright-report

workflows:
version: 2
build-and-test:
Expand Down Expand Up @@ -374,3 +426,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
2 changes: 2 additions & 0 deletions packages/react-on-rails-pro/src/RSCRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

/// <reference types="react/experimental" />

'use client';

import * as React from 'react';
import { useRSC } from './RSCProvider.tsx';
import { ServerComponentFetchError } from './ServerComponentFetchError.ts';
Expand Down
18 changes: 18 additions & 0 deletions react_on_rails_pro/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,24 @@ export default defineConfig([
],
},
},
{
files: ['spec/dummy/e2e-tests/*'],

rules: {
'no-empty-pattern': [
'error',
{
allowObjectPatternsAsParameters: true,
},
],
},
},
{
files: ['spec/dummy/e2e-tests/*'],
rules: {
'react-hooks/rules-of-hooks': ['off'],
},
},
Comment on lines +199 to +216
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Combine duplicate configuration blocks.

Both blocks target the same file pattern. Merge them into a single configuration to reduce duplication and improve maintainability.

Apply this diff to consolidate the blocks:

  {
    files: ['spec/dummy/e2e-tests/*'],

    rules: {
      'no-empty-pattern': [
        'error',
        {
          allowObjectPatternsAsParameters: true,
        },
      ],
-    },
-  },
-  {
-    files: ['spec/dummy/e2e-tests/*'],
-    rules: {
      'react-hooks/rules-of-hooks': ['off'],
    },
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
files: ['spec/dummy/e2e-tests/*'],
rules: {
'no-empty-pattern': [
'error',
{
allowObjectPatternsAsParameters: true,
},
],
},
},
{
files: ['spec/dummy/e2e-tests/*'],
rules: {
'react-hooks/rules-of-hooks': ['off'],
},
},
{
files: ['spec/dummy/e2e-tests/*'],
rules: {
'no-empty-pattern': [
'error',
{
allowObjectPatternsAsParameters: true,
},
],
'react-hooks/rules-of-hooks': ['off'],
},
},
🤖 Prompt for AI Agents
In react_on_rails_pro/eslint.config.mjs around lines 199 to 216, there are two
separate override objects targeting the same files pattern
('spec/dummy/e2e-tests/*'); merge them into a single override object that
contains both rules ('no-empty-pattern' with allowObjectPatternsAsParameters:
true, and 'react-hooks/rules-of-hooks' set to 'off') to remove duplication and
keep configuration consolidated.

// must be the last config in the array
// https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs
prettierRecommended,
Expand Down
2 changes: 1 addition & 1 deletion react_on_rails_pro/package-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ scripts:
script: nps lint && nps format.listDifferent && nps test && nps check-typescript
check-typescript:
description: Check for TypeScript errors
script: nps "build --noEmit" && tsc --project packages/node-renderer/tests && cd spec/dummy && yarn run tsc -p client/tsconfig.json --noEmit
script: nps "build --noEmit" && tsc --project packages/node-renderer/tests && cd spec/dummy && yarn run tsc -p ./tsconfig.json --noEmit
fix:
description: Run all code fixes before committing
script: nps eslint.fix && nps format
Expand Down
7 changes: 7 additions & 0 deletions react_on_rails_pro/spec/dummy/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# React on Rails Pro license file
config/react_on_rails_pro_license.key

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- paragraph: Waiting for the key "Item3"
- paragraph: Waiting for the key "Item4"
- paragraph: Waiting for the key "Item5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- paragraph: Waiting for the key "Item3"
- paragraph: Waiting for the key "Item4"
- paragraph: Waiting for the key "Item5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- paragraph: Waiting for the key "Item3"
- listitem: "Value of \"Item4\": Incremental Value4"
- paragraph: Waiting for the key "Item5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- paragraph: Waiting for the key "Item3"
- listitem: "Value of \"Item4\": Incremental Value4"
- paragraph: Waiting for the key "Item5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- listitem: "Value of \"Item3\": Incremental Value3"
- listitem: "Value of \"Item4\": Incremental Value4"
- paragraph: Waiting for the key "Item5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- 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"
- listitem: "Value of \"Item3\": Incremental Value3"
- listitem: "Value of \"Item4\": Incremental Value4"
- listitem: "Value of \"Item5\": Incremental Value5"
36 changes: 36 additions & 0 deletions react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,42 @@ def rsc_posts_page_over_redis
raise "Redis thread timed out"
end

def redis_receiver
@request_id = SecureRandom.uuid

redis_thread = Thread.new do
redis = ::Redis.new
5.times do |index|
sleep 1
redis.xadd("stream:#{@request_id}", { ":Item#{index}" => "Value of Item#{index + 1}".to_json })
end
rescue StandardError => e
Rails.logger.error "Error writing Items to Redis: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
raise e
ensure
begin
redis&.close
rescue StandardError => close_err
Rails.logger.warn "Failed to close Redis: #{close_err.message}"
end
end

stream_view_containing_react_components(template: "/pages/redis_receiver")

return if redis_thread.join(10)

Rails.logger.error "Redis thread timed out"
raise "Redis thread timed out"
end

def redis_receiver_for_testing
@request_id = params[:request_id]
raise "request_id is required at the url" if @request_id.blank?

stream_view_containing_react_components(template: "/pages/redis_receiver")
end

def async_on_server_sync_on_client
@render_on_server = true
stream_view_containing_react_components(template: "/pages/async_on_server_sync_on_client")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%= stream_react_component("RedisReceiver",
props: { requestId: @request_id, asyncToggleContainer: params[:async_toggle_container] },
prerender: true,
trace: true,
id: "RedisReceiver-react-component-0") %>
<hr/>

<h1>React Rails Server Streaming Redis Receiver</h1>
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<%= stream_react_component("ServerComponentRouter",
props: @app_props_server_render.merge(artificialDelay: params[:artificial_delay] || 0, postsCount: params[:posts_count] || 2),
props: @app_props_server_render.merge(
artificialDelay: params[:artificial_delay] || 0,
postsCount: params[:posts_count] || 2,
requestId: params[:request_id],
),
trace: true,
id: "ServerComponentRouter-react-component-0") %>
<hr/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ const ToggleContainer = ({ children, childrenTitle }) => {

return (
<div style={{ border: '1px solid black', margin: '10px', padding: '10px' }}>
<button onClick={() => setIsVisible(!isVisible)} style={{ border: '1px solid black' }} type="button">
<button
className="toggle-button"
onClick={() => setIsVisible(!isVisible)}
style={{ border: '1px solid black' }}
type="button"
>
{showOrHideText}
</button>
{isVisible && children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export default function App({ basePath = '/server_router', ...props }: { basePat
Server Component with visible streaming behavior
</Link>
</li>
<li>
<Link to={`${basePath}/redis-receiver-for-testing`}>Redis Receiver For Testing</Link>
</li>
<li>
<Link to={`${basePath}/server-component-with-retry`}>Server Component with Retry</Link>
</li>
Expand Down Expand Up @@ -71,6 +74,10 @@ export default function App({ basePath = '/server_router', ...props }: { basePat
/>
<Route path="client-component" element={<EchoProps {...props} />} />
</Route>
<Route
path={`${basePath}/redis-receiver-for-testing`}
element={<RSCRoute componentName="RedisReceiver" componentProps={props} />}
/>
<Route
path={`${basePath}/streaming-server-component`}
element={<RSCRoute componentName="AsyncComponentsTreeForTesting" componentProps={props} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { Suspense } from 'react';
import ToggleContainer from '../components/RSCPostsPage/ToggleContainerForServerComponents';
import { listenToRequestData } from '../utils/redisReceiver';
import { ErrorBoundary } from '../components/ErrorBoundary';

const RedisItem = async ({ getValue, itemIndex }) => {
const value = await getValue(`Item${itemIndex}`);
return (
<li className={`redis-item${itemIndex}`}>
Value of &quot;Item{itemIndex + 1}&quot;: {value}
</li>
);
};

const RedisItemWithWrapper = ({ getValue, itemIndex }) => (
<section className={`redis-item${itemIndex}-container`}>
<Suspense
fallback={
<p className={`redis-item${itemIndex}-fallback`}>
Waiting for the key &quot;Item{itemIndex + 1}&quot;
</p>
}
>
<RedisItem getValue={getValue} itemIndex={itemIndex} />
</Suspense>
</section>
);

// 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 <ToggleContainer childrenTitle={childrenTitle}>{children}</ToggleContainer>;
};

const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => {
const { getValue, close } = listenToRequestData(requestId);

if ('addPostSSRHook' in railsContext) {
railsContext.addPostSSRHook(close);
}

const UsedToggleContainer = asyncToggleContainer ? AsyncToggleContainer : ToggleContainer;

return () => (
<ErrorBoundary>
<main className="redis-receiver-container">
<h1 className="redis-receiver-header">A list of items received from Redis:</h1>
<Suspense fallback={<div>Loading ToggleContainer</div>}>
<UsedToggleContainer childrenTitle="Redis Items" {...(asyncToggleContainer ? { getValue } : {})}>
<ol className="redis-items-container">
{[0, 1, 2, 3, 4].map((index) => (
<RedisItemWithWrapper key={index} getValue={getValue} itemIndex={index} />
))}
</ol>
</UsedToggleContainer>
</Suspense>
</main>
</ErrorBoundary>
);
};

export default RedisReceiver;
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createClient, RedisClientType } from 'redis';

const REDIS_READ_TIMEOUT = 10000;

/**
* Redis xRead result message structure
*/
Expand Down Expand Up @@ -292,7 +294,7 @@ export function listenToRequestData(requestId: string): RequestListener {
);
// Keep the pending promise in the dictionary with the error state
}
}, 8000);
}, REDIS_READ_TIMEOUT);

// Store the promise and its controllers
if (resolvePromise && rejectPromise) {
Expand Down
2 changes: 2 additions & 0 deletions react_on_rails_pro/spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
get "apollo_graphql" => "pages#apollo_graphql", as: :apollo_graphql
get "lazy_apollo_graphql" => "pages#lazy_apollo_graphql", as: :lazy_apollo_graphql
get "console_logs_in_async_server" => "pages#console_logs_in_async_server", as: :console_logs_in_async_server
get "redis_receiver" => "pages#redis_receiver", as: :redis_receiver
get "redis_receiver_for_testing" => "pages#redis_receiver_for_testing", as: :redis_receiver_for_testing
get "stream_async_components" => "pages#stream_async_components", as: :stream_async_components
get "stream_async_components_for_testing" => "pages#stream_async_components_for_testing",
as: :stream_async_components_for_testing
Expand Down
Loading
Loading