├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── capybara_active_admin.gemspec ├── docs ├── .vuepress │ ├── config.js │ ├── public │ │ └── .keep │ └── styles │ │ └── index.styl ├── README.md └── guide │ └── README.md ├── lib ├── capybara │ ├── active_admin.rb │ └── active_admin │ │ ├── actions.rb │ │ ├── actions │ │ ├── attributes_table.rb │ │ ├── form.rb │ │ ├── layout.rb │ │ └── table.rb │ │ ├── finders.rb │ │ ├── finders │ │ ├── attributes_table.rb │ │ ├── form.rb │ │ ├── layout.rb │ │ └── table.rb │ │ ├── matchers.rb │ │ ├── matchers │ │ ├── attributes_table.rb │ │ ├── form.rb │ │ ├── layout.rb │ │ └── table.rb │ │ ├── rspec.rb │ │ ├── selectors.rb │ │ ├── selectors │ │ ├── attributes_table.rb │ │ ├── form.rb │ │ ├── layout.rb │ │ └── table.rb │ │ ├── test_helpers.rb │ │ ├── util.rb │ │ └── version.rb └── capybara_active_admin.rb ├── package.json ├── spec ├── dummy │ ├── assets │ │ ├── active_admin.js │ │ └── active_admin.scss │ ├── log │ │ └── .keep │ └── test_application.rb ├── lib │ └── capybara_active_admin_spec.rb ├── rails_helper.rb └── system │ ├── business_employee_show_spec.rb │ ├── business_employees_index_spec.rb │ ├── users_index_spec.rb │ └── users_new_spec.rb └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /spec/dummy/tmp/ 10 | /spec/dummy/log/*.log 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | 15 | Gemfile.lock 16 | 17 | # docs 18 | /docs/.vuepress/dist/ 19 | /docs/.vuepress/public/api/ 20 | /node_modules/ 21 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require rails_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | TargetRubyVersion: 2.3 4 | Exclude: 5 | - vendor/**/* 6 | - tmp/**/* 7 | - docs/**/* 8 | 9 | # Capybara/RSpec uses have_ had_ as name convention for matchers. 10 | Naming/PredicateName: 11 | Enabled: false 12 | 13 | Lint/RaiseException: 14 | Enabled: true 15 | 16 | Lint/StructNewOverride: 17 | Enabled: true 18 | 19 | Layout/LineLength: 20 | Max: 180 21 | 22 | Style/SymbolArray: 23 | EnforcedStyle: brackets 24 | 25 | Style/Lambda: 26 | EnforcedStyle: literal 27 | 28 | Naming/MethodParameterName: 29 | AllowedNames: 30 | - id 31 | 32 | Style/HashEachMethods: 33 | Enabled: true 34 | 35 | Style/HashTransformKeys: 36 | Enabled: true 37 | 38 | Style/HashTransformValues: 39 | Enabled: true 40 | 41 | Style/Documentation: 42 | Enabled: false 43 | 44 | Metrics/PerceivedComplexity: 45 | Enabled: false 46 | 47 | Metrics/MethodLength: 48 | Enabled: false 49 | 50 | Metrics/AbcSize: 51 | Enabled: false 52 | 53 | Metrics/ModuleLength: 54 | Enabled: false 55 | 56 | Metrics/BlockLength: 57 | Enabled: false 58 | 59 | Metrics/ClassLength: 60 | Enabled: false 61 | 62 | Metrics/CyclomaticComplexity: 63 | Enabled: false 64 | 65 | Layout/MultilineOperationIndentation: 66 | Enabled: false 67 | 68 | Layout/FirstHashElementIndentation: 69 | Enabled: false 70 | 71 | Layout/FirstArrayElementIndentation: 72 | Enabled: false 73 | 74 | Layout/FirstArgumentIndentation: 75 | Enabled: false 76 | 77 | Layout/ClosingParenthesisIndentation: 78 | Enabled: false 79 | 80 | Layout/ArgumentAlignment: 81 | Enabled: false 82 | 83 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # test config on https://config.travis-ci.com/explore 3 | language: ruby 4 | cache: bundler 5 | before_install: 6 | - gem install bundler -v 2.2.32 7 | stages: 8 | - test 9 | - deploy 10 | jobs: 11 | include: 12 | - name: "Test Ruby-3.0.3 Rails-6.1" 13 | stage: test 14 | rvm: 3.0.3 15 | env: RAILS_VERSION="~> 6.1" 16 | before_script: bundle install 17 | script: bundle exec rake default 18 | - name: "Test Ruby-2.5.7 Rails-6.0" 19 | stage: test 20 | rvm: 2.5.7 21 | env: RAILS_VERSION="~> 6.0" 22 | before_script: bundle install 23 | script: bundle exec rake default 24 | - name: "Test Ruby-2.5.7 Rails-5.2" 25 | stage: test 26 | rvm: 2.5.7 27 | env: RAILS_VERSION="~> 5.2" 28 | before_script: bundle install 29 | script: bundle exec rake default 30 | - name: "Test Ruby-2.3.8 Rails-5.2" 31 | stage: test 32 | rvm: 2.3.8 33 | env: RAILS_VERSION="~> 5.2" 34 | before_script: bundle install 35 | script: bundle exec rake default 36 | - name: "Deploy documentation" 37 | stage: deploy 38 | before_script: yarn install 39 | script: 40 | - bundle exec rake yard 41 | - yarn docs:build 42 | deploy: 43 | provider: pages 44 | edge: true 45 | verbose: true 46 | cleanup: false 47 | local_dir: docs/.vuepress/dist 48 | token: $GITHUB_TOKEN 49 | keep_history: true 50 | on: 51 | branch: master 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.3] - 2020-04-17 10 | ### Changed 11 | - `batch_action_selector`, `click_batch_action` finds element by link text 12 | 13 | ### Added 14 | - `select_table_row`, `open_batch_action_menu` actions 15 | - `have_batch_action` matcher 16 | - tests for batch actions 17 | 18 | ## [0.3.2] - 2020-04-16 19 | ### Changed 20 | - remove model_name from table related DSL arguments 21 | - improve API 22 | 23 | ## [0.3.1] - 2020-04-15 24 | ### Added 25 | - `within_attribute_row` finder 26 | 27 | ### Removed 28 | - `find_input` finder 29 | - `have_input`, `have_no_input` matchers 30 | - `input_selector` selector 31 | 32 | ## [0.3.0] - 2020-04-15 33 | ### Added 34 | - implement DSL for tabs, batch actions, modal dialog, attributes table, panel, sidebar, footer 35 | - improve form DSL 36 | 37 | ## [0.2.1] - 2020-04-14 38 | ### Fixed 39 | - `table_selector`, `within_table_for` incorrect resource name 40 | 41 | ### Changed 42 | - `table_selector` receives 2 arguments 43 | - `within_table_for` receives 2 arguments 44 | - `current_table_name` renamed to `current_table_model_name` 45 | - `table_row_selector` can receive modal class as model name 46 | 47 | # Added 48 | - `have_table` matcher 49 | - add tests for table with namespaced resource class and multi-word resource name 50 | 51 | ## [0.2.0] - 2020-04-14 52 | ### Changed 53 | - rename `have_table_col` to `have_table_cell` 54 | - change options of `have_table_cell` 55 | - change options of `have_table_row` 56 | - refactor modules hierarchy: split DSL into selectors, finders, matchers and actions 57 | 58 | ### Added 59 | - form DSL 60 | - changelog 61 | - follow semver 62 | 63 | ## [0.1.0] - 2020-04-14 64 | ### Added 65 | - travic CI 66 | - rubocop 67 | - capybara DSL to check page title, action items, table columns/rows 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at senid231@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in capybara_active_admin.gemspec 6 | gemspec 7 | 8 | gem 'capybara' 9 | gem 'selenium-webdriver' 10 | # https://github.com/mattheworiordan/capybara-screenshot/issues/225#issuecomment-409407825 11 | gem 'cuprite' 12 | gem 'puma' 13 | gem 'rake', '~> 12.0' 14 | gem 'rspec-rails', '~> 4.0' 15 | gem 'rubocop', '~> 0.81.0', require: false 16 | gem 'yard', require: false 17 | 18 | gem 'activeadmin', ENV.fetch('ACTIVE_ADMIN_VERSION', '~> 2.0'), require: false 19 | gem 'rails', ENV.fetch('RAILS_VERSION', '6.0.0') 20 | 21 | # responders 3 drops ruby 2.3 support 22 | if RUBY_VERSION =~ /^2\.3/ 23 | gem 'responders', '~> 2.4' 24 | else 25 | gem 'responders' # rubocop:disable Bundler/DuplicatedGem 26 | end 27 | 28 | gem 'sassc-rails', '2.1.2' 29 | gem 'sprockets', '3.7.2' 30 | gem 'sqlite3' 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Denis Talakevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capybara Active Admin 2 | 3 | [![Build Status](https://travis-ci.com/activeadmin-plugins/capybara_active_admin.svg?branch=master)](https://travis-ci.com/activeadmin-plugins/capybara_active_admin) [![Gem Version](https://badge.fury.io/rb/capybara_active_admin.svg)](https://badge.fury.io/rb/capybara_active_admin) [![Downloads](https://img.shields.io/gem/dt/capybara_active_admin)](https://rubygems.org/gems/capybara_active_admin) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Capybara DSL for fast and easy testing Active Admin applications. 6 | 7 | Check out our docs at [activeadmin-plugins.github.io/capybara_active_admin](https://activeadmin-plugins.github.io/capybara_active_admin) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | group :test do 15 | gem 'capybara_active_admin' 16 | end 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle install 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install capybara_active_admin 26 | 27 | **Note: `capybara_active_admin` should be required after `capybara`.** 28 | 29 | ## Usage 30 | 31 | `rails_helper.rb` 32 | ```ruby 33 | require 'capybara/active_admin/rspec' 34 | ``` 35 | 36 | `spec/system/users_spec.rb` 37 | ```ruby 38 | RSpec.describe 'Users', js: true do 39 | subject do 40 | visit admin_users_path 41 | end 42 | 43 | let!(:john) { User.create!(full_name: 'John Doe') } 44 | let!(:jane) { User.create!(full_name: 'Jane Air') } 45 | 46 | it 'have john and jane in users table' do 47 | subject 48 | 49 | expect(page).to have_action_item('New User') 50 | expect(page).to_not have_action_item('Edit User') 51 | 52 | within_table_for('users') do 53 | expect(page).to have_table_row(count: 2) 54 | expect(page).to have_table_cell('John Doe') 55 | 56 | within_table_row(id: john.id) do 57 | expect(page).to have_table_cell('John Doe', row_id: john.id) 58 | expect(page).to have_table_cell('John Doe', row_id: john.id, col_name: 'Full Name') 59 | expect(page).to_not have_table_cell('John Doe', row_id: john.id, col_name: 'Id') 60 | end 61 | 62 | within_table_row(id: jane.id) do 63 | expect(page).to_not have_table_cell('John Doe') 64 | expect(page).to_not have_table_cell('John Doe', col_name: 'Full Name') 65 | end 66 | end 67 | end 68 | 69 | it 'creates user' do 70 | subject 71 | 72 | click_action_item('New User') 73 | expect(page).to have_current_path(new_admin_user_path) 74 | 75 | within_form_for(User) do 76 | fill_in 'Full name', with: 'Johny Cage' 77 | click_submit 'Create User' 78 | end 79 | 80 | expect(page).to have_flash_message('User was successfully created.', type: :notice) 81 | user = User.last! 82 | expect(page).to have_current_path admin_user_path(user.id) 83 | 84 | expect(User.count).to eq(1) 85 | expect(user).to have_attributes(full_name: 'Johny Cage') 86 | end 87 | end 88 | ``` 89 | 90 | See `spec/support` for more user examples. 91 | See `capybara/active_admin/test_helpers.rb` for available DSL methods. 92 | 93 | ## Development 94 | 95 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 96 | 97 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 98 | 99 | ## Contributing 100 | 101 | Bug reports and pull requests are welcome on GitHub at https://github.com/activeadmin-plugins/capybara_active_admin. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/activeadmin-plugins/capybara_active_admin/blob/master/CODE_OF_CONDUCT.md). 102 | 103 | ## License 104 | 105 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 106 | 107 | ## Code of Conduct 108 | 109 | Everyone interacting in the Capybara::ActiveAdmin project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/activeadmin-plugins/capybara_active_admin/blob/master/CODE_OF_CONDUCT.md). 110 | 111 | ## Notes 112 | 113 | Project uses [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention. 114 | Project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 115 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | require 'yard' 7 | require 'yard/rake/yardoc_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | RuboCop::RakeTask.new(:rubocop) 11 | YARD::Rake::YardocTask.new(:yard) do |task| 12 | task.options += [ 13 | %(--output-dir=./docs/.vuepress/public/api/), 14 | %(--title=Capybara Active Admin API Reference) 15 | ] 16 | end 17 | 18 | task default: [:rubocop, :spec] 19 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'capybara_active_admin' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /capybara_active_admin.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/capybara/active_admin/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'capybara_active_admin' 7 | spec.version = Capybara::ActiveAdmin::VERSION 8 | spec.authors = ['Denis Talakevich'] 9 | spec.email = ['senid231@gmail.com'] 10 | 11 | spec.summary = 'Capybara DSL for fast and easy testing Active Admin applications.' 12 | spec.description = 'Capybara DSL for fast and easy testing Active Admin applications.' 13 | spec.homepage = 'https://github.com/active_admin_plugins/capybara_active_admin' 14 | spec.license = 'MIT' 15 | spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0') 16 | 17 | spec.metadata['homepage_uri'] = spec.homepage 18 | spec.metadata['source_code_uri'] = spec.homepage 19 | spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md" 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | end 26 | spec.bindir = 'exe' 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ['lib'] 29 | 30 | spec.add_dependency 'activeadmin' 31 | # spec.add_dependency 'devise' 32 | spec.add_dependency 'rspec', '~> 3.0' 33 | end 34 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Capybara Active Admin', 3 | description: 'Capybara DSL for fast and easy testing Active Admin applications.', 4 | base: '/capybara_active_admin/', 5 | themeConfig: { 6 | nav: [ 7 | { text: 'Guide', link: '/guide/' }, 8 | { text: 'API Reference', link: '/api/', target: '_blank' }, 9 | { text: 'GitHub', link: 'https://github.com/activeadmin-plugins/capybara_active_admin' } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vuepress/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activeadmin-plugins/capybara_active_admin/d2cdc2c0a5478d4ee1afb30f3465c7be3b760a86/docs/.vuepress/public/.keep -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | div.theme-default-content:not(.custom) { 2 | max-width: 75%; 3 | } 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: Get Started → 4 | actionLink: /guide/ 5 | footer: MIT Licensed | Copyright © 2020-present Denis Talakevich 6 | --- 7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 |

Fast Test Writing

15 |

Allow to focus on what you want to test instead of how to do it.

16 |
17 |
18 |

Simplicity

19 |

Following Ruby an Capybara naming conventions allows easily to read and understand tests code.

20 |
21 |
22 |

Customizable

23 |

You can easy extend, customize, and mix parts of library to fit your needs.

24 |
25 |
26 | 27 | ### As Easy as 1, 2, 3 28 | 29 | ```bash 30 | # install 31 | gem install capybara_active_admin 32 | # OR echo "gem 'capybara_active_admin', group: :test, require: false" >> Gemfile 33 | ``` 34 | ```ruby 35 | # require in in rspec/rails_helper.rb 36 | require 'capybara/active_admin/rspec' 37 | ``` 38 | 39 | ::: warning COMPATIBILITY NOTE 40 | requires Ruby >= 2.3. 41 | ::: 42 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 4 | 5 | `Capybara::ActiveAdmin` created to write tests as fast as `ActiveAdmin` resource which they are test. 6 | 7 | ## Getting Started 8 | 9 | ### Installation 10 | 11 | ```ruby 12 | # Gemfile 13 | 14 | gem 'capybara_active_admin', group: :test, require: false 15 | ``` 16 | 17 | ```ruby 18 | # spec/rails_helper.rb 19 | 20 | # after require 'rspec' or 'rspec/rails' 21 | require 'capybara/active_admin/rspec' 22 | ``` 23 | 24 | ### Usage 25 | 26 | For example we have such database table 27 | ```ruby 28 | create_table :users do |t| 29 | t.string :full_name 30 | t.timestamps 31 | end 32 | ``` 33 | And such `ActiveRecord` model 34 | ```ruby 35 | class User < ActiveRecord::Base 36 | validates :full_name, presence: true 37 | end 38 | ``` 39 | And there is our `ActiveAdmin` resource 40 | ```ruby 41 | ActiveAdmin.register User do 42 | permit_params :full_name 43 | end 44 | ``` 45 | 46 | Our test can look like below 47 | ```ruby 48 | # spec/system/users_spec.rb 49 | 50 | RSpec.describe 'Users' do 51 | 52 | describe 'index page' do 53 | subject do 54 | visit admin_users_path 55 | end 56 | 57 | it 'have no table' do 58 | subject 59 | 60 | expect(page).to have_text('There are no Users yet.') 61 | expect(page).to have_action_item('New User') 62 | expect(page).to_not have_action_item('Edit User') 63 | expect(page).to_not have_table 64 | end 65 | 66 | context 'with users' do 67 | let!(:users) do 68 | [ 69 | User.create!(full_name: 'John Doe'), 70 | User.create!(full_name: 'Jane Air') 71 | ] 72 | end 73 | 74 | it 'have correct table rows' do 75 | subject 76 | 77 | # you can check that table is visible on page 78 | expect(page).to have_table 79 | 80 | # checkout data within table, 81 | # same as `within(table_selector) do` 82 | within_table_for do 83 | # check how many rows in table (tr) 84 | expect(page).to have_table_row(count: 2) 85 | # or cells (td) 86 | expect(page).to have_table_cell(count: 10) 87 | # or check cell of specific column contain specific value, 88 | # accepts same options as `have_selector`, such as :text, :exact_text, :count, etc. 89 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 90 | 91 | # we can check data inside specific row by record id, 92 | # or we can find it by index in table `within_table_row(index: 0) do`. 93 | within_table_row(id: users.first.id) do 94 | # default columns for users table are id, full_name, created_at, updated_at, actions. 95 | expect(page).to have_table_cell(count: 5) 96 | expect(page).to have_table_cell(text: 'John Doe') 97 | # or you can write 98 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 99 | # negate matcher 100 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id') 101 | end 102 | 103 | within_table_row(id: users.second.id) do 104 | expect(page).to have_table_cell(text: users.second.id, column: 'Id') 105 | expect(page).to have_table_cell(text: 'Jane Air', column: 'Full Name') 106 | expect(page).to_not have_table_cell(text: 'John Doe') 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | ``` 114 | -------------------------------------------------------------------------------- /lib/capybara/active_admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/active_admin/version' 4 | require 'capybara/active_admin/util' 5 | require 'capybara/active_admin/selectors' 6 | require 'capybara/active_admin/finders' 7 | require 'capybara/active_admin/matchers' 8 | require 'capybara/active_admin/actions' 9 | require 'capybara/active_admin/test_helpers' 10 | 11 | module Capybara 12 | module ActiveAdmin 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/actions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/active_admin/actions/layout' 4 | require 'capybara/active_admin/actions/table' 5 | require 'capybara/active_admin/actions/attributes_table' 6 | require 'capybara/active_admin/actions/form' 7 | 8 | module Capybara 9 | module ActiveAdmin 10 | module Actions 11 | # Actions are interactions with page that change something (click button, fill field, etc). 12 | # Good method names starts with *click_*, *scroll_*, *fill_*, *clear_*, *switch_*, *open_*. 13 | 14 | include Actions::Layout 15 | include Actions::Table 16 | include Actions::AttributesTable 17 | include Actions::Form 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/actions/attributes_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Actions 6 | module AttributesTable 7 | # todo 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/actions/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Actions 6 | module Form 7 | def click_submit(value, options = {}) 8 | selector = form_submit_selector(value) 9 | find(selector, **options).click 10 | end 11 | 12 | def fill_in_file(label, options = {}) 13 | path = options.delete(:with) 14 | attach_file(label, path) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/actions/layout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Actions 6 | # Actions for common Active Admin components. 7 | module Layout 8 | def click_action_item(title, options = {}) 9 | within(action_items_container_selector) do 10 | click_link(title, **options) 11 | end 12 | end 13 | 14 | def switch_tab(tab_name, options = {}) 15 | opts = Util.options_with_text(tab_name, options) 16 | find(tab_header_link_selector, **opts).click 17 | end 18 | 19 | def click_batch_action(title, exact: true) 20 | open_batch_action_menu 21 | within(dropdown_list_selector) do 22 | selector = batch_action_selector 23 | opts = Util.options_with_text(title, exact: exact) 24 | find(selector, **opts).click 25 | end 26 | end 27 | 28 | def open_batch_action_menu 29 | return if find_all(dropdown_list_selector).present? 30 | 31 | find(batch_actions_button_selector).click 32 | end 33 | 34 | def confirm_modal_dialog 35 | within_modal_dialog { click_button 'OK' } 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/actions/table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Actions 6 | module Table 7 | def select_table_row(id: nil, index: nil) 8 | raise ArgumentError, "can't use both :id and :index" if id && index 9 | raise ArgumentError, 'must provide :id or :index' if id.nil? && index.nil? 10 | 11 | if id 12 | find("input#batch_action_item_#{id}").click 13 | return 14 | end 15 | 16 | selector = %(input[id^="batch_action_item_"]) 17 | find_all(selector, minimum: index + 1)[index].click 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/finders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/active_admin/finders/layout' 4 | require 'capybara/active_admin/finders/table' 5 | require 'capybara/active_admin/finders/attributes_table' 6 | require 'capybara/active_admin/finders/form' 7 | 8 | module Capybara 9 | module ActiveAdmin 10 | # Finders are methods that find DOM element(s) or change current scope to node element. 11 | # 12 | # Find element(s) method names should start with *find_*. 13 | # 14 | # Change current scope method names should start with *within_*. 15 | module Finders 16 | include Finders::Layout 17 | include Finders::Table 18 | include Finders::AttributesTable 19 | include Finders::Form 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/finders/attributes_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Finders 6 | # Finder methods for ActiveAdmin attributes_table_for can be found here. 7 | # @see Capybara::ActiveAdmin::Finders base finders module. 8 | module AttributesTable 9 | # Calls block within attributes table. 10 | # @param model [Class, nil] model name or class. 11 | # @param id [String, Numeric, nil] record ID. 12 | # @yield within attributes table. 13 | def within_attributes_table_for(model: nil, id: nil) 14 | selector = attributes_table_selector(model: model, id: id) 15 | within(selector) { yield } 16 | end 17 | 18 | # Calls block within attributes table row. 19 | # @param label [String] row label. 20 | # @yield within attributes table. 21 | def within_attribute_row(label) 22 | selector = attributes_row_selector(label) 23 | within(selector) { yield } 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/finders/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Finders 6 | # Finders for *active_admin_form_for* and related form components. 7 | module Form 8 | # @param model_name [Class, String, nil] form record class or model name (default nil). 9 | # @yield within form 10 | def within_form_for(model_name = nil) 11 | selector = form_selector(model_name) 12 | within(selector) { yield } 13 | end 14 | 15 | # @param association_name [String] 16 | # @param index [String] index of fieldset, starts with 0 (default 0). 17 | # @yield within fieldset>ol 18 | def within_form_has_many(association_name, index: 0) 19 | selector = has_many_fields_selector(association_name) 20 | fieldset = find_all(selector, minimum: index + 1)[index] 21 | 22 | within(fieldset) { yield } 23 | end 24 | 25 | # @param association_name [String] 26 | # @yield within container have_many by passed association_name 27 | def within_has_many(association_name) 28 | selector = has_many_container_selector(association_name) 29 | within(selector) { yield } 30 | end 31 | 32 | # @yield within filters container. 33 | def within_filters 34 | selector = filter_form_selector 35 | within(selector) { yield } 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/finders/layout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Finders 6 | # Finders for common Active Admin components. 7 | module Layout 8 | def find_footer(options = {}) 9 | selector = footer_selector 10 | have_selector(selector, **options) 11 | end 12 | 13 | def within_tab_body 14 | selector = tab_content_selector 15 | within(selector) { yield } 16 | end 17 | 18 | def within_sidebar(title, exact: nil) 19 | selector = sidebar_selector 20 | 21 | within(selector) do 22 | within_panel(title, exact: exact) { yield } 23 | end 24 | end 25 | 26 | def within_panel(title, exact: nil) 27 | title_selector = "#{panel_selector} > #{panel_title_selector}" 28 | title_opts = Util.options_with_text(title, exact: exact) 29 | panel_title = find(title_selector, **title_opts) 30 | panel_content = panel_title.sibling(panel_content_selector) 31 | 32 | within(panel_content) { yield } 33 | end 34 | 35 | def within_modal_dialog 36 | within(modal_dialog_selector) { yield } 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/finders/table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Finders 6 | # Finders for *table_for*, it's rows and cells. 7 | module Table 8 | # @param resource_name [String, nil] resource name of index page. 9 | # @yield within table 10 | def within_table_for(resource_name = nil) 11 | selector = table_selector(resource_name) 12 | 13 | within(selector) { yield } 14 | end 15 | 16 | # id [String, Integer, nil] record ID. 17 | # index [Integer] row index in table (starts with 0). 18 | # @yield within table>tbody>tr 19 | def within_table_row(id: nil, index: nil) 20 | row = find_table_row(id: id, index: index) 21 | within(row) { yield } 22 | end 23 | 24 | def find_table_row(id: nil, index: nil) 25 | raise ArgumentError, "can't use both :id and :index" if id && index 26 | raise ArgumentError, 'must provide :id or :index' if id.nil? && index.nil? 27 | 28 | if id 29 | selector = table_row_selector(id) 30 | return find(selector) 31 | end 32 | 33 | selector = table_row_selector(nil) 34 | find_all(selector, minimum: index + 1)[index] 35 | end 36 | 37 | # @yield within table>tbody>tr>td 38 | def within_table_cell(name) 39 | cell = find_table_cell(name) 40 | within(cell) { yield } 41 | end 42 | 43 | def find_table_cell(column) 44 | selector = table_cell_selector(column) 45 | find(selector) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/active_admin/matchers/layout' 4 | require 'capybara/active_admin/matchers/table' 5 | require 'capybara/active_admin/matchers/attributes_table' 6 | require 'capybara/active_admin/matchers/form' 7 | 8 | module Capybara 9 | module ActiveAdmin 10 | module Matchers 11 | # RSpec matchers should be putted here. 12 | 13 | include Matchers::Layout 14 | include Matchers::Table 15 | include Matchers::AttributesTable 16 | include Matchers::Form 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/matchers/attributes_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Matchers 6 | module AttributesTable 7 | def have_attributes_table(options = {}) 8 | model = options.delete(:model) 9 | id = options.delete(:id) 10 | selector = attributes_table_selector(model: model, id: id) 11 | have_selector(selector, **options) 12 | end 13 | 14 | def have_attribute_row(label, options = {}) 15 | selector = attributes_row_selector(label) 16 | have_selector(selector, **options) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/matchers/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Matchers 6 | module Form 7 | def have_form_error(text, options = {}) 8 | field = options.delete(:field) 9 | opts = Util.options_with_text(text, options) 10 | li_selector = input_container_selector field, **options.slice(:exact) 11 | 12 | have_selector("#{li_selector} #{inline_error_selector}", **opts) 13 | end 14 | 15 | def have_no_form_errors(options = {}) 16 | field = options.delete(:field) 17 | li_selector = input_container_selector field, **options.slice(:exact) 18 | 19 | have_none_of_selectors(:css, "#{li_selector} #{inline_error_selector}", **options) 20 | end 21 | 22 | def have_semantic_error(text, options = {}) 23 | opts = Util.options_with_text(text, options) 24 | have_selector(semantic_error_selector, **opts) 25 | end 26 | 27 | def have_semantic_errors(options = {}) 28 | have_selector(semantic_error_selector, **options) 29 | end 30 | 31 | def have_has_many_fields_for(association_name, options = {}) 32 | selector = has_many_fields_selector(association_name) 33 | have_selector(selector, **options) 34 | end 35 | 36 | # @param text [String] button title 37 | # @param options [Hash] 38 | # @option selector [String, nil] optional selector to append 39 | # @option disabled [Boolean] button disabled or not (default false) 40 | # @example 41 | # expect(page).to have_submit_input('Submit') # check that submit input is present 42 | # expect(page).to have_submit_input('Submit', selector: '.custom-class') # check custom class presence 43 | # expect(page).to have_submit_input('Submit', disabled: true) # check that submit input is disabled 44 | # expect(page).to have_submit_input('Submit', disabled: true, count: 0) # check that submit input is enabled 45 | # 46 | def have_submit_input(text, options = {}) 47 | selector = "#{form_submit_selector(text)}#{options.delete(:selector)}" 48 | selector += '[disabled="disabled"].disabled' if options.delete(:disabled) 49 | have_selector(selector, **options) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/matchers/layout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Matchers 6 | # Matchers for common Active Admin components. 7 | module Layout 8 | def have_action_item(text, options = {}) 9 | opts = Util.options_with_text(text, options) 10 | have_selector(action_item_selector, **opts) 11 | end 12 | 13 | def have_page_title(text, options = {}) 14 | opts = Util.options_with_text(text, options) 15 | have_selector(page_title_selector, **opts) 16 | end 17 | 18 | def have_flash_message(text, options = {}) 19 | type = options.delete(:type) 20 | opts = Util.options_with_text(text, options) 21 | selector = flash_message_selector(type) 22 | have_selector(selector, **opts) 23 | end 24 | 25 | def have_footer(options = {}) 26 | selector = footer_selector 27 | have_selector(selector, **options) 28 | end 29 | 30 | def have_panel(title, options = {}) 31 | title_selector = "#{panel_selector} > #{panel_title_selector}" 32 | opts = Util.options_with_text(title, options) 33 | have_selector(title_selector, **opts) 34 | end 35 | 36 | def have_sidebar(title, options = {}) 37 | title_selector = "#{sidebar_selector} #{panel_selector} > #{panel_title_selector}" 38 | opts = Util.options_with_text(title, options) 39 | have_selector(title_selector, **opts) 40 | end 41 | 42 | def have_batch_action(title, exact: true) 43 | selector = "#{dropdown_list_selector} #{batch_action_selector}" 44 | opts = Util.options_with_text(title, exact: exact) 45 | have_selector(selector, **opts) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/matchers/table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Matchers 6 | module Table 7 | # @param options [Hash] 8 | # :resource_name [String, nil] active admin page resource name 9 | # for other options @see Capybara::RSpecMatchers#have_selector 10 | # @example 11 | # expect(page).to have_table 12 | # expect(page).to have_table(resource_name: 'users') 13 | # 14 | def have_table(options = {}) 15 | resource_name = options.delete(:resource_name) 16 | selector = table_selector(resource_name) 17 | have_selector(selector, **options) 18 | end 19 | 20 | # @param options [Hash] 21 | # :text [String, nil] cell content 22 | # :exact_text [String, nil] cell content exact matching 23 | # :id [String, Number, nil] record ID 24 | # for other options @see Capybara::RSpecMatchers#have_selector 25 | # @example 26 | # within_table_for('users') do 27 | # expect(page).to have_table_row(id: user.id) 28 | # end 29 | # 30 | def have_table_row(options = {}) 31 | row_id = options.delete(:id) 32 | selector = table_row_selector(row_id) 33 | have_selector(selector, **options) 34 | end 35 | 36 | # @param options [Hash] 37 | # :text [String, nil] cell content include matching 38 | # :exact_text [String, nil] cell content exact matching 39 | # :column [String, nil] cell header name 40 | # for other options @see Capybara::RSpecMatchers#have_selector 41 | # @example 42 | # within_table_for('users') do 43 | # within_table_row(id: user.id) do 44 | # expect(page).to have_table_cell(count: 5) 45 | # expect(page).to have_table_cell(text: user.id.to_s) 46 | # expect(page).to have_table_cell(text: 'John Doe', column: 'Full name') 47 | # end 48 | # end 49 | # 50 | def have_table_cell(options = {}) 51 | column = options.delete(:column) 52 | selector = table_cell_selector(column) 53 | 54 | have_selector(selector, **options) 55 | end 56 | 57 | # @param options [Hash] 58 | # @option count [Integer] qty of nodes 59 | def have_table_scopes(options = {}) 60 | have_selector("#{table_scopes_container_selector} > #{table_scope_selector}", **options) 61 | end 62 | 63 | # @param options [Hash] 64 | # @option exact_text [String] title of scope 65 | # @option counter [Integer,String,nil] counter value in brackets (nil if skipped) 66 | # @option selected [Boolean] is scope active (default false) 67 | def have_table_scope(options = {}) 68 | active = options.delete(:active) 69 | selector = "#{table_scopes_container_selector} > #{table_scope_selector}" 70 | selector = active ? "#{selector}.selected" : "#{selector}:not(.selected)" 71 | 72 | have_selector(selector, **options) 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec' 4 | require 'capybara/active_admin' 5 | 6 | RSpec.configure do |config| 7 | config.include Capybara::ActiveAdmin::TestHelpers, type: :system 8 | config.include Capybara::ActiveAdmin::TestHelpers, type: :feature 9 | end 10 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/selectors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | require 'capybara/active_admin/selectors/layout' 5 | require 'capybara/active_admin/selectors/table' 6 | require 'capybara/active_admin/selectors/attributes_table' 7 | require 'capybara/active_admin/selectors/form' 8 | 9 | module Capybara 10 | module ActiveAdmin 11 | module Selectors 12 | # Methods that return css/xpath selectors should be placed here. 13 | 14 | include Selectors::Layout 15 | include Selectors::Table 16 | include Selectors::AttributesTable 17 | include Selectors::Form 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/selectors/attributes_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Selectors 6 | module AttributesTable 7 | # @return [String] selector. 8 | def attributes_table_selector(model: nil, id: nil) 9 | return 'div.attributes_table' if model.nil? 10 | 11 | model = Util.parse_model_name(model) 12 | selector = "div.attributes_table.#{model}" 13 | selector += "#attributes_table_#{model}_#{id}" if id 14 | selector 15 | end 16 | 17 | # @return [String] selector. 18 | def attributes_row_selector(label = nil) 19 | return 'tr.row > td' if label.nil? 20 | 21 | label = label.to_s.gsub(' ', '_').downcase 22 | "tr.row.row-#{label} > td" 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/selectors/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Selectors 6 | # Selectors for *active_admin_form_for* and related form components. 7 | module Form 8 | # @param model_name [String, nil] form model name 9 | # @return [String] selector. 10 | def form_selector(model_name = nil) 11 | return 'form.formtastic' if model_name.nil? 12 | 13 | model_name = Util.parse_model_name(model_name, singular: true) 14 | "form.formtastic.#{model_name}" 15 | end 16 | 17 | # @return [String] selector. 18 | def label_selector 19 | 'label.label' 20 | end 21 | 22 | # @return [String] selector. 23 | def inline_error_selector 24 | 'p.inline-errors' 25 | end 26 | 27 | # @return [String] selector. 28 | def semantic_error_selector 29 | 'ul.errors > li' 30 | end 31 | 32 | # @param association_name [String] 33 | # @return [String] selector. 34 | def has_many_fields_selector(association_name) 35 | "div.has_many_container.#{association_name} > fieldset.inputs.has_many_fields" 36 | end 37 | 38 | # @param association_name [String] 39 | # @return [String] .has_many_container selector. 40 | def has_many_container_selector(association_name) 41 | ".has_many_container.#{association_name}" 42 | end 43 | 44 | # @param text [String, nil] submit button text. 45 | # @return [String] selector. 46 | def form_submit_selector(text = nil) 47 | return %(input[type="submit"]) if text.nil? 48 | 49 | %(input[type="submit"][value="#{text}"]) 50 | end 51 | 52 | # @param label [String, nil] field label. 53 | # @param exact [Boolean, nil] match by exact label text (default false). 54 | # @return [String] selector. 55 | def input_container_selector(label = nil, exact: nil) 56 | return 'li' if label.nil? 57 | 58 | label_opts = Util.options_with_text(label, exact: exact) 59 | label_node = find(label_selector, **label_opts) 60 | li_id = label_node.ancestor('li')[:id] 61 | "li##{li_id}" 62 | end 63 | 64 | # @return [String] selector. 65 | def filter_form_selector 66 | '.filter_form' 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/selectors/layout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Selectors 6 | # Selectors for common Active Admin components. 7 | module Layout 8 | # @return [String] selector. 9 | def action_items_container_selector 10 | '#titlebar_right .action_items' 11 | end 12 | 13 | # @return [String] selector. 14 | def action_item_selector 15 | "#{action_items_container_selector} .action_item" 16 | end 17 | 18 | # @return [String] selector. 19 | def page_title_selector 20 | '#page_title' 21 | end 22 | 23 | # @return [String] selector. 24 | def flash_message_selector(type = nil) 25 | return ".flashes .flash.flash_#{type}" if type 26 | 27 | '.flashes .flash' 28 | end 29 | 30 | # @return [String] selector. 31 | def footer_selector 32 | 'div.footer#footer' 33 | end 34 | 35 | # @return [String] selector. 36 | def tab_header_link_selector 37 | '.tabs.ui-tabs li.ui-tabs-tab a' 38 | end 39 | 40 | # @return [String] selector. 41 | def tab_content_selector 42 | '.tab-content' 43 | end 44 | 45 | # @return [String] selector. 46 | def sidebar_selector 47 | '#sidebar' 48 | end 49 | 50 | # @return [String] selector. 51 | def panel_selector 52 | '.panel' 53 | end 54 | 55 | # @return [String] selector. 56 | def panel_title_selector 57 | 'h3' 58 | end 59 | 60 | # @return [String] selector. 61 | def panel_content_selector 62 | '.panel_contents' 63 | end 64 | 65 | # @return [String] selector. 66 | def batch_actions_button_selector 67 | 'div.batch_actions_selector' 68 | end 69 | 70 | # @return [String] selector. 71 | def dropdown_list_selector 72 | 'ul.dropdown_menu_list' 73 | end 74 | 75 | # @return [String] selector. 76 | def batch_action_selector 77 | 'li a[data-action]' 78 | end 79 | 80 | # @return [String] selector. 81 | def modal_dialog_selector 82 | '.active_admin_dialog' 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/selectors/table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Selectors 6 | # Selectors for *table_for*, it's rows and cells. 7 | module Table 8 | # @param resource_name [String, nil] active admin resource name. 9 | # @return selector. 10 | def table_selector(resource_name = nil) 11 | return 'table.index_table' if resource_name.nil? 12 | 13 | resource_name = resource_name.to_s.gsub(' ', '_').pluralize.downcase 14 | "table#index_table_#{resource_name}" 15 | end 16 | 17 | # @param record_id [String, Integer, nil] record ID. 18 | # @return selector. 19 | def table_row_selector(record_id = nil) 20 | return 'tbody > tr' if record_id.nil? 21 | 22 | %(tbody > tr[id$="_#{record_id}"]) 23 | end 24 | 25 | # @return selector. 26 | def table_header_selector 27 | 'thead > tr > th.col' 28 | end 29 | 30 | # @param column [String, nil] column name. 31 | # @return selector. 32 | def table_cell_selector(column = nil) 33 | return 'td.col' if column.nil? 34 | 35 | column = column.to_s.gsub(' ', '_').downcase 36 | "td.col.col-#{column}" 37 | end 38 | 39 | def table_scopes_container_selector 40 | '.scopes > ul.table_tools_segmented_control' 41 | end 42 | 43 | def table_scope_selector 44 | 'li.scope' 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/test_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module TestHelpers 6 | include Selectors 7 | include Finders 8 | include Matchers 9 | include Actions 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | module Util 6 | # Common pure utility functions 7 | 8 | def parse_model_name(model_name, singular: true) 9 | return if model_name.nil? 10 | 11 | model_name = model_name.model_name.singular if model_name.is_a?(Class) 12 | model_name = model_name.to_s.gsub(' ', '_').downcase 13 | singular ? model_name.singularize : model_name.pluralize 14 | end 15 | 16 | def options_with_text(text, options = {}) 17 | key = options[:exact] ? :exact_text : :text 18 | 19 | options.except(:exact).merge(key => text) 20 | end 21 | 22 | module_function :parse_model_name, :options_with_text 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/capybara/active_admin/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module ActiveAdmin 5 | VERSION = '0.3.3' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/capybara_active_admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/active_admin' 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:dev": "vuepress dev docs", 4 | "docs:build": "vuepress build docs" 5 | }, 6 | "devDependencies": { 7 | "vuepress": "^1.4.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/dummy/assets/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require active_admin/base 2 | -------------------------------------------------------------------------------- /spec/dummy/assets/active_admin.scss: -------------------------------------------------------------------------------- 1 | @import "active_admin/mixins"; 2 | @import "active_admin/base"; 3 | -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activeadmin-plugins/capybara_active_admin/d2cdc2c0a5478d4ee1afb30f3465c7be3b760a86/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/test_application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/inline' 4 | 5 | require 'active_record' 6 | require 'sassc-rails' 7 | 8 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 9 | ActiveRecord::Base.logger = if ENV['CI'] 10 | ActiveSupport::Logger.new(STDOUT) 11 | else 12 | ActiveSupport::Logger.new(File.join(__dir__, 'log/test.log')) 13 | end 14 | 15 | ActiveRecord::Schema.define do 16 | create_table :active_admin_comments do |t| 17 | t.string :namespace 18 | t.text :body 19 | t.references :resource, polymorphic: true 20 | t.references :author, polymorphic: true 21 | t.timestamps 22 | end 23 | 24 | create_table :users, force: true do |t| 25 | t.string :full_name 26 | t.timestamps 27 | end 28 | 29 | create_table :employees, force: true do |t| 30 | t.string :full_name 31 | t.decimal :salary 32 | t.timestamps 33 | end 34 | 35 | create_table :duties, force: true do |t| 36 | t.string :name 37 | t.string :duty_type 38 | t.integer :employee_id 39 | t.timestamps 40 | end 41 | end 42 | 43 | require 'action_controller/railtie' 44 | require 'action_view/railtie' 45 | require 'sprockets/railtie' 46 | require 'active_admin' 47 | 48 | class TestApplication < Rails::Application 49 | config.root = __dir__ 50 | config.session_store :cookie_store, key: 'cookie_store_key' 51 | secrets.secret_token = 'secret_token' 52 | secrets.secret_key_base = 'secret_key_base' 53 | 54 | config.eager_load = false 55 | config.logger = ActiveRecord::Base.logger 56 | 57 | config.hosts = %w[127.0.0.1] 58 | 59 | # Configure sprockets assets 60 | config.assets.paths = [File.join(config.root, 'assets')] 61 | config.assets.unknown_asset_fallback = false 62 | config.assets.digest = false 63 | config.assets.debug = false 64 | config.assets.compile = true 65 | # config.public_file_server.enabled 66 | end 67 | 68 | class ApplicationController < ActionController::Base 69 | include Rails.application.routes.url_helpers 70 | 71 | rescue_from StandardError, with: :render_server_error 72 | 73 | def render_server_error(error) 74 | log_error(error) 75 | respond_with do |format| 76 | # we will see exceptions in failed screenshots 77 | format.html do 78 | msg = "

#{error.class}: #{error.message}

" 79 | bt = error.backtrace.map { |line| "
#{line}
" } 80 | html = "

Something went wrong

#{msg}#{bt.join}".html_safe 81 | render status: 500, html: html 82 | end 83 | format.json do 84 | render status: 500, json: { error: 'server_error' } 85 | end 86 | end 87 | end 88 | 89 | def log_error(error, causes: []) 90 | logger.error { "<#{error.class}>: #{error.message}\n#{error.backtrace.join("\n")}" } 91 | return if error.cause.nil? || error.cause == error || causes.include?(error.cause) 92 | 93 | causes.push(error) 94 | log_error(error, causes) 95 | end 96 | end 97 | 98 | class User < ActiveRecord::Base 99 | validates :full_name, presence: true 100 | end 101 | 102 | module Billing 103 | class Employee < ActiveRecord::Base 104 | has_many :duties, class_name: 'Billing::Duty' 105 | accepts_nested_attributes_for :duties 106 | validates :full_name, presence: true 107 | validates :salary, numericality: { greater_than_or_equal_to: 0.01 } 108 | end 109 | 110 | class Duty < ActiveRecord::Base 111 | belongs_to :employee, class_name: 'Billing::Employee' 112 | validates :name, presence: true 113 | validates :duty_type, inclusion: { in: %w[common extra] } 114 | 115 | def common? 116 | duty_type == 'common' 117 | end 118 | end 119 | end 120 | 121 | ActiveAdmin.setup do |config| 122 | # Disabling authentication in specs so that we don't have to worry about 123 | # it allover the place 124 | config.authentication_method = false 125 | config.current_user_method = false 126 | config.batch_actions = true 127 | end 128 | 129 | Rails.application.initialize! 130 | # to execute prepare hooks 131 | ActiveSupport::Reloader.prepare! 132 | 133 | ActiveAdmin.register_page 'Dashboard' do 134 | menu priority: 1, label: proc { I18n.t('active_admin.dashboard') } 135 | content do 136 | 'Test Me' 137 | end 138 | end 139 | 140 | ActiveAdmin.register User do 141 | permit_params :full_name 142 | end 143 | 144 | ActiveAdmin.register Billing::Employee, as: 'Business Employee' do 145 | permit_params :full_name, :salary, duties_attributes: [:id, :name, :duty_type] 146 | includes :duties 147 | 148 | batch_action :update_salary, form: -> { { salary: :text } } do |ids, inputs| 149 | batch_action_collection.where(id: ids).each { |r| r.update!(salary: inputs['salary']) } 150 | flash[:notice] = 'Salary was updated successfully' 151 | redirect_back(fallback_location: admin_root_path) 152 | end 153 | 154 | index do 155 | actions 156 | id_column 157 | selectable_column 158 | column :full_name 159 | column(:salary) { |r| number_to_currency(r.salary) } 160 | column(:common_duties) { |r| r.duties.select(&:common?).map(&:name).join(', ') } 161 | column(:extra_duties) { |r| r.duties.reject(&:common?).map(&:name).join(', ') } 162 | column :created_at 163 | column :updated_at 164 | end 165 | 166 | show do 167 | tabs do 168 | tab 'Details' do 169 | attributes_table do 170 | row :id 171 | row :full_name 172 | row(:salary) { |r| number_to_currency(r.salary) } 173 | row :created_at 174 | row :updated_at 175 | end 176 | end 177 | 178 | tab 'Duties' do 179 | table_for resource.duties, class: 'index_table' do 180 | column :id 181 | column :name 182 | column :type, :duty_type 183 | column :created_at 184 | column :updated_at 185 | end 186 | end 187 | end 188 | end 189 | 190 | form do |f| 191 | f.semantic_errors(*f.object.errors.full_messages) 192 | 193 | f.inputs do 194 | f.input :full_name 195 | f.input :salary 196 | end 197 | 198 | f.has_many :duties do |d| 199 | d.input :name 200 | d.input :duty_type, label: 'Type', as: :select, collection: %w[common extra] 201 | end 202 | 203 | f.actions 204 | end 205 | end 206 | 207 | Rails.application.routes.draw do 208 | ActiveAdmin.routes(self) 209 | end 210 | -------------------------------------------------------------------------------- /spec/lib/capybara_active_admin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Capybara::ActiveAdmin do 4 | it 'has a version number' do 5 | expect(Capybara::ActiveAdmin::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | ENV['RAILS_ENV'] = 'test' 6 | 7 | require_relative 'dummy/test_application' 8 | 9 | require 'rspec/rails' 10 | require 'capybara/rails' 11 | require 'capybara/rspec' 12 | require 'capybara/cuprite' 13 | 14 | # gem files 15 | require 'capybara/active_admin/rspec' 16 | 17 | # Force deprecations to raise an exception. 18 | ActiveSupport::Deprecation.behavior = :raise 19 | 20 | # Capybara generic config 21 | Capybara.default_max_wait_time = 5 22 | Capybara.configure do |config| 23 | config.match = :prefer_exact 24 | end 25 | 26 | # Capybara JS driver 27 | cuprite_opts = { 28 | js_errors: true, 29 | window_size: [1920, 1080], 30 | timeout: 10, 31 | process_timeout: 10, 32 | url_whitelist: %w[http://127.0.0.1:* http://localhost:* http://lvh.me:*], 33 | browser_options: { 34 | 'disable-gpu' => nil, 35 | 'no-sandbox' => nil, 36 | 'disable-setuid-sandbox' => nil, 37 | 'start-maximized' => nil 38 | } 39 | } 40 | 41 | Capybara.register_driver :cuprite_headless do |app| 42 | Capybara::Cuprite::Driver.new(app, cuprite_opts) 43 | end 44 | 45 | Capybara.register_driver :cuprite do |app| 46 | Capybara::Cuprite::Driver.new(app, cuprite_opts.merge(headless: false, window_size: [1440, 900])) 47 | end 48 | 49 | Capybara.javascript_driver = ENV['JS_DRIVER'].presence&.to_sym || :cuprite_headless 50 | 51 | # Capybara server 52 | Capybara.register_server :puma do |app, port, host, options = {}| 53 | require 'rack/handler/puma' 54 | puma_opts = { Host: host, Port: port, Threads: '0:1', workers: 0, daemon: false } 55 | Rack::Handler::Puma.run(app, **puma_opts.merge(options)) 56 | end 57 | 58 | Capybara.server = :puma 59 | 60 | RSpec.configure do |config| 61 | # RSpec generic configuration 62 | config.disable_monkey_patching! 63 | config.filter_run focus: true 64 | config.run_all_when_everything_filtered = true 65 | config.color = true 66 | config.order = :random 67 | config.example_status_persistence_file_path = '.rspec_status' 68 | 69 | config.expect_with :rspec do |c| 70 | c.syntax = :expect 71 | end 72 | 73 | # RSpec rails configurations 74 | config.infer_spec_type_from_file_location! 75 | config.use_transactional_fixtures = true 76 | config.use_instantiated_fixtures = false 77 | 78 | # config.include Devise::Test::ControllerHelpers, type: :controller 79 | 80 | config.before(:each, type: :system) do 81 | driven_by :rack_test 82 | end 83 | 84 | config.before(:each, type: :system, js: true) do 85 | driven_by Capybara.javascript_driver 86 | end 87 | 88 | config.after(:each, type: :system) do 89 | Capybara.reset_sessions! 90 | end 91 | 92 | config.before(:suite) do 93 | # Clear old screenshots before tests started. 94 | screenshot_path = Rails.root.join('tmp/screenshots') 95 | FileUtils.rm_f Dir["#{screenshot_path}/*"] 96 | 97 | # Truncate log file before tests started. 98 | log_file = Rails.root.join('tmp/log/test.log') 99 | File.truncate(log_file, 0) if File.exist?(log_file) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/system/business_employee_show_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Business Employees show', js: true do 4 | subject do 5 | visit admin_business_employee_path(record.id) 6 | end 7 | 8 | let!(:record) { Billing::Employee.create!(full_name: 'John Doe', salary: 100) } 9 | before do 10 | Billing::Duty.create!( 11 | [ 12 | { name: 'Normal work', duty_type: 'common', employee_id: record.id }, 13 | { name: 'Extra work', duty_type: 'extra', employee_id: record.id } 14 | ] 15 | ) 16 | end 17 | 18 | it 'have correct show page' do 19 | subject 20 | 21 | expect(page).to have_title(record.full_name) 22 | expect(page).to have_page_title(record.full_name) 23 | 24 | expect(page).to have_action_item('Edit Business Employee') 25 | expect(page).to have_action_item('Delete Business Employee') 26 | 27 | expect(page).to have_attributes_table 28 | expect(page).to have_attributes_table(model: Billing::Employee) 29 | expect(page).to have_attributes_table(model: Billing::Employee, id: record.id) 30 | 31 | within_attributes_table_for(model: Billing::Employee, id: record.id) do 32 | expect(page).to have_attribute_row('Full name') 33 | expect(page).to have_attribute_row('Full name', text: record.full_name) 34 | expect(page).to have_attribute_row('Salary', exact_text: '$100.00') 35 | end 36 | 37 | switch_tab('Duties') 38 | 39 | within_table_for do 40 | expect(page).to have_table_row(count: 2) 41 | expect(page).to have_table_row(id: record.duties.first.id) 42 | expect(page).to have_table_row(id: record.duties.second.id) 43 | end 44 | end 45 | 46 | it 'clicks edit link' do 47 | subject 48 | 49 | click_action_item('Edit Business Employee') 50 | 51 | expect(page).to have_current_path edit_admin_business_employee_path(record.id) 52 | end 53 | 54 | it 'clicks delete link' do 55 | subject 56 | 57 | accept_confirm do 58 | click_action_item('Delete Business Employee') 59 | end 60 | 61 | expect(page).to have_current_path admin_business_employees_path 62 | expect(page).to have_flash_message('Employee was successfully destroyed.', type: :notice, exact: true) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/system/business_employees_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Business Employees index', js: true do 4 | subject do 5 | visit admin_business_employees_path 6 | end 7 | 8 | it 'checks elements on empty users page' do 9 | subject 10 | 11 | expect(page).to have_text('There are no Business Employees yet.') 12 | expect(page).to have_action_item('New Business Employee') 13 | expect(page).to_not have_action_item('Edit Business Employee') 14 | end 15 | 16 | it 'clicks on New resource_name: ' do 17 | subject 18 | 19 | click_action_item('New Business Employee') 20 | expect(page).to have_current_path(new_admin_business_employee_path) 21 | end 22 | 23 | it 'finds data in table' do 24 | john = Billing::Employee.create!(full_name: 'John Doe', salary: 100) 25 | jane = Billing::Employee.create!(full_name: 'Jane Air', salary: 101) 26 | john.update! duties_attributes: [name: 'hard work', duty_type: 'common'] 27 | jane.update! duties_attributes: [name: 'home work', duty_type: 'extra'] 28 | subject 29 | 30 | expect(page).to have_table 31 | expect(page).to have_table(resource_name: 'Business Employees') 32 | within_table_for do 33 | expect(page).to have_table_row(count: 2) 34 | # 2x9 (id, selectable, full_name, salary, common_duties, extra_duties, created_at, updated_at, actions) 35 | expect(page).to have_table_cell(count: 18) 36 | 37 | expect(page).to have_table_cell(text: 'John Doe') 38 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 39 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id') 40 | 41 | within_table_row(id: john.id) do 42 | expect(page).to have_table_cell(count: 9) 43 | expect(page).to have_table_cell(text: 'John Doe') 44 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 45 | expect(page).to have_table_cell(text: '$100.00', column: 'Salary') 46 | expect(page).to have_table_cell(exact_text: 'hard work', column: 'Common Duties') 47 | expect(page).to have_table_cell(exact_text: '', column: 'Extra Duties') 48 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id') 49 | end 50 | 51 | within_table_row(id: jane.id) do 52 | expect(page).to have_table_cell(count: 9) 53 | expect(page).to have_table_cell(text: jane.id, column: 'Id') 54 | expect(page).to have_table_cell(text: 'Jane Air', column: 'Full Name') 55 | expect(page).to have_table_cell(exact_text: '$101.00', column: 'Salary') 56 | expect(page).to have_table_cell(exact_text: '', column: 'Common Duties') 57 | expect(page).to have_table_cell(exact_text: 'home work', column: 'Extra Duties') 58 | expect(page).to_not have_table_cell(text: 'John Doe') 59 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Full Name') 60 | end 61 | end 62 | end 63 | 64 | it 'updates salary via batch action' do 65 | john = Billing::Employee.create!(full_name: 'John Doe', salary: 100) 66 | jane = Billing::Employee.create!(full_name: 'Jane Air', salary: 101) 67 | 5.times { |i| Billing::Employee.create!(full_name: "Jack #{i}", salary: 10) } 68 | 69 | subject 70 | 71 | within_table_for do 72 | select_table_row(id: john.id) 73 | select_table_row(id: jane.id) 74 | end 75 | 76 | open_batch_action_menu 77 | expect(page).to have_batch_action('Update Salary Selected', exact: true) 78 | expect(page).to have_batch_action('Delete Selected', exact: true) 79 | 80 | click_batch_action('Update Salary Selected', exact: true) 81 | within_modal_dialog do 82 | fill_in 'salary', with: '200.35' 83 | end 84 | confirm_modal_dialog 85 | 86 | expect(page).to have_flash_message('Salary was updated successfully', exact: true) 87 | 88 | expect(john.reload.salary).to eq 200.35 89 | expect(jane.reload.salary).to eq 200.35 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/system/users_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Users index', js: true do 4 | subject do 5 | visit admin_users_path 6 | end 7 | 8 | it 'checks elements on empty users page' do 9 | subject 10 | 11 | expect(page).to have_text('There are no Users yet.') 12 | expect(page).to have_action_item('New User') 13 | expect(page).to_not have_action_item('Edit User') 14 | expect(page).to_not have_table 15 | expect(page).to_not have_table(resource_name: 'Users') 16 | end 17 | 18 | it 'clicks on New User' do 19 | subject 20 | 21 | click_action_item('New User') 22 | expect(page).to have_current_path(new_admin_user_path) 23 | end 24 | 25 | it 'finds data in table' do 26 | john = User.create!(full_name: 'John Doe') 27 | jane = User.create!(full_name: 'Jane Air') 28 | subject 29 | 30 | expect(page).to have_table 31 | expect(page).to have_table(resource_name: 'Users') 32 | within_table_for(User) do 33 | expect(page).to have_table_row(count: 2) 34 | # 2x6 (selectable, id, full_name, created_at, updated_at, actions) 35 | expect(page).to have_table_cell(count: 12) 36 | 37 | expect(page).to have_table_cell(text: 'John Doe') 38 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 39 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id') 40 | 41 | within_table_row(id: john.id) do 42 | # selectable, id, full_name, created_at, updated_at, actions 43 | expect(page).to have_table_cell(count: 6) 44 | expect(page).to have_table_cell(text: 'John Doe') 45 | expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name') 46 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id') 47 | end 48 | 49 | within_table_row(id: jane.id) do 50 | expect(page).to have_table_cell(count: 6) 51 | expect(page).to have_table_cell(text: jane.id, column: 'Id') 52 | expect(page).to have_table_cell(text: 'Jane Air', column: 'Full Name') 53 | expect(page).to_not have_table_cell(text: 'John Doe') 54 | expect(page).to_not have_table_cell(text: 'John Doe', column: 'Full Name') 55 | end 56 | end 57 | end 58 | 59 | # TODO: filters expect to have 60 | # TODO: filters fill and click 61 | # TODO: breadcrumbs expect to have 62 | # TODO: breadcrumbs click 63 | # TODO: menu items expect to have 64 | # TODO: menu items click 65 | end 66 | -------------------------------------------------------------------------------- /spec/system/users_new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Users new', js: true do 4 | subject do 5 | visit new_admin_user_path 6 | end 7 | 8 | it 'checks new user page' do 9 | subject 10 | 11 | expect(page).to have_title('New User') 12 | expect(page).to have_page_title('New User') 13 | 14 | within_form_for(User) do 15 | expect(page).to have_field('Full name') 16 | expect(page).to_not have_field('Test') 17 | fill_in 'Full name', with: 'John Doe' 18 | click_submit 'Create User' 19 | end 20 | 21 | # TODO: nested has_many 22 | # TODO: nested has_one 23 | 24 | expect(page).to have_flash_message('User was successfully created.', type: :notice) 25 | user = User.last! 26 | expect(page).to have_current_path admin_user_path(user.id) 27 | 28 | expect(User.count).to eq(1) 29 | expect(user).to have_attributes( 30 | full_name: 'John Doe' 31 | ) 32 | end 33 | end 34 | --------------------------------------------------------------------------------