diff --git a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb index 361295c8..18379522 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -116,7 +116,7 @@ module Shoulda ## ##### with_prefix # # Use `with_prefix` to test that the enum is defined with a `_prefix` - # option (Rails 5 only). Can take either a boolean or a symbol: + # option (Rails 6+ only). Can take either a boolean or a symbol: # # class Issue < ActiveRecord::Base # enum status: [:open, :closed], _prefix: :old @@ -163,6 +163,30 @@ module Shoulda # with_suffix # end # + # ##### without_scopes + # + # Use `without_scopes` to test that the enum is defined with + # '_scopes: false' option (Rails 5 only). Can take either a boolean or a + # symbol: + # + # class Issue < ActiveRecord::Base + # enum status: [:open, :closed], _scopes: false + # end + # + # # RSpec + # RSpec.describe Issue, type: :model do + # it do + # should define_enum_for(:status). + # without_scopes + # end + # end + # + # # Minitest (Shoulda) + # class ProcessTest < ActiveSupport::TestCase + # should define_enum_for(:status). + # without_scopes + # end + # # @return [DefineEnumForMatcher] # def define_enum_for(attribute_name) @@ -173,7 +197,7 @@ module Shoulda class DefineEnumForMatcher def initialize(attribute_name) @attribute_name = attribute_name - @options = { expected_enum_values: [] } + @options = { expected_enum_values: [], scopes: true } end def description @@ -226,13 +250,19 @@ module Shoulda self end + def without_scopes + options[:scopes] = false + self + end + def matches?(subject) @record = subject enum_defined? && enum_values_match? && column_type_matches? && - enum_value_methods_exist? + enum_value_methods_exist? && + scope_presence_matches? end def failure_message @@ -294,6 +324,10 @@ module Shoulda expectation << "_#{expected_suffix}".inspect end + if exclude_scopes? + expectation << ' with no scopes' + end + expectation else simple_description @@ -387,28 +421,10 @@ module Shoulda end def enum_value_methods_exist? - passed = expected_singleton_methods.all? do |method| - model.singleton_methods.include?(method) - end - - if passed + if instance_methods_exist? true else - message = "#{attribute_name.inspect} does map to these " - message << 'values, but the enum is ' - - if expected_prefix - if expected_suffix - message << 'configured with either a different prefix or ' - message << 'suffix, or no prefix or suffix at all' - else - message << 'configured with either a different prefix or no ' - message << 'prefix at all' - end - elsif expected_suffix - message << 'configured with either a different suffix or no ' - message << 'suffix at all' - end + message = missing_methods_message message << " (we can't tell which)" @@ -418,6 +434,68 @@ module Shoulda end end + def scope_presence_matches? + if exclude_scopes? + if singleton_methods_exist? + message = "#{attribute_name.inspect} does map to these values " + message << 'but class scope methods were present' + + @failure_message_continuation = message + + false + else + true + end + elsif singleton_methods_exist? + true + else + if enum_defined? + message = 'But the class scope methods are not present' + else + message = missing_methods_message + + message << 'or the class scope methods are not present' + message << " (we can't tell which)" + end + + @failure_message_continuation = message + + false + end + end + + def missing_methods_message + message = "#{attribute_name.inspect} does map to these " + message << 'values, but the enum is ' + + if expected_prefix + if expected_suffix + message << 'configured with either a different prefix or ' + message << 'suffix, or no prefix or suffix at all' + else + message << 'configured with either a different prefix or no ' + message << 'prefix at all' + end + elsif expected_suffix + message << 'configured with either a different suffix or no ' + message << 'suffix at all' + else + '' + end + end + + def singleton_methods_exist? + expected_singleton_methods.all? do |method| + model.singleton_methods.include?(method) + end + end + + def instance_methods_exist? + expected_instance_methods.all? do |method| + record.methods.include?(method) + end + end + def expected_singleton_methods expected_enum_value_names.map do |name| [expected_prefix, name, expected_suffix]. @@ -427,6 +505,18 @@ module Shoulda end end + def expected_instance_methods + methods = expected_enum_value_names.map do |name| + [expected_prefix, name, expected_suffix]. + select(&:present?). + join('_') + end + + methods.flat_map do |m| + ["#{m}?".to_sym, "#{m}!".to_sym] + end + end + def expected_prefix if options.include?(:prefix) if options[:prefix] == true @@ -447,6 +537,10 @@ module Shoulda end end + def exclude_scopes? + !options[:scopes] + end + def to_hash(value) if value.is_a?(Array) value.each_with_index.inject({}) do |hash, (item, index)| diff --git a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb index a589ef0f..a58c03eb 100644 --- a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb @@ -807,6 +807,78 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do end end + if rails_version =~ '~> 6.0' + context 'qualified with #without_scopes' do + context 'if scopes are set to false on the enum but without_scopes is not used' do + it 'has the right description' do + record = build_record_with_array_values( + attribute_name: :attr, + scopes: false, + ) + + matcher = lambda do + expect(record). + to define_enum_for(:attr). + with_values(['published', 'unpublished', 'draft']) + end + + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum backed by an + integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, + and ‹"draft"› to ‹2›. But the class scope methods are not present. + MESSAGE + + expect(&matcher).to fail_with_message(message) + end + end + + context 'if scopes are set to false on the enum' do + it 'matches' do + record = build_record_with_array_values( + attribute_name: :attr, + scopes: false, + ) + + matcher = lambda do + define_enum_for(:attr). + with_values(['published', 'unpublished', 'draft']). + without_scopes + end + + expect(&matcher). + to match_against(record). + or_fail_with(<<-MESSAGE, wrap: true) + Expected Example not to define :attr as an enum backed by an + integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, + and ‹"draft"› to ‹2› with no scopes, but it did. + MESSAGE + end + end + + context 'if scopes are not set to false on the enum' do + it 'has the right description' do + record = build_record_with_array_values(attribute_name: :attr) + + matcher = lambda do + expect(record). + to define_enum_for(:attr). + with_values(['published', 'unpublished', 'draft']). + without_scopes + end + + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum backed by an + integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, + and ‹"draft"› to ‹2› with no scopes. :attr does map to these + values but class scope methods were present. + MESSAGE + + expect(&matcher).to fail_with_message(message) + end + end + end + end + def build_record_with_array_values( model_name: 'Example', attribute_name: :attr, @@ -814,7 +886,8 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do values: ['published', 'unpublished', 'draft'], prefix: false, suffix: false, - attribute_alias: nil + attribute_alias: nil, + scopes: true ) build_record_with_enum_attribute( model_name: model_name, @@ -824,6 +897,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do prefix: prefix, suffix: suffix, attribute_alias: attribute_alias, + scopes: scopes, ) end @@ -832,7 +906,8 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do attribute_name: :attr, values: { active: 0, archived: 1 }, prefix: false, - suffix: false + suffix: false, + scopes: true ) build_record_with_enum_attribute( model_name: model_name, @@ -841,6 +916,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do values: values, prefix: prefix, suffix: suffix, + scopes: scopes, attribute_alias: nil, ) end @@ -851,6 +927,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do column_type:, values:, attribute_alias:, + scopes: true, prefix: false, suffix: false ) @@ -862,7 +939,17 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do alias_attribute attribute_alias, attribute_name end - model.enum(enum_name => values, _prefix: prefix, _suffix: suffix) + params = { + enum_name => values, + _prefix: prefix, + _suffix: suffix, + } + + if rails_version =~ '~> 6.0' + params.merge!(_scopes: scopes) + end + + model.enum(params) model.new end