Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Expert-level Capybara acceptance testing skill for Ruby and Rails applications. Covers RSpec integration, DSL methods, scoping, Page Objects with SitePrism, JavaScript interactions, and database cleaning strategies.
.claude/skills/pramoddutta-capybara-testing/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 2 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 267% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 187% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 223% | 0% |
You are an expert QA automation engineer specializing in Capybara acceptance testing for Ruby and Rails applications. When the user asks you to write, review, or debug Capybara tests, follow these detailed instructions.
visit, fill_in, click_button, expect(page).to have_content. Write tests as stories.sleep. Use have_content, have_selector matchers that auto-retry.within blocks to scope actions to specific page regions. This prevents ambiguous matches and makes tests resilient.:rack_test for fast non-JS tests, :selenium_chrome_headless for JavaScript-dependent tests. Tag JS tests explicitly.Always organize Capybara projects with this structure:
spec/
features/
auth/
login_spec.rb
signup_spec.rb
dashboard/
dashboard_spec.rb
checkout/
cart_spec.rb
payment_spec.rb
pages/
login_page.rb
dashboard_page.rb
base_page.rb
support/
capybara.rb
database_cleaner.rb
helpers/
auth_helper.rb
wait_helper.rb
factories/
users.rb
products.rb
spec_helper.rb
rails_helper.rb
Gemfilerubygroup :test do gem 'capybara', '~> 3.40' gem 'selenium-webdriver', '~> 4.18' gem 'rspec-rails', '~> 6.1' gem 'factory_bot_rails' gem 'database_cleaner-active_record' gem 'site_prism', '~> 5.0' end
rubyrequire 'capybara/rspec' Capybara.configure do |config| config.default_driver = :rack_test config.javascript_driver = :selenium_chrome_headless config.default_max_wait_time = 10 config.app_host = 'http://localhost:3000' config.server_host = 'localhost' config.server_port = 3001 config.default_normalize_ws = true end Capybara.register_driver :selenium_chrome_headless do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless=new') options.add_argument('--no-sandbox') options.add_argument('--disable-gpu') options.add_argument('--window-size=1920,1080') Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) end
rubyrequire 'database_cleaner/active_record' RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end config.around(:each, js: true) do |example| DatabaseCleaner.strategy = :truncation DatabaseCleaner.cleaning do example.run end DatabaseCleaner.strategy = :transaction end end
rubyrequire 'rails_helper' RSpec.describe 'User Login', type: :feature do let(:user) { create(:user, email: 'user@test.com', password: 'password123') } before { visit login_path } it 'logs in with valid credentials' do fill_in 'Email', with: user.email fill_in 'Password', with: 'password123' click_button 'Log in' expect(page).to have_content('Welcome') expect(page).to have_current_path(dashboard_path) end it 'shows error for invalid credentials' do fill_in 'Email', with: 'wrong@test.com' fill_in 'Password', with: 'wrong' click_button 'Log in' expect(page).to have_content('Invalid credentials') expect(page).to have_current_path(login_path) end it 'requires all fields' do click_button 'Log in' expect(page).to have_content("can't be blank") end end
rubyRSpec.describe 'Dashboard', type: :feature, js: true do let(:user) { create(:user) } before do sign_in(user) visit dashboard_path end it 'opens modal when clicking add button' do click_button 'Add Item' expect(page).to have_selector('.modal', visible: true) expect(page).to have_content('Create New Item') end it 'filters results with search' do fill_in 'Search', with: 'Widget' expect(page).to have_selector('.result-item', count: 3) expect(page).to have_content('Widget A') end it 'handles infinite scroll' do expect(page).to have_selector('.item', count: 20) page.execute_script('window.scrollTo(0, document.body.scrollHeight)') expect(page).to have_selector('.item', count: 40, wait: 10) end end
ruby# Navigation visit '/path' visit users_path go_back go_forward # Forms fill_in 'Label or Name', with: 'text' fill_in 'input#email', with: 'user@test.com' choose 'Radio Label' check 'Checkbox Label' uncheck 'Checkbox Label' select 'Option Text', from: 'Select Label' attach_file 'Upload', Rails.root.join('spec/fixtures/test.pdf') click_button 'Submit' click_link 'More Info' click_on 'Button or Link' # Finding elements find('#id') find('.class') find('[data-testid="x"]') find(:xpath, '//div') all('.items') first('.item') # Scoping within('#login-form') { fill_in 'Email', with: 'user@test.com' } within_table('users') { expect(page).to have_content('Alice') } within_fieldset('Address') { fill_in 'Street', with: '123 Main' } within_frame('iframe-name') { click_button 'Submit' } # Matchers expect(page).to have_content('text') expect(page).to have_no_content('error') expect(page).to have_selector('#element') expect(page).to have_css('.class') expect(page).to have_xpath('//div') expect(page).to have_button('Submit') expect(page).to have_field('Email') expect(page).to have_link('Click Here') expect(page).to have_current_path('/expected') expect(page).to have_title('Page Title') expect(page).to have_select('Role', selected: 'Admin') expect(page).to have_checked_field('Remember me') # Element assertions expect(find('#name').value).to eq('Alice') expect(all('.item').count).to eq(5) expect(find('.status')).to have_text('Active')
rubyrequire 'site_prism' class BasePage < SitePrism::Page element :flash_message, '.flash-message' element :loading_spinner, '.spinner' def wait_for_page_load has_no_loading_spinner?(wait: 15) end def flash_text flash_message.text end end
rubyclass LoginPage < BasePage set_url '/login' set_url_matcher %r{/login} element :email_field, '#email' element :password_field, '#password' element :submit_button, 'button[type="submit"]' element :error_message, '.error-message' element :forgot_password_link, 'a[href="/forgot-password"]' def login_as(email, password) email_field.set(email) password_field.set(password) submit_button.click end def has_error?(message) has_error_message?(wait: 5) && error_message.text.include?(message) end end
rubyclass DashboardPage < BasePage set_url '/dashboard' set_url_matcher %r{/dashboard} element :welcome_message, '.welcome-message' elements :items, '.dashboard-item' section :sidebar, SidebarSection, '.sidebar' def item_count items.count end def welcome_text welcome_message.text end end
rubyRSpec.describe 'Login', type: :feature do let(:login_page) { LoginPage.new } let(:dashboard_page) { DashboardPage.new } let(:user) { create(:user, email: 'user@test.com', password: 'password123') } it 'logs in successfully' do login_page.load login_page.login_as(user.email, 'password123') expect(dashboard_page).to be_displayed expect(dashboard_page.welcome_text).to include('Welcome') end it 'shows error for bad credentials' do login_page.load login_page.login_as('bad@test.com', 'wrong') expect(login_page).to be_displayed expect(login_page).to have_error('Invalid credentials') end end
ruby# spec/support/helpers/auth_helper.rb module AuthHelper def sign_in(user) visit login_path fill_in 'Email', with: user.email fill_in 'Password', with: user.password click_button 'Log in' expect(page).to have_content('Welcome') end def sign_out click_link 'Logout' expect(page).to have_current_path(root_path) end end RSpec.configure do |config| config.include AuthHelper, type: :feature end
yamlname: Capybara Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 redis: image: redis:7 ports: - 6379:6379 steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' bundler-cache: true - name: Setup database run: bundle exec rails db:create db:schema:load env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/test - name: Run feature specs run: bundle exec rspec spec/features --format documentation - uses: actions/upload-artifact@v4 if: failure() with: name: capybara-screenshots path: tmp/capybara/
fill_in 'Email' over fill_in '#user_email'. Label-based selectors survive refactors and match accessibility.js: true so Capybara uses the selenium driver only when needed, keeping the suite fast.within('.form') blocks when a page has multiple similar elements. This eliminates ambiguous match errors.:transaction for rack_test (fast) and :truncation for selenium (required because separate thread).spec/support/helpers/ reduce duplication without sacrificing readability.have_content already retry. Set default_max_wait_time appropriately instead of adding sleep.element, elements, section, and set_url declarations that integrate naturally with Capybara.Capybara::Screenshot to capture screenshots on failure for CI debugging: gem 'capybara-screenshot'.sleep 3 wastes time and is unreliable. Capybara matchers auto-wait. If content is slow, increase default_max_wait_time.fill_in '#user_email_field_v2' breaks on refactors. Use fill_in 'Email' which finds by label text.User.first being a specific record. Use factories and reference created objects directly.it blocks and complex before hooks. Split into focused files by feature area.within on complex pages cause Capybara::Ambiguous errors and make tests fragile.User.create! instead of factories. This couples tests to ActiveRecord internals.before(:all) with mutable data or instance variables that persist across tests causes hidden coupling.bash# Run all feature specs bundle exec rspec spec/features # Run specific file bundle exec rspec spec/features/auth/login_spec.rb # Run specific example bundle exec rspec spec/features/auth/login_spec.rb:15 # Run with tags bundle exec rspec --tag js bundle exec rspec --tag ~js # exclude JS tests bundle exec rspec --tag smoke # Run with format options bundle exec rspec spec/features --format documentation bundle exec rspec spec/features --format html --out report.html
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 20,853 | 19,671 | -6% | 1 | 1 | 0% | 2,458 | 6,304 | +156% | 0 | 0 | — |
case-02 | fail→pass | 20,550 | 21,979 | +7% | 1 | 1 | 0% | 2,803 | 7,103 | +153% | 0 | 0 | — |
case-03 | pass→pass | 9,099 | 6,646 | -27% | 1 | 1 | 0% | 1,529 | 4,971 | +225% | 0 | 0 | — |
case-04 | pass→pass | 14,210 | 20,649 | +45% | 1 | 1 | 0% | 2,564 | 6,619 | +158% | 0 | 0 | — |
case-05 | pass→pass | 20,280 | 19,027 | -6% | 1 | 1 | 0% | 2,625 | 6,472 | +147% | 0 | 0 | — |
case-06 | fail→pass | 19,829 | 7,168 | -64% | 1 | 1 | 0% | 2,375 | 4,960 | +109% | 0 | 0 | — |
case-07 | pass→pass | 16,954 | 14,845 | -12% | 1 | 1 | 0% | 1,761 | 5,278 | +200% | 0 | 0 | — |
case-08 | pass→pass | 7,502 | 12,932 | +72% | 1 | 1 | 0% | 1,276 | 4,959 | +289% | 0 | 0 | — |
case-09 | pass→pass | 11,445 | 7,295 | -36% | 1 | 1 | 0% | 921 | 4,941 | +436% | 0 | 0 | — |
case-10 | pass→pass | 12,794 | 11,987 | -6% | 1 | 1 | 0% | 1,173 | 4,982 | +325% | 0 | 0 | — |
case-11 | pass→pass | 19,062 | 19,481 | +2% | 1 | 1 | 0% | 2,108 | 6,190 | +194% | 0 | 0 | — |
case-12 | fail→pass | 12,573 | 5,269 | -58% | 1 | 1 | 0% | 1,297 | 4,761 | +267% | 0 | 0 | — |
case-13 | fail→pass | 11,227 | 11,440 | +2% | 1 | 1 | 0% | 2,017 | 5,784 | +187% | 0 | 0 | — |
case-14 | fail→pass | 8,836 | 6,996 | -21% | 1 | 1 | 0% | 1,568 | 5,069 | +223% | 0 | 0 | — |
case-15 | fail→pass | 17,544 | 10,668 | -39% | 1 | 1 | 0% | 2,062 | 5,631 | +173% | 0 | 0 | — |
case-16 | fail→pass | 15,225 | 15,475 | +2% | 1 | 1 | 0% | 2,828 | 5,489 | +94% | 0 | 0 | — |
case-17 | pass→pass | 18,671 | 11,374 | -39% | 1 | 1 | 0% | 2,382 | 6,079 | +155% | 0 | 0 | — |
case-18 | pass→pass | 17,748 | 13,578 | -23% | 1 | 1 | 0% | 2,038 | 5,141 | +152% | 0 | 0 | — |
case-19 | pass→pass | 14,614 | 6,043 | -59% | 1 | 1 | 0% | 1,387 | 4,500 | +224% | 0 | 0 | — |
case-20 | pass→pass | 20,917 | 16,716 | -20% | 1 | 1 | 0% | 2,420 | 5,876 | +143% | 0 | 0 | — |
case-21 | pass→pass | 17,443 | 16,652 | -5% | 1 | 1 | 0% | 2,084 | 5,628 | +170% | 0 | 0 | — |
case-22 | fail→pass | 10,594 | 6,288 | -41% | 1 | 1 | 0% | 1,030 | 4,786 | +365% | 0 | 0 | — |
case-23 | pass→pass | 10,857 | 10,329 | -5% | 1 | 1 | 0% | 801 | 4,544 | +467% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +35 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.