From ae37d418c288bee323c8500765b1836701c6104b Mon Sep 17 00:00:00 2001 From: Sergio Marques Date: Thu, 24 Nov 2016 17:14:05 +0000 Subject: [PATCH] Create custom RSpec matcher to validate form group HTML output The specs that validate what kind of content is generated for all the supported HTML tag built through the Form Builder have a lot of repeated code. This commit tackles that issue by supporting a custom RSpec matcher that validates the HTML output generated by the Form Builder. This commit does NOT change any behaviour in the way that the Form Build works. --- .../form_builder_spec.rb | 74 ++--------- spec/rails_helper.rb | 2 + spec/support/matcher_helpers.rb | 7 ++ .../matchers/form_group_matcher_helpers.rb | 115 ++++++++++++++++++ 4 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 spec/support/matcher_helpers.rb create mode 100644 spec/support/matchers/form_group_matcher_helpers.rb diff --git a/spec/lib/govuk_elements_form_builder/form_builder_spec.rb b/spec/lib/govuk_elements_form_builder/form_builder_spec.rb index 5439dbf..f03d43f 100644 --- a/spec/lib/govuk_elements_form_builder/form_builder_spec.rb +++ b/spec/lib/govuk_elements_form_builder/form_builder_spec.rb @@ -30,89 +30,42 @@ def type_for(method, type) end shared_examples_for 'input field' do |method, type| - - def size(method, size) - method == :text_area ? '' : %'size="#{size}" ' - end + let(:default_builder_options) { { resource: 'person' } } it 'outputs label and input wrapped in div' do output = builder.send method, :name - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control" #{type_for(method, type)}name="person[name]" id="person_name" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :name, input_type: method)) end it 'supports attributes defined as a string' do - output = builder.send method, 'name', class: 'custom-class' + output = builder.send method, 'name' - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control custom-class" #{type_for(method, type)}name="person[name]" id="person_name" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :name, input_type: method)) end it 'adds custom class to input when passed class: "custom-class"' do output = builder.send method, :name, class: 'custom-class' - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control custom-class" #{type_for(method, type)}name="person[name]" id="person_name" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :name, input_type: method, class: 'custom-class')) end it 'adds custom classes to input when passed class: ["custom-class", "another-class"]' do output = builder.send method, :name, class: ['custom-class', 'another-class'] - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control custom-class another-class" #{type_for(method, type)}name="person[name]" id="person_name" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :name, input_type: method, class: 'custom-class another-class')) end it 'passes options passed to text_field onto super text_field implementation' do output = builder.send method, :name, size: 100 - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} #{size(method, 100)}class="form-control" #{type_for(method, type)}name="person[name]" id="person_name" />', - '
' - ] + + expect(output).to match_form_group(default_builder_options.merge(field: :name, input_type: method, size: 100)) end context 'when hint text provided' do it 'outputs hint text in span inside label' do output = builder.send method, :ni_number - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control" #{type_for(method, type)}name="person[ni_number]" id="person_ni_number" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :ni_number, input_type: method)) end end @@ -121,14 +74,7 @@ def size(method, size) output = builder.fields_for(:address, Address.new) do |f| f.send method, :postcode end - expect_equal output, [ - '
', - '', - %'<#{element_for(method)} class="form-control" #{type_for(method, type)}name="person[address_attributes][postcode]" id="person_address_attributes_postcode" />', - '
' - ] + expect(output).to match_form_group(default_builder_options.merge(field: :postcode, input_type: method, fields_for: :address)) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 241a969..f271a2b 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -21,6 +21,7 @@ # # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } require_relative 'support/translation_helper.rb' +require_relative 'support/matcher_helpers.rb' # Checks for pending migrations before tests are run. # If you are not using ActiveRecord, you can remove this line. @@ -49,4 +50,5 @@ # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! + config.include MatcherHelpers end diff --git a/spec/support/matcher_helpers.rb b/spec/support/matcher_helpers.rb new file mode 100644 index 0000000..ba561d8 --- /dev/null +++ b/spec/support/matcher_helpers.rb @@ -0,0 +1,7 @@ +require_relative 'matchers/form_group_matcher_helpers' + +module MatcherHelpers + def self.included(base) + base.send(:include, FormGroupMatcherHelpers) + end +end diff --git a/spec/support/matchers/form_group_matcher_helpers.rb b/spec/support/matchers/form_group_matcher_helpers.rb new file mode 100644 index 0000000..405a6a9 --- /dev/null +++ b/spec/support/matchers/form_group_matcher_helpers.rb @@ -0,0 +1,115 @@ +require 'rspec/expectations' + +module FormGroupMatcherHelpers + extend RSpec::Matchers::DSL + + class FormGroupObject + attr_reader :options, :resource, :field, + :label, :input_type, :input_class, :input_size, + :fields_for + + def initialize(options) + @options = options + @resource = options[:resource] + @field = options.fetch(:field) + @label = options.fetch(:label) { @field.to_s.humanize } + @input_type = options.fetch(:input_type) + @input_class = options[:class] + @input_size = options[:size] + @fields_for = options[:fields_for] + end + + def field_id + [resource, fields_for_attributes, field].compact.join('_') + end + + def field_name + return "#{resource}[#{fields_for_attributes}][#{field}]" if resource && fields_for_attributes + return "#{resource}[#{field}]" if resource + field + end + + def to_s + ['
', label_tag, field_tag, '
'].join("\n") + end + + private + + def fields_for_attributes + return unless fields_for + "#{fields_for}_attributes" + end + + def text_area_input? + input_type.to_s == 'text_area' + end + + def input_tag + return '', localized_hint, ''].join("\n") + end + + def label_tag + localized_path = ['helpers', 'label', resource, field].compact.join('.') + localized_label = I18n.t(localized_path, default: field.to_s.humanize) + [%['].compact.join("\n") + end + + def field_tag + [input_tag, input_tag_size, input_tag_class, input_tag_type, %[name="#{field_name}"], %[id="#{field_id}"], '/>'].compact.join(' ') + end + end + + matcher :match_form_group do |expected| + define_method :formatted_expected do + FormGroupObject.new(expected).to_s + end + + define_method :formatted_actual do |actual| + actual.gsub(">\n", ' />').split("<").join("\n<").split(">").join(">\n").squeeze("\n").strip + '>' + end + + match do |actual| + formatted_actual(actual) == formatted_expected + end + + failure_message do |actual| + "expected\n#{formatted_actual(actual)}\nto match\n#{formatted_expected}" + end + end +end