Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
508f061
Add Playwright E2E testing via cypress-on-rails gem
justin808 Oct 2, 2025
89d8b53
Enhance Playwright E2E testing configuration
justin808 Nov 7, 2025
7f26ca2
Fix CI failures for Playwright E2E tests
justin808 Nov 7, 2025
f44bd5b
Install yalc globally in Playwright CI workflow
justin808 Nov 7, 2025
dcabbe1
Fix Playwright config to prevent running Jest tests
justin808 Nov 7, 2025
bdd7003
Fix Playwright CI by adding pack generation and asset build step
justin808 Nov 7, 2025
5ab7dbd
Update knip config to ignore Playwright-related dead code warnings
justin808 Nov 7, 2025
42ef1f4
Exclude binaries check from knip to fix CI build
justin808 Nov 7, 2025
6a924fc
Fix shellcheck warning in Playwright workflow
justin808 Nov 7, 2025
4d00510
Re-enable RouterApp components for integration tests
justin808 Nov 8, 2025
4aaf34e
Remove .js extension from react-router-dom/server import
justin808 Nov 8, 2025
5fe3e1c
Fix Playwright support/index.js comments and remove Cypress references
justin808 Nov 9, 2025
34461c8
Restrict Playwright workflow to master branch with manual trigger option
justin808 Nov 9, 2025
59cd828
Fix Playwright E2E test security issues and code quality
justin808 Nov 9, 2025
849599d
Restore eslint-disable directives needed for CI
justin808 Nov 10, 2025
5853cbf
Fix spread operator syntax in playwright.config.js
justin808 Nov 10, 2025
54695ed
Add explicit hydration waits to interactive Playwright tests
justin808 Nov 10, 2025
17750ed
Fix ESLint configuration to prevent false positives in Pro package
justin808 Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint-js-and-ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ jobs:
run: cd spec/dummy && RAILS_ENV="test" bundle exec rake react_on_rails:generate_packs
- name: Detect dead code
run: |
yarn run knip
yarn run knip --production
yarn run knip --exclude binaries
yarn run knip --production --exclude binaries
- name: Lint JS
run: yarn run eslint --report-unused-disable-directives
- name: Check formatting
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Playwright E2E Tests

on:
push:
branches: [master]
workflow_dispatch:

jobs:
playwright:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: Install yalc globally
run: yarn global add yalc

- name: Install root dependencies
run: yarn install

- name: Install dummy app dependencies
working-directory: spec/dummy
run: |
bundle install
yarn install

- name: Install Playwright browsers
working-directory: spec/dummy
run: yarn playwright install --with-deps

- name: Generate React on Rails packs
working-directory: spec/dummy
env:
RAILS_ENV: test
run: bundle exec rake react_on_rails:generate_packs

- name: Build test assets
working-directory: spec/dummy
run: yarn build:test

- name: Run Playwright tests
working-directory: spec/dummy
run: yarn test:e2e

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: spec/dummy/e2e/playwright-report/
retention-days: 30
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ ssr-generated
# Claude Code local settings
.claude/settings.local.json
.claude/.fuse_hidden*

# Playwright test artifacts (from cypress-on-rails gem)
/spec/dummy/e2e/playwright-report/
/spec/dummy/test-results/
144 changes: 144 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Pre-commit hooks automatically run:
- **Run tests**:
- Ruby tests: `rake run_rspec`
- JavaScript tests: `yarn run test` or `rake js_tests`
- Playwright E2E tests: See Playwright section below
- All tests: `rake` (default task runs lint and all tests except examples)
- **Linting** (MANDATORY BEFORE EVERY COMMIT):
- **REQUIRED**: `bundle exec rubocop` - Must pass with zero offenses
Expand Down Expand Up @@ -233,10 +234,153 @@ rm debug-*.js
- Generated examples are in `gen-examples/` (ignored by git)
- Only use `yarn` as the JS package manager, never `npm`

## Playwright E2E Testing

### Overview
Playwright E2E testing is integrated via the `cypress-on-rails` gem (v1.19+), which provides seamless integration between Playwright and Rails. This allows you to control Rails application state during tests, use factory_bot, and more.

### Setup
The gem and Playwright are already configured. To install Playwright browsers:

```bash
cd spec/dummy
yarn playwright install --with-deps
```

### Running Playwright Tests

**Note:** Playwright will automatically start the Rails server on port 5017 before running tests. You don't need to manually start the server.

```bash
cd spec/dummy

# Run all tests (Rails server auto-starts)
yarn test:e2e

# Run tests in UI mode (interactive debugging)
yarn test:e2e:ui

# Run tests with visible browser
yarn test:e2e:headed

# Debug a specific test
yarn test:e2e:debug

# View test report
yarn test:e2e:report

# Run specific test file
yarn test:e2e e2e/playwright/e2e/react_on_rails/basic_components.spec.js
```

### Writing Tests

Tests are located in `spec/dummy/e2e/playwright/e2e/`. The gem provides helpful commands for Rails integration:

```javascript
import { test, expect } from "@playwright/test";
import { app, appEval, appFactories } from '../../support/on-rails';

test.describe("My React Component", () => {
test.beforeEach(async ({ page }) => {
// Clean database before each test
await app('clean');
});

test("should interact with component", async ({ page }) => {
// Create test data using factory_bot
await appFactories([['create', 'user', { name: 'Test User' }]]);

// Or run arbitrary Ruby code
await appEval('User.create!(email: "[email protected]")');

// Navigate and test
await page.goto("/");
const component = page.locator('#MyComponent-react-component-0');
await expect(component).toBeVisible();
});
});
```

### Available Rails Helpers

The `cypress-on-rails` gem provides these helpers (imported from `support/on-rails.js`):

- `app('clean')` - Clean database
- `appEval(code)` - Run arbitrary Ruby code
- `appFactories(options)` - Create records via factory_bot
- `appScenario(name)` - Load predefined scenario
- See `e2e/playwright/app_commands/` for available commands

### Creating App Commands

Add custom commands in `e2e/playwright/app_commands/`:

```ruby
# e2e/playwright/app_commands/my_command.rb
CypressOnRails::SmartFactoryWrapper.configure(
always_reload: !Rails.configuration.cache_classes,
factory: :factory_bot,
dir: "{#{FactoryBot.definition_file_paths.join(',')}}"
)

command 'my_command' do |options|
# Your custom Rails code
{ success: true, data: options }
end
```

### Test Organization

```
spec/dummy/e2e/
├── playwright.config.js # Playwright configuration
├── playwright/
│ ├── support/
│ │ ├── index.js # Test setup
│ │ └── on-rails.js # Rails helper functions
│ ├── e2e/
│ │ ├── react_on_rails/ # React on Rails specific tests
│ │ │ └── basic_components.spec.js
│ │ └── rails_examples/ # Example tests
Comment on lines +272 to +345
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Playwright paths/extensions to match the actual test layout.

The examples here still point to spec/dummy/e2e/playwright/e2e/... and .spec.js, but this PR adds the suites as .spec.ts files alongside spec/dummy/playwright.config.ts. Please adjust the documentation so newcomers can copy/paste the correct TypeScript paths and config location.

🤖 Prompt for AI Agents
In CLAUDE.md around lines 272 to 345, the Playwright example paths and file
extensions are incorrect for the new layout; update all references from
spec/dummy/e2e/playwright/... to spec/dummy/playwright/... and change example
test filenames from .spec.js to .spec.ts, update the run command example to
reference playwright.config.ts in spec/dummy (or the new config location), and
adjust import paths in the code snippets to match the TypeScript helpers (e.g.,
.ts modules and correct relative paths) so the docs reflect the actual test
layout and can be copy/pasted.

│ │ └── using_scenarios.spec.js
│ └── app_commands/ # Rails helper commands
│ ├── clean.rb
│ ├── factory_bot.rb
│ ├── eval.rb
│ └── scenarios/
│ └── basic.rb
```
Comment on lines +334 to +353
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Test organization mixes Playwright and Cypress structures.

The documented directory structure shows:

  • Playwright-named directories (playwright/)
  • Cypress-specific infrastructure (app_commands/ at line 342, which belongs to cypress-on-rails)
  • Redundant path nesting (e2e/playwright/e2e/ - note "e2e" appears twice)

This inconsistent structure reflects the broader framework confusion in this PR. The actual directory structure should match whichever framework is genuinely being implemented.

Additionally, the path shown at line 268 (e2e/playwright/e2e/react_on_rails/basic_components.spec.js) has redundant "e2e" nesting that should be simplified.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

330-330: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In CLAUDE.md around lines 329 to 348, the documented test tree mixes Playwright
and Cypress conventions and contains redundant "e2e" nesting; remove the
cypress-on-rails specific items (e.g., app_commands/) if this project uses
Playwright, or conversely convert/playwright-named folders to Cypress
equivalents if using Cypress, and flatten the paths so there is a single "e2e"
level (e.g., change e2e/playwright/e2e/react_on_rails/... to
e2e/playwright/react_on_rails/... or to e2e/react_on_rails/... depending on
chosen layout); update the example paths in the doc (including the path shown at
line ~268) to reflect the chosen framework and flat structure and ensure all
entries consistently follow that layout.


### Best Practices

- Use `app('clean')` in `beforeEach` to ensure clean state
- Leverage Rails helpers (`appFactories`, `appEval`) instead of UI setup
- Test React on Rails specific features: SSR, hydration, component registry
- Use component IDs like `#ComponentName-react-component-0` for selectors
- Monitor console errors during tests
- Test across different browsers with `--project` flag

### Debugging

- Run in UI mode: `yarn test:e2e:ui`
- Use `page.pause()` to pause execution
- Check `playwright-report/` for detailed results after test failures
- Enable debug logging in `playwright.config.js`

### CI Integration

Playwright E2E tests run automatically in CI via GitHub Actions (`.github/workflows/playwright.yml`). The workflow:
- Runs on all PRs and pushes to master
- Uses GitHub Actions annotations for test failures
- Uploads HTML reports as artifacts (available for 30 days)
- Auto-starts Rails server before running tests

## IDE Configuration

Exclude these directories to prevent IDE slowdowns:

- `/coverage`, `/tmp`, `/gen-examples`, `/packages/react-on-rails/lib`
- `/node_modules`, `/spec/dummy/node_modules`, `/spec/dummy/tmp`
- `/spec/dummy/app/assets/webpack`, `/spec/dummy/log`
- `/spec/dummy/e2e/playwright-report`, `/spec/dummy/test-results`
1 change: 1 addition & 0 deletions Gemfile.development_dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ group :test do
gem "capybara"
gem "capybara-screenshot"
gem "coveralls", require: false
gem "cypress-on-rails", "~> 1.19"
gem "equivalent-xml"
gem "generator_spec"
gem "launchy"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ GEM
thor (>= 0.19.4, < 2.0)
tins (~> 1.6)
crass (1.0.6)
cypress-on-rails (1.19.0)
rack
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
Expand Down Expand Up @@ -418,6 +420,7 @@ DEPENDENCIES
capybara
capybara-screenshot
coveralls
cypress-on-rails (~> 1.19)
debug
equivalent-xml
gem-release
Expand Down
4 changes: 2 additions & 2 deletions bin/lefthook/eslint-lint
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ if [ -n "$root_and_packages_pro_files" ]; then
fi
printf " %s\n" $root_and_packages_pro_files

if ! yarn run eslint $root_and_packages_pro_files --report-unused-disable-directives --fix; then
if ! yarn run eslint $root_and_packages_pro_files --fix; then
exit_code=1
fi

Expand All @@ -53,7 +53,7 @@ if [ -n "$react_on_rails_pro_files" ]; then
# Strip react_on_rails_pro/ prefix for running in Pro directory
react_on_rails_pro_files_relative=$(echo "$react_on_rails_pro_files" | sed 's|^react_on_rails_pro/||')

if ! (cd react_on_rails_pro && yarn run eslint $react_on_rails_pro_files_relative --report-unused-disable-directives --fix); then
if ! (cd react_on_rails_pro && yarn run eslint $react_on_rails_pro_files_relative --fix); then
exit_code=1
fi

Expand Down
4 changes: 4 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ const config = tsEslint.config([
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
// Allow deprecated React APIs for backward compatibility with React < 18
'@typescript-eslint/no-deprecated': 'off',
// Allow unbound methods - needed for method reassignment patterns
'@typescript-eslint/unbound-method': 'off',
},
},
{
Expand Down
14 changes: 13 additions & 1 deletion knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,17 @@ const config: KnipConfig = {
'config/webpack/webpack.config.js',
// SWC configuration for Shakapacker
'config/swc.config.js',
// Playwright E2E test configuration and tests
'e2e/playwright.config.js',
'e2e/playwright/e2e/**/*.spec.js',
// CI workflow files that reference package.json scripts
'../../.github/workflows/playwright.yml',
],
ignore: [
'**/app-react16/**/*',
// Playwright support files and helpers - generated by cypress-on-rails gem
'e2e/playwright/support/**',
],
ignore: ['**/app-react16/**/*'],
project: ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!', 'config/webpack/*.js'],
paths: {
'Assets/*': ['client/app/assets/*'],
Expand All @@ -127,10 +136,13 @@ const config: KnipConfig = {
'node-libs-browser',
// The below dependencies are not detected by the Webpack plugin
// due to the config issue.
'css-loader',
'expose-loader',
'file-loader',
'imports-loader',
'null-loader',
'sass',
'sass-loader',
'sass-resources-loader',
'style-loader',
'url-loader',
Expand Down
1 change: 0 additions & 1 deletion packages/react-on-rails-pro/src/ClientSideRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ You should return a React.Component always for the client side entry point.`);
}

try {
// eslint-disable-next-line @typescript-eslint/no-deprecated
unmountComponentAtNode(domNode);
} catch (e: unknown) {
const error = e instanceof Error ? e : new Error('Unknown error');
Expand Down
2 changes: 0 additions & 2 deletions packages/react-on-rails-pro/src/createReactOnRailsPro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,11 @@ export default function createReactOnRailsPro(

if (reactOnRailsPro.streamServerRenderedReactComponent) {
reactOnRailsProSpecificFunctions.streamServerRenderedReactComponent =
// eslint-disable-next-line @typescript-eslint/unbound-method
reactOnRailsPro.streamServerRenderedReactComponent;
}

if (reactOnRailsPro.serverRenderRSCReactComponent) {
reactOnRailsProSpecificFunctions.serverRenderRSCReactComponent =
// eslint-disable-next-line @typescript-eslint/unbound-method
reactOnRailsPro.serverRenderRSCReactComponent;
}

Expand Down
1 change: 1 addition & 0 deletions spec/dummy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
playwright-report/
3 changes: 3 additions & 0 deletions spec/dummy/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ GEM
thor (>= 0.19.4, < 2.0)
tins (~> 1.6)
crass (1.0.6)
cypress-on-rails (1.19.0)
rack
date (3.4.1)
debug (1.9.2)
irb (~> 1.10)
Expand Down Expand Up @@ -412,6 +414,7 @@ DEPENDENCIES
capybara
capybara-screenshot
coveralls
cypress-on-rails (~> 1.19)
debug
equivalent-xml
generator_spec
Expand Down
Loading
Loading