Skip to content

Commit

Permalink
Raise when declared elements conflict with Capybara's RSpec matchers
Browse files Browse the repository at this point in the history
What?
=====

PageEz defines predicate methods for each element declared, making it
easy to assert against presence or absence on a page. RSpec
then takes these predicate methods to generate implicit matchers.

However, concrete matchers take precedence over predicate matchers. In
the case of elements that may feasibly be defined in a page object, like
`title`, `button`, `field`, `select`, and `table`, this may present
challenges if arguments are passed that differ from the matcher
implementation.

This introduces a new configuration setting, which defaults to raising
an exception, when a collision occurs.
  • Loading branch information
joshuaclayton committed Jul 12, 2023
1 parent 3baad36 commit 6847911
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 7 deletions.
1 change: 1 addition & 0 deletions lib/page_ez.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative "page_ez/configuration"
require_relative "page_ez/null_logger"
require_relative "page_ez/delegates_to"
require_relative "page_ez/visitors/matcher_collision_visitor"
require_relative "page_ez/visitors/depth_visitor"
require_relative "page_ez/visitors/debug_visitor"
require_relative "page_ez/visitors/registered_name_visitor"
Expand Down
11 changes: 10 additions & 1 deletion lib/page_ez/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module PageEz
class Configuration
VALID_MISMATCH_BEHAVIORS = [:warn, :raise, nil].freeze
attr_accessor :logger
attr_reader :on_pluralization_mismatch
attr_reader :on_pluralization_mismatch, :on_matcher_collision

def initialize
reset
Expand All @@ -16,9 +16,18 @@ def on_pluralization_mismatch=(value)
@on_pluralization_mismatch = value
end

def on_matcher_collision=(value)
if !VALID_MISMATCH_BEHAVIORS.include?(value)
raise ArgumentError, "#{value.inspect} must be one of #{VALID_MISMATCH_BEHAVIORS}"
end

@on_matcher_collision = value
end

def reset
self.logger = NullLogger.new
self.on_pluralization_mismatch = nil
self.on_matcher_collision = :raise
end
end
end
2 changes: 2 additions & 0 deletions lib/page_ez/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ class Error < StandardError; end

class PluralizationMismatchError < StandardError; end

class MatcherCollisionError < StandardError; end

class DuplicateElementDeclarationError < StandardError; end
end
15 changes: 9 additions & 6 deletions lib/page_ez/page_visitor.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
module PageEz
class PageVisitor
def initialize
reset
@visitors = [
Visitors::DebugVisitor.new,
Visitors::RegisteredNameVisitor.new,
Visitors::MacroPluralizationVisitor.new,
Visitors::MatcherCollisionVisitor.new
]
end

def begin_block_evaluation
Expand Down Expand Up @@ -35,11 +40,9 @@ def process_macro(macro, name, selector)
end

def reset
@visitors = [
Visitors::DebugVisitor.new,
Visitors::RegisteredNameVisitor.new,
Visitors::MacroPluralizationVisitor.new
]
@visitors.each do |visitor|
visitor.reset
end
end
end
end
59 changes: 59 additions & 0 deletions lib/page_ez/visitors/matcher_collision_visitor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module PageEz
module Visitors
class MatcherCollisionVisitor
def initialize
reset
end

def begin_block_evaluation
@depth_visitor.begin_block_evaluation
end

def end_block_evaluation
@depth_visitor.end_block_evaluation
end

def define_method(name)
@depth_visitor.define_method(name)
end

def inherit_from(subclass)
@depth_visitor.inherit_from(subclass)
end

def process_macro(macro, name, selector)
@depth_visitor.process_macro(macro, name, selector)
if existing_matchers.include?(name.to_s)
rendered_macro = "#{macro} :#{name}, \"#{selector}\""
whitespace = " " * @depth_visitor.depth
message = "#{whitespace}#{rendered_macro} will conflict with Capybara's `have_#{name}` matcher"

case PageEz.configuration.on_matcher_collision
when :warn
PageEz.configuration.logger.warn(message)
when :raise
raise MatcherCollisionError, message
end
end
end

def reset
@depth_visitor = DepthVisitor.new
end

private

def existing_matchers
if defined?(Capybara::RSpecMatchers)
Capybara::RSpecMatchers.instance_methods.filter_map do |method_name|
if (match = method_name.to_s.match(/^have_(?!no_)(.+)$/))
match[1]
end
end
else
[]
end
end
end
end
end
51 changes: 51 additions & 0 deletions spec/features/capybara_overlap_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "spec_helper"

RSpec.describe "Declaring elements that overlap with Capybara matchers", type: :feature do
it "warns that those matchers will be used instead of the predicate methods" do
PageEz.configure do |config|
config.on_matcher_collision = :warn
end

logger = configure_fake_logger

Class.new(PageEz::Page) do
has_one :thing, "section" do
has_one :title, "h3"
end
end

expect(logger.warns).to contain_in_order(
" has_one :title, \"h3\" will conflict with Capybara's `have_title` matcher"
)
end

it "raises when configured to raise" do
PageEz.configure do |config|
config.on_matcher_collision = :raise
end

expect do
Class.new(PageEz::Page) do
has_one :thing, "section" do
has_one :title, "h3"
end
end
end.to raise_error(PageEz::MatcherCollisionError, /will conflict/)
end

it "does not raise or warn when configured to ignore" do
PageEz.configure do |config|
config.on_matcher_collision = nil
end

logger = configure_fake_logger

Class.new(PageEz::Page) do
has_one :thing, "section" do
has_one :title, "h3"
end
end

expect(logger.warns).to be_empty
end
end

0 comments on commit 6847911

Please sign in to comment.