From 03dd196600d05d371ffb0e02283d56e6f97da43e Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Wed, 13 Sep 2023 18:55:55 +0100 Subject: [PATCH] Add helper `fill_in_govuk_text_field` (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is aimed to be a drop-in replacement for the standard `fill_in` helper, but with some additional checks and features. The helper will: * make sure you've specified the full field label text, rather than usual a partial match or an ID reference * check the label is associated with the field using the `for="x"` matching an `id="x"` * check that the referenced ID is unique on the page * check that any `aria-describedby` IDs match IDs on the page Additionally, you can optionally specify the hint text: ```ruby fill_in_govuk_text_field("What is the name of the event?", hint: "The name you’ll use on promotional material", with: "Design System Day" ) ``` ...and if you do, it'll check that that the hint is correctly associated with the field. I've not made this required though, so if there's a hint in the HTML but it's not specified within the test, then it'll still pass (as some teams might consider specifying the hint text in tests to be overkill?) --- docs/fill_in_govuk_text_field.md | 45 ++++ docs/index.njk | 3 +- docs/summarise-errors.md | 2 +- docs/summarise.md | 2 +- lib/fill_in_govuk_text_field.rb | 150 ++++++++++++ lib/govuk_rspec_helpers.rb | 1 + spec/fill_in_govuk_text_field_spec.rb | 316 ++++++++++++++++++++++++++ 7 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 docs/fill_in_govuk_text_field.md create mode 100644 lib/fill_in_govuk_text_field.rb create mode 100644 spec/fill_in_govuk_text_field_spec.rb diff --git a/docs/fill_in_govuk_text_field.md b/docs/fill_in_govuk_text_field.md new file mode 100644 index 0000000..226dca1 --- /dev/null +++ b/docs/fill_in_govuk_text_field.md @@ -0,0 +1,45 @@ +--- +title: Fill in a text field +layout: sub-navigation +order: 3 +--- + +This helper is a drop-in replacement for the standard `fill_in` helper, which adds some additional usability and accessibility checks. + +Use this within tests that navigate between multiple pages. + +Here’s a simple example: + +```ruby +scenario "Filling in an event name" do + visit "/" + + fill_in_govuk_text_field("Event name", with: "Design System Day") + click_govuk_button("Continue") + + expect(page).to have_content("Design System Day") +end +``` + +The helper will check that: + +* there is a label with the given text +* a text field is correctly associated with that label using the `for` attribute + +## Hints + +If a text field has a hint, you can check that this is correctly associated with the field. + +```ruby +scenario "Filling in an event name" do + visit "/" + + fill_in_govuk_text_field("What is the name of the event?", + hint: "The name you’ll use on promotional material", + with: "Design System Day" + ) + click_govuk_button("Continue") + + expect(page).to have_content("Design System Day") +end +``` diff --git a/docs/index.njk b/docs/index.njk index 5fa430d..48dfc19 100644 --- a/docs/index.njk +++ b/docs/index.njk @@ -7,9 +7,10 @@ description: A set of helpers to make it easier to test GOV.UK services using th startButton: href: /get-started content: | - ## Navigating + ## Navigating and filling in forms * [Clicking links](click-govuk-link) + * [Filling in text fields](fill_in_govuk_text_field) ## Expecting content diff --git a/docs/summarise-errors.md b/docs/summarise-errors.md index 57c06a2..8dcaaa0 100644 --- a/docs/summarise-errors.md +++ b/docs/summarise-errors.md @@ -1,7 +1,7 @@ --- title: Error summaries layout: sub-navigation -order: 3 +order: 4 --- Use this helper to test that the [Error summary component](https://design-system.service.gov.uk/components/error-summary/) is visible on the page with the correct content. diff --git a/docs/summarise.md b/docs/summarise.md index a576215..88f2526 100644 --- a/docs/summarise.md +++ b/docs/summarise.md @@ -1,7 +1,7 @@ --- title: Summary lists layout: sub-navigation -order: 4 +order: 5 --- Use this helper to test that the [Summary list component](https://design-system.service.gov.uk/components/summary-list/) is visible on the page with the correct content, for example on a Check your answers page. diff --git a/lib/fill_in_govuk_text_field.rb b/lib/fill_in_govuk_text_field.rb new file mode 100644 index 0000000..8720f86 --- /dev/null +++ b/lib/fill_in_govuk_text_field.rb @@ -0,0 +1,150 @@ +module GovukRSpecHelpers + class FillInGovUKTextField + + attr_reader :page, :label, :hint, :with + + def initialize(page:, label:, hint:, with:) + @page = page + @label = label + @hint = hint + @with = with + end + + def fill_in + labels = page.all('label', text: label, exact_text: true, normalize_ws: true) + + if labels.size == 0 + check_for_inexact_label_match + check_for_field_name_match + + raise "Unable to find label with the text #{label}" + end + + @label = labels.first + + check_label_has_a_for_attribute + + label_for = @label[:for] + @inputs = page.all(id: label_for) + + check_label_is_associated_with_a_field + check_there_is_only_1_element_with_the_associated_id + + @input = @inputs.first + + check_associated_element_is_a_form_field + check_input_type_is_text + + aria_described_by_ids = @input["aria-describedby"].to_s.strip.split(/\s+/) + + @described_by_elements = [] + + if aria_described_by_ids.size > 0 + aria_described_by_ids.each do |aria_described_by_id| + + check_there_is_only_one_element_with_id(aria_described_by_id) + @described_by_elements << page.find(id: aria_described_by_id) + + end + end + + if hint + check_field_is_described_by_a_hint + check_hint_matches_text_given + end + + @input.set(with) + end + + private + + def check_for_inexact_label_match + labels_not_using_exact_match = page.all('label', text: label) + if labels_not_using_exact_match.size > 0 + raise "Unable to find label with the text \"#{label}\" but did find label with the text \"#{labels_not_using_exact_match.first.text}\" - use the full label text" + end + end + + def check_for_field_name_match + inputs_matching_name = page.all("input[name=\"#{label}\"]") + + if inputs_matching_name.size > 0 + + input_matching_name = inputs_matching_name.first + + labels = page.all("label[for=\"#{input_matching_name['id']}\"]") + + if labels.size > 0 + raise "Use the full label text \"#{labels.first.text}\" instead of the field name" + end + end + end + + def check_label_has_a_for_attribute + if @label[:for].to_s.strip == "" + raise "Found the label but it is missing a \"for\" attribute to associate it with an input" + end + end + + def check_label_is_associated_with_a_field + if @inputs.size == 0 + raise "Found the label but there is no field with the ID \"#{@label[:for]}\" which matches the label‘s \"for\" attribute" + end + end + + def check_there_is_only_1_element_with_the_associated_id + if @inputs.size > 1 + raise "Found the label but there there are #{@inputs.size} elements with the ID \"#{@label[:for]}\" which matches the label‘s \"for\" attribute" + end + end + + def check_associated_element_is_a_form_field + if !['input', 'textarea', 'select'].include?(@input.tag_name) + raise "Found the label but but it is associated with a <#{@input.tag_name}> element instead of a form field" + end + end + + def check_input_type_is_text + raise "Found the field, but it has type=\"#{@input[:type]}\", expected type=\"text\"" unless @input[:type] == "text" + end + + def check_field_is_described_by_a_hint + if @described_by_elements.size == 0 + check_if_the_hint_exists_but_is_not_associated_with_field + + raise "Found the field but could not find the hint \"#{hint}\"" + end + end + + def check_if_the_hint_exists_but_is_not_associated_with_field + if page.all('.govuk-hint', text: hint).size > 0 + raise "Found the field and the hint, but not field is not associated with the hint using aria-describedby" + end + end + + def check_hint_matches_text_given + hint_matching_id = @described_by_elements.find {|element| element[:class].include?("govuk-hint") } + if hint_matching_id.text != hint + raise "Found the label but the associated hint is \"#{hint_matching_id.text}\" not \"#{hint}\"" + end + end + + def check_there_is_only_one_element_with_id(aria_described_by_id) + elements_matching_id = page.all(id: aria_described_by_id) + if elements_matching_id.size == 0 + raise "Found the field but it has an \"aria-describedby=#{aria_described_by_id}\" attribute and no hint with that ID exists" + elsif elements_matching_id.size > 1 + raise "Found the field but it has an \"aria-describedby=#{aria_described_by_id}\" attribute and 2 elements with that ID exist" + end + end + + end + + def fill_in_govuk_text_field(label, hint: nil, with:) + FillInGovUKTextField.new(page:, label:, hint:, with:).fill_in + end + + RSpec.configure do |rspec| + rspec.include self + end +end diff --git a/lib/govuk_rspec_helpers.rb b/lib/govuk_rspec_helpers.rb index 0567c45..185f657 100644 --- a/lib/govuk_rspec_helpers.rb +++ b/lib/govuk_rspec_helpers.rb @@ -6,3 +6,4 @@ require_relative 'summarise_matcher' require_relative 'click_govuk_link' +require_relative 'fill_in_govuk_text_field' diff --git a/spec/fill_in_govuk_text_field_spec.rb b/spec/fill_in_govuk_text_field_spec.rb new file mode 100644 index 0000000..63e0ba5 --- /dev/null +++ b/spec/fill_in_govuk_text_field_spec.rb @@ -0,0 +1,316 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "fill_in_govuk_text_field", type: :feature do + + context "where the input is correctly associated with a label" do + before do + TestApp.body = '
+

+ +

+ +
' + visit('/') + end + + context "and the full label is specified" do + it 'should be successful' do + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + expect(page.find_field("What is the name of the event?").value).to eql("Design System Day") + end + end + + context "and a partial label is specified" do + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name", with: "Design System Day") + }.to raise_error('Unable to find label with the text "What is the name" but did find label with the text "What is the name of the event?" - use the full label text') + end + end + + context "and the name of the input is used" do + it 'should raise an error' do + expect { + fill_in_govuk_text_field("eventName", with: "Design System Day") + }.to raise_error('Use the full label text "What is the name of the event?" instead of the field name') + end + end + + context "and a hint is is specified" do + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", hint: "The name you’ll use on promotional material", with: "Design System Day") + }.to raise_error('Found the field but could not find the hint "The name you’ll use on promotional material"') + end + end + end + + context "where the label is missing a 'for' attribute" do + before do + TestApp.body = '
+

+ +

+ +
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the label but it is missing a "for" attribute to associate it with an input') + end + end + + context "where the label has an empty 'for' attribute" do + before do + TestApp.body = '
+

+ +

+ +
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the label but it is missing a "for" attribute to associate it with an input') + end + end + + context "where the label has a 'for' attribute but it doesn’t match an input ID" do + before do + TestApp.body = '
+

+ +

+ +
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the label but there is no field with the ID "event-name" which matches the label‘s "for" attribute') + end + end + + context "where the label has a 'for' attribute but there are 2 elements with that ID" do + before do + TestApp.body = '
+

+ +

+ +
+
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the label but there there are 2 elements with the ID "event-name" which matches the label‘s "for" attribute') + end + end + + context "where the label is associated with an element that isn’t a field" do + before do + TestApp.body = '
+

+ +

+
+
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the label but but it is associated with a
element instead of a form field') + end + end + + context "where the input also has a hint associated with it" do + before do + TestApp.body = '
+

+ +

+
+ The name you’ll use on promotional material +
+ +
' + visit('/') + end + + context "and the label is specified but the hint isn’t" do + it "should be successful" do + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + expect(page.find_field("What is the name of the event?").value).to eql("Design System Day") + end + end + + context "and the label and the hint are specified" do + it "should be successful" do + fill_in_govuk_text_field("What is the name of the event?", hint: "The name you’ll use on promotional material", with: "Design System Day") + expect(page.find_field("What is the name of the event?").value).to eql("Design System Day") + end + end + + context "and a different hint is specified" do + it "should be raise an error" do + expect { + fill_in_govuk_text_field("What is the name of the event?", hint: "Make it a catchy name", with: "Design System Day") + }.to raise_error('Found the label but the associated hint is "The name you’ll use on promotional material" not "Make it a catchy name"') + end + end + end + + context "where the input is described by a hint which doesn’t exist" do + before do + TestApp.body = '
+

+ +

+
+ The name you’ll use on promotional material +
+ +
' + visit('/') + end + + it "should raise an error" do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the field but it has an "aria-describedby=event-name-hint" attribute and no hint with that ID exists') + end + end + + context "where the input is described by a hint but 2 elements with that ID exist" do + before do + TestApp.body = '
+

+ +

+
+ The name you’ll use on promotional material +
+
+ Keep it short +
+ +
' + visit('/') + end + + it "should raise an error" do + expect { + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + }.to raise_error('Found the field but it has an "aria-describedby=event-name-hint" attribute and 2 elements with that ID exist') + end + end + + context "where the there is a hint but the aria-describeby is missing" do + before do + TestApp.body = '
+

+ +

+
+ The name you’ll use on promotional material +
+ +
' + visit('/') + end + + context "and the hint is specified" do + it "should raise an error" do + expect { + fill_in_govuk_text_field("What is the name of the event?", hint: "The name you’ll use on promotional material", with: "Design System Day") + }.to raise_error('Found the field and the hint, but not field is not associated with the hint using aria-describedby') + end + end + end + + context "where the input is described by a hint and an error message" do + before do + TestApp.body = '
+

+ +

+
+ The name you’ll use on promotional material +
+

+ Error: Enter an event name +

+ +
' + visit('/') + end + + context "and the just the label is specified" do + it 'should be successful' do + fill_in_govuk_text_field("What is the name of the event?", with: "Design System Day") + expect(page.find_field("What is the name of the event?").value).to eql("Design System Day") + end + end + + context "and the the label and hint are specified" do + it 'should be successful' do + fill_in_govuk_text_field("What is the name of the event?", hint: "The name you’ll use on promotional material", with: "Design System Day") + expect(page.find_field("What is the name of the event?").value).to eql("Design System Day") + end + end + + context "where the input has type=email" do + before do + TestApp.body = '
+ + +
' + visit('/') + end + + it 'should raise an error' do + expect { + fill_in_govuk_text_field("Email address", with: "test@example.com") + }.to raise_error('Found the field, but it has type="email", expected type="text"') + end + end + end +end