diff --git a/Appraisals b/Appraisals index 1123ebf67..b32c19130 100644 --- a/Appraisals +++ b/Appraisals @@ -126,6 +126,9 @@ if Gem::Requirement.new('>= 2.5.0').satisfied_by?(Gem::Version.new(RUBY_VERSION) gem 'selenium-webdriver' gem 'webdrivers' + # Other dependencies + gem 'actiontext', '~> 6.0.2.1' + # Database adapters gem 'pg', '>= 0.18', '< 2.0' gem 'sqlite3', '~> 1.4' diff --git a/README.md b/README.md index f32aad602..b6addc4d6 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,8 @@ about any of them, make sure to [consult the documentation][rubydocs]! tests your `has_one` associations. * **[have_readonly_attribute](lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb)** tests usage of the `attr_readonly` macro. +* **[have_rich_text](lib/shoulda/matchers/active_record/have_rich_text_matcher.rb)** + tests your `has_rich_text` associations. * **[serialize](lib/shoulda/matchers/active_record/serialize_matcher.rb)** tests usage of the `serialize` macro. * **[validate_uniqueness_of](lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb)** diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 642ff4473..0455c1277 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -33,5 +33,6 @@ gem "listen", ">= 3.0.5", "< 3.2" gem "spring-watcher-listen", "~> 2.0.0" gem "selenium-webdriver" gem "webdrivers" +gem "actiontext", "~> 6.0.2.1" gem "pg", ">= 0.18", "< 2.0" gem "sqlite3", "~> 1.4" diff --git a/gemfiles/rails_6_0.gemfile.lock b/gemfiles/rails_6_0.gemfile.lock index 585332309..73a641da8 100644 --- a/gemfiles/rails_6_0.gemfile.lock +++ b/gemfiles/rails_6_0.gemfile.lock @@ -262,6 +262,7 @@ PLATFORMS ruby DEPENDENCIES + actiontext (~> 6.0.2.1) appraisal (= 2.2.0) bcrypt (~> 3.1.7) bootsnap (>= 1.4.2) diff --git a/lib/shoulda/matchers/active_record.rb b/lib/shoulda/matchers/active_record.rb index 00d30a927..fd1ff777d 100644 --- a/lib/shoulda/matchers/active_record.rb +++ b/lib/shoulda/matchers/active_record.rb @@ -15,6 +15,7 @@ require "shoulda/matchers/active_record/have_db_column_matcher" require "shoulda/matchers/active_record/have_db_index_matcher" require "shoulda/matchers/active_record/have_readonly_attribute_matcher" +require "shoulda/matchers/active_record/have_rich_text_matcher" require "shoulda/matchers/active_record/have_secure_token_matcher" require "shoulda/matchers/active_record/serialize_matcher" require "shoulda/matchers/active_record/accept_nested_attributes_for_matcher" diff --git a/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb b/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb new file mode 100644 index 000000000..d3b2f5a3a --- /dev/null +++ b/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb @@ -0,0 +1,79 @@ +module Shoulda + module Matchers + module ActiveRecord + # The `have_rich_text` matcher tests usage of the + # `has_rich_text` macro. + # + # #### Example + # + # class Post < ActiveRecord + # has_rich_text :content + # end + # + # # RSpec + # RSpec.describe Post, type: :model do + # it { is_expected.to have_rich_text(:content) } + # end + # + # # Minitest (Shoulda) + # class PostTest < ActiveSupport::TestCase + # should have_rich_text(:content) + # end + # + # @return [HaveRichText] + # + def have_rich_text(rich_text_attribute) + HaveRichText.new(rich_text_attribute) + end + + # @private + class HaveRichText + def initialize(rich_text_attribute) + @rich_text_attribute = rich_text_attribute + end + + def description + "have configured :#{rich_text_attribute} as a ActionText::RichText association" + end + + def failure_message + "Expected #{subject.class} to #{error_description}" + end + + def failure_message_when_negated + "Did not expect #{subject.class} to have ActionText::RichText :#{rich_text_attribute}" + end + + def matches?(subject) + @subject = subject + @error = run_checks + @error.nil? + end + + private + + attr_reader :error, :rich_text_attribute, :subject + + def run_checks + if !has_attribute? + ":#{rich_text_attribute} does not exist" + elsif !has_expected_action_text? + :default + end + end + + def has_attribute? + @subject.respond_to?(rich_text_attribute.to_s) + end + + def has_expected_action_text? + @subject.send(rich_text_attribute).class.name == 'ActionText::RichText' + end + + def error_description + error == :default ? description : "#{description} but #{error}" + end + end + end + end +end diff --git a/spec/support/unit/rails_application.rb b/spec/support/unit/rails_application.rb index 2f5d5dc71..6ee85425a 100644 --- a/spec/support/unit/rails_application.rb +++ b/spec/support/unit/rails_application.rb @@ -25,6 +25,11 @@ def create def load load_environment + + if rails_version > 5 && bundle.includes?('actiontext') + add_action_text_migration + end + run_migrations end @@ -165,6 +170,12 @@ class DevelopmentRecord < ActiveRecord::Base TEXT end + def add_action_text_migration + fs.within_project do + run_command! 'bundle exec rake action_text:install:migrations' + end + end + def add_initializer_for_time_zone_aware_types path = 'config/initializers/configure_time_zone_aware_types.rb' fs.write(path, <<-TEXT) diff --git a/spec/unit/shoulda/matchers/active_record/have_rich_text_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/have_rich_text_matcher_spec.rb new file mode 100644 index 000000000..e932b01d4 --- /dev/null +++ b/spec/unit/shoulda/matchers/active_record/have_rich_text_matcher_spec.rb @@ -0,0 +1,85 @@ +require 'unit_spec_helper' + +describe Shoulda::Matchers::ActiveRecord::HaveRichText, type: :model do + def self.rich_text_is_defined? + defined?(ActionText::RichText) + end + + context '#description' do + it 'returns the message including the name of the provided association' do + matcher = have_rich_text(:content) + expect(matcher.description). + to eq('have configured :content as a ActionText::RichText association') + end + end + + if rich_text_is_defined? + context 'when the model has a RichText association' do + it 'matches when the subject configures has_rich_text' do + valid_record = new_post(is_rich_text_association: true) + + expected_message = 'Did not expect Post to have ActionText::RichText :content' + + expect { have_rich_text(:content) }. + to match_against(valid_record). + or_fail_with(expected_message) + end + end + + context 'when the model does not have a RichText association' do + it 'does not match when provided with a model attribute that exist' do + invalid_record = new_post(has_invalid_content: true) + expected_message = 'Expected Post to have configured :invalid_content as a ' \ + 'ActionText::RichText association' + + expect { have_rich_text(:invalid_content) }. + not_to match_against(invalid_record). + and_fail_with(expected_message) + end + + it 'does not match when provided with a model attribute that does not exist' do + invalid_record = new_post + expected_message = 'Expected Post to have configured :invalid_attribute as a ' \ + 'ActionText::RichText association but :invalid_attribute does not exist' + + expect { have_rich_text(:invalid_attribute) }. + not_to match_against(invalid_record). + and_fail_with(expected_message) + end + end + else + it 'does not match when provided with a model attribute that exist' do + invalid_record = new_post(has_invalid_content: true) + expected_message = 'Expected Post to have configured :invalid_content as a ' \ + 'ActionText::RichText association' + + expect { have_rich_text(:invalid_content) }. + not_to match_against(invalid_record). + and_fail_with(expected_message) + end + + it 'does not match when provided with a model attribute that does not exist' do + invalid_record = new_post + expected_message = 'Expected Post to have configured :invalid_attribute as a ' \ + 'ActionText::RichText association but :invalid_attribute does not exist' + + expect { have_rich_text(:invalid_attribute) }. + not_to match_against(invalid_record). + and_fail_with(expected_message) + end + end + + def new_post(has_invalid_content: false, is_rich_text_association: false) + columns = {} + + if has_invalid_content + columns[:invalid_content] = :string + end + + define_model 'Post', columns do + if is_rich_text_association + has_rich_text :content + end + end.new + end +end