From 67b2ffc87f01c6a10fae3d6ff9b26f04084d2a99 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sat, 18 Oct 2025 15:46:12 +0300 Subject: [PATCH 01/24] revert this: making some variations to the streaming tests to check the source of the failure --- .../AsyncComponentsTreeForTesting.jsx | 2 +- .../config/initializers/react_on_rails_pro.rb | 2 +- .../dummy/spec/support/selenium_logger.rb | 14 + .../dummy/spec/system/integration_spec.rb | 14 + .../spec/dummy/spec/system/test.html | 336 ++++++++++++++++++ 5 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 react_on_rails_pro/spec/dummy/spec/system/test.html diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx index 16456de8c..dc6e85287 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx @@ -5,7 +5,7 @@ import HydrationStatus from '../components/HydrationStatus'; const AsyncComponentsBranch = ({ branchName, level }) => { const buildResult = () => { console.log(`${branchName} (level ${level})`); - console.error('Error message', { branchName, level }); + // console.error('Error message', { branchName, level }); if (level === 0) { return
{`${branchName} (level 0)`}
; } diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb index 1bd2a2472..d2ed179fd 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb @@ -28,7 +28,7 @@ # If true, then cache the evaluation of JS for prerendering using the standard Rails cache. # Applies to all rendering engines. # Default for `prerender_caching` is false. - config.prerender_caching = true + config.prerender_caching = false # Retry request in case of time out on the node-renderer side # 0 - no retry diff --git a/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb b/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb index 31c292e42..3bbc7cade 100644 --- a/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb +++ b/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb @@ -29,4 +29,18 @@ raise("JavaScript Error(s) on the page:\n\n#{errors.join("\n")}") if cleaned_errors.present? end + + config.after(:each, type: :system, js: true) do + errors = page.driver.browser.logs.get(:browser) + if errors.present? + aggregate_failures 'javascript errrors' do + errors.each do |error| + expect(error.level).not_to eq('SEVERE'), error.message + next unless error.level == 'WARNING' + STDERR.puts 'WARN: javascript warning' + STDERR.puts error.message + end + end + end + end end 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..68ed1f1e8 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 @@ -571,6 +571,11 @@ def rsc_payload_fetch_requests describe "Pages/server_router", :js do subject { page } + it "renders /server_router/simple-server-component properly" do + navigate_with_streaming("/server_router/simple-server-component") + expect_client_component_inside_server_component_hydrated(page) + end + it "navigates between pages" do navigate_with_streaming("/server_router/simple-server-component") expect_client_component_inside_server_component_hydrated(page) @@ -608,6 +613,15 @@ def rsc_payload_fetch_requests expect(page).not_to have_text("Loading branch1 at level 1...") expect(page).not_to have_text("Loading branch1 at level 3...") end + + it "renders /server_router/streaming-server-component with no errors" do + visit("/server_router/simple-server-component") + click_link "Server Component with visible streaming behavior" + + sleep 4 + expect(page).to have_text("branch2 (level 0)") + expect(page).to have_text(/HydrationStatus\: (Page loaded|Hydrated)/) + end end def async_on_server_sync_on_client_client_render_logs diff --git a/react_on_rails_pro/spec/dummy/spec/system/test.html b/react_on_rails_pro/spec/dummy/spec/system/test.html new file mode 100644 index 000000000..f52456d36 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/spec/system/test.html @@ -0,0 +1,336 @@ + + + + + + + + + + + Dummy + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+

+ React on Rails Pro +

+

+ By ShakaCode +

+
+
+

+ Specific to React on Rails Pro +

+ + +

+ Other examples +

+

+ matches react_on_rails/spec_dummy +

+ +
+ +
+
+
+
+ + +
+ + + + + + +
+
+

Header for AsyncComponentsTreeForTesting

+
+
+

Hello, Mr. Server Side Rendering!

+

Say hello to:

+
+
HydrationStatus: Hydrated
+
+

branch1 (level 4)

+
+

branch1 (level 3)

+
+

branch1 (level 2)

+
+

branch1 (level 1)

+
branch1 (level 0)
+
+
+
+
+
+

branch2 (level 1)

+
branch2 (level 0)
+
+
+

Footer for AsyncComponentsTreeForTesting

+
+
+
+ + + + +
+ +

React Rails Server Streaming Server Rendered RSC Components

+ +
+ + +
+
+
+ + + + + + + + + + + + \ No newline at end of file From 45458054b614c38d1d4e4be92b07043f4aee8664 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sat, 18 Oct 2025 15:46:18 +0300 Subject: [PATCH 02/24] Revert "revert this: making some variations to the streaming tests to check the source of the failure" This reverts commit 67b2ffc87f01c6a10fae3d6ff9b26f04084d2a99. --- .../AsyncComponentsTreeForTesting.jsx | 2 +- .../config/initializers/react_on_rails_pro.rb | 2 +- .../dummy/spec/support/selenium_logger.rb | 14 - .../dummy/spec/system/integration_spec.rb | 14 - .../spec/dummy/spec/system/test.html | 336 ------------------ 5 files changed, 2 insertions(+), 366 deletions(-) delete mode 100644 react_on_rails_pro/spec/dummy/spec/system/test.html diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx index dc6e85287..16456de8c 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncComponentsTreeForTesting.jsx @@ -5,7 +5,7 @@ import HydrationStatus from '../components/HydrationStatus'; const AsyncComponentsBranch = ({ branchName, level }) => { const buildResult = () => { console.log(`${branchName} (level ${level})`); - // console.error('Error message', { branchName, level }); + console.error('Error message', { branchName, level }); if (level === 0) { return
{`${branchName} (level 0)`}
; } diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb index d2ed179fd..1bd2a2472 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb @@ -28,7 +28,7 @@ # If true, then cache the evaluation of JS for prerendering using the standard Rails cache. # Applies to all rendering engines. # Default for `prerender_caching` is false. - config.prerender_caching = false + config.prerender_caching = true # Retry request in case of time out on the node-renderer side # 0 - no retry diff --git a/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb b/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb index 3bbc7cade..31c292e42 100644 --- a/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb +++ b/react_on_rails_pro/spec/dummy/spec/support/selenium_logger.rb @@ -29,18 +29,4 @@ raise("JavaScript Error(s) on the page:\n\n#{errors.join("\n")}") if cleaned_errors.present? end - - config.after(:each, type: :system, js: true) do - errors = page.driver.browser.logs.get(:browser) - if errors.present? - aggregate_failures 'javascript errrors' do - errors.each do |error| - expect(error.level).not_to eq('SEVERE'), error.message - next unless error.level == 'WARNING' - STDERR.puts 'WARN: javascript warning' - STDERR.puts error.message - end - end - end - end end 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 68ed1f1e8..839c8b9a1 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 @@ -571,11 +571,6 @@ def rsc_payload_fetch_requests describe "Pages/server_router", :js do subject { page } - it "renders /server_router/simple-server-component properly" do - navigate_with_streaming("/server_router/simple-server-component") - expect_client_component_inside_server_component_hydrated(page) - end - it "navigates between pages" do navigate_with_streaming("/server_router/simple-server-component") expect_client_component_inside_server_component_hydrated(page) @@ -613,15 +608,6 @@ def rsc_payload_fetch_requests expect(page).not_to have_text("Loading branch1 at level 1...") expect(page).not_to have_text("Loading branch1 at level 3...") end - - it "renders /server_router/streaming-server-component with no errors" do - visit("/server_router/simple-server-component") - click_link "Server Component with visible streaming behavior" - - sleep 4 - expect(page).to have_text("branch2 (level 0)") - expect(page).to have_text(/HydrationStatus\: (Page loaded|Hydrated)/) - end end def async_on_server_sync_on_client_client_render_logs diff --git a/react_on_rails_pro/spec/dummy/spec/system/test.html b/react_on_rails_pro/spec/dummy/spec/system/test.html deleted file mode 100644 index f52456d36..000000000 --- a/react_on_rails_pro/spec/dummy/spec/system/test.html +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - - - - - - Dummy - - - - - - - - - - - - - - - - - - - -
-
-
- -
-

- React on Rails Pro -

-

- By ShakaCode -

-
-
-

- Specific to React on Rails Pro -

- - -

- Other examples -

-

- matches react_on_rails/spec_dummy -

- -
- -
-
-
-
- - -
- - - - - - -
-
-

Header for AsyncComponentsTreeForTesting

-
-
-

Hello, Mr. Server Side Rendering!

-

Say hello to:

-
-
HydrationStatus: Hydrated
-
-

branch1 (level 4)

-
-

branch1 (level 3)

-
-

branch1 (level 2)

-
-

branch1 (level 1)

-
branch1 (level 0)
-
-
-
-
-
-

branch2 (level 1)

-
branch2 (level 0)
-
-
-

Footer for AsyncComponentsTreeForTesting

-
-
-
- - - - -
- -

React Rails Server Streaming Server Rendered RSC Components

- -
- - -
-
-
- - - - - - - - - - - - \ No newline at end of file From 31bca6869f991676eae44afd0433fc57570413c1 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 19 Oct 2025 13:45:44 +0300 Subject: [PATCH 03/24] initial installation of playwright --- react_on_rails_pro/.gitignore | 7 ++ react_on_rails_pro/e2e-tests/example.spec.ts | 18 +++++ react_on_rails_pro/package.json | 2 + react_on_rails_pro/playwright.config.ts | 79 ++++++++++++++++++++ react_on_rails_pro/yarn.lock | 38 ++++++++++ 5 files changed, 144 insertions(+) create mode 100644 react_on_rails_pro/e2e-tests/example.spec.ts create mode 100644 react_on_rails_pro/playwright.config.ts diff --git a/react_on_rails_pro/.gitignore b/react_on_rails_pro/.gitignore index 0134fd426..c7c07b7f9 100644 --- a/react_on_rails_pro/.gitignore +++ b/react_on_rails_pro/.gitignore @@ -75,3 +75,10 @@ yalc.lock # React on Rails Pro License Key config/react_on_rails_pro_license.key + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/react_on_rails_pro/e2e-tests/example.spec.ts b/react_on_rails_pro/e2e-tests/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/react_on_rails_pro/e2e-tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/react_on_rails_pro/package.json b/react_on_rails_pro/package.json index 11b5f9330..83bd1a1ce 100644 --- a/react_on_rails_pro/package.json +++ b/react_on_rails_pro/package.json @@ -56,12 +56,14 @@ "@babel/preset-typescript": "^7.27.0", "@eslint/compat": "^1.2.8", "@honeybadger-io/js": "^6.10.1", + "@playwright/test": "^1.56.1", "@sentry/node": "^7.120.0", "@tsconfig/node14": "^14.1.2", "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.10", "@types/lockfile": "^1.0.4", + "@types/node": "^24.8.1", "@types/touch": "^3.1.5", "babel-jest": "^29.7.0", "concurrently": "^9.1.0", diff --git a/react_on_rails_pro/playwright.config.ts b/react_on_rails_pro/playwright.config.ts new file mode 100644 index 000000000..badddb256 --- /dev/null +++ b/react_on_rails_pro/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* 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', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/react_on_rails_pro/yarn.lock b/react_on_rails_pro/yarn.lock index 73fd9af02..b1d569dce 100644 --- a/react_on_rails_pro/yarn.lock +++ b/react_on_rails_pro/yarn.lock @@ -1576,6 +1576,13 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.1.tgz#6d083acfddae21fb329c8df8c94bf895ce7d0c15" integrity sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg== +"@playwright/test@^1.56.1": + version "1.56.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" + integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== + dependencies: + playwright "1.56.1" + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -1897,6 +1904,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^24.8.1": + version "24.8.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.8.1.tgz#74c8ae00b045a0a351f2837ec00f25dfed0053be" + integrity sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q== + dependencies: + undici-types "~7.14.0" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -4234,6 +4248,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -6713,6 +6732,20 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" +playwright-core@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== + +playwright@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== + dependencies: + playwright-core "1.56.1" + optionalDependencies: + fsevents "2.3.2" + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -8097,6 +8130,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" + integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From a3727f8e74181b87c328dcb4115293eafb2a3247 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 19 Oct 2025 17:45:36 +0300 Subject: [PATCH 04/24] add redis receiver component and add a e2e test for it --- .circleci/config.yml | 45 ++++++++++++++ react_on_rails_pro/e2e-tests/example.spec.ts | 55 +++++++++++++---- react_on_rails_pro/package.json | 3 +- react_on_rails_pro/playwright.config.ts | 5 +- .../dummy/app/controllers/pages_controller.rb | 30 ++++++++++ .../app/views/pages/redis_receiver.html.erb | 8 +++ .../RedisReceiver.jsx | 35 +++++++++++ .../spec/dummy/config/routes.rb | 2 + react_on_rails_pro/yarn.lock | 60 +++++++++---------- 9 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb create mode 100644 react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx diff --git a/.circleci/config.yml b/.circleci/config.yml index 40dc6270a..94e739379 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -343,6 +343,51 @@ jobs: - 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 + 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: 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/results.xml + - store_artifacts: + path: react_on_rails_pro/spec/dummy/playwright-report + workflows: version: 2 build-and-test: diff --git a/react_on_rails_pro/e2e-tests/example.spec.ts b/react_on_rails_pro/e2e-tests/example.spec.ts index 54a906a4e..8326927b9 100644 --- a/react_on_rails_pro/e2e-tests/example.spec.ts +++ b/react_on_rails_pro/e2e-tests/example.spec.ts @@ -1,18 +1,51 @@ -import { test, expect } from '@playwright/test'; +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('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); + const requestId = randomUUID(); + await page.goto(`http://localhost:3000/redis_receiver_for_testing?request_id=${requestId}`, { waitUntil: "commit" }); - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); + 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); -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); + redisClient.xAdd(`stream:${requestId}`, '*', { ':Item4': JSON.stringify('Value4') }); + sentValues.push(4); + await assertPageState(page, sentValues); - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); + redisClient.xAdd(`stream:${requestId}`, '*', { ':Item2': JSON.stringify('Value2') }); + sentValues.push(2); + await assertPageState(page, sentValues); - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); + redisClient.xAdd(`stream:${requestId}`, '*', { ':Item3': JSON.stringify('Value3') }); + sentValues.push(3); + await assertPageState(page, sentValues); }); diff --git a/react_on_rails_pro/package.json b/react_on_rails_pro/package.json index 83bd1a1ce..fe88570a5 100644 --- a/react_on_rails_pro/package.json +++ b/react_on_rails_pro/package.json @@ -90,7 +90,7 @@ "pino-pretty": "^13.0.0", "prettier": "^3.2.5", "react-on-rails": "link:.yalc/react-on-rails", - "redis": "^5.0.1", + "redis": "^5.8.3", "release-it": "^17.6.0", "sentry-testkit": "^5.0.6", "touch": "^3.1.0", @@ -122,6 +122,7 @@ "postinstall": "test -f post-yarn-install.local && ./post-yarn-install.local || true", "link-source": "cd ../packages/react-on-rails && yarn && yalc publish", "test": "nps test", + "e2e-test": "playwright test", "prepack": "nps build.prepack", "prepare": "nps build.prepack", "prepublishOnly": "nps build", diff --git a/react_on_rails_pro/playwright.config.ts b/react_on_rails_pro/playwright.config.ts index badddb256..c97fae8bc 100644 --- a/react_on_rails_pro/playwright.config.ts +++ b/react_on_rails_pro/playwright.config.ts @@ -22,7 +22,10 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: process.env.ci ? [ + ['html'], + ['junit', { outputFile: 'test-results/results.xml' }] + ] : 'html', /* 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('')`. */ diff --git a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb index ec668f4f0..9c745dafb 100644 --- a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +++ b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb @@ -69,6 +69,36 @@ 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 + 1}" => "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 + 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.empty? + + 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") 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 new file mode 100644 index 000000000..af9f75c0e --- /dev/null +++ b/react_on_rails_pro/spec/dummy/app/views/pages/redis_receiver.html.erb @@ -0,0 +1,8 @@ +<%= stream_react_component("RedisReceiver", + props: { requestId: @request_id }, + prerender: true, + trace: true, + id: "RedisReceiver-react-component-0") %> +
+ +

React Rails Server Streaming Redis Receiver

diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx new file mode 100644 index 000000000..e01204138 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -0,0 +1,35 @@ +import React, { Suspense } from 'react'; +import { listenToRequestData } from '../utils/redisReceiver'; +import { ErrorBoundary } from '../components/ErrorBoundary'; + +const RedisItem = async ({ getValue, redisKey }) => { + const value = await getValue(redisKey); + return

Value of "{redisKey}": {value}

+} + +const RedisItemWithWrapper = ({ getValue, redisKey }) => ( + Waiting for the key "{redisKey}"

}> + +
+) + +const RedisReceiver = ({ requestId }, railsContext) => { + const { getValue, close } = listenToRequestData(requestId); + + if ('addPostSSRHook' in railsContext) { + railsContext.addPostSSRHook(close); + } + + return ( + +

A list of items received from Redis:

+
    + { + [1,2,3,4,5].map(index => ) + } +
+
+ ) +} + +export default RedisReceiver; \ No newline at end of file diff --git a/react_on_rails_pro/spec/dummy/config/routes.rb b/react_on_rails_pro/spec/dummy/config/routes.rb index 2df51415a..1d2f2b4e0 100644 --- a/react_on_rails_pro/spec/dummy/config/routes.rb +++ b/react_on_rails_pro/spec/dummy/config/routes.rb @@ -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 diff --git a/react_on_rails_pro/yarn.lock b/react_on_rails_pro/yarn.lock index b1d569dce..f8dc3458c 100644 --- a/react_on_rails_pro/yarn.lock +++ b/react_on_rails_pro/yarn.lock @@ -1604,32 +1604,32 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" -"@redis/bloom@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-5.0.1.tgz#5f4c6bb2cce3908a4c3d246c69187321db9e1818" - integrity sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g== +"@redis/bloom@5.8.3": + version "5.8.3" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-5.8.3.tgz#c0f04cba102eeb3a68792a599f7c320915eba366" + integrity sha512-1eldTzHvdW3Oi0TReb8m1yiFt8ZwyF6rv1NpZyG5R4TpCwuAdKQetBKoCw7D96tNFgsVVd6eL+NaGZZCqhRg4g== -"@redis/client@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-5.0.1.tgz#d39d9c0114b865e2186e46b6d891701f46b293ad" - integrity sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A== +"@redis/client@5.8.3": + version "5.8.3" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-5.8.3.tgz#186ebdd30ff874eae35b8d0ea3151137c22802be" + integrity sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg== dependencies: cluster-key-slot "1.1.2" -"@redis/json@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@redis/json/-/json-5.0.1.tgz#7eef09a2fac9e6ca6188ec34f9d313d4617f5b5b" - integrity sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg== +"@redis/json@5.8.3": + version "5.8.3" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-5.8.3.tgz#f4efab4245b9f5c1d4ab80ec6f3c13aecb663c1e" + integrity sha512-DRR09fy/u8gynHGJ4gzXYeM7D8nlS6EMv5o+h20ndTJiAc7RGR01fdk2FNjnn1Nz5PjgGGownF+s72bYG4nZKQ== -"@redis/search@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@redis/search/-/search-5.0.1.tgz#805038a010b3d58765c65374d1a730247dfd2a5f" - integrity sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw== +"@redis/search@5.8.3": + version "5.8.3" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-5.8.3.tgz#eb1159d1f6576244f47d779535515e83561ec1a4" + integrity sha512-EMIvEeGRR2I0BJEz4PV88DyCuPmMT1rDtznlsHY3cKSDcc9vj0Q411jUnX0iU2vVowUgWn/cpySKjpXdZ8m+5g== -"@redis/time-series@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-5.0.1.tgz#b63654dad199fcbcd8315c27d6ae46db9120c211" - integrity sha512-k6PgbrakhnohsEWEAdQZYt3e5vSKoIzpKvgQt8//lnWLrTZx+c3ed2sj0+pKIF4FvnSeuXLo4bBWcH0Z7Urg1A== +"@redis/time-series@5.8.3": + version "5.8.3" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-5.8.3.tgz#d20158dceba05fd456f2abc58b05fe16c736d18e" + integrity sha512-5Jwy3ilsUYQjzpE7WZ1lEeG1RkqQ5kHtwV1p8yxXHSEmyUbC/T/AVgyjMcm52Olj/Ov/mhDKjx6ndYUi14bXsw== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -7020,16 +7020,16 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redis@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/redis/-/redis-5.0.1.tgz#2eda8388e1350638616fa4b2dc4a9f5dbdfa1911" - integrity sha512-J8nqUjrfSq0E8NQkcHDZ4HdEQk5RMYjP3jZq02PE+ERiRxolbDNxPaTT4xh6tdrme+lJ86Goje9yMt9uzh23hQ== - dependencies: - "@redis/bloom" "5.0.1" - "@redis/client" "5.0.1" - "@redis/json" "5.0.1" - "@redis/search" "5.0.1" - "@redis/time-series" "5.0.1" +redis@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/redis/-/redis-5.8.3.tgz#c65d52ff9099579e278bf8ce100efbadafe5580a" + integrity sha512-MfSrfV6+tEfTw8c4W0yFp6XWX8Il4laGU7Bx4kvW4uiYM1AuZ3KGqEGt1LdQHeD1nEyLpIWetZ/SpY3kkbgrYw== + dependencies: + "@redis/bloom" "5.8.3" + "@redis/client" "5.8.3" + "@redis/json" "5.8.3" + "@redis/search" "5.8.3" + "@redis/time-series" "5.8.3" reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" From e8bc6f1e3c2cce6210e7753e64ceb9a5cda8ed8c Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 19 Oct 2025 17:58:39 +0300 Subject: [PATCH 05/24] move playwright tests from reat_on_rails_pro to spec/dummy directory --- react_on_rails_pro/.gitignore | 7 ---- react_on_rails_pro/package.json | 5 +-- react_on_rails_pro/spec/dummy/.gitignore | 7 ++++ .../dummy}/e2e-tests/example.spec.ts | 0 react_on_rails_pro/spec/dummy/package.json | 3 ++ .../{ => spec/dummy}/playwright.config.ts | 0 react_on_rails_pro/spec/dummy/yarn.lock | 38 ++++++++++++++++++ react_on_rails_pro/yarn.lock | 40 +------------------ 8 files changed, 50 insertions(+), 50 deletions(-) rename react_on_rails_pro/{ => spec/dummy}/e2e-tests/example.spec.ts (100%) rename react_on_rails_pro/{ => spec/dummy}/playwright.config.ts (100%) diff --git a/react_on_rails_pro/.gitignore b/react_on_rails_pro/.gitignore index c7c07b7f9..0134fd426 100644 --- a/react_on_rails_pro/.gitignore +++ b/react_on_rails_pro/.gitignore @@ -75,10 +75,3 @@ yalc.lock # React on Rails Pro License Key config/react_on_rails_pro_license.key - -# Playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ -/playwright/.auth/ diff --git a/react_on_rails_pro/package.json b/react_on_rails_pro/package.json index fe88570a5..11b5f9330 100644 --- a/react_on_rails_pro/package.json +++ b/react_on_rails_pro/package.json @@ -56,14 +56,12 @@ "@babel/preset-typescript": "^7.27.0", "@eslint/compat": "^1.2.8", "@honeybadger-io/js": "^6.10.1", - "@playwright/test": "^1.56.1", "@sentry/node": "^7.120.0", "@tsconfig/node14": "^14.1.2", "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.10", "@types/lockfile": "^1.0.4", - "@types/node": "^24.8.1", "@types/touch": "^3.1.5", "babel-jest": "^29.7.0", "concurrently": "^9.1.0", @@ -90,7 +88,7 @@ "pino-pretty": "^13.0.0", "prettier": "^3.2.5", "react-on-rails": "link:.yalc/react-on-rails", - "redis": "^5.8.3", + "redis": "^5.0.1", "release-it": "^17.6.0", "sentry-testkit": "^5.0.6", "touch": "^3.1.0", @@ -122,7 +120,6 @@ "postinstall": "test -f post-yarn-install.local && ./post-yarn-install.local || true", "link-source": "cd ../packages/react-on-rails && yarn && yalc publish", "test": "nps test", - "e2e-test": "playwright test", "prepack": "nps build.prepack", "prepare": "nps build.prepack", "prepublishOnly": "nps build", diff --git a/react_on_rails_pro/spec/dummy/.gitignore b/react_on_rails_pro/spec/dummy/.gitignore index 6fc839654..8ddc54955 100644 --- a/react_on_rails_pro/spec/dummy/.gitignore +++ b/react_on_rails_pro/spec/dummy/.gitignore @@ -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/ diff --git a/react_on_rails_pro/e2e-tests/example.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/example.spec.ts similarity index 100% rename from react_on_rails_pro/e2e-tests/example.spec.ts rename to react_on_rails_pro/spec/dummy/e2e-tests/example.spec.ts diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json index 1ae3c8e2b..055bc0247 100644 --- a/react_on_rails_pro/spec/dummy/package.json +++ b/react_on_rails_pro/spec/dummy/package.json @@ -77,11 +77,13 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.23.2", + "@playwright/test": "^1.56.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.3", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", "@tsconfig/create-react-app": "^2.0.1", + "@types/node": "^24.8.1", "@types/react-dom": "^18.2.14", "jsdom": "^16.4.0", "pino-pretty": "^13.0.0", @@ -95,6 +97,7 @@ "scripts": { "test": "yarn run build:test && yarn run lint && rspec", "lint": "cd ../.. && nps lint", + "e2e-test": "playwright test", "preinstall": "yarn run link-source && yalc add --link react-on-rails-pro && cd .yalc/react-on-rails-pro && yalc add --link react-on-rails && cd ../.. && yalc add --link @shakacode-tools/react-on-rails-pro-node-renderer", "link-source": "cd ../../.. && yarn && yarn run yalc:publish && cd react_on_rails_pro && yarn && yalc publish", "postinstall": "test -f post-yarn-install.local && ./post-yarn-install.local || true", diff --git a/react_on_rails_pro/playwright.config.ts b/react_on_rails_pro/spec/dummy/playwright.config.ts similarity index 100% rename from react_on_rails_pro/playwright.config.ts rename to react_on_rails_pro/spec/dummy/playwright.config.ts diff --git a/react_on_rails_pro/spec/dummy/yarn.lock b/react_on_rails_pro/spec/dummy/yarn.lock index 95c3d951e..c7d322fe2 100644 --- a/react_on_rails_pro/spec/dummy/yarn.lock +++ b/react_on_rails_pro/spec/dummy/yarn.lock @@ -1124,6 +1124,13 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@^1.56.1": + version "1.56.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" + integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== + dependencies: + playwright "1.56.1" + "@pmmmwh/react-refresh-webpack-plugin@0.5.3": version "0.5.3" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.3.tgz#b8f0e035f6df71b5c4126cb98de29f65188b9e7b" @@ -1387,6 +1394,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^24.8.1": + version "24.8.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.8.1.tgz#74c8ae00b045a0a351f2837ec00f25dfed0053be" + integrity sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q== + dependencies: + undici-types "~7.14.0" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -3409,6 +3423,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -4941,6 +4960,20 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +playwright-core@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== + +playwright@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== + dependencies: + playwright-core "1.56.1" + optionalDependencies: + fsevents "2.3.2" + postcss-calc@^8.2.3: version "8.2.4" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" @@ -6513,6 +6546,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" + integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" diff --git a/react_on_rails_pro/yarn.lock b/react_on_rails_pro/yarn.lock index f8dc3458c..57f7780ab 100644 --- a/react_on_rails_pro/yarn.lock +++ b/react_on_rails_pro/yarn.lock @@ -1576,13 +1576,6 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.1.tgz#6d083acfddae21fb329c8df8c94bf895ce7d0c15" integrity sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg== -"@playwright/test@^1.56.1": - version "1.56.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" - integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== - dependencies: - playwright "1.56.1" - "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -1904,13 +1897,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@^24.8.1": - version "24.8.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.8.1.tgz#74c8ae00b045a0a351f2837ec00f25dfed0053be" - integrity sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q== - dependencies: - undici-types "~7.14.0" - "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -4248,11 +4234,6 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -6732,20 +6713,6 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -playwright-core@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" - integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== - -playwright@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" - integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== - dependencies: - playwright-core "1.56.1" - optionalDependencies: - fsevents "2.3.2" - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -7020,7 +6987,7 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redis@^5.8.3: +redis@^5.0.1: version "5.8.3" resolved "https://registry.yarnpkg.com/redis/-/redis-5.8.3.tgz#c65d52ff9099579e278bf8ce100efbadafe5580a" integrity sha512-MfSrfV6+tEfTw8c4W0yFp6XWX8Il4laGU7Bx4kvW4uiYM1AuZ3KGqEGt1LdQHeD1nEyLpIWetZ/SpY3kkbgrYw== @@ -8130,11 +8097,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~7.14.0: - version "7.14.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" - integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From 5a070b6e37db46dcbda02e631ce24624540afdaf Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 19 Oct 2025 18:01:26 +0300 Subject: [PATCH 06/24] set proper names and description for the test file --- .../spec/dummy/e2e-tests/{example.spec.ts => streaming.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename react_on_rails_pro/spec/dummy/e2e-tests/{example.spec.ts => streaming.spec.ts} (96%) diff --git a/react_on_rails_pro/spec/dummy/e2e-tests/example.spec.ts b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts similarity index 96% rename from react_on_rails_pro/spec/dummy/e2e-tests/example.spec.ts rename to react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts index 8326927b9..9e423a56d 100644 --- a/react_on_rails_pro/spec/dummy/e2e-tests/example.spec.ts +++ b/react_on_rails_pro/spec/dummy/e2e-tests/streaming.spec.ts @@ -25,7 +25,7 @@ const assertPageState = async(page: Page, sentValues: Number[]) => { })); } -test('has title', async ({ page }) => { +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" }); From 94aa9442c71d96a18634a08c9a8b1fe9217a6ca6 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 16:36:22 +0300 Subject: [PATCH 07/24] add streaming tests --- packages/react-on-rails-pro/src/RSCRoute.tsx | 2 + .../streaming.spec.ts/stage0.aria.yml | 8 ++ .../streaming.spec.ts/stage1.aria.yml | 8 ++ .../streaming.spec.ts/stage2.aria.yml | 8 ++ .../streaming.spec.ts/stage3.aria.yml | 8 ++ .../streaming.spec.ts/stage4.aria.yml | 8 ++ .../streaming.spec.ts/stage5.aria.yml | 8 ++ .../dummy/app/controllers/pages_controller.rb | 2 +- .../app/views/pages/server_router.html.erb | 6 +- .../app/components/ServerComponentRouter.tsx | 9 ++ .../RedisReceiver.jsx | 44 ++++++--- .../spec/dummy/e2e-tests/dummy.spec.ts | 12 +++ .../spec/dummy/e2e-tests/fixture.ts | 92 +++++++++++++++++++ .../spec/dummy/e2e-tests/streaming.spec.ts | 90 ++++++++---------- .../spec/dummy/playwright.config.ts | 7 +- 15 files changed, 244 insertions(+), 68 deletions(-) create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml create mode 100644 react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts create mode 100644 react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts diff --git a/packages/react-on-rails-pro/src/RSCRoute.tsx b/packages/react-on-rails-pro/src/RSCRoute.tsx index e2d614567..7ccaa265a 100644 --- a/packages/react-on-rails-pro/src/RSCRoute.tsx +++ b/packages/react-on-rails-pro/src/RSCRoute.tsx @@ -1,3 +1,5 @@ +"use client"; + /* * Copyright (c) 2025 Shakacode LLC * 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 new file mode 100644 index 000000000..26c751575 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage0.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file 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 new file mode 100644 index 000000000..773d8465f --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage1.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file 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 new file mode 100644 index 000000000..92af35045 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage2.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file 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 new file mode 100644 index 000000000..5b91a7ed5 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage3.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file 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 new file mode 100644 index 000000000..98e11cf5c --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage4.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file 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 new file mode 100644 index 000000000..e0257e200 --- /dev/null +++ b/react_on_rails_pro/spec/dummy/__snapshots__/streaming.spec.ts/stage5.aria.yml @@ -0,0 +1,8 @@ +- main: + - heading "A list of items received from Redis:" [level=1] + - 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" \ No newline at end of file diff --git a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb index 9c745dafb..0f0a8abf8 100644 --- a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +++ b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb @@ -76,7 +76,7 @@ def redis_receiver redis = ::Redis.new 5.times do |index| sleep 1 - redis.xadd("stream:#{@request_id}", { ":Item#{index + 1}" => "Value of Item#{index + 1}".to_json }) + 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}" diff --git a/react_on_rails_pro/spec/dummy/app/views/pages/server_router.html.erb b/react_on_rails_pro/spec/dummy/app/views/pages/server_router.html.erb index c5b3cb405..91347c221 100644 --- a/react_on_rails_pro/spec/dummy/app/views/pages/server_router.html.erb +++ b/react_on_rails_pro/spec/dummy/app/views/pages/server_router.html.erb @@ -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") %>
diff --git a/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx b/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx index 35c5f8016..de5f709d4 100644 --- a/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx +++ b/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx @@ -41,6 +41,11 @@ export default function App({ basePath = '/server_router', ...props }: { basePat Server Component with visible streaming behavior +
  • + + Redis Receiver For Testing + +
  • Server Component with Retry
  • @@ -71,6 +76,10 @@ export default function App({ basePath = '/server_router', ...props }: { basePat /> } /> + } + /> } diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index e01204138..16d5b100c 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -1,33 +1,49 @@ 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, redisKey }) => { - const value = await getValue(redisKey); - return

    Value of "{redisKey}": {value}

    +const RedisItem = async ({ getValue, itemIndex }) => { + const value = await getValue(`Item${itemIndex}`); + return
  • Value of "Item{itemIndex + 1}": {value}
  • } -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 (
    - {isVisible && children} diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index 16d5b100c..321957111 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -31,18 +31,21 @@ const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { } const UsedToggleContainer = asyncToggleContainer ? AsyncToggleContainer : ToggleContainer; + const toggleContainerGetValueParam = asyncToggleContainer ? getValue : undefined; - return ( + return () => (

    A list of items received from Redis:

    - -
      - { - [0,1,2,3,4].map(index => ) - } -
    -
    + Loading ToggleContainer
    }> + +
      + { + [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)
    ") - rendering_stages << 3 - expect(component_logs).to include(async_component_rendered_message(1, 0)) - expect(component_logs).to include(async_component_rendered_message(2, 0)) - end - end - - expect(rendering_stages).to eq([1, 2, 3]) - end - - it "hydrates the client component inside server component before the full page is loaded" do - chunks_count = 0 - client_component_hydrated_on_chunk = nil - component_logs = [] - navigate_with_streaming "/async_on_server_sync_on_client" do |_content| - chunks_count += 1 - component_logs += async_on_server_sync_on_client_client_render_logs - - if client_component_hydrated_on_chunk.nil? && component_logs.include?(async_component_hydrated_message(3, 0)) - client_component_hydrated_on_chunk = chunks_count - expect_client_component_inside_server_component_hydrated(async_component) - end - end - expect(client_component_hydrated_on_chunk).to be < chunks_count - end - - it "Server component is pre-rendered on the server and not showing loading component on the client" do - navigate_with_streaming "/async_on_server_sync_on_client" - component_logs = async_on_server_sync_on_client_client_render_logs - expect(component_logs).not_to include(async_loading_component_message(3)) - end -end From 6064b027f8ca0994bab7d04757ccb13a753167f3 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 20:25:34 +0300 Subject: [PATCH 13/24] fix node renderer build --- packages/react-on-rails-pro/src/RSCRoute.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-on-rails-pro/src/RSCRoute.tsx b/packages/react-on-rails-pro/src/RSCRoute.tsx index 7ccaa265a..0f0076b7e 100644 --- a/packages/react-on-rails-pro/src/RSCRoute.tsx +++ b/packages/react-on-rails-pro/src/RSCRoute.tsx @@ -1,5 +1,3 @@ -"use client"; - /* * Copyright (c) 2025 Shakacode LLC * @@ -15,6 +13,7 @@ */ /// +"use client"; import * as React from 'react'; import { useRSC } from './RSCProvider.tsx'; From 5d7cfa279392527dbdae49c9510fcaa30207a4bb Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 20:42:05 +0300 Subject: [PATCH 14/24] fix CI failure and other tiny improvements --- .circleci/config.yml | 4 +++ .../dummy/app/controllers/pages_controller.rb | 2 +- .../RedisReceiver.jsx | 2 +- .../spec/dummy/e2e-tests/dummt-fixture.ts | 33 ------------------- .../spec/dummy/e2e-tests/dummy.spec.ts | 22 ------------- .../spec/dummy/e2e-tests/fixture.ts | 5 +-- .../spec/dummy/e2e-tests/streaming.spec.ts | 22 ++++++------- .../spec/dummy/playwright.config.ts | 2 +- 8 files changed, 21 insertions(+), 71 deletions(-) delete mode 100644 react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts delete mode 100644 react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index a491fe1d4..5a9459e60 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -380,6 +380,10 @@ jobs: 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 diff --git a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb index 0f0a8abf8..85fa2a232 100644 --- a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +++ b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb @@ -94,7 +94,7 @@ def redis_receiver def redis_receiver_for_testing @request_id = params[:request_id] - raise "request_id is required at the url" if @request_id.empty? + raise "request_id is required at the url" if @request_id.blank? stream_view_containing_react_components(template: "/pages/redis_receiver") end diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index 321957111..ca98ffb4f 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -9,7 +9,7 @@ const RedisItem = async ({ getValue, itemIndex }) => { } const RedisItemWithWrapper = ({ getValue, itemIndex }) => ( -
    +
    Waiting for the key "Item{itemIndex + 1}"

    }>
    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 deleted file mode 100644 index 16521bf3f..000000000 --- a/react_on_rails_pro/spec/dummy/e2e-tests/dummt-fixture.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index d50fabcf3..000000000 --- a/react_on_rails_pro/spec/dummy/e2e-tests/dummy.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -// 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 f6fcc110b..533d54a72 100644 --- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts +++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts @@ -17,7 +17,7 @@ type RedisReceiverPageFixture = { export type RedisReceiverControllerFixture = { sendRedisValue: (key: string, value: unknown) => Promise; - sendRedisItemValue: (itemIndex: Number, value: unknown) => Promise; + sendRedisItemValue: (itemIndex: number, value: unknown) => Promise; matchPageSnapshot: (snapshotPath: string) => Promise; waitForConsoleMessage: (msg: string) => Promise; getNetworkRequests: (requestUrlPattern: RegExp) => Promise; @@ -33,8 +33,9 @@ const redisControlledTest = base.extend { + redisRequestId: async ({ redisClient }, use) => { await use(randomUUID()); + redisClient.del }, nonBlockingNavigateWithRequestId: async ({ redisRequestId, page }, use) => { 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 a7d146ae7..ca553f95c 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 @@ -22,24 +22,24 @@ import { test(`incremental rendering of page: ${pageName}`, async ({ matchPageSnapshot, sendRedisItemValue }) => { await matchPageSnapshot('stage0'); - sendRedisItemValue(0, 'Incremental Value1'); + await sendRedisItemValue(0, 'Incremental Value1'); await matchPageSnapshot('stage1'); - sendRedisItemValue(3, 'Incremental Value4'); + await sendRedisItemValue(3, 'Incremental Value4'); await matchPageSnapshot('stage2'); - sendRedisItemValue(1, 'Incremental Value2'); + await sendRedisItemValue(1, 'Incremental Value2'); await matchPageSnapshot('stage3'); - sendRedisItemValue(2, 'Incremental Value3'); + await sendRedisItemValue(2, 'Incremental Value3'); await matchPageSnapshot('stage4'); - sendRedisItemValue(4, 'Incremental Value5'); + await sendRedisItemValue(4, 'Incremental Value5'); await matchPageSnapshot('stage5'); }); test(`early hydration of page: ${pageName}`, async ({ page, waitForConsoleMessage, matchPageSnapshot, sendRedisItemValue }) => { - waitForConsoleMessage('ToggleContainer with title'); + await waitForConsoleMessage('ToggleContainer with title'); await page.click('.toggle-button'); await expect(page.getByText(/Waiting for the key "Item\d"/)).not.toBeVisible(); @@ -52,19 +52,19 @@ import { } await matchPageSnapshot('stage0'); - sendRedisItemValue(0, 'Incremental Value1'); + await sendRedisItemValue(0, 'Incremental Value1'); await matchPageSnapshot('stage1'); - sendRedisItemValue(3, 'Incremental Value4'); + await sendRedisItemValue(3, 'Incremental Value4'); await matchPageSnapshot('stage2'); - sendRedisItemValue(1, 'Incremental Value2'); + await sendRedisItemValue(1, 'Incremental Value2'); await matchPageSnapshot('stage3'); - sendRedisItemValue(2, 'Incremental Value3'); + await sendRedisItemValue(2, 'Incremental Value3'); await matchPageSnapshot('stage4'); - sendRedisItemValue(4, 'Incremental Value5'); + await 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 37a9239ba..e0c9ad7a3 100644 --- a/react_on_rails_pro/spec/dummy/playwright.config.ts +++ b/react_on_rails_pro/spec/dummy/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.ci ? [ + reporter: process.env.CI ? [ ['html'], ['junit', { outputFile: 'test-results/results.xml' }] ] : 'html', From 3954ee2336e631cbedaa477e70f6818b37ea7622 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 20:59:37 +0300 Subject: [PATCH 15/24] tiny fixes --- .circleci/config.yml | 2 +- .../client/app/ror-auto-load-components/RedisReceiver.jsx | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a9459e60..2b5852f35 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -389,7 +389,7 @@ jobs: working_directory: react_on_rails_pro/spec/dummy command: yarn e2e-test - store_test_results: - path: react_on_rails_pro/spec/dummy/results.xml + path: react_on_rails_pro/spec/dummy/test-results/results.xml - store_artifacts: path: react_on_rails_pro/spec/dummy/playwright-report diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index ca98ffb4f..395c2354a 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -20,7 +20,7 @@ const RedisItemWithWrapper = ({ getValue, itemIndex }) => ( // 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 + return {children} } const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { @@ -31,14 +31,16 @@ const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { } const UsedToggleContainer = asyncToggleContainer ? AsyncToggleContainer : ToggleContainer; - const toggleContainerGetValueParam = asyncToggleContainer ? getValue : undefined; return () => (

    A list of items received from Redis:

    Loading ToggleContainer}> - +
      { [0,1,2,3,4].map(index => ) From 8d4b4c42173aafa06b80be9012e071c05744b46f Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 21:15:13 +0300 Subject: [PATCH 16/24] improve redis tear down --- .../spec/dummy/app/controllers/pages_controller.rb | 6 ++++++ react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb index 85fa2a232..f2f6d309e 100644 --- a/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +++ b/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb @@ -82,6 +82,12 @@ def redis_receiver 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") 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 533d54a72..eb9f61a56 100644 --- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts +++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts @@ -30,12 +30,11 @@ const redisControlledTest = base.extend { await use(randomUUID()); - redisClient.del }, nonBlockingNavigateWithRequestId: async ({ redisRequestId, page }, use) => { From 182e040de014a4839370670efd8fb115a653dabf Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 21:20:59 +0300 Subject: [PATCH 17/24] tiny fixes --- .../client/app/ror-auto-load-components/RedisReceiver.jsx | 2 +- react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index 395c2354a..0a890be38 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -39,7 +39,7 @@ const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { Loading ToggleContainer}>
        { 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 eb9f61a56..ad386ce0a 100644 --- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts +++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts @@ -34,7 +34,13 @@ const redisControlledTest = base.extend { - await use(randomUUID()); + const id = randomUUID(); + try { + await use(id); + } finally { + // cleanup of the stream for this request + await redisClient.del(`stream:${id}`); + } }, nonBlockingNavigateWithRequestId: async ({ redisRequestId, page }, use) => { From 916f7c882735b21732345d1d5ca6eaa0d6184a7a Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 20 Oct 2025 21:43:16 +0300 Subject: [PATCH 18/24] linting --- packages/react-on-rails-pro/src/RSCRoute.tsx | 3 +- packages/react-on-rails-pro/tsconfig.json | 3 +- .../RSCPostsPage/ToggleContainer.jsx | 7 +- .../app/components/ServerComponentRouter.tsx | 4 +- .../RedisReceiver.jsx | 43 +++-- .../spec/dummy/e2e-tests/fixture.ts | 156 ++++++++++-------- .../spec/dummy/e2e-tests/streaming.spec.ts | 69 +++++--- .../spec/dummy/playwright.config.ts | 5 +- 8 files changed, 168 insertions(+), 122 deletions(-) diff --git a/packages/react-on-rails-pro/src/RSCRoute.tsx b/packages/react-on-rails-pro/src/RSCRoute.tsx index 0f0076b7e..1c514b8cf 100644 --- a/packages/react-on-rails-pro/src/RSCRoute.tsx +++ b/packages/react-on-rails-pro/src/RSCRoute.tsx @@ -13,7 +13,8 @@ */ /// -"use client"; + +'use client'; import * as React from 'react'; import { useRSC } from './RSCProvider.tsx'; diff --git a/packages/react-on-rails-pro/tsconfig.json b/packages/react-on-rails-pro/tsconfig.json index 040770d8e..f9d429ac7 100644 --- a/packages/react-on-rails-pro/tsconfig.json +++ b/packages/react-on-rails-pro/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "outDir": "./lib" }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "allowDefaultProject": "../spec/dummy/e2e-tests/*" } 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 7ff7d9798..174866ce1 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,12 @@ const ToggleContainer = ({ children, childrenTitle }) => { return (
        - {isVisible && children} diff --git a/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx b/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx index de5f709d4..3c36bdd1b 100644 --- a/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx +++ b/react_on_rails_pro/spec/dummy/client/app/components/ServerComponentRouter.tsx @@ -42,9 +42,7 @@ export default function App({ basePath = '/server_router', ...props }: { basePat
      1. - - Redis Receiver For Testing - + Redis Receiver For Testing
      2. Server Component with Retry diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx index 0a890be38..f871a73c3 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RedisReceiver.jsx @@ -5,23 +5,33 @@ import { ErrorBoundary } from '../components/ErrorBoundary'; const RedisItem = async ({ getValue, itemIndex }) => { const value = await getValue(`Item${itemIndex}`); - return
      3. Value of "Item{itemIndex + 1}": {value}
      4. -} + return ( +
      5. + Value of "Item{itemIndex + 1}": {value} +
      6. + ); +}; const RedisItemWithWrapper = ({ getValue, itemIndex }) => (
        - Waiting for the key "Item{itemIndex + 1}"

        }> + + Waiting for the key "Item{itemIndex + 1}" +

        + } + >
        -) +); // 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 {children} -} + return {children}; +}; const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { const { getValue, close } = listenToRequestData(requestId); @@ -34,23 +44,20 @@ const RedisReceiver = ({ requestId, asyncToggleContainer }, railsContext) => { return () => ( -
        +

        A list of items received from Redis:

        Loading ToggleContainer
        }> - -
          - { - [0,1,2,3,4].map(index => ) - } + +
            + {[0, 1, 2, 3, 4].map((index) => ( + + ))}
    - ) -} + ); +}; -export default RedisReceiver; \ No newline at end of file +export default RedisReceiver; 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 ad386ce0a..874bdbc6d 100644 --- a/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts +++ b/react_on_rails_pro/spec/dummy/e2e-tests/fixture.ts @@ -8,12 +8,12 @@ type RedisClientFixture = { type RedisRequestIdFixture = { redisRequestId: string; - nonBlockingNavigateWithRequestId: (path: string) => Promise -} + nonBlockingNavigateWithRequestId: (path: string) => Promise; +}; type RedisReceiverPageFixture = { pagePath: string; -} +}; export type RedisReceiverControllerFixture = { sendRedisValue: (key: string, value: unknown) => Promise; @@ -21,17 +21,20 @@ export type RedisReceiverControllerFixture = { matchPageSnapshot: (snapshotPath: string) => Promise; waitForConsoleMessage: (msg: string) => Promise; getNetworkRequests: (requestUrlPattern: RegExp) => 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.quit(); - }, { scope: 'worker' }], + 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.quit(); + }, + { scope: 'worker' }, + ], redisRequestId: async ({ redisClient }, use) => { const id = randomUUID(); @@ -47,84 +50,99 @@ const redisControlledTest = base.extend { const requestIdParam = `request_id=${redisRequestId}`; const fullPath = path.includes('?') ? `${path}&${requestIdParam}` : `${path}?${requestIdParam}`; - return page.goto(fullPath, { waitUntil: "commit" }) - }) + return page.goto(fullPath, { waitUntil: 'commit' }); + }); }, }); const redisReceiverPageController = redisControlledTest.extend({ - sendRedisValue: async({ redisClient, redisRequestId }, use) => { - await use(async(key, value) => { + 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) => { + sendRedisItemValue: async ({ sendRedisValue }, use) => { + await use(async (itemIndex, value) => { await sendRedisValue(`Item${itemIndex}`, value); - }) + }); }, - matchPageSnapshot: async({ page }, use) => { - await use(async(snapshotPath) => { + 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` }); - }) + await expect(page.locator('.redis-receiver-container:visible').first()).toMatchAriaSnapshot({ + name: `${snapshotPath}.aria.yml`, + }); + }); }, - waitForConsoleMessage: async({ page }, use) =>{ - await use(async(msg) => { - if ((await page.consoleMessages()).find(consoleMsg => consoleMsg.text().includes(msg))) { + waitForConsoleMessage: async ({ page }, use) => { + 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), - }) - }) + }); + }); }, - getNetworkRequests: async({ page }, use) => { - await use(async(requestUrlPattern) => { - return (await page.requests()).filter(request => request.url().match(requestUrlPattern)) - }) - } -}) + getNetworkRequests: async ({ page }, use) => { + await use(async (requestUrlPattern) => { + return (await page.requests()).filter((request) => request.url().match(requestUrlPattern)); + }); + }, +}); const redisReceiverPageTest = redisReceiverPageController.extend({ - pagePath: [async({ nonBlockingNavigateWithRequestId }, use) => { - const pagePath = '/redis_receiver_for_testing'; - await nonBlockingNavigateWithRequestId(pagePath); - await use(pagePath); - }, { auto: true }] -}) - -const redisReceiverPageWithAsyncClientComponentTest = 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 }] -}) + pagePath: [ + async ({ nonBlockingNavigateWithRequestId }, use) => { + const pagePath = '/redis_receiver_for_testing'; + await nonBlockingNavigateWithRequestId(pagePath); + await use(pagePath); + }, + { auto: true }, + ], +}); + +const redisReceiverPageWithAsyncClientComponentTest = + 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'; - await nonBlockingNavigateWithRequestId(pagePath); - await use(pagePath); - }, { auto: true }] -}) + 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 }] -}) + 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 { redisReceiverPageController, @@ -132,4 +150,4 @@ export { 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 ca553f95c..6ea43a406 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 @@ -13,12 +13,17 @@ import { // - 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], - ['RedisReceiver with Async Toggle Container Client Component', redisReceiverPageWithAsyncClientComponentTest], -] as const).forEach(([pageName, test]) => { +( + [ + ['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(`incremental rendering of page: ${pageName}`, async ({ matchPageSnapshot, sendRedisItemValue }) => { await matchPageSnapshot('stage0'); @@ -38,7 +43,12 @@ import { await matchPageSnapshot('stage5'); }); - test(`early hydration of page: ${pageName}`, async ({ page, waitForConsoleMessage, matchPageSnapshot, sendRedisItemValue }) => { + test(`early hydration of page: ${pageName}`, async ({ + page, + waitForConsoleMessage, + matchPageSnapshot, + sendRedisItemValue, + }) => { await waitForConsoleMessage('ToggleContainer with title'); await page.click('.toggle-button'); @@ -66,21 +76,30 @@ import { await sendRedisItemValue(4, 'Incremental Value5'); 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); -}) + }); +}); + +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); + }, +); diff --git a/react_on_rails_pro/spec/dummy/playwright.config.ts b/react_on_rails_pro/spec/dummy/playwright.config.ts index e0c9ad7a3..10e3eaa08 100644 --- a/react_on_rails_pro/spec/dummy/playwright.config.ts +++ b/react_on_rails_pro/spec/dummy/playwright.config.ts @@ -27,10 +27,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [ - ['html'], - ['junit', { outputFile: 'test-results/results.xml' }] - ] : 'html', + reporter: process.env.CI ? [['html'], ['junit', { outputFile: 'test-results/results.xml' }]] : 'html', /* 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('')`. */ From b72d1988587c1128262556cb6c47920a772abf0e Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 10:23:01 +0300 Subject: [PATCH 19/24] increase redis timeout --- .../spec/dummy/client/app/utils/redisReceiver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/react_on_rails_pro/spec/dummy/client/app/utils/redisReceiver.ts b/react_on_rails_pro/spec/dummy/client/app/utils/redisReceiver.ts index beeeac14c..bf8b84369 100644 --- a/react_on_rails_pro/spec/dummy/client/app/utils/redisReceiver.ts +++ b/react_on_rails_pro/spec/dummy/client/app/utils/redisReceiver.ts @@ -1,5 +1,7 @@ import { createClient, RedisClientType } from 'redis'; +const REDIS_READ_TIMEOUT = 10000; + /** * Redis xRead result message structure */ @@ -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) { From 57bcf7d5cb668e6e985894754abb398d14de031b Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 11:00:04 +0300 Subject: [PATCH 20/24] Empty commit From bf0652a60214851fcc1bfed96ac4351849aa3820 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 12:38:39 +0300 Subject: [PATCH 21/24] fix config issue at eslint ts configs --- packages/react-on-rails-pro/tsconfig.json | 1 - react_on_rails_pro/spec/dummy/{client => }/tsconfig.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename react_on_rails_pro/spec/dummy/{client => }/tsconfig.json (70%) diff --git a/packages/react-on-rails-pro/tsconfig.json b/packages/react-on-rails-pro/tsconfig.json index f9d429ac7..b61154df3 100644 --- a/packages/react-on-rails-pro/tsconfig.json +++ b/packages/react-on-rails-pro/tsconfig.json @@ -4,5 +4,4 @@ "outDir": "./lib" }, "include": ["src/**/*"], - "allowDefaultProject": "../spec/dummy/e2e-tests/*" } diff --git a/react_on_rails_pro/spec/dummy/client/tsconfig.json b/react_on_rails_pro/spec/dummy/tsconfig.json similarity index 70% rename from react_on_rails_pro/spec/dummy/client/tsconfig.json rename to react_on_rails_pro/spec/dummy/tsconfig.json index d728ed443..4c306341d 100644 --- a/react_on_rails_pro/spec/dummy/client/tsconfig.json +++ b/react_on_rails_pro/spec/dummy/tsconfig.json @@ -7,5 +7,5 @@ "strict": true, "skipLibCheck": true }, - "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"] + "include": ["client/**/*.ts", "client/**/*.tsx", "client/**/*.d.ts", "e2e-tests/*"] } From 6454b9ea8a353234b972811cef304f9701d98d54 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 12:48:48 +0300 Subject: [PATCH 22/24] store capybara screenshots and html for failed tests --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b5852f35..0b641949c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -338,6 +338,8 @@ 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: From 7176d4962d8074cc04868fcdaf6bd0276d4872d8 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 13:20:47 +0300 Subject: [PATCH 23/24] adjust eslint config to work with e2e-tests --- packages/react-on-rails-pro/tsconfig.json | 2 +- react_on_rails_pro/eslint.config.mjs | 18 ++++++++++++++++++ .../spec/dummy/e2e-tests/streaming.spec.ts | 12 +++++------- react_on_rails_pro/spec/dummy/tsconfig.json | 8 +++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/react-on-rails-pro/tsconfig.json b/packages/react-on-rails-pro/tsconfig.json index b61154df3..040770d8e 100644 --- a/packages/react-on-rails-pro/tsconfig.json +++ b/packages/react-on-rails-pro/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "./lib" }, - "include": ["src/**/*"], + "include": ["src/**/*"] } diff --git a/react_on_rails_pro/eslint.config.mjs b/react_on_rails_pro/eslint.config.mjs index 7dc0c1515..cdfff0838 100644 --- a/react_on_rails_pro/eslint.config.mjs +++ b/react_on_rails_pro/eslint.config.mjs @@ -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'], + }, + }, // must be the last config in the array // https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs prettierRecommended, 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 6ea43a406..0bd5ea2a6 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,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { expect } from '@playwright/test'; import { redisReceiverPageController, redisReceiverPageTest, @@ -57,9 +57,7 @@ import { 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 Promise.all((await fallbackElements.all()).map((el) => expect(el).toBeVisible())); await matchPageSnapshot('stage0'); await sendRedisItemValue(0, 'Incremental Value1'); @@ -82,14 +80,14 @@ import { redisReceiverInsideRouterPageTest( 'no RSC payload request is made when the page is server side rendered', async ({ getNetworkRequests }) => { - await expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(0); + 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); + expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(1); }, ); @@ -100,6 +98,6 @@ redisReceiverPageController( await expect(page.getByText('Post 1')).toBeVisible(); await expect(page.getByText('Toggle')).toBeVisible(); - await expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(1); + expect(await getNetworkRequests(/rsc_payload/)).toHaveLength(1); }, ); diff --git a/react_on_rails_pro/spec/dummy/tsconfig.json b/react_on_rails_pro/spec/dummy/tsconfig.json index 4c306341d..1bc5a8763 100644 --- a/react_on_rails_pro/spec/dummy/tsconfig.json +++ b/react_on_rails_pro/spec/dummy/tsconfig.json @@ -7,5 +7,11 @@ "strict": true, "skipLibCheck": true }, - "include": ["client/**/*.ts", "client/**/*.tsx", "client/**/*.d.ts", "e2e-tests/*"] + "include": [ + "client/**/*.ts", + "client/**/*.tsx", + "client/**/*.d.ts", + "e2e-tests/*", + "./playwright.config.ts" + ] } From f5de26b5492798a26ff2fa65d1fc5e04e3949893 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 21 Oct 2025 13:32:48 +0300 Subject: [PATCH 24/24] update path to tsconfig at the typescript check command --- react_on_rails_pro/package-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react_on_rails_pro/package-scripts.yml b/react_on_rails_pro/package-scripts.yml index 8072f5fac..f9971f10a 100644 --- a/react_on_rails_pro/package-scripts.yml +++ b/react_on_rails_pro/package-scripts.yml @@ -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