From 3a46e6cc7d4d5e06a9977eea1ed837743a147e46 Mon Sep 17 00:00:00 2001 From: Stephen Nelson Date: Wed, 25 Oct 2023 20:37:44 +1030 Subject: [PATCH] Split up matchers into separate files --- lib/katalyst/kpop/matchers.rb | 178 ++++++------------ lib/katalyst/kpop/matchers/base.rb | 18 ++ .../kpop/matchers/capybara_matcher.rb | 46 +++++ lib/katalyst/kpop/matchers/capybara_parser.rb | 17 ++ lib/katalyst/kpop/matchers/chained_matcher.rb | 40 ++++ lib/katalyst/kpop/matchers/frame_matcher.rb | 16 ++ lib/katalyst/kpop/matchers/modal_matcher.rb | 20 ++ lib/katalyst/kpop/matchers/redirect_finder.rb | 16 ++ .../{kpop_dismiss.rb => redirect_matcher.rb} | 21 +-- lib/katalyst/kpop/matchers/redirect_to.rb | 48 ----- lib/katalyst/kpop/matchers/render_kpop.rb | 73 ------- .../kpop/matchers/response_matcher.rb | 33 ++++ lib/katalyst/kpop/matchers/stream_matcher.rb | 16 ++ lib/katalyst/kpop/matchers/title_finder.rb | 16 ++ lib/katalyst/kpop/matchers/title_matcher.rb | 28 +++ 15 files changed, 328 insertions(+), 258 deletions(-) create mode 100644 lib/katalyst/kpop/matchers/base.rb create mode 100644 lib/katalyst/kpop/matchers/capybara_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/capybara_parser.rb create mode 100644 lib/katalyst/kpop/matchers/chained_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/frame_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/modal_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/redirect_finder.rb rename lib/katalyst/kpop/matchers/{kpop_dismiss.rb => redirect_matcher.rb} (52%) delete mode 100644 lib/katalyst/kpop/matchers/redirect_to.rb delete mode 100644 lib/katalyst/kpop/matchers/render_kpop.rb create mode 100644 lib/katalyst/kpop/matchers/response_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/stream_matcher.rb create mode 100644 lib/katalyst/kpop/matchers/title_finder.rb create mode 100644 lib/katalyst/kpop/matchers/title_matcher.rb diff --git a/lib/katalyst/kpop/matchers.rb b/lib/katalyst/kpop/matchers.rb index 1a3ab6e..9adf684 100644 --- a/lib/katalyst/kpop/matchers.rb +++ b/lib/katalyst/kpop/matchers.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/base.rb b/lib/katalyst/kpop/matchers/base.rb new file mode 100644 index 0000000..fccf718 --- /dev/null +++ b/lib/katalyst/kpop/matchers/base.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/capybara_matcher.rb b/lib/katalyst/kpop/matchers/capybara_matcher.rb new file mode 100644 index 0000000..89c8c73 --- /dev/null +++ b/lib/katalyst/kpop/matchers/capybara_matcher.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/capybara_parser.rb b/lib/katalyst/kpop/matchers/capybara_parser.rb new file mode 100644 index 0000000..7bd1b7f --- /dev/null +++ b/lib/katalyst/kpop/matchers/capybara_parser.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/chained_matcher.rb b/lib/katalyst/kpop/matchers/chained_matcher.rb new file mode 100644 index 0000000..5f128a3 --- /dev/null +++ b/lib/katalyst/kpop/matchers/chained_matcher.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/frame_matcher.rb b/lib/katalyst/kpop/matchers/frame_matcher.rb new file mode 100644 index 0000000..e0a71e9 --- /dev/null +++ b/lib/katalyst/kpop/matchers/frame_matcher.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/modal_matcher.rb b/lib/katalyst/kpop/matchers/modal_matcher.rb new file mode 100644 index 0000000..519cc89 --- /dev/null +++ b/lib/katalyst/kpop/matchers/modal_matcher.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/redirect_finder.rb b/lib/katalyst/kpop/matchers/redirect_finder.rb new file mode 100644 index 0000000..cf4ec64 --- /dev/null +++ b/lib/katalyst/kpop/matchers/redirect_finder.rb @@ -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 diff --git a/lib/katalyst/kpop/matchers/kpop_dismiss.rb b/lib/katalyst/kpop/matchers/redirect_matcher.rb similarity index 52% rename from lib/katalyst/kpop/matchers/kpop_dismiss.rb rename to lib/katalyst/kpop/matchers/redirect_matcher.rb index 705997c..503272e 100644 --- a/lib/katalyst/kpop/matchers/kpop_dismiss.rb +++ b/lib/katalyst/kpop/matchers/redirect_matcher.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true +require "katalyst/kpop/matchers/base" + module Katalyst module Kpop module Matchers - class RedirectFinder < CapybaraMatcher - def initialize - super("[data-controller='kpop--redirect']") - end - end - # @api private - class RedirectMatcher < BaseMatcher + class RedirectMatcher < Base def match(expected, actual) actual["data-kpop--redirect-path-value"].to_s.match?(expected) end @@ -27,17 +23,6 @@ def failure_message_when_negated "expected not to find a kpop redirect to #{expected.inspect}" end 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.new, - CapybaraParser, - StreamMatcher.new(id:, action: "append")) - end end end end diff --git a/lib/katalyst/kpop/matchers/redirect_to.rb b/lib/katalyst/kpop/matchers/redirect_to.rb deleted file mode 100644 index 14a4bb9..0000000 --- a/lib/katalyst/kpop/matchers/redirect_to.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -module Katalyst - module Kpop - module Matchers - class RedirectFinder < CapybaraMatcher - def initialize - super("[data-controller='kpop--redirect']") - end - end - - # @api private - class RedirectMatcher < BaseMatcher - def match(expected, actual) - actual["data-kpop--redirect-path-value"].to_s.match?(expected) - end - - def description - "kpop redirect to #{expected.inspect}" - end - - def failure_message - "expected a kpop redirect to #{expected.inspect} but received #{actual.native.to_html.inspect} instead" - end - - def failure_message_when_negated - "expected not to find a kpop redirect to #{expected.inspect}" - end - 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.new, - CapybaraParser, - StreamMatcher.new(id:, action: "append"), - RedirectFinder, - RedirectMatcher.new(target)) - end - end - end -end diff --git a/lib/katalyst/kpop/matchers/render_kpop.rb b/lib/katalyst/kpop/matchers/render_kpop.rb deleted file mode 100644 index 5b22f32..0000000 --- a/lib/katalyst/kpop/matchers/render_kpop.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -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 - - # @api private - class TitleFinder < CapybaraMatcher - def initialize - super(".kpop-title") - end - end - - # @api private - class TitleMatcher < BaseMatcher - def description - "contain a kpop modal with title #{expected.inspect}" - end - - def match(expected, actual) - expected.match?(actual.text) - end - - def failure_message - "expected a kpop modal with title #{expected.inspect} but received #{actual.native.to_html.inspect} instead" - end - - def failure_message_when_negated - "expected not to find a kpop modal with title #{expected}" - end - 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 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.new, CapybaraParser, FrameMatcher.new(id:), ModalMatcher) - matcher << TitleFinder << TitleMatcher.new(title) if title.present? - matcher - end - end - end -end diff --git a/lib/katalyst/kpop/matchers/response_matcher.rb b/lib/katalyst/kpop/matchers/response_matcher.rb new file mode 100644 index 0000000..a98bf1d --- /dev/null +++ b/lib/katalyst/kpop/matchers/response_matcher.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "katalyst/kpop/matchers/base" + +module Katalyst + module Kpop + module Matchers + # @api private + class ResponseMatcher < Base + 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 + end + end +end diff --git a/lib/katalyst/kpop/matchers/stream_matcher.rb b/lib/katalyst/kpop/matchers/stream_matcher.rb new file mode 100644 index 0000000..c402e4e --- /dev/null +++ b/lib/katalyst/kpop/matchers/stream_matcher.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "katalyst/kpop/matchers/capybara_matcher" + +module Katalyst + module Kpop + module Matchers + # @api private + class StreamMatcher < CapybaraMatcher + def initialize(id: "kpop", action: "update") + super("turbo-stream[action='#{action}'][target='#{id}']") + end + end + end + end +end diff --git a/lib/katalyst/kpop/matchers/title_finder.rb b/lib/katalyst/kpop/matchers/title_finder.rb new file mode 100644 index 0000000..0566ef0 --- /dev/null +++ b/lib/katalyst/kpop/matchers/title_finder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "katalyst/kpop/matchers/capybara_matcher" + +module Katalyst + module Kpop + module Matchers + # @api private + class TitleFinder < CapybaraMatcher + def initialize + super(".kpop-title") + end + end + end + end +end diff --git a/lib/katalyst/kpop/matchers/title_matcher.rb b/lib/katalyst/kpop/matchers/title_matcher.rb new file mode 100644 index 0000000..49a2251 --- /dev/null +++ b/lib/katalyst/kpop/matchers/title_matcher.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "katalyst/kpop/matchers/base" + +module Katalyst + module Kpop + module Matchers + # @api private + class TitleMatcher < Base + def description + "contain a kpop modal with title #{expected.inspect}" + end + + def match(expected, actual) + expected.match?(actual.text) + end + + def failure_message + "expected a kpop modal with title #{expected.inspect} but received #{actual.native.to_html.inspect} instead" + end + + def failure_message_when_negated + "expected not to find a kpop modal with title #{expected}" + end + end + end + end +end