Skip to content

Commit

Permalink
Split up matchers into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
sfnelson committed Oct 25, 2023
1 parent 7521c11 commit 3a46e6c
Show file tree
Hide file tree
Showing 15 changed files with 328 additions and 258 deletions.
178 changes: 59 additions & 119 deletions lib/katalyst/kpop/matchers.rb
Original file line number Diff line number Diff line change
@@ -1,138 +1,78 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/base"
require "katalyst/kpop/matchers/capybara_matcher"
require "katalyst/kpop/matchers/capybara_parser"
require "katalyst/kpop/matchers/chained_matcher"
require "katalyst/kpop/matchers/frame_matcher"
require "katalyst/kpop/matchers/modal_matcher"
require "katalyst/kpop/matchers/redirect_finder"
require "katalyst/kpop/matchers/redirect_matcher"
require "katalyst/kpop/matchers/response_matcher"
require "katalyst/kpop/matchers/stream_matcher"
require "katalyst/kpop/matchers/title_finder"
require "katalyst/kpop/matchers/title_matcher"

module Katalyst
module Kpop
module Matchers
class BaseMatcher < RSpec::Rails::Matchers::BaseMatcher
attr_reader :matched

def matches?(actual)
@matched = super
@matched.present?
end
end

# @api private
class CapybaraMatcher < BaseMatcher
attr_reader :matched

def matches?(actual)
super
rescue Capybara::ElementNotFound
nil
end

def match(expected, actual)
actual.find(expected)
end

def description
"match #{expected}"
end

def describe_expected
expected.inspect
end

def describe_actual
response = actual.native.children.to_html.gsub(/\s+/, " ")
response = "#{response[0..120]}..." if response.length > 120
response.inspect
end

def failure_message
"expected #{describe_expected} but received #{describe_actual} instead"
end

def failure_message_when_negated
"expected not to find #{expected}"
end
end

# @api private
class ResponseMatcher < BaseMatcher
def match(_, actual)
case actual
when ::ActionDispatch::Response
::ActionDispatch::TestResponse.from_response(actual)
when ::ActionDispatch::TestResponse
actual
end
end

def description
"a response"
end

def failure_message
"expected a response but received #{actual.inspect} instead"
end

def failure_message_when_negated
"expected not to receive a response"
end
end

# @api private
class CapybaraParser < BaseMatcher
def match(_, actual)
@html = Nokogiri::HTML5.parse(actual.body)
Capybara::Node::Simple.new(@html)
end
# @api public
# Passes if `response` contains a turbo response with a kpop dismiss action.
#
# @example
# expect(response).to kpop_dismiss
def kpop_dismiss(id: "kpop")
ChainedMatcher.new(ResponseMatcher, CapybaraParser, StreamMatcher.new(id:, action: "append"))
end

class ChainedMatcher < RSpec::Rails::Matchers::BaseMatcher
Input = Struct.new(:matched)

delegate :failure_message, :failure_message_when_negated, to: :@matcher

def initialize(*matchers)
super()
matchers.each { |m| self << m }
end

def <<(matcher)
matcher = matcher.new if matcher.is_a?(Class)
(@matchers ||= []) << matcher
self
end

def match(_, actual)
@matcher = Input.new(actual)
@matchers.all? do |matcher|
input = @matcher.matched
@matcher = matcher
matcher.matches?(input)
end
end

def description
@matchers.last.description
end
# @api public
# Passes if `response` contains a turbo response with a kpop redirect to
# the provided `target`.
#
# @example Matching a path against a turbo response containing a kpop redirect
# expect(response).to kpop_redirect_to("/path/to/resource")
def kpop_redirect_to(target, id: "kpop")
raise ArgumentError, "Invalid target: nil" unless target

ChainedMatcher.new(ResponseMatcher,
CapybaraParser,
StreamMatcher.new(id:, action: "append"),
RedirectFinder,
RedirectMatcher.new(target))
end

# @api private
class StreamMatcher < CapybaraMatcher
def initialize(id: "kpop", action: "update")
super("turbo-stream[action='#{action}'][target='#{id}']")
end
# @api public
# Passes if `response` contains a turbo stream response with a kpop modal.
# Supports matching on:
# * id – kpop frame id
# * title - modal title
#
# @example Matching turbo stream response with a Shopping Cart modal
# expect(response).to render_kpop_stream(title: "Shopping Cart")
def render_kpop_stream(id: "kpop", title: nil)
matcher = ChainedMatcher.new(ResponseMatcher, CapybaraParser, StreamMatcher.new(id:, action: "kpop_open"),
ModalMatcher)
matcher << TitleFinder << TitleMatcher.new(title) if title.present?
matcher
end

# @api private
class FrameMatcher < CapybaraMatcher
def initialize(id: "kpop")
super("turbo-frame##{id}")
end
# @api public
# Passes if `response` contains a turbo frame with a kpop modal.
# Supports matching on:
# * id – turbo frame id
# * title - modal title
#
# @example Matching turbo stream response with a Shopping Cart modal
# expect(response).to render_kpop_frame(title: "Shopping Cart")
def render_kpop_frame(id: "kpop", title: nil)
matcher = ChainedMatcher.new(ResponseMatcher, CapybaraParser, FrameMatcher.new(id:), ModalMatcher)
matcher << TitleFinder << TitleMatcher.new(title) if title.present?
matcher
end
end
end
end

require "katalyst/kpop/matchers"
require "katalyst/kpop/matchers/redirect_to"
require "katalyst/kpop/matchers/render_kpop"
require "katalyst/kpop/matchers/kpop_dismiss"

RSpec.configure do |config|
config.include Katalyst::Kpop::Matchers, type: :component
config.include Katalyst::Kpop::Matchers, type: :request
Expand Down
18 changes: 18 additions & 0 deletions lib/katalyst/kpop/matchers/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "rspec/rails/matchers/base_matcher"

module Katalyst
module Kpop
module Matchers
class Base < RSpec::Rails::Matchers::BaseMatcher
attr_reader :matched

def matches?(actual)
@matched = super
@matched.present?
end
end
end
end
end
46 changes: 46 additions & 0 deletions lib/katalyst/kpop/matchers/capybara_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/base"

module Katalyst
module Kpop
module Matchers
# @api private
class CapybaraMatcher < Base
attr_reader :matched

def matches?(actual)
super
rescue ::Capybara::ElementNotFound
nil
end

def match(expected, actual)
actual.find(expected)
end

def description
"match #{expected}"
end

def describe_expected
expected.inspect
end

def describe_actual
response = actual.native.children.to_html.gsub(/\s+/, " ")
response = "#{response[0..120]}..." if response.length > 120
response.inspect
end

def failure_message
"expected #{describe_expected} but received #{describe_actual} instead"
end

def failure_message_when_negated
"expected not to find #{expected}"
end
end
end
end
end
17 changes: 17 additions & 0 deletions lib/katalyst/kpop/matchers/capybara_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/base"

module Katalyst
module Kpop
module Matchers
# @api private
class CapybaraParser < Base
def match(_, actual)
@html = Nokogiri::HTML5.parse(actual.body)
Capybara::Node::Simple.new(@html)
end
end
end
end
end
40 changes: 40 additions & 0 deletions lib/katalyst/kpop/matchers/chained_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "rspec/rails/matchers/base_matcher"

module Katalyst
module Kpop
module Matchers
# @api private
class ChainedMatcher < RSpec::Rails::Matchers::BaseMatcher
Input = Struct.new(:matched)

delegate :failure_message, :failure_message_when_negated, to: :@matcher

def initialize(*matchers)
super()
matchers.each { |m| self << m }
end

def <<(matcher)
matcher = matcher.new if matcher.is_a?(Class)
(@matchers ||= []) << matcher
self
end

def match(_, actual)
@matcher = Input.new(actual)
@matchers.all? do |matcher|
input = @matcher.matched
@matcher = matcher
matcher.matches?(input)
end
end

def description
@matchers.last.description
end
end
end
end
end
16 changes: 16 additions & 0 deletions lib/katalyst/kpop/matchers/frame_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/capybara_matcher"

module Katalyst
module Kpop
module Matchers
# @api private
class FrameMatcher < CapybaraMatcher
def initialize(id: "kpop")
super("turbo-frame##{id}")
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/katalyst/kpop/matchers/modal_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/capybara_matcher"

module Katalyst
module Kpop
module Matchers
# @api private
class ModalMatcher < CapybaraMatcher
def initialize
super("[data-controller*='kpop--modal']")
end

def description
"contain a kpop modal"
end
end
end
end
end
16 changes: 16 additions & 0 deletions lib/katalyst/kpop/matchers/redirect_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require "katalyst/kpop/matchers/capybara_matcher"

module Katalyst
module Kpop
module Matchers
# @api private
class RedirectFinder < CapybaraMatcher
def initialize
super("[data-controller='kpop--redirect']")
end
end
end
end
end
Loading

0 comments on commit 3a46e6c

Please sign in to comment.